From bb50fbc5894ca8290c2d51eb3e57a533dd2f23d5 Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Fri, 7 Jul 2023 20:37:50 +0800 Subject: [PATCH] #54 snippet group --- ios/Podfile.lock | 8 +-- lib/core/extension/order.dart | 20 ++++++-- lib/data/model/server/snippet.dart | 6 ++- lib/data/model/server/snippet.g.dart | 7 ++- lib/data/provider/snippet.dart | 38 +++++++++++++- lib/data/store/setting.dart | 5 ++ lib/view/page/server/detail.dart | 6 ++- lib/view/page/server/tab.dart | 58 ++++----------------- lib/view/page/snippet/edit.dart | 44 ++++++++++------ lib/view/page/snippet/list.dart | 44 +++++++++++++--- lib/view/page/ssh/virt_key_setting.dart | 2 +- lib/view/widget/tag_switcher.dart | 67 +++++++++++++++++++++++++ 12 files changed, 223 insertions(+), 82 deletions(-) create mode 100644 lib/view/widget/tag_switcher.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5d27faa3..f47959d9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - countly_flutter (22.09.0): + - countly_flutter (23.6.0): - Flutter - file_picker (0.0.1): - Flutter @@ -55,15 +55,15 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - countly_flutter: 135f1a4930f8e26ba223a14201d3f265ea7b4c83 + countly_flutter: 4eeee607183664b871589250a0bd049cfd2697eb file_picker: 1d63c4949e05e386da864365f8c13e1e64787675 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 - path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1 r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114 - share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 PODFILE CHECKSUM: 7fb15c416f8685fca4966867a8da218ec592ec2e diff --git a/lib/core/extension/order.dart b/lib/core/extension/order.dart index 4a33a2f0..5a7294c8 100644 --- a/lib/core/extension/order.dart +++ b/lib/core/extension/order.dart @@ -1,9 +1,15 @@ import 'package:toolbox/core/persistant_store.dart'; typedef Order = List; +typedef _OnMove = void Function(Order); extension OrderX on Order { - void move(int oldIndex, int newIndex, StoreProperty> property) { + void move( + int oldIndex, + int newIndex, { + StoreProperty>? property, + _OnMove? onMove, + }) { if (oldIndex == newIndex) return; if (oldIndex < newIndex) { newIndex -= 1; @@ -11,7 +17,8 @@ extension OrderX on Order { final item = this[oldIndex]; removeAt(oldIndex); insert(newIndex, item); - property.put(this); + property?.put(this); + onMove?.call(this); } void update(T id, T newId) { @@ -24,11 +31,16 @@ extension OrderX on Order { return indexOf(id); } - void moveById(T oid, T nid, StoreProperty> property) { + void moveById( + T oid, + T nid, { + StoreProperty>? property, + _OnMove? onMove, + }) { final index = indexOf(oid); if (index == -1) return; final newIndex = indexOf(nid); if (newIndex == -1) return; - move(index, newIndex, property); + move(index, newIndex, property: property, onMove: onMove); } } diff --git a/lib/data/model/server/snippet.dart b/lib/data/model/server/snippet.dart index 20c504a4..ece8b628 100644 --- a/lib/data/model/server/snippet.dart +++ b/lib/data/model/server/snippet.dart @@ -8,16 +8,20 @@ class Snippet { late String name; @HiveField(1) late String script; - Snippet(this.name, this.script); + @HiveField(2) + late List? tags; + Snippet(this.name, this.script, this.tags); Snippet.fromJson(Map json) { name = json['name'].toString(); script = json['script'].toString(); + tags = json['tags']?.cast(); } Map toJson() { final data = {}; data['name'] = name; data['script'] = script; + data['tags'] = tags; return data; } } diff --git a/lib/data/model/server/snippet.g.dart b/lib/data/model/server/snippet.g.dart index acef2f9a..5c194f89 100644 --- a/lib/data/model/server/snippet.g.dart +++ b/lib/data/model/server/snippet.g.dart @@ -19,17 +19,20 @@ class SnippetAdapter extends TypeAdapter { return Snippet( fields[0] as String, fields[1] as String, + (fields[2] as List?)?.cast(), ); } @override void write(BinaryWriter writer, Snippet obj) { writer - ..writeByte(2) + ..writeByte(3) ..writeByte(0) ..write(obj.name) ..writeByte(1) - ..write(obj.script); + ..write(obj.script) + ..writeByte(2) + ..write(obj.tags); } @override diff --git a/lib/data/provider/snippet.dart b/lib/data/provider/snippet.dart index f385a8bb..3d3ce42c 100644 --- a/lib/data/provider/snippet.dart +++ b/lib/data/provider/snippet.dart @@ -5,25 +5,44 @@ import 'package:toolbox/data/model/server/snippet.dart'; import 'package:toolbox/data/store/snippet.dart'; import 'package:toolbox/locator.dart'; +import '../../core/extension/order.dart'; + class SnippetProvider extends BusyProvider { - List get snippets => _snippets; + late Order _snippets; + Order get snippets => _snippets; + + final _tags = []; + List get tags => _tags; + final _store = locator(); - late List _snippets; void loadData() { _snippets = _store.fetch(); } + void _updateTags() { + _tags.clear(); + for (final s in _snippets) { + if (s.tags?.isEmpty ?? true) { + continue; + } + _tags.addAll(s.tags!); + } + _tags.toSet().toList(); + } + void add(Snippet snippet) { _snippets.add(snippet); _store.put(snippet); notifyListeners(); + _updateTags(); } void del(Snippet snippet) { _snippets.remove(snippet); _store.delete(snippet); notifyListeners(); + _updateTags(); } void update(Snippet old, Snippet newOne) { @@ -32,6 +51,21 @@ class SnippetProvider extends BusyProvider { _snippets.remove(old); _snippets.add(newOne); notifyListeners(); + _updateTags(); + } + + void renameTag(String old, String newOne) { + for (final s in _snippets) { + if (s.tags?.contains(old) ?? false) { + s.tags?.remove(old); + s.tags?.add(newOne); + } + } + for (final s in _snippets) { + _store.put(s); + } + notifyListeners(); + _updateTags(); } String get export => json.encode(snippets); diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 9a187f8e..1fa27ffb 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -46,6 +46,11 @@ class SettingStore extends PersistentStore { StoreProperty> get serverOrder => property('serverOrder', defaultValue: null); + StoreProperty> get snippetOrder => property( + 'snippetOrder', + defaultValue: null, + ); + // Server details page cards order StoreProperty> get detailCardOrder => property('detailCardPrder', defaultValue: defaultDetailCardOrder); diff --git a/lib/view/page/server/detail.dart b/lib/view/page/server/detail.dart index b09c26af..649135df 100644 --- a/lib/view/page/server/detail.dart +++ b/lib/view/page/server/detail.dart @@ -74,7 +74,11 @@ class _ServerDetailPageState extends State left: 13, right: 13, top: 13, bottom: _media.padding.bottom), onReorder: (int oldIndex, int newIndex) { setState(() { - _cardsOrder.move(oldIndex, newIndex, _setting.detailCardOrder); + _cardsOrder.move( + oldIndex, + newIndex, + property: _setting.detailCardOrder, + ); }); }, footer: height13, diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index 5073a486..fe5bff56 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -9,6 +9,7 @@ import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/utils/misc.dart'; import 'package:toolbox/view/page/process.dart'; +import 'package:toolbox/view/widget/tag_switcher.dart'; import '../../../core/route.dart'; import '../../../core/utils/ui.dart'; @@ -103,13 +104,21 @@ class _ServerPageState extends State (pro.servers[e]?.spi.tags?.contains(_tag) ?? false)) .toList(); return ReorderableListView.builder( - header: _buildTagsSwitcher(pro.tags), + header: TagSwitcher( + tags: pro.tags, + width: _media.size.width, + onTagChanged: (p0) => setState(() { + _tag = p0; + }), + initTag: _tag, + all: _s.all, + ), padding: const EdgeInsets.fromLTRB(7, 10, 7, 7), onReorder: (oldIndex, newIndex) => setState(() { pro.serverOrder.moveById( filtered[oldIndex], filtered[newIndex], - _settingStore.serverOrder, + property: _settingStore.serverOrder, ); }), itemBuilder: (_, index) => _buildEachServerCard( @@ -122,51 +131,6 @@ class _ServerPageState extends State ); } - Widget _buildTagsSwitcher(List tags) { - if (tags.isEmpty) return nil; - final items = [null, ...tags]; - return Container( - height: 37, - width: _media.size.width, - alignment: Alignment.center, - color: Colors.transparent, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) => _buildTagItem(items[index]), - itemCount: items.length, - ), - ); - } - - Widget _buildTagItem(String? tag) { - return Padding( - padding: const EdgeInsets.only(left: 4, right: 5, bottom: 9), - child: GestureDetector( - onTap: () { - setState(() { - _tag = tag; - }); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20.0)), - color: primaryColor.withAlpha(20), - ), - padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 2.7), - child: Center( - child: Text( - tag == null ? _s.all : '#$tag', - style: TextStyle( - color: _tag == tag ? null : _theme.disabledColor, - fontSize: 15, - fontWeight: FontWeight.w500, - ), - ), - )), - ), - ); - } - Widget _buildEachServerCard(Server? si) { if (si == null) { return nil; diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index 9c7f3176..943a0288 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -1,7 +1,6 @@ import 'package:after_layout/after_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:nil/nil.dart'; import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/view/widget/input_field.dart'; @@ -10,6 +9,7 @@ import '../../../data/model/server/snippet.dart'; import '../../../data/provider/snippet.dart'; import '../../../data/res/ui.dart'; import '../../../locator.dart'; +import '../../widget/tag_editor.dart'; class SnippetEditPage extends StatefulWidget { const SnippetEditPage({Key? key, this.snippet}) : super(key: key); @@ -29,6 +29,8 @@ class _SnippetEditPageState extends State late SnippetProvider _provider; late S _s; + List _tags = []; + @override void initState() { super.initState(); @@ -46,24 +48,29 @@ class _SnippetEditPageState extends State return Scaffold( appBar: AppBar( title: Text(_s.edit, style: textSize18), - actions: [ - widget.snippet != null - ? IconButton( - onPressed: () { - _provider.del(widget.snippet!); - context.pop(); - }, - tooltip: _s.delete, - icon: const Icon(Icons.delete), - ) - : nil - ], + actions: _buildAppBarActions(), ), body: _buildBody(), floatingActionButton: _buildFAB(), ); } + List? _buildAppBarActions() { + if (widget.snippet == null) { + return null; + } + return [ + IconButton( + onPressed: () { + _provider.del(widget.snippet!); + context.pop(); + }, + tooltip: _s.delete, + icon: const Icon(Icons.delete), + ) + ]; + } + Widget _buildFAB() { return FloatingActionButton( heroTag: 'snippet', @@ -75,7 +82,7 @@ class _SnippetEditPageState extends State showSnackBar(context, Text(_s.fieldMustNotEmpty)); return; } - final snippet = Snippet(name, script); + final snippet = Snippet(name, script, _tags); if (widget.snippet != null) { _provider.update(widget.snippet!, snippet); } else { @@ -106,6 +113,15 @@ class _SnippetEditPageState extends State label: _s.snippet, icon: Icons.code, ), + TagEditor( + tags: _tags, + onChanged: (p0) => setState(() { + _tags = p0; + }), + s: _s, + tagSuggestions: [..._provider.tags], + onRenameTag: _provider.renameTag, + ) ], ); } diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index 641f9876..e5975dab 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:provider/provider.dart'; +import 'package:toolbox/core/extension/order.dart'; +import 'package:toolbox/view/widget/tag_switcher.dart'; +import '../../../data/store/setting.dart'; +import '../../../locator.dart'; import '/core/route.dart'; import '/data/provider/snippet.dart'; import 'edit.dart'; @@ -16,11 +20,17 @@ class SnippetListPage extends StatefulWidget { class _SnippetListPageState extends State { late S _s; + late MediaQueryData _media; + + final _settingStore = locator(); + + String? _tag; @override void didChangeDependencies() { super.didChangeDependencies(); _s = S.of(context)!; + _media = MediaQuery.of(context); } @override @@ -47,26 +57,48 @@ class _SnippetListPageState extends State { ); } - return ListView.builder( + final filtered = provider.snippets + .where((e) => _tag == null || (e.tags?.contains(_tag) ?? false)) + .toList(); + + return ReorderableListView.builder( padding: const EdgeInsets.all(13), - itemCount: provider.snippets.length, + itemCount: filtered.length, + onReorder: (oldIdx, newIdx) => setState(() { + provider.snippets.moveById( + filtered[oldIdx], + filtered[newIdx], + onMove: (p0) { + _settingStore.snippetOrder.put(p0.map((e) => e.name).toList()); + }, + ); + }), + header: TagSwitcher( + tags: provider.tags, + onTagChanged: (tag) => setState(() => _tag = tag), + initTag: _tag, + all: _s.all, + width: _media.size.width, + ), itemBuilder: (context, idx) { + final snippet = filtered[idx]; return RoundRectCard( ListTile( contentPadding: const EdgeInsets.only(left: 23, right: 17), title: Text( - provider.snippets[idx].name, + snippet.name, overflow: TextOverflow.ellipsis, maxLines: 1, ), trailing: IconButton( onPressed: () => AppRoute( - SnippetEditPage(snippet: provider.snippets[idx]), - 'snippet edit page') - .go(context), + SnippetEditPage(snippet: snippet), + 'snippet edit page', + ).go(context), icon: const Icon(Icons.edit), ), ), + key: ValueKey(snippet.name), ); }, ); diff --git a/lib/view/page/ssh/virt_key_setting.dart b/lib/view/page/ssh/virt_key_setting.dart index 5adb26ac..f6b91bdd 100644 --- a/lib/view/page/ssh/virt_key_setting.dart +++ b/lib/view/page/ssh/virt_key_setting.dart @@ -58,7 +58,7 @@ class _SSHVirtKeySettingPageState extends State { showSnackBar(context, Text(_s.disabled)); return; } - keys.moveById(keys[o], keys[n], _setting.sshVirtKeys); + keys.moveById(keys[o], keys[n], property: _setting.sshVirtKeys); setState(() {}); }, ); diff --git a/lib/view/widget/tag_switcher.dart b/lib/view/widget/tag_switcher.dart new file mode 100644 index 00000000..80aaef61 --- /dev/null +++ b/lib/view/widget/tag_switcher.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:nil/nil.dart'; + +import '../../data/res/color.dart'; + +class TagSwitcher extends StatelessWidget { + final List tags; + final double width; + final void Function(String?) onTagChanged; + final String? initTag; + final String all; + + const TagSwitcher({ + Key? key, + required this.tags, + required this.width, + required this.onTagChanged, + required this.all, + this.initTag, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return _buildTagsSwitcher(tags); + } + + Widget _buildTagsSwitcher(List tags) { + if (tags.isEmpty) return nil; + final items = [null, ...tags]; + return Container( + height: 37, + width: width, + alignment: Alignment.center, + color: Colors.transparent, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => _buildTagItem(items[index]), + itemCount: items.length, + ), + ); + } + + Widget _buildTagItem(String? tag) { + return Padding( + padding: const EdgeInsets.only(left: 4, right: 5, bottom: 9), + child: GestureDetector( + onTap: () => onTagChanged(tag), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + color: primaryColor.withAlpha(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 2.7), + child: Center( + child: Text( + tag == null ? all : '#$tag', + style: TextStyle( + color: initTag == tag ? null : Colors.grey, + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + )), + ), + ); + } +}