diff --git a/lib/data/store/history.dart b/lib/data/store/history.dart index 6d2d708c..77ee4e80 100644 --- a/lib/data/store/history.dart +++ b/lib/data/store/history.dart @@ -19,6 +19,11 @@ class _ListHistory { } List get all => _history; + + void clear() { + _history.clear(); + _box.put(_name, _history); + } } class _MapHistory { @@ -44,13 +49,13 @@ class HistoryStore extends HiveStore { static final instance = HistoryStore._(); - /// Paths that user has visited by 'Locate' button late final sftpGoPath = _ListHistory(box: box, name: 'sftpPath'); late final sftpLastPath = _MapHistory(box: box, name: 'sftpLastPath'); late final sshCmds = _ListHistory(box: box, name: 'sshCmds'); - /// Notify users that this app will write script to server to works properly + late final sshServerHistory = _ListHistory(box: box, name: 'sshServerHistory'); + late final writeScriptTipShown = propertyDefault('writeScriptTipShown', false); } diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 968e91d7..612dfa86 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -282,4 +282,7 @@ class SettingStore extends HiveStore { /// Hide port forward beta warning late final portForwardBetaWarned = propertyDefault('portForwardBetaWarned', false); + + late final sshPageSortBy = propertyDefault('sshPageSortBy', 0); + late final sshPageSortAsc = propertyDefault('sshPageSortAsc', true); } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 1a804470..703d5ab6 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1709,6 +1709,54 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Sponsor'** String get sponsor; + + /// No description provided for @sort. + /// + /// In en, this message translates to: + /// **'Sort'** + String get sort; + + /// No description provided for @sortByName. + /// + /// In en, this message translates to: + /// **'By name'** + String get sortByName; + + /// No description provided for @sortByJoinTime. + /// + /// In en, this message translates to: + /// **'By join time'** + String get sortByJoinTime; + + /// No description provided for @ascending. + /// + /// In en, this message translates to: + /// **'Ascending'** + String get ascending; + + /// No description provided for @descending. + /// + /// In en, this message translates to: + /// **'Descending'** + String get descending; + + /// No description provided for @searchServer. + /// + /// In en, this message translates to: + /// **'Search server'** + String get searchServer; + + /// No description provided for @serverHistory. + /// + /// In en, this message translates to: + /// **'Server history'** + String get serverHistory; + + /// No description provided for @clearHistory. + /// + /// In en, this message translates to: + /// **'Clear history'** + String get clearHistory; } class _AppLocalizationsDelegate diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index df99a9b7..c3702b9f 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -929,4 +929,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get sponsor => 'Sponsor'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 666aa363..f9aca603 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -920,4 +920,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sponsor => 'Sponsor'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index e94d853d..f98ec1c3 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -931,4 +931,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get sponsor => 'Patrocinador'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index bfdbce2b..c0f72bb5 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -934,4 +934,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get sponsor => 'Soutenir'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 2b8f161e..db87f23b 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -920,4 +920,28 @@ class AppLocalizationsId extends AppLocalizations { @override String get sponsor => 'Sponsor'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index cb962e56..dbb2d0d5 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -926,4 +926,28 @@ class AppLocalizationsIt extends AppLocalizations { @override String get sponsor => 'Sponsor'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index f3924745..4bc381d8 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -890,4 +890,28 @@ class AppLocalizationsJa extends AppLocalizations { @override String get sponsor => '赞助'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index b09ad0a9..8effb15b 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -889,4 +889,28 @@ class AppLocalizationsKo extends AppLocalizations { @override String get sponsor => '후원'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 33916c5c..2b9cf240 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -927,4 +927,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get sponsor => 'Sponsor'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 8c6add83..4e3f2f14 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -922,4 +922,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get sponsor => 'Patrocinador'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index d56c5953..40c42fe2 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -926,4 +926,28 @@ class AppLocalizationsRu extends AppLocalizations { @override String get sponsor => 'Спонсор'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index beed936c..d0c44a06 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -921,4 +921,28 @@ class AppLocalizationsTr extends AppLocalizations { @override String get sponsor => 'Sponsor'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 519384ac..9601e5b1 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -926,4 +926,28 @@ class AppLocalizationsUk extends AppLocalizations { @override String get sponsor => 'Спонсор'; + + @override + String get sort => 'Sort'; + + @override + String get sortByName => 'By name'; + + @override + String get sortByJoinTime => 'By join time'; + + @override + String get ascending => 'Ascending'; + + @override + String get descending => 'Descending'; + + @override + String get searchServer => 'Search server'; + + @override + String get serverHistory => 'Server history'; + + @override + String get clearHistory => 'Clear history'; } diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index d09eda69..22f20c3b 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -868,6 +868,30 @@ class AppLocalizationsZh extends AppLocalizations { @override String get sponsor => '赞助'; + + @override + String get sort => '排序'; + + @override + String get sortByName => '按名称'; + + @override + String get sortByJoinTime => '按加入时间'; + + @override + String get ascending => '升序'; + + @override + String get descending => '降序'; + + @override + String get searchServer => '搜索服务器'; + + @override + String get serverHistory => '服务器历史'; + + @override + String get clearHistory => '清空历史'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5f0dcfe5..aef32ceb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -266,5 +266,13 @@ "portForward_type_local": "Local", "portForward_type_remote": "Remote", "portForward_deleteConfirmFmt": "Delete {name}?", - "sponsor": "Sponsor" + "sponsor": "Sponsor", + "sort": "Sort", + "sortByName": "By name", + "sortByJoinTime": "By join time", + "ascending": "Ascending", + "descending": "Descending", + "searchServer": "Search server", + "serverHistory": "Server history", + "clearHistory": "Clear history" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index e5d199da..540bd380 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -263,5 +263,13 @@ "portForward_type_local": "本地", "portForward_type_remote": "远程", "portForward_deleteConfirmFmt": "删除 {name}?", - "sponsor": "赞助" + "sponsor": "赞助", + "sort": "排序", + "sortByName": "按名称", + "sortByJoinTime": "按加入时间", + "ascending": "升序", + "descending": "降序", + "searchServer": "搜索服务器", + "serverHistory": "服务器历史", + "clearHistory": "清空历史" } diff --git a/lib/view/page/ssh/tab.dart b/lib/view/page/ssh/tab.dart index ab7c64e4..0b5788b0 100644 --- a/lib/view/page/ssh/tab.dart +++ b/lib/view/page/ssh/tab.dart @@ -5,8 +5,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; +import 'package:server_box/core/extension/context/locale.dart'; +import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/provider/server/all.dart'; +import 'package:server_box/data/res/store.dart'; import 'package:server_box/view/page/server/edit/edit.dart'; import 'package:server_box/view/page/ssh/page/page.dart'; @@ -23,10 +26,11 @@ typedef _TabMap = Map; class _SSHTabPageState extends ConsumerState with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)}; + late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(sortVersionVN: _sortVersionVN, onTapInitCard: _onTapInitCard, onLongPressInitCard: _onLongPressInitCard), focus: null)}; final _pageCtrl = PageController(); final _fabVN = 0.vn; final _tabRN = RNode(); + final _sortVersionVN = 0.vn; @override void dispose() { @@ -34,6 +38,7 @@ class _SSHTabPageState extends ConsumerState _pageCtrl.dispose(); _tabRN.dispose(); _fabVN.dispose(); + _sortVersionVN.dispose(); } @override @@ -43,7 +48,15 @@ class _SSHTabPageState extends ConsumerState appBar: PreferredSizeListenBuilder( listenable: _tabRN, builder: () { - return _TabBar(idxVN: _fabVN, map: _tabMap, onTap: _onTapTab, onClose: _onTapClose); + return _TabBar( + idxVN: _fabVN, + map: _tabMap, + onTap: _onTapTab, + onClose: _onTapClose, + sortBtn: buildSortBtn(context), + searchBtn: buildSearchBtn(context), + historyBtn: buildHistoryBtn(context), + ); }, ), body: _buildBody(), @@ -115,12 +128,17 @@ extension on _SSHTabPageState { focus: FocusNode(), ); _tabRN.notify(); + Stores.history.sshServerHistory.add(spi.id); // Wait for the page to be built await Future.delayed(Durations.short3); final idx = _tabMap.keys.toList().indexOf(name); await _toPage(idx); } + void _onLongPressInitCard(Spi spi) { + ServerEditPage.route.go(context, args: SpiRequiredArgs(spi)); + } + Future _toPage(int idx) async { await _pageCtrl.animateToPage(idx, duration: Durations.short3, curve: Curves.fastEaseInToSlowEaseOut); final focus = _tabMap.values.elementAt(idx).focus; @@ -146,15 +164,197 @@ extension on _SSHTabPageState { _tabRN.notify(); _pageCtrl.previousPage(duration: Durations.medium1, curve: Curves.fastEaseInToSlowEaseOut); } + + Widget buildSortBtn(BuildContext context) { + final sortBy = Stores.setting.sshPageSortBy.fetch(); + final sortAsc = Stores.setting.sshPageSortAsc.fetch(); + final sortIcon = sortBy == 0 + ? (sortAsc ? Icons.sort_by_alpha : Icons.sort) + : (sortAsc ? Icons.arrow_upward : Icons.arrow_downward); + + return Btn.icon( + icon: Icon(sortIcon, size: 18), + onTap: () => showSortMenu(context), + ); + } + + void showSortMenu(BuildContext context) { + final sortBy = Stores.setting.sshPageSortBy.fetch(); + final sortAsc = Stores.setting.sshPageSortAsc.fetch(); + + context.showRoundDialog( + title: l10n.sort, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _SortOption( + icon: Icons.sort_by_alpha, + label: '${l10n.sortByName} (A-Z)', + selected: sortBy == 0 && sortAsc, + onTap: () { + Stores.setting.sshPageSortBy.put(0); + Stores.setting.sshPageSortAsc.put(true); + _tabRN.notify(); + _sortVersionVN.notify(); + context.pop(); + }, + ), + _SortOption( + icon: Icons.sort, + label: '${l10n.sortByName} (Z-A)', + selected: sortBy == 0 && !sortAsc, + onTap: () { + Stores.setting.sshPageSortBy.put(0); + Stores.setting.sshPageSortAsc.put(false); + _tabRN.notify(); + _sortVersionVN.notify(); + context.pop(); + }, + ), + _SortOption( + icon: Icons.arrow_upward, + label: '${l10n.sortByJoinTime} (${l10n.ascending})', + selected: sortBy == 1 && sortAsc, + onTap: () { + Stores.setting.sshPageSortBy.put(1); + Stores.setting.sshPageSortAsc.put(true); + _tabRN.notify(); + _sortVersionVN.notify(); + context.pop(); + }, + ), + _SortOption( + icon: Icons.arrow_downward, + label: '${l10n.sortByJoinTime} (${l10n.descending})', + selected: sortBy == 1 && !sortAsc, + onTap: () { + Stores.setting.sshPageSortBy.put(1); + Stores.setting.sshPageSortAsc.put(false); + _tabRN.notify(); + _sortVersionVN.notify(); + context.pop(); + }, + ), + ], + ), + ); + } + + Widget buildSearchBtn(BuildContext context) { + return Btn.icon( + icon: const Icon(Icons.search, size: 18), + onTap: () => showSearchDialog(context), + ); + } + + void showSearchDialog(BuildContext context) { + final serverState = ref.read(serversProvider); + final allServers = serverState.serverOrder + .map((id) => serverState.servers[id]) + .whereType() + .toList(); + + showSearch( + context: context, + delegate: SearchPage( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + future: (q) async { + if (q.isEmpty) return []; + return allServers.where((spi) => + spi.name.toLowerCase().contains(q.toLowerCase()) || + spi.user.toLowerCase().contains(q.toLowerCase()) || + spi.ip.contains(q) + ).toList(); + }, + builder: (ctx, spi) => ListTile( + title: Text(spi.name), + subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + ctx.pop(); + _onTapInitCard(spi); + }, + ), + ), + ); + } + + Widget buildHistoryBtn(BuildContext context) { + return Btn.icon( + icon: const Icon(Icons.history, size: 18), + onTap: () => showHistoryDialog(context), + ); + } + + void showHistoryDialog(BuildContext context) { + final history = Stores.history.sshServerHistory.all.cast(); + if (history.isEmpty) { + context.showRoundDialog( + title: l10n.serverHistory, + child: Text(libL10n.empty), + actions: [Btn.ok(onTap: context.pop)], + ); + return; + } + + final serverState = ref.read(serversProvider); + context.showRoundDialog( + title: l10n.serverHistory, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: history.length, + itemBuilder: (_, idx) { + final id = history[idx]; + final spi = serverState.servers[id]; + return ListTile( + title: Text(spi?.name ?? id), + subtitle: spi != null ? Text('${spi.user}@${spi.ip}:${spi.port}') : null, + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.pop(); + if (spi != null) { + _onTapInitCard(spi); + } else { + context.showSnackBar(libL10n.error); + } + }, + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Stores.history.sshServerHistory.clear(); + context.pop(); + }, + child: Text(l10n.clearHistory), + ), + Btn.ok(onTap: context.pop), + ], + ); + } } final class _TabBar extends StatelessWidget implements PreferredSizeWidget { - const _TabBar({required this.idxVN, required this.map, required this.onTap, required this.onClose}); + const _TabBar({ + required this.idxVN, + required this.map, + required this.onTap, + required this.onClose, + required this.sortBtn, + required this.searchBtn, + required this.historyBtn, + }); final ValueListenable idxVN; final _TabMap map; final void Function(int idx) onTap; final void Function(String name) onClose; + final Widget sortBtn; + final Widget searchBtn; + final Widget historyBtn; List get names => map.keys.toList(); @@ -166,15 +366,38 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget { return ListenBuilder( listenable: idxVN, builder: () { - return ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 5), - itemCount: names.length, - itemBuilder: (_, idx) => _buildItem(idx), - separatorBuilder: (_, _) => Padding( - padding: const EdgeInsets.symmetric(vertical: 17), - child: Container(color: const Color.fromARGB(61, 158, 158, 158), width: 3), - ), + return Row( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 5), + itemCount: names.length, + itemBuilder: (_, idx) => _buildItem(idx), + separatorBuilder: (_, _) => Padding( + padding: const EdgeInsets.symmetric(vertical: 17), + child: Container(color: Theme.of(context).dividerColor.withAlpha(61), width: 3), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 17), + child: Container(color: Theme.of(context).dividerColor.withAlpha(61), width: 3), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 7), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + sortBtn, + const SizedBox(width: 7), + searchBtn, + const SizedBox(width: 7), + historyBtn, + ], + ), + ), + ], ); }, ); @@ -236,20 +459,65 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget { } } -class _AddPage extends ConsumerWidget { - const _AddPage({required this.onTapInitCard}); +class _AddPage extends ConsumerStatefulWidget { + const _AddPage({required this.sortVersionVN, required this.onTapInitCard, required this.onLongPressInitCard}); + final ValueListenable sortVersionVN; final void Function(Spi spi) onTapInitCard; + final void Function(Spi spi) onLongPressInitCard; + + @override + ConsumerState<_AddPage> createState() => _AddPageState(); +} + +class _AddPageState extends ConsumerState<_AddPage> { + @override + void initState() { + super.initState(); + widget.sortVersionVN.addListener(_onSortVersionChanged); + } + + @override + void dispose() { + widget.sortVersionVN.removeListener(_onSortVersionChanged); + super.dispose(); + } + + void _onSortVersionChanged() { + if (mounted) setState(() {}); + } Widget get _placeholder => const Expanded(child: UIs.placeholder); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { const viewPadding = 7.0; final viewWidth = context.windowSize.width - 2 * viewPadding; final serverState = ref.watch(serversProvider); - final itemCount = serverState.servers.length; + final sortBy = Stores.setting.sshPageSortBy.fetch(); + final sortAsc = Stores.setting.sshPageSortAsc.fetch(); + + final order = serverState.serverOrder.toList(); + if (sortBy == 0) { + order.sort((a, b) { + final nameA = serverState.servers[a]?.name ?? ''; + final nameB = serverState.servers[b]?.name ?? ''; + return sortAsc ? nameA.compareTo(nameB) : nameB.compareTo(nameA); + }); + } else if (sortBy == 1) { + final indexMap = {}; + for (var i = 0; i < serverState.serverOrder.length; i++) { + indexMap[serverState.serverOrder[i]] = i; + } + order.sort((a, b) { + final idxA = indexMap[a] ?? -1; + final idxB = indexMap[b] ?? -1; + return sortAsc ? idxA.compareTo(idxB) : idxB.compareTo(idxA); + }); + } + + final itemCount = order.length; const itemPadding = 1.0; const itemWidth = 150.0; const itemHeight = 50.0; @@ -258,13 +526,10 @@ class _AddPage extends ConsumerWidget { final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1); final mainCount = itemCount ~/ crossCount + 1; - final order = serverState.serverOrder; - if (order.isEmpty) { return Center(child: Text(libL10n.empty, textAlign: TextAlign.center)); } - // Custom grid return ListView( padding: const EdgeInsets.all(viewPadding), children: List.generate( @@ -280,7 +545,8 @@ class _AddPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(itemPadding), child: InkWell( - onTap: () => onTapInitCard(spi), + onTap: () => widget.onTapInitCard(spi), + onLongPress: () => widget.onLongPressInitCard(spi), child: Container( height: itemHeight, alignment: Alignment.centerLeft, @@ -308,3 +574,27 @@ class _AddPage extends ConsumerWidget { ); } } + +class _SortOption extends StatelessWidget { + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + + const _SortOption({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final primaryColor = Theme.of(context).colorScheme.primary; + return ListTile( + leading: Icon(icon, color: selected ? primaryColor : null), + title: Text(label, style: TextStyle(color: selected ? primaryColor : null)), + onTap: onTap, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a1e08dbd..a2e2d02e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -902,10 +902,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -1490,26 +1490,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.15" tuple: dependency: transitive description: