#54 snippet group

This commit is contained in:
lollipopkit
2023-07-07 20:37:50 +08:00
parent d8155e7771
commit bb50fbc589
12 changed files with 223 additions and 82 deletions

View File

@@ -1,5 +1,5 @@
PODS: PODS:
- countly_flutter (22.09.0): - countly_flutter (23.6.0):
- Flutter - Flutter
- file_picker (0.0.1): - file_picker (0.0.1):
- Flutter - Flutter
@@ -55,15 +55,15 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
countly_flutter: 135f1a4930f8e26ba223a14201d3f265ea7b4c83 countly_flutter: 4eeee607183664b871589250a0bd049cfd2697eb
file_picker: 1d63c4949e05e386da864365f8c13e1e64787675 file_picker: 1d63c4949e05e386da864365f8c13e1e64787675
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1 plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114 r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: 7fb15c416f8685fca4966867a8da218ec592ec2e PODFILE CHECKSUM: 7fb15c416f8685fca4966867a8da218ec592ec2e

View File

@@ -1,9 +1,15 @@
import 'package:toolbox/core/persistant_store.dart'; import 'package:toolbox/core/persistant_store.dart';
typedef Order<T> = List<T>; typedef Order<T> = List<T>;
typedef _OnMove<T> = void Function(Order<T>);
extension OrderX<T> on Order<T> { extension OrderX<T> on Order<T> {
void move(int oldIndex, int newIndex, StoreProperty<List<T>> property) { void move(
int oldIndex,
int newIndex, {
StoreProperty<List<T>>? property,
_OnMove<T>? onMove,
}) {
if (oldIndex == newIndex) return; if (oldIndex == newIndex) return;
if (oldIndex < newIndex) { if (oldIndex < newIndex) {
newIndex -= 1; newIndex -= 1;
@@ -11,7 +17,8 @@ extension OrderX<T> on Order<T> {
final item = this[oldIndex]; final item = this[oldIndex];
removeAt(oldIndex); removeAt(oldIndex);
insert(newIndex, item); insert(newIndex, item);
property.put(this); property?.put(this);
onMove?.call(this);
} }
void update(T id, T newId) { void update(T id, T newId) {
@@ -24,11 +31,16 @@ extension OrderX<T> on Order<T> {
return indexOf(id); return indexOf(id);
} }
void moveById(T oid, T nid, StoreProperty<List<T>> property) { void moveById(
T oid,
T nid, {
StoreProperty<List<T>>? property,
_OnMove<T>? onMove,
}) {
final index = indexOf(oid); final index = indexOf(oid);
if (index == -1) return; if (index == -1) return;
final newIndex = indexOf(nid); final newIndex = indexOf(nid);
if (newIndex == -1) return; if (newIndex == -1) return;
move(index, newIndex, property); move(index, newIndex, property: property, onMove: onMove);
} }
} }

View File

@@ -8,16 +8,20 @@ class Snippet {
late String name; late String name;
@HiveField(1) @HiveField(1)
late String script; late String script;
Snippet(this.name, this.script); @HiveField(2)
late List<String>? tags;
Snippet(this.name, this.script, this.tags);
Snippet.fromJson(Map<String, dynamic> json) { Snippet.fromJson(Map<String, dynamic> json) {
name = json['name'].toString(); name = json['name'].toString();
script = json['script'].toString(); script = json['script'].toString();
tags = json['tags']?.cast<String>();
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, dynamic>{}; final data = <String, dynamic>{};
data['name'] = name; data['name'] = name;
data['script'] = script; data['script'] = script;
data['tags'] = tags;
return data; return data;
} }
} }

View File

@@ -19,17 +19,20 @@ class SnippetAdapter extends TypeAdapter<Snippet> {
return Snippet( return Snippet(
fields[0] as String, fields[0] as String,
fields[1] as String, fields[1] as String,
(fields[2] as List?)?.cast<String>(),
); );
} }
@override @override
void write(BinaryWriter writer, Snippet obj) { void write(BinaryWriter writer, Snippet obj) {
writer writer
..writeByte(2) ..writeByte(3)
..writeByte(0) ..writeByte(0)
..write(obj.name) ..write(obj.name)
..writeByte(1) ..writeByte(1)
..write(obj.script); ..write(obj.script)
..writeByte(2)
..write(obj.tags);
} }
@override @override

View File

@@ -5,25 +5,44 @@ import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/store/snippet.dart'; import 'package:toolbox/data/store/snippet.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import '../../core/extension/order.dart';
class SnippetProvider extends BusyProvider { class SnippetProvider extends BusyProvider {
List<Snippet> get snippets => _snippets; late Order<Snippet> _snippets;
Order<Snippet> get snippets => _snippets;
final _tags = <String>[];
List<String> get tags => _tags;
final _store = locator<SnippetStore>(); final _store = locator<SnippetStore>();
late List<Snippet> _snippets;
void loadData() { void loadData() {
_snippets = _store.fetch(); _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) { void add(Snippet snippet) {
_snippets.add(snippet); _snippets.add(snippet);
_store.put(snippet); _store.put(snippet);
notifyListeners(); notifyListeners();
_updateTags();
} }
void del(Snippet snippet) { void del(Snippet snippet) {
_snippets.remove(snippet); _snippets.remove(snippet);
_store.delete(snippet); _store.delete(snippet);
notifyListeners(); notifyListeners();
_updateTags();
} }
void update(Snippet old, Snippet newOne) { void update(Snippet old, Snippet newOne) {
@@ -32,6 +51,21 @@ class SnippetProvider extends BusyProvider {
_snippets.remove(old); _snippets.remove(old);
_snippets.add(newOne); _snippets.add(newOne);
notifyListeners(); 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); String get export => json.encode(snippets);

View File

@@ -46,6 +46,11 @@ class SettingStore extends PersistentStore {
StoreProperty<List<String>> get serverOrder => StoreProperty<List<String>> get serverOrder =>
property('serverOrder', defaultValue: null); property('serverOrder', defaultValue: null);
StoreProperty<List<String>> get snippetOrder => property(
'snippetOrder',
defaultValue: null,
);
// Server details page cards order // Server details page cards order
StoreProperty<List<String>> get detailCardOrder => StoreProperty<List<String>> get detailCardOrder =>
property('detailCardPrder', defaultValue: defaultDetailCardOrder); property('detailCardPrder', defaultValue: defaultDetailCardOrder);

View File

@@ -74,7 +74,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
left: 13, right: 13, top: 13, bottom: _media.padding.bottom), left: 13, right: 13, top: 13, bottom: _media.padding.bottom),
onReorder: (int oldIndex, int newIndex) { onReorder: (int oldIndex, int newIndex) {
setState(() { setState(() {
_cardsOrder.move(oldIndex, newIndex, _setting.detailCardOrder); _cardsOrder.move(
oldIndex,
newIndex,
property: _setting.detailCardOrder,
);
}); });
}, },
footer: height13, footer: height13,

View File

@@ -9,6 +9,7 @@ import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/core/utils/misc.dart'; import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/view/page/process.dart'; import 'package:toolbox/view/page/process.dart';
import 'package:toolbox/view/widget/tag_switcher.dart';
import '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../core/utils/ui.dart'; import '../../../core/utils/ui.dart';
@@ -103,13 +104,21 @@ class _ServerPageState extends State<ServerPage>
(pro.servers[e]?.spi.tags?.contains(_tag) ?? false)) (pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
.toList(); .toList();
return ReorderableListView.builder( 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), padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
onReorder: (oldIndex, newIndex) => setState(() { onReorder: (oldIndex, newIndex) => setState(() {
pro.serverOrder.moveById( pro.serverOrder.moveById(
filtered[oldIndex], filtered[oldIndex],
filtered[newIndex], filtered[newIndex],
_settingStore.serverOrder, property: _settingStore.serverOrder,
); );
}), }),
itemBuilder: (_, index) => _buildEachServerCard( itemBuilder: (_, index) => _buildEachServerCard(
@@ -122,51 +131,6 @@ class _ServerPageState extends State<ServerPage>
); );
} }
Widget _buildTagsSwitcher(List<String> tags) {
if (tags.isEmpty) return nil;
final items = <String?>[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) { Widget _buildEachServerCard(Server? si) {
if (si == null) { if (si == null) {
return nil; return nil;

View File

@@ -1,7 +1,6 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:nil/nil.dart';
import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/view/widget/input_field.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/provider/snippet.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../../locator.dart'; import '../../../locator.dart';
import '../../widget/tag_editor.dart';
class SnippetEditPage extends StatefulWidget { class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key, this.snippet}) : super(key: key); const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
@@ -29,6 +29,8 @@ class _SnippetEditPageState extends State<SnippetEditPage>
late SnippetProvider _provider; late SnippetProvider _provider;
late S _s; late S _s;
List<String> _tags = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -46,9 +48,19 @@ class _SnippetEditPageState extends State<SnippetEditPage>
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(_s.edit, style: textSize18), title: Text(_s.edit, style: textSize18),
actions: [ actions: _buildAppBarActions(),
widget.snippet != null ),
? IconButton( body: _buildBody(),
floatingActionButton: _buildFAB(),
);
}
List<Widget>? _buildAppBarActions() {
if (widget.snippet == null) {
return null;
}
return [
IconButton(
onPressed: () { onPressed: () {
_provider.del(widget.snippet!); _provider.del(widget.snippet!);
context.pop(); context.pop();
@@ -56,12 +68,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
tooltip: _s.delete, tooltip: _s.delete,
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
) )
: nil ];
],
),
body: _buildBody(),
floatingActionButton: _buildFAB(),
);
} }
Widget _buildFAB() { Widget _buildFAB() {
@@ -75,7 +82,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
showSnackBar(context, Text(_s.fieldMustNotEmpty)); showSnackBar(context, Text(_s.fieldMustNotEmpty));
return; return;
} }
final snippet = Snippet(name, script); final snippet = Snippet(name, script, _tags);
if (widget.snippet != null) { if (widget.snippet != null) {
_provider.update(widget.snippet!, snippet); _provider.update(widget.snippet!, snippet);
} else { } else {
@@ -106,6 +113,15 @@ class _SnippetEditPageState extends State<SnippetEditPage>
label: _s.snippet, label: _s.snippet,
icon: Icons.code, icon: Icons.code,
), ),
TagEditor(
tags: _tags,
onChanged: (p0) => setState(() {
_tags = p0;
}),
s: _s,
tagSuggestions: [..._provider.tags],
onRenameTag: _provider.renameTag,
)
], ],
); );
} }

View File

@@ -1,7 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.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 '/core/route.dart';
import '/data/provider/snippet.dart'; import '/data/provider/snippet.dart';
import 'edit.dart'; import 'edit.dart';
@@ -16,11 +20,17 @@ class SnippetListPage extends StatefulWidget {
class _SnippetListPageState extends State<SnippetListPage> { class _SnippetListPageState extends State<SnippetListPage> {
late S _s; late S _s;
late MediaQueryData _media;
final _settingStore = locator<SettingStore>();
String? _tag;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_s = S.of(context)!; _s = S.of(context)!;
_media = MediaQuery.of(context);
} }
@override @override
@@ -47,26 +57,48 @@ class _SnippetListPageState extends State<SnippetListPage> {
); );
} }
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), 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) { itemBuilder: (context, idx) {
final snippet = filtered[idx];
return RoundRectCard( return RoundRectCard(
ListTile( ListTile(
contentPadding: const EdgeInsets.only(left: 23, right: 17), contentPadding: const EdgeInsets.only(left: 23, right: 17),
title: Text( title: Text(
provider.snippets[idx].name, snippet.name,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
), ),
trailing: IconButton( trailing: IconButton(
onPressed: () => AppRoute( onPressed: () => AppRoute(
SnippetEditPage(snippet: provider.snippets[idx]), SnippetEditPage(snippet: snippet),
'snippet edit page') 'snippet edit page',
.go(context), ).go(context),
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
), ),
), ),
key: ValueKey(snippet.name),
); );
}, },
); );

View File

@@ -58,7 +58,7 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
showSnackBar(context, Text(_s.disabled)); showSnackBar(context, Text(_s.disabled));
return; return;
} }
keys.moveById(keys[o], keys[n], _setting.sshVirtKeys); keys.moveById(keys[o], keys[n], property: _setting.sshVirtKeys);
setState(() {}); setState(() {});
}, },
); );

View File

@@ -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<String> 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<String> tags) {
if (tags.isEmpty) return nil;
final items = <String?>[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,
),
),
)),
),
);
}
}