diff --git a/lib/app.dart b/lib/app.dart index b75f8b6b..ecbba68a 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -43,7 +43,7 @@ class MyApp extends StatelessWidget { brightness: Brightness.dark, colorSchemeSeed: primaryColor, ), - home: const MyHomePage(), + home: const HomePage(), ); }, ); diff --git a/lib/core/utils/ui.dart b/lib/core/utils/ui.dart index d416bf72..630d8fc2 100644 --- a/lib/core/utils/ui.dart +++ b/lib/core/utils/ui.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:toolbox/core/extension/navigator.dart'; +import 'package:toolbox/data/model/app/tab.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../data/model/server/snippet.dart'; @@ -99,17 +100,15 @@ void setTransparentNavigationBar(BuildContext context) { } } -String tabTitleName(BuildContext context, int i) { +String tabTitleName(BuildContext context, AppTab tab) { final s = S.of(context)!; - switch (i) { - case 0: + switch (tab) { + case AppTab.servers: return s.server; - case 1: + case AppTab.snippet: return s.convert; - case 2: + case AppTab.ping: return 'Ping'; - default: - return ''; } } diff --git a/lib/data/model/app/tab.dart b/lib/data/model/app/tab.dart index 31f2aaa1..a869491d 100644 --- a/lib/data/model/app/tab.dart +++ b/lib/data/model/app/tab.dart @@ -1 +1 @@ -enum AppTab { servers, encode, ping } +enum AppTab { servers, snippet, ping } diff --git a/lib/data/model/server/snippet.dart b/lib/data/model/server/snippet.dart index 20c504a4..36c18573 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) + 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..948c7e5e 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, + tags: (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 e6e32289..f385a8bb 100644 --- a/lib/data/provider/snippet.dart +++ b/lib/data/provider/snippet.dart @@ -15,31 +15,22 @@ class SnippetProvider extends BusyProvider { } void add(Snippet snippet) { - if (have(snippet)) return; _snippets.add(snippet); _store.put(snippet); notifyListeners(); } void del(Snippet snippet) { - if (!have(snippet)) return; - _snippets.removeAt(index(snippet)); + _snippets.remove(snippet); _store.delete(snippet); notifyListeners(); } - int index(Snippet snippet) { - return _snippets.indexWhere((e) => e.name == snippet.name); - } - - bool have(Snippet snippet) { - return index(snippet) != -1; - } - void update(Snippet old, Snippet newOne) { - if (!have(old)) return; - _snippets[index(old)] = newOne; + _store.delete(old); _store.put(newOne); + _snippets.remove(old); + _snippets.add(newOne); notifyListeners(); } diff --git a/lib/view/page/convert.dart b/lib/view/page/convert.dart index bfd5d4e7..4ba39de2 100644 --- a/lib/view/page/convert.dart +++ b/lib/view/page/convert.dart @@ -44,6 +44,9 @@ class _ConvertPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( + appBar: AppBar( + title: Text(_s.convert), + ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 7), controller: ScrollController(), diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 0e9e175a..daf81bfd 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -29,14 +29,14 @@ import 'setting.dart'; import 'sftp/downloaded.dart'; import 'snippet/list.dart'; -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key}) : super(key: key); +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); @override - State createState() => _MyHomePageState(); + State createState() => _HomePageState(); } -class _MyHomePageState extends State +class _HomePageState extends State with AutomaticKeepAliveClientMixin, AfterLayoutMixin, @@ -122,7 +122,7 @@ class _MyHomePageState extends State FocusScope.of(context).requestFocus(FocusNode()); }); }, - children: const [ServerPage(), ConvertPage(), PingPage()], + children: const [ServerPage(), SnippetListPage(), PingPage()], ), bottomNavigationBar: _buildBottomBar(context), ); @@ -151,12 +151,14 @@ class _MyHomePageState extends State selectedIcon: const Icon(Icons.cloud), ), NavigationDestination( - icon: const Icon(Icons.code), - label: _s.convert, + icon: const Icon(Icons.snippet_folder_outlined), + label: _s.snippet, + selectedIcon: const Icon(Icons.snippet_folder), ), const NavigationDestination( - icon: Icon(Icons.leak_add), + icon: Icon(Icons.network_check_outlined), label: 'Ping', + selectedIcon: Icon(Icons.network_check), ), ], ); @@ -221,11 +223,11 @@ class _MyHomePageState extends State ).go(context), ), ListTile( - leading: const Icon(Icons.snippet_folder), - title: Text(_s.snippet), + leading: const Icon(Icons.code), + title: Text(_s.convert), onTap: () => AppRoute( - const SnippetListPage(), - 'snippet list', + const ConvertPage(), + 'convert page', ).go(context), ), ListTile( diff --git a/lib/view/page/setting.dart b/lib/view/page/setting.dart index 4e35355c..c1c4c724 100644 --- a/lib/view/page/setting.dart +++ b/lib/view/page/setting.dart @@ -291,7 +291,7 @@ class _SettingPageState extends State { .map( (e) => PopupMenuItem( value: e.index, - child: Text(tabTitleName(context, e.index)), + child: Text(tabTitleName(context, e)), ), ) .toList(); @@ -316,7 +316,7 @@ class _SettingPageState extends State { child: ConstrainedBox( constraints: BoxConstraints(maxWidth: _media.size.width * 0.35), child: Text( - tabTitleName(context, _launchPageIdx), + tabTitleName(context, AppTab.values[_launchPageIdx]), textAlign: TextAlign.right, style: textSize15, ), diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index 56059e31..87890638 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/view/widget/input_field.dart'; +import 'package:toolbox/view/widget/tag.dart'; import '../../../core/utils/ui.dart'; import '../../../data/model/server/snippet.dart'; @@ -28,6 +29,8 @@ class _SnippetEditPageState extends State late SnippetProvider _provider; late S _s; + var _tags = []; + @override void initState() { super.initState(); @@ -53,7 +56,8 @@ class _SnippetEditPageState extends State context.pop(); }, tooltip: _s.delete, - icon: const Icon(Icons.delete)) + icon: const Icon(Icons.delete), + ) : placeholder ], ), @@ -72,7 +76,7 @@ class _SnippetEditPageState extends State showSnackBar(context, Text(_s.fieldMustNotEmpty)); return; } - final snippet = Snippet(name, script); + final snippet = Snippet(name, script, tags: _tags); if (widget.snippet != null) { _provider.update(widget.snippet!, snippet); } else { @@ -103,6 +107,12 @@ class _SnippetEditPageState extends State label: _s.snippet, icon: Icons.code, ), + TagEditor( + tags: widget.snippet?.tags ?? [], + onChanged: (p0) => setState(() { + _tags = p0; + }), + ) ], ); } @@ -112,6 +122,7 @@ class _SnippetEditPageState extends State if (widget.snippet != null) { _nameController.text = widget.snippet!.name; _scriptController.text = widget.snippet!.script; + _tags = widget.snippet!.tags ?? []; } } } diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index b4d44da4..141db7c9 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:provider/provider.dart'; -import '../../../data/res/ui.dart'; import '/core/route.dart'; import '/data/provider/snippet.dart'; import 'edit.dart'; @@ -27,9 +26,6 @@ class _SnippetListPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(_s.snippet, style: textSize18), - ), body: _buildBody(), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), @@ -41,8 +37,8 @@ class _SnippetListPageState extends State { Widget _buildBody() { return Consumer( - builder: (_, key, __) { - if (key.snippets.isEmpty) { + builder: (_, provider, __) { + if (provider.snippets.isEmpty) { return Center( child: Text(_s.noSavedSnippet), ); @@ -50,19 +46,22 @@ class _SnippetListPageState extends State { return ListView.builder( padding: const EdgeInsets.all(13), - itemCount: key.snippets.length, + itemCount: provider.snippets.length, itemBuilder: (context, idx) { return RoundRectCard( ListTile( + contentPadding: const EdgeInsets.only(left: 23, right: 17), title: Text( - key.snippets[idx].name, + provider.snippets[idx].name, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - trailing: TextButton( + trailing: IconButton( onPressed: () => AppRoute( - SnippetEditPage(snippet: key.snippets[idx]), + SnippetEditPage(snippet: provider.snippets[idx]), 'snippet edit page') .go(context), - child: Text(_s.edit), + icon: const Icon(Icons.edit), ), ), ); diff --git a/lib/view/widget/input_field.dart b/lib/view/widget/input_field.dart index 46ee8b7f..29fb9357 100644 --- a/lib/view/widget/input_field.dart +++ b/lib/view/widget/input_field.dart @@ -9,12 +9,15 @@ class Input extends StatelessWidget { final String? hint; final String? label; final void Function(String)? onSubmitted; + final void Function(String)? onChanged; final bool obscureText; final IconData? icon; final TextInputType? type; final FocusNode? node; final bool autoCorrect; final bool suggestiion; + final String? errorText; + final Widget? prefix; const Input({ super.key, @@ -24,12 +27,15 @@ class Input extends StatelessWidget { this.hint, this.label, this.onSubmitted, + this.onChanged, this.obscureText = false, this.icon, this.type, this.node, this.autoCorrect = false, this.suggestiion = false, + this.errorText, + this.prefix, }); @override Widget build(BuildContext context) { @@ -40,16 +46,18 @@ class Input extends StatelessWidget { maxLines: maxLines, minLines: minLines, onSubmitted: onSubmitted, + onChanged: onChanged, keyboardType: type, focusNode: node, autocorrect: autoCorrect, enableSuggestions: suggestiion, decoration: InputDecoration( - label: label != null ? Text(label!) : null, - hintText: hint, - icon: icon != null ? Icon(icon) : null, - border: InputBorder.none, - ), + label: label != null ? Text(label!) : null, + hintText: hint, + icon: icon != null ? Icon(icon) : null, + border: InputBorder.none, + errorText: errorText, + prefix: prefix), controller: controller, obscureText: obscureText, ), diff --git a/lib/view/widget/tag.dart b/lib/view/widget/tag.dart new file mode 100644 index 00000000..7ae43f64 --- /dev/null +++ b/lib/view/widget/tag.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:toolbox/view/widget/input_field.dart'; +import 'package:toolbox/view/widget/round_rect_card.dart'; + +import '../../data/res/color.dart'; + +class TagEditor extends StatelessWidget { + final List tags; + final void Function(List)? onChanged; + + const TagEditor({super.key, required this.tags, this.onChanged}); + + @override + Widget build(BuildContext context) { + return RoundRectCard(ListTile( + leading: const Icon(Icons.tag), + title: _buildTags( + tags, + _onTapDelete, + ), + trailing: InkWell( + child: const Icon(Icons.add), + onTap: () { + _showTagDialog(context, tags, onChanged); + }, + ), + )); + } + + void _onTapDelete(String tag) { + tags.remove(tag); + onChanged?.call(tags); + } + + Widget _buildTags( + List tags, + Function(String) onTagDelete, + ) { + if (tags.isEmpty) return Text('Tags'); + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: tags.map((e) => _buildTagItem(e, onTagDelete)).toList(), + ), + ); + } + + Widget _buildTagItem(String tag, Function(String) onTagDelete) { + return Padding( + padding: EdgeInsets.only(right: 7), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(20.0), + ), + color: primaryColor, + ), + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '#$tag', + style: const TextStyle(color: Colors.white), + ), + const SizedBox(width: 4.0), + InkWell( + child: const Icon( + Icons.cancel, + size: 14.0, + color: Colors.white, + ), + onTap: () { + onTagDelete(tag); + }, + ) + ], + ), + ), + ); + } + + void _showTagDialog( + BuildContext context, + List tags, + void Function(List)? onChanged, + ) { + final _textEditingController = TextEditingController(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Add Tag'), + content: Input( + controller: _textEditingController, + hint: 'Tag', + ), + actions: [ + TextButton( + onPressed: () { + final tag = _textEditingController.text; + tags.add(tag.trim()); + onChanged?.call(tags); + Navigator.pop(context); + }, + child: const Text('Add'), + ), + ], + ); + }, + ); + } +}