diff --git a/lib/data/model/app/tab.dart b/lib/data/model/app/tab.dart index c63adb6e3..1e948afab 100644 --- a/lib/data/model/app/tab.dart +++ b/lib/data/model/app/tab.dart @@ -1,12 +1,4 @@ -import 'package:fl_lib/fl_lib.dart'; -import 'package:flutter/material.dart'; import 'package:hive_ce_flutter/adapters.dart'; -import 'package:icons_plus/icons_plus.dart'; -import 'package:server_box/view/page/server/tab/tab.dart'; -// import 'package:server_box/view/page/setting/entry.dart'; -import 'package:server_box/view/page/snippet/list.dart'; -import 'package:server_box/view/page/ssh/tab.dart'; -import 'package:server_box/view/page/storage/local.dart'; part 'tab.g.dart'; @@ -19,89 +11,7 @@ enum AppTab { @HiveField(2) file, @HiveField(3) - snippet - //settings, - ; - - Widget get page { - return switch (this) { - server => const ServerPage(), - //settings => const SettingsPage(), - ssh => const SSHTabPage(), - file => const LocalFilePage(), - snippet => const SnippetListPage(), - }; - } - - NavigationDestination get navDestination { - return switch (this) { - server => NavigationDestination( - icon: const Icon(BoxIcons.bx_server), - label: libL10n.server, - selectedIcon: const Icon(BoxIcons.bxs_server), - ), - // settings => NavigationDestination( - // icon: const Icon(Icons.settings), - // label: libL10n.setting, - // selectedIcon: const Icon(Icons.settings), - // ), - ssh => const NavigationDestination( - icon: Icon(Icons.terminal_outlined), - label: 'SSH', - selectedIcon: Icon(Icons.terminal), - ), - snippet => NavigationDestination( - icon: const Icon(Icons.code), - label: libL10n.snippet, - selectedIcon: const Icon(Icons.code), - ), - file => NavigationDestination( - icon: const Icon(Icons.folder_open), - label: libL10n.file, - selectedIcon: const Icon(Icons.folder), - ), - }; - } - - NavigationRailDestination get navRailDestination { - return switch (this) { - server => NavigationRailDestination( - icon: const Icon(BoxIcons.bx_server), - label: Text(libL10n.server), - selectedIcon: const Icon(BoxIcons.bxs_server), - ), - // settings => NavigationRailDestination( - // icon: const Icon(Icons.settings), - // label: libL10n.setting, - // selectedIcon: const Icon(Icons.settings), - // ), - ssh => const NavigationRailDestination( - icon: Icon(Icons.terminal_outlined), - label: Text('SSH'), - selectedIcon: Icon(Icons.terminal), - ), - snippet => NavigationRailDestination( - icon: const Icon(Icons.code), - label: Text(libL10n.snippet), - selectedIcon: const Icon(Icons.code), - ), - file => NavigationRailDestination( - icon: const Icon(Icons.folder_open), - label: Text(libL10n.file), - selectedIcon: const Icon(Icons.folder), - ), - }; - } - - static List get navDestinations { - return AppTab.values.map((e) => e.navDestination).toList(); - } - - static List get navRailDestinations { - return AppTab.values.map((e) => e.navRailDestination).toList(); - } - - + snippet; /// Helper function to parse AppTab list from stored object static List parseAppTabsFromObj(dynamic val) { @@ -123,7 +33,9 @@ enum AppTab { if (e is AppTab) { return e; } else if (e is String) { - return AppTab.values.firstWhereOrNull((t) => t.name == e); + for (final tab in AppTab.values) { + if (tab.name == e) return tab; + } } else if (e is int) { if (e >= 0 && e < AppTab.values.length) { return AppTab.values[e]; diff --git a/lib/data/model/pkg/manager.dart b/lib/data/model/pkg/manager.dart deleted file mode 100644 index a8b58f7ec..000000000 --- a/lib/data/model/pkg/manager.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:server_box/data/model/server/dist.dart'; - -enum PkgManager { - apt, - yum, - zypper, - pacman, - opkg, - apk; - - String? get listUpdate { - switch (this) { - case PkgManager.yum: - return 'yum check-update'; - case PkgManager.apt: - return 'apt list --upgradeable'; - case PkgManager.zypper: - return 'zypper lu'; - case PkgManager.pacman: - return 'pacman -Qu'; - case PkgManager.opkg: - return 'opkg list-upgradable'; - case PkgManager.apk: - return 'apk list --upgradable'; - } - } - - String? get update { - switch (this) { - case PkgManager.apt: - return 'apt update'; - case PkgManager.pacman: - return 'pacman -Sy'; - case PkgManager.opkg: - return 'opkg update'; - case PkgManager.apk: - return 'apk update'; - default: - return null; - } - } - - String? upgrade(String args) { - switch (this) { - case PkgManager.yum: - return 'yum upgrade -y'; - case PkgManager.apt: - return 'apt upgrade -y'; - case PkgManager.zypper: - return 'zypper up -y'; - case PkgManager.pacman: - return 'pacman -Syu --noconfirm'; - case PkgManager.opkg: - return 'opkg upgrade $args'; - case PkgManager.apk: - return 'apk upgrade'; - } - } - - List updateListRemoveUnused(List list) { - switch (this) { - case PkgManager.yum: - list = list.sublist(2); - list.removeWhere((element) => element.isEmpty); - final endLine = list.lastIndexWhere((element) => element.contains('Obsoleting Packages')); - if (endLine != -1 && list.isNotEmpty) { - list = list.sublist(0, endLine); - } - break; - case PkgManager.apt: - // avoid other outputs - // such as: [Could not chdir to home directory /home/test: No such file or directory, , WARNING: apt does not have a stable CLI interface. Use with caution in scripts., , Listing...] - final idx = list.indexWhere((element) => element.contains('[upgradable from:')); - if (idx == -1) { - return []; - } - list = list.sublist(idx); - list.removeWhere((element) => element.isEmpty); - break; - case PkgManager.zypper: - list = list.sublist(4); - break; - case PkgManager.pacman: - case PkgManager.opkg: - case PkgManager.apk: - break; - } - list.removeWhere((element) => element.isEmpty); - return list; - } - - static PkgManager? fromDist(Dist? dist) { - switch (dist) { - case Dist.centos: - case Dist.rocky: - case Dist.fedora: - return PkgManager.yum; - case Dist.debian: - case Dist.ubuntu: - case Dist.kali: - case Dist.armbian: - case Dist.deepin: - return PkgManager.apt; - case Dist.opensuse: - return PkgManager.zypper; - case Dist.coreelec: - case Dist.wrt: - return PkgManager.opkg; - case Dist.arch: - return PkgManager.pacman; - case Dist.alpine: - return PkgManager.apk; - case null: - return null; - } - } -} diff --git a/lib/data/provider/providers.dart b/lib/data/provider/providers.dart deleted file mode 100644 index 841fef497..000000000 --- a/lib/data/provider/providers.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:fl_lib/fl_lib.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_riverpod/misc.dart'; - -import 'package:server_box/data/provider/private_key.dart'; -import 'package:server_box/data/provider/server/all.dart'; -import 'package:server_box/data/provider/sftp.dart'; -import 'package:server_box/data/provider/snippet.dart'; - -/// !library; -/// ref.useNotifier, ref.readProvider, ref.watchProvider -/// -/// Usage: -/// - `providers.read.server` -> `ref.read(serversProvider)` -/// - `providers.use.snippet` -> `ref.read(snippetsNotifierProvider.notifier)` - -extension RiverpodNotifiers on ConsumerState { - T useNotifier>(NotifierProvider provider) { - return ref.read(provider.notifier); - } - - T readProvider(ProviderBase provider) { - return ref.read(provider); - } - - T watchProvider(ProviderBase provider) { - return ref.watch(provider); - } - - MyProviders get providers => MyProviders(ref); -} - -final class MyProviders { - final WidgetRef ref; - const MyProviders(this.ref); - - ReadMyProvider get read => ReadMyProvider(ref); - WatchMyProvider get watch => WatchMyProvider(ref); - UseNotifierMyProvider get use => UseNotifierMyProvider(ref); -} - -final class ReadMyProvider { - final WidgetRef ref; - const ReadMyProvider(this.ref); - - T call(ProviderBase provider) => ref.read(provider); - - // Specific provider getters - ServersState get server => ref.read(serversProvider); - SnippetState get snippet => ref.read(snippetProvider); - AppState get app => ref.read(appStatesProvider); - PrivateKeyState get privateKey => ref.read(privateKeyProvider); - SftpState get sftp => ref.read(sftpProvider); -} - -final class WatchMyProvider { - final WidgetRef ref; - const WatchMyProvider(this.ref); - - T call(ProviderBase provider) => ref.watch(provider); - - // Specific provider getters - ServersState get server => ref.watch(serversProvider); - SnippetState get snippet => ref.watch(snippetProvider); - AppState get app => ref.watch(appStatesProvider); - PrivateKeyState get privateKey => ref.watch(privateKeyProvider); - SftpState get sftp => ref.watch(sftpProvider); -} - -final class UseNotifierMyProvider { - final WidgetRef ref; - const UseNotifierMyProvider(this.ref); - - T call>(NotifierProvider provider) => - ref.read(provider.notifier); - - // Specific provider notifier getters - ServersNotifier get server => ref.read(serversProvider.notifier); - SnippetNotifier get snippet => ref.read(snippetProvider.notifier); - AppStates get app => ref.read(appStatesProvider.notifier); - PrivateKeyNotifier get privateKey => ref.read(privateKeyProvider.notifier); - SftpNotifier get sftp => ref.read(sftpProvider.notifier); -} \ No newline at end of file diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index fe7a7d5ed..609314285 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -8,12 +8,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:server_box/core/chan.dart'; import 'package:server_box/core/sync.dart'; -import 'package:server_box/data/model/app/menu/platform.dart'; import 'package:server_box/data/model/app/tab.dart'; import 'package:server_box/data/provider/server/all.dart'; import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/url.dart'; +import 'package:server_box/view/page/home_tab.dart'; +import 'package:server_box/view/page/macos_menu_bar.dart'; import 'package:server_box/view/page/setting/entry.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -161,9 +162,10 @@ class _HomePageState extends ConsumerState if (Platform.isMacOS) { return PlatformMenuBar( - menus: MacOSMenuBarManager.buildMenuBar(context, (int index) { - _onDestinationSelected(index); - }), + menus: MacOSMenuBarManager.buildMenuBar( + context, + _onDestinationSelected, + ), child: mainContent, ); } diff --git a/lib/view/page/home_tab.dart b/lib/view/page/home_tab.dart new file mode 100644 index 000000000..890fb2ef3 --- /dev/null +++ b/lib/view/page/home_tab.dart @@ -0,0 +1,69 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/material.dart'; +import 'package:icons_plus/icons_plus.dart'; +import 'package:server_box/data/model/app/tab.dart'; +import 'package:server_box/view/page/server/tab/tab.dart'; +import 'package:server_box/view/page/snippet/list.dart'; +import 'package:server_box/view/page/ssh/tab.dart'; +import 'package:server_box/view/page/storage/local.dart'; + +extension AppTabViewX on AppTab { + Widget get page { + return switch (this) { + AppTab.server => const ServerPage(), + AppTab.ssh => const SSHTabPage(), + AppTab.file => const LocalFilePage(), + AppTab.snippet => const SnippetListPage(), + }; + } + + NavigationDestination get navDestination { + return switch (this) { + AppTab.server => NavigationDestination( + icon: const Icon(BoxIcons.bx_server), + label: libL10n.server, + selectedIcon: const Icon(BoxIcons.bxs_server), + ), + AppTab.ssh => const NavigationDestination( + icon: Icon(Icons.terminal_outlined), + label: 'SSH', + selectedIcon: Icon(Icons.terminal), + ), + AppTab.snippet => NavigationDestination( + icon: const Icon(Icons.code_outlined), + label: libL10n.snippet, + selectedIcon: const Icon(Icons.code), + ), + AppTab.file => NavigationDestination( + icon: const Icon(Icons.folder_open), + label: libL10n.file, + selectedIcon: const Icon(Icons.folder), + ), + }; + } + + NavigationRailDestination get navRailDestination { + return switch (this) { + AppTab.server => NavigationRailDestination( + icon: const Icon(BoxIcons.bx_server), + label: Text(libL10n.server), + selectedIcon: const Icon(BoxIcons.bxs_server), + ), + AppTab.ssh => const NavigationRailDestination( + icon: Icon(Icons.terminal_outlined), + label: Text('SSH'), + selectedIcon: Icon(Icons.terminal), + ), + AppTab.snippet => NavigationRailDestination( + icon: const Icon(Icons.code_outlined), + label: Text(libL10n.snippet), + selectedIcon: const Icon(Icons.code), + ), + AppTab.file => NavigationRailDestination( + icon: const Icon(Icons.folder_open), + label: Text(libL10n.file), + selectedIcon: const Icon(Icons.folder), + ), + }; + } +} diff --git a/lib/data/model/app/menu/platform.dart b/lib/view/page/macos_menu_bar.dart similarity index 76% rename from lib/data/model/app/menu/platform.dart rename to lib/view/page/macos_menu_bar.dart index a6c220c60..e409caa14 100644 --- a/lib/data/model/app/menu/platform.dart +++ b/lib/view/page/macos_menu_bar.dart @@ -9,9 +9,11 @@ import 'package:server_box/generated/l10n/l10n.dart'; import 'package:server_box/view/page/setting/entry.dart'; import 'package:url_launcher/url_launcher.dart'; -/// macOS Menu Bar class MacOSMenuBarManager { - static List buildMenuBar(BuildContext context, Function(int) onTabChanged) { + static List buildMenuBar( + BuildContext context, + void Function(int) onTabChanged, + ) { final l10n = context.l10n; final homeTabs = Stores.setting.homeTabs.fetch(); return [ @@ -20,17 +22,23 @@ class MacOSMenuBarManager { menus: [ PlatformMenuItem( label: libL10n.about, - onSelected: () => _showAboutDialog(context), + onSelected: () => _showAboutDialog(), ), PlatformMenuItem( label: libL10n.menuSettings, - shortcut: const SingleActivator(LogicalKeyboardKey.comma, meta: true), - onSelected: () => _openSettings(context), + shortcut: const SingleActivator( + LogicalKeyboardKey.comma, + meta: true, + ), + onSelected: () => SettingsPage.route.go(context), ), PlatformMenuItem( label: libL10n.menuQuit, - shortcut: const SingleActivator(LogicalKeyboardKey.keyQ, meta: true), - onSelected: () => SystemNavigator.pop(), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyQ, + meta: true, + ), + onSelected: SystemNavigator.pop, ), ], ), @@ -61,7 +69,7 @@ class MacOSMenuBarManager { static List _buildNavigateMenuItems( AppLocalizations l10n, List homeTabs, - Function(int) onTabChanged, + void Function(int) onTabChanged, ) { final menuItems = []; final tabLabels = { @@ -75,13 +83,15 @@ class MacOSMenuBarManager { final label = tabLabels[tab]; if (label == null) continue; final shortcutKey = _getShortcutKeyForIndex(i); - menuItems.add(PlatformMenuItem( - label: label, - shortcut: shortcutKey != null - ? SingleActivator(shortcutKey, meta: true) - : null, - onSelected: () => onTabChanged(i), - )); + menuItems.add( + PlatformMenuItem( + label: label, + shortcut: shortcutKey != null + ? SingleActivator(shortcutKey, meta: true) + : null, + onSelected: () => onTabChanged(i), + ), + ); } return menuItems; } @@ -101,19 +111,15 @@ class MacOSMenuBarManager { return index < keys.length ? keys[index] : null; } - static Future _showAboutDialog(BuildContext context) async { + static Future _showAboutDialog() async { const channel = MethodChannel('about'); await channel.invokeMethod('showAboutPanel'); } - static void _openSettings(BuildContext context) { - SettingsPage.route.go(context); - } - static Future _openURL(String url) async { final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri); } } -} \ No newline at end of file +} diff --git a/lib/view/page/ping.dart b/lib/view/page/ping.dart deleted file mode 100644 index cc749aa87..000000000 --- a/lib/view/page/ping.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:async'; - -import 'package:fl_lib/fl_lib.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:server_box/core/extension/context/locale.dart'; -import 'package:server_box/data/model/server/ping_result.dart'; -import 'package:server_box/data/provider/server/all.dart'; -import 'package:server_box/data/provider/server/single.dart'; - -/// Only permit ipv4 / ipv6 / domain chars -final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+'); - -class PingPage extends ConsumerStatefulWidget { - const PingPage({super.key}); - - @override - ConsumerState createState() => _PingPageState(); - - static const route = AppRouteNoArg(page: PingPage.new, path: '/ping'); -} - -class _PingPageState extends ConsumerState with AutomaticKeepAliveClientMixin { - late TextEditingController _textEditingController; - final _results = ValueNotifier([]); - bool get isInit => _results.value.isEmpty; - - @override - void dispose() { - super.dispose(); - _textEditingController.dispose(); - _results.dispose(); - } - - @override - void initState() { - super.initState(); - _textEditingController = TextEditingController(text: ''); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold(body: _results.listenVal(_buildBody), floatingActionButton: _buildFAB()); - } - - Widget _buildFAB() { - return FloatingActionButton( - heroTag: 'ping', - onPressed: () { - context.showRoundDialog( - title: libL10n.select, - child: Input( - autoFocus: true, - controller: _textEditingController, - hint: 'example.com', - maxLines: 2, - minLines: 1, - onSubmitted: (_) => _doPing(), - ), - actions: Btn.ok(onTap: _doPing).toList, - ); - }, - child: const Icon(Icons.search), - ); - } - - Future _doPing() async { - context.pop(); - try { - await doPing(); - } catch (e) { - context.showRoundDialog( - title: libL10n.error, - child: Text(e.toString()), - actions: [TextButton(onPressed: () => Pfs.copy(e.toString()), child: Text(libL10n.copy))], - ); - rethrow; - } - } - - Widget _buildBody(List results) { - if (isInit) { - return Center(child: Text(libL10n.empty)); - } - return ListView.builder( - padding: const EdgeInsets.all(11), - controller: ScrollController(), - itemCount: results.length, - itemBuilder: (_, index) => _buildResultItem(results[index]), - ); - } - - Widget _buildResultItem(PingResult result) { - final unknown = l10n.unknown; - final ms = libL10n.ms; - return CardX( - child: ListTile( - contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17), - title: Text( - result.serverName, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: UIs.primaryColor), - ), - subtitle: Text(_buildPingSummary(result, unknown, ms), style: UIs.text11), - trailing: Text( - '${libL10n.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? l10n.unknown} $ms', - style: TextStyle(fontSize: 14, color: UIs.primaryColor), - ), - ), - ); - } - - String _buildPingSummary(PingResult result, String unknown, String ms) { - final ip = result.ip ?? unknown; - if (result.results == null || result.results!.isEmpty) { - return '$ip - ${libL10n.empty}'; - } - final ttl = result.results?.firstOrNull?.ttl ?? unknown; - final loss = result.statistic?.loss ?? unknown; - final min = result.statistic?.min ?? unknown; - final max = result.statistic?.max ?? unknown; - return '$ip\n${libL10n.ttl}: $ttl, ${libL10n.loss}: $loss%\n${l10n.min}: $min $ms, ${l10n.max}: $max $ms'; - } - - Future doPing() async { - FocusScope.of(context).requestFocus(FocusNode()); - _results.value.clear(); - final target = _textEditingController.text.trim(); - if (target.isEmpty) { - context.showSnackBar(l10n.pingInputIP); - return; - } - - if (ref.read(serversProvider).serverOrder.isEmpty) { - context.showSnackBar(l10n.pingNoServer); - return; - } - - /// avoid ping command injection - if (!targetReg.hasMatch(target)) { - context.showSnackBar(l10n.pingInputIP); - return; - } - - await Future.wait( - ref.read(serversProvider).servers.values.map((spi) async { - final serverState = ref.read(serverProvider(spi.id)); - if (serverState.client == null) { - return; - } - final result = await serverState.client!.run('ping -c 3 $target').string; - _results.value.add(PingResult.parse(spi.name, result)); - // [ValueNotifier] only notify when value is changed - // But we just add a element to list without changing the list itself - // So we need to notify manually - // - // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - _results.notifyListeners(); - }), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/view/page/setting/entries/home_tabs.dart b/lib/view/page/setting/entries/home_tabs.dart index ba6e5eee2..8bb149d65 100644 --- a/lib/view/page/setting/entries/home_tabs.dart +++ b/lib/view/page/setting/entries/home_tabs.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/tab.dart'; import 'package:server_box/data/res/store.dart'; +import 'package:server_box/view/page/home_tab.dart'; class HomeTabsConfigPage extends ConsumerStatefulWidget { const HomeTabsConfigPage({super.key}); diff --git a/lib/view/page/snippet/result.dart b/lib/view/page/snippet/result.dart deleted file mode 100644 index a0252f6b6..000000000 --- a/lib/view/page/snippet/result.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:fl_lib/fl_lib.dart'; -import 'package:flutter/material.dart'; -import 'package:server_box/core/extension/context/locale.dart'; -import 'package:server_box/data/model/server/snippet.dart'; - -class SnippetResultPage extends StatelessWidget { - final List args; - - const SnippetResultPage({super.key, required this.args}); - - static const route = AppRouteArg(page: SnippetResultPage.new, path: '/snippets/result'); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: Text(l10n.result)), - body: _buildBody(), - ); - } - - Widget _buildBody() { - return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 17), - itemCount: args.length, - itemBuilder: (_, index) { - final item = args[index]; - if (item == null) return UIs.placeholder; - return CardX( - child: ExpandTile( - initiallyExpanded: args.length == 1, - title: Text(item.dest ?? ''), - subtitle: Text(item.time.toString(), style: UIs.textGrey), - children: [ - SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 17), - scrollDirection: Axis.horizontal, - child: Text(item.result, textAlign: TextAlign.start), - ), - ], - ), - ); - }, - ); - } -}