From 78ef181d4af36172e7f0c56657c7e97593c7f7d7 Mon Sep 17 00:00:00 2001 From: lxdklp Date: Wed, 10 Dec 2025 18:05:30 +0800 Subject: [PATCH] feat: support macOS menubar (#976) * feat: macOS menubar * feat: Dynamic NavigateMenuItems * fix: simplify shortcut config * fix: Simplify the code * fix: More suitable tab name --- lib/data/model/app/menu/platform.dart | 119 ++++++++++++++++++++++++++ lib/generated/l10n/l10n.dart | 42 +++++++++ lib/generated/l10n/l10n_de.dart | 21 +++++ lib/generated/l10n/l10n_en.dart | 21 +++++ lib/generated/l10n/l10n_es.dart | 21 +++++ lib/generated/l10n/l10n_fr.dart | 21 +++++ lib/generated/l10n/l10n_id.dart | 21 +++++ lib/generated/l10n/l10n_ja.dart | 21 +++++ lib/generated/l10n/l10n_nl.dart | 21 +++++ lib/generated/l10n/l10n_pt.dart | 21 +++++ lib/generated/l10n/l10n_ru.dart | 21 +++++ lib/generated/l10n/l10n_tr.dart | 21 +++++ lib/generated/l10n/l10n_uk.dart | 21 +++++ lib/generated/l10n/l10n_zh.dart | 21 +++++ lib/l10n/app_en.arb | 9 +- lib/l10n/app_zh.arb | 9 +- lib/view/page/home.dart | 15 +++- macos/Runner/AppDelegate.swift | 15 ++++ pubspec.yaml | 1 + 19 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 lib/data/model/app/menu/platform.dart diff --git a/lib/data/model/app/menu/platform.dart b/lib/data/model/app/menu/platform.dart new file mode 100644 index 00000000..a15ba374 --- /dev/null +++ b/lib/data/model/app/menu/platform.dart @@ -0,0 +1,119 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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/data/res/url.dart'; +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) { + final l10n = context.l10n; + final homeTabs = Stores.setting.homeTabs.fetch(); + return [ + PlatformMenu( + label: 'Server Box', + menus: [ + PlatformMenuItem( + label: libL10n.about, + onSelected: () => _showAboutDialog(context), + ), + PlatformMenuItem( + label: l10n.menuSettings, + shortcut: const SingleActivator(LogicalKeyboardKey.comma, meta: true), + onSelected: () => _openSettings(context), + ), + PlatformMenuItem( + label: l10n.menuQuit, + shortcut: const SingleActivator(LogicalKeyboardKey.keyQ, meta: true), + onSelected: () => SystemNavigator.pop(), + ), + ], + ), + PlatformMenu( + label: l10n.menuNavigate, + menus: _buildNavigateMenuItems(l10n, homeTabs, onTabChanged), + ), + PlatformMenu( + label: l10n.menuInfo, + menus: [ + PlatformMenuItem( + label: l10n.menuGitHubRepository, + onSelected: () => _openURL(Urls.thisRepo), + ), + PlatformMenuItem( + label: l10n.menuWiki, + onSelected: () => _openURL(Urls.appWiki), + ), + PlatformMenuItem( + label: l10n.menuHelp, + onSelected: () => _openURL(Urls.appHelp), + ), + ], + ), + ]; + } + + static List _buildNavigateMenuItems( + AppLocalizations l10n, + List homeTabs, + Function(int) onTabChanged, + ) { + final menuItems = []; + final tabLabels = { + AppTab.server: l10n.server, + AppTab.ssh: 'SSH', + AppTab.file: libL10n.file, + AppTab.snippet: l10n.snippet, + }; + for (var i = 0; i < homeTabs.length; i++) { + final tab = homeTabs[i]; + 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), + )); + } + return menuItems; + } + + static LogicalKeyboardKey? _getShortcutKeyForIndex(int index) { + const keys = [ + LogicalKeyboardKey.digit1, + LogicalKeyboardKey.digit2, + LogicalKeyboardKey.digit3, + LogicalKeyboardKey.digit4, + LogicalKeyboardKey.digit5, + LogicalKeyboardKey.digit6, + LogicalKeyboardKey.digit7, + LogicalKeyboardKey.digit8, + LogicalKeyboardKey.digit9, + ]; + return index < keys.length ? keys[index] : null; + } + + static Future _showAboutDialog(BuildContext context) 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/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 940da745..9ba4b7af 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1885,6 +1885,48 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'** String get writeScriptTip; + + /// No description provided for @menuSettings. + /// + /// In en, this message translates to: + /// **'Setting'** + String get menuSettings; + + /// No description provided for @menuQuit. + /// + /// In en, this message translates to: + /// **'Quit'** + String get menuQuit; + + /// No description provided for @menuNavigate. + /// + /// In en, this message translates to: + /// **'Navigate'** + String get menuNavigate; + + /// No description provided for @menuInfo. + /// + /// In en, this message translates to: + /// **'Info'** + String get menuInfo; + + /// No description provided for @menuGitHubRepository. + /// + /// In en, this message translates to: + /// **'GitHub Repository'** + String get menuGitHubRepository; + + /// No description provided for @menuWiki. + /// + /// In en, this message translates to: + /// **'Wiki'** + String get menuWiki; + + /// No description provided for @menuHelp. + /// + /// In en, this message translates to: + /// **'Help'** + String get menuHelp; } class _AppLocalizationsDelegate diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index f5a2c202..e8450cb5 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -1007,4 +1007,25 @@ class AppLocalizationsDe extends AppLocalizations { @override String get writeScriptTip => 'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 9c7ff4fe..eeb52e30 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -998,4 +998,25 @@ class AppLocalizationsEn extends AppLocalizations { @override String get writeScriptTip => 'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 01e0b741..5f1d7bcc 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -1009,4 +1009,25 @@ class AppLocalizationsEs extends AppLocalizations { @override String get writeScriptTip => 'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 6283e5b0..99e5dba0 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -1012,4 +1012,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get writeScriptTip => 'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 205a2473..a21473fd 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -998,4 +998,25 @@ class AppLocalizationsId extends AppLocalizations { @override String get writeScriptTip => 'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index bc284c4d..43c7d33f 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -968,4 +968,25 @@ class AppLocalizationsJa extends AppLocalizations { @override String get writeScriptTip => 'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index fde37b1c..70cc97c4 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -1005,4 +1005,25 @@ class AppLocalizationsNl extends AppLocalizations { @override String get writeScriptTip => 'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 935393fd..694b0bf0 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -1000,4 +1000,25 @@ class AppLocalizationsPt extends AppLocalizations { @override String get writeScriptTip => 'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index fe323ffd..a83c4d35 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -1004,4 +1004,25 @@ class AppLocalizationsRu extends AppLocalizations { @override String get writeScriptTip => 'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index f5e97d13..168be4e6 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -999,4 +999,25 @@ class AppLocalizationsTr extends AppLocalizations { @override String get writeScriptTip => 'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index b731c937..505564f3 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -1004,4 +1004,25 @@ class AppLocalizationsUk extends AppLocalizations { @override String get writeScriptTip => 'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.'; + + @override + String get menuSettings => 'Setting'; + + @override + String get menuQuit => 'Quit'; + + @override + String get menuNavigate => 'Navigate'; + + @override + String get menuInfo => 'Info'; + + @override + String get menuGitHubRepository => 'GitHub Repository'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => 'Help'; } diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index a2395a7d..b7a848aa 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -953,6 +953,27 @@ class AppLocalizationsZh extends AppLocalizations { @override String get writeScriptTip => '在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。'; + + @override + String get menuSettings => '设置'; + + @override + String get menuQuit => '退出'; + + @override + String get menuNavigate => '导航'; + + @override + String get menuInfo => '信息'; + + @override + String get menuGitHubRepository => 'GitHub 仓库'; + + @override + String get menuWiki => 'Wiki'; + + @override + String get menuHelp => '帮助'; } /// 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 53467523..33ea5d40 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -296,5 +296,12 @@ "wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.", "write": "Write", "writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.", - "writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content." + "writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.", + "menuSettings": "Setting", + "menuQuit": "Quit", + "menuNavigate": "Navigate", + "menuInfo": "Info", + "menuGitHubRepository": "GitHub Repository", + "menuWiki": "Wiki", + "menuHelp": "Help" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9e619600..c593561e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -293,5 +293,12 @@ "wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求", "write": "写", "writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等", - "writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。" + "writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。", + "menuSettings": "设置", + "menuQuit": "退出", + "menuNavigate": "导航", + "menuInfo": "信息", + "menuGitHubRepository": "GitHub 仓库", + "menuWiki": "Wiki", + "menuHelp": "帮助" } diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 4ef46fb4..89b27165 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/foundation.dart' show kReleaseMode; import 'package:flutter/material.dart'; @@ -5,6 +7,7 @@ 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'; @@ -134,7 +137,7 @@ class _HomePageState extends ConsumerState super.build(context); final isMobile = ResponsiveBreakpoints.of(context).isMobile; - return Scaffold( + final Widget mainContent = Scaffold( appBar: _AppBar(MediaQuery.paddingOf(context).top), body: Row( children: [ @@ -157,6 +160,16 @@ class _HomePageState extends ConsumerState ), bottomNavigationBar: isMobile ? _buildBottomBar() : null, ); + + if (Platform.isMacOS) { + return PlatformMenuBar( + menus: MacOSMenuBarManager.buildMenuBar(context, (int index) { + _onDestinationSelected(index); + }), + child: mainContent, + ); + } + return mainContent; } Widget _buildBottomBar() { diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index b3c17614..cedf0256 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -10,4 +10,19 @@ class AppDelegate: FlutterAppDelegate { override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } + + override func applicationDidFinishLaunching(_ notification: Notification) { + if let controller = mainFlutterWindow?.contentViewController as? FlutterViewController { + let channel = FlutterMethodChannel(name: "about", binaryMessenger: controller.engine.binaryMessenger) + channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in + if call.method == "showAboutPanel" { + NSApp.orderFrontStandardAboutPanel(nil) + result(nil) + } else { + result(FlutterMethodNotImplemented) + } + } + } + super.applicationDidFinishLaunching(notification) + } } diff --git a/pubspec.yaml b/pubspec.yaml index 52988dac..997e861d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: wake_on_lan: ^4.1.1+3 webdav_client_plus: ^1.0.2 xml: ^6.4.2 # for parsing nvidia-smi + url_launcher: ^6.2.6 dartssh2: git: url: https://github.com/lollipopkit/dartssh2