From 1c84971e0795e76bcdcdb83998cae2e9f86ac94c Mon Sep 17 00:00:00 2001 From: "Dee.H.Y" Date: Thu, 1 Aug 2024 09:09:26 +0800 Subject: [PATCH] feat(app): add system tray functionality - Update pubspec.yaml with tray_manager dependency. - Initialize system tray in app.dart and handle menu item clicks. --- .github/workflows/ci.yaml | 2 +- .github/workflows/release.yml | 2 +- assets/images/tray_icon.ico | Bin 0 -> 188478 bytes assets/images/tray_icon.png | Bin 0 -> 4312 bytes lib/app/view/app.dart | 20 ++- lib/app/view/system_tray.dart | 62 +++++++ lib/common/data/close_btn_action.dart | 16 ++ lib/common/view/header_bar.dart | 93 ++++++++++ lib/constants.dart | 1 + lib/l10n/app_en.arb | 10 +- lib/l10n/app_zh.arb | 13 +- lib/l10n/app_zh_CN.arb | 13 +- lib/main.dart | 10 ++ lib/settings/settings_model.dart | 9 + lib/settings/settings_service.dart | 21 +++ lib/settings/view/settings_page.dart | 58 +++++- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + needs_translation.json | 170 +++++++++++++++--- pubspec.lock | 24 +++ pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 24 files changed, 503 insertions(+), 35 deletions(-) create mode 100644 assets/images/tray_icon.ico create mode 100644 assets/images/tray_icon.png create mode 100644 lib/app/view/system_tray.dart create mode 100644 lib/common/data/close_btn_action.dart diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 077edd402..1bc2b0139 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,6 +49,6 @@ jobs: channel: 'stable' flutter-version: ${{env.FLUTTER_VERSION}} - run: sudo apt update - - run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip libunwind-dev libmpv-dev + - run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip libunwind-dev libmpv-dev libappindicator3-1 libappindicator3-dev - run: flutter pub get - run: flutter build linux -v \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4550d41e..614a2bb68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: # channel: 'stable' # flutter-version: ${{env.FLUTTER_VERSION}} # - run: sudo apt update - # - run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip libunwind-dev libmpv-dev + # - run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip libunwind-dev libmpv-dev libappindicator3-1 libappindicator3-dev # - run: flutter pub get # - uses: snapcore/action-build@v1 diff --git a/assets/images/tray_icon.ico b/assets/images/tray_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2c7869b948619f5cb3822a421a5a8be728c821b4 GIT binary patch literal 188478 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 VU`4p0lohV41S{;;X@@oA{4WXULQVhx literal 0 HcmV?d00001 diff --git a/assets/images/tray_icon.png b/assets/images/tray_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..27b14684efb9051231e8e897e9e8d0a92988eba8 GIT binary patch literal 4312 zcmd6rXEb6obq_0D%$iGa<5Q(3HFW;OH@4Tl0sHG%=Jl(MgB zks$9Qo>zy_GKKW^l10;zt4voRGBv)qt#xB=^T-VzS(65d~w)DrVZRYuNy zPn~2UINSFD%PxoR*c5?j>90QYe6l^I*5%!f#Z86_$q^_&FLeTLmUilidGNgsS;$N; zIVMrt~6$ucj9Go5SM5=TQj=brrI2K2) z3C8>z2~Se4)zgC~^<_Z^ab}l)bF?51Xd!xIh=cEAKs+foxf&2B7?GF;Uk;j(nAimW z-{UIn!)cy3u?8{jN>%Yc@@@;%Co#>^f!?3XbkV<9iVL?sS&~pvdcB{BJa5M;MUKx% zK^zQqL5E*t_^8yaW1z&PruW|Pq!wdXQcG`3;f06nRm=y-T|x6^!0%fr;Ky)sF=4Aj zp=KJGv{L{UM|DgZ;D&pDHf=xGLAKhck@%p50<063MMNg~$GUXX{Vh~``;5Oj4|LGd z8REcjQqR1FqIq!B-JT3G6PtuJSJvZ?pIp{%I%R!QBp*n}?l6c33rec-2F#L=qhg?@ z+K3Rc%_lebrzu$Ev%N0sBr!R?68ZDWn4)4IJh6z6lNR^GwMhPa#L{756Q1;}Ga!JO z%zrH>AW>O8k!gYQA2m);M8!ZWIHdV+A+sz}VbB_hV*!bOv5bl>>VuX)kprX4kOT*H zkEW#kJFafz)181{uyHKu+=UKtWtzzu+z(yzT8jpE# zT#vA|CH{%dkKJ$9f7RWJUqZ4U_2w+}tsq7}xD?4RZAs;cw+Ru8y54i+Qc?Gf`P45( zUkJvAy5FMD*656IGZVJ0Zit}+XN6YGkdg}F8n z0{QqE2KjiUG2kn7(AMq~nejc`q+*k#DDjH>T%#$hn#)S15P^1e1zSvUAtUY=)Z^Yp zZIde)ZfxCJhxqSQb+l{RH87xL7avP__UQv?c;3SJ%KtJ;zo}^M*vE!M@Te8cYw4W? zB}Kg$B`VC#fl%uU5ZW>fLjO4n z4jF$d{a3y_=&wE+^k?Y^LgB&QHl!2E-%5u|K=~ z2z~k-g#O>zzbtlS)yqJm9Hp?!qw37_z2a}xYv^6YPLaZ%0Ac8$sZE!?`@>Gcc6#5w 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 createState() => _MusicPodAppState(); } -class _MusicPodAppState extends State<_MusicPodApp> { +class _MusicPodAppState extends State<_MusicPodApp> with WindowListener { late Future _initFuture; @override @@ -118,9 +120,25 @@ class _MusicPodAppState extends State<_MusicPodApp> { await di().init(); if (!mounted) return false; di().init(); + di().updateTrayMenuItems(context); + windowManager.addListener(this); return true; } + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowEvent(String eventName) { + if ('show' == eventName || 'hide' == eventName) { + di().updateTrayMenuItems(context); + } + super.onWindowEvent(eventName); + } + @override Widget build(BuildContext context) { final themeIndex = watchPropertyValue((SettingsModel m) => m.themeIndex); diff --git a/lib/app/view/system_tray.dart b/lib/app/view/system_tray.dart new file mode 100644 index 000000000..bffb54342 --- /dev/null +++ b/lib/app/view/system_tray.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; + +String trayIcon() { + if (Platform.isWindows) { + return 'assets/images/tray_icon.ico'; + } else { + return 'assets/images/tray_icon.png'; + } +} + +class SystemTray with TrayListener { + late List trayMenuItems; + + Future init() async { + trayManager.addListener(this); + await trayManager.setIcon(trayIcon()); + } + + Future dispose() async { + trayManager.removeListener(this); + } + + Future updateTrayMenuItems( + BuildContext context, + ) async { + bool isVisible = await windowManager.isVisible(); + + trayMenuItems = [ + MenuItem( + key: 'show_hide_window', + label: isVisible ? 'Hide Window' : 'Show Window', + ), + MenuItem.separator(), + MenuItem( + key: 'close_application', + label: 'Close Application', + ), + ]; + + await trayManager.setContextMenu(Menu(items: trayMenuItems)); + } + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + switch (menuItem.key) { + case 'show_hide_window': + windowManager.isVisible().then((value) { + if (value) { + windowManager.hide(); + } else { + windowManager.show(); + } + }); + case 'close_application': + windowManager.close(); + } + } +} diff --git a/lib/common/data/close_btn_action.dart b/lib/common/data/close_btn_action.dart new file mode 100644 index 000000000..4f61d82eb --- /dev/null +++ b/lib/common/data/close_btn_action.dart @@ -0,0 +1,16 @@ +import '../../l10n/l10n.dart'; + +enum CloseBtnAction { + alwaysAsk, + hideToTray, + close; + + @override + String toString() => name; + + String localize(AppLocalizations l10n) => switch (this) { + alwaysAsk => l10n.alwaysAsk, + hideToTray => l10n.hideToTray, + close => l10n.closeApp, + }; +} diff --git a/lib/common/view/header_bar.dart b/lib/common/view/header_bar.dart index 1020867d8..3364f96be 100644 --- a/lib/common/view/header_bar.dart +++ b/lib/common/view/header_bar.dart @@ -2,7 +2,10 @@ import 'dart:io'; import '../../app/app_model.dart'; import '../../extensions/build_context_x.dart'; +import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; +import '../../settings/settings_model.dart'; +import '../data/close_btn_action.dart'; import 'global_keys.dart'; import 'icons.dart'; import 'nav_back_button.dart'; @@ -11,6 +14,8 @@ import 'package:phoenix_theme/phoenix_theme.dart' hide isMobile; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import 'theme.dart'; + class HeaderBar extends StatelessWidget with WatchItMixin implements PreferredSizeWidget { @@ -40,6 +45,8 @@ class HeaderBar extends StatelessWidget @override Widget build(BuildContext context) { final canPop = watchPropertyValue((LibraryModel m) => m.canPop); + final closeBtnAction = + watchPropertyValue((SettingsModel m) => m.closeBtnActionIndex); Widget? leading; @@ -89,6 +96,21 @@ class HeaderBar extends StatelessWidget backgroundColor: backgroundColor ?? context.theme.scaffoldBackgroundColor, style: theStyle, foregroundColor: foregroundColor, + onClose: (context) { + switch (closeBtnAction) { + case CloseBtnAction.alwaysAsk: + showDialog( + context: context, + builder: (context) { + return const CloseWindowActionConfirmDialog(); + }, + ); + case CloseBtnAction.hideToTray: + YaruWindow.hide(context); + case CloseBtnAction.close: + YaruWindow.close(context); + } + }, ); } @@ -101,6 +123,77 @@ class HeaderBar extends StatelessWidget ); } +class CloseWindowActionConfirmDialog extends StatefulWidget { + const CloseWindowActionConfirmDialog({super.key}); + + @override + State createState() => + _CloseWindowActionConfirmDialogState(); +} + +class _CloseWindowActionConfirmDialogState + extends State { + bool rememberChoice = false; + @override + Widget build(BuildContext context) { + final model = di(); + return AlertDialog( + title: yaruStyled + ? YaruDialogTitleBar( + backgroundColor: Colors.transparent, + title: Text(context.l10n.closeMusicPod), + ) + : null, + titlePadding: yaruStyled ? EdgeInsets.zero : null, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 12), + Text( + context.l10n.confirmCloseOrHideTip, + ), + CheckboxListTile( + title: Text(context.l10n.doNotAskAgain), + value: rememberChoice, + onChanged: (value) { + setState(() { + rememberChoice = value!; + }); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + if (rememberChoice) { + model.setCloseBtnActionIndex(CloseBtnAction.hideToTray); + } + Navigator.of(context).pop(); + YaruWindow.hide(context); + }, + child: Text(context.l10n.hideToTray), + ), + TextButton( + onPressed: () { + if (rememberChoice) { + model.setCloseBtnActionIndex(CloseBtnAction.close); + } + Navigator.of(context).pop(); + YaruWindow.close(context); + }, + child: Text(context.l10n.closeApp), + ), + ], + ); + } +} + class SidebarButton extends StatelessWidget { const SidebarButton({super.key}); diff --git a/lib/constants.dart b/lib/constants.dart index bd94299f9..b0bff436e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -161,6 +161,7 @@ const kFavCountryCodes = 'favCountryCodes'; const kFavLanguageCodes = 'favLanguageCodes'; const kAscendingFeeds = 'ascendingfeed:::'; const kPatchNotesDisposed = 'kPatchNotesDisposed'; +const kCloseBtnAction = 'closeBtnAction'; const shops = { 'https://us.7digital.com/': '7digital', diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0b66ea41f..9e73d6141 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -331,5 +331,13 @@ "replayAllEpisodes": "Replay all episodes", "checkForUpdates": "Check for updates", "playbackWillStopIn": "Playback will stop in: {duration} ({timeOfDay})", - "schedulePlaybackStopTimer": "Schedule a time to stop playback" + "schedulePlaybackStopTimer": "Schedule a time to stop playback", + "alwaysAsk": "Always ask", + "hideToTray": "Hide to tray", + "closeBtnAction": "Close Button Action", + "whenCloseBtnClicked": "When close button is clicked", + "closeApp": "Close Application", + "closeMusicPod": "Close MusicPod?", + "confirmCloseOrHideTip": "Please confirm if you need to close the application or hide it?", + "doNotAskAgain": "Do not ask again" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index e53f7d1b3..5e702dbc2 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -91,7 +91,7 @@ "cryptocurrencyXXXPodcastIndexOnly": "加密货币", "cultureXXXPodcastIndexOnly": "文化", "dailyXXXPodcastIndexOnly": "每日", - "dark": "暗色", + "dark": "浅色", "decreaseSearchLimit": "请减少搜索限制", "deletePlaylist": "删除播放列表", "dependencies": "依赖", @@ -330,5 +330,14 @@ "writeMetadata": "写入元数据", "year": "年份", "years": "年份", - "playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。" + "playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。", + "schedulePlaybackStopTimer": "计划停止播放的时间", + "alwaysAsk": "总是询问", + "hideToTray": "隐藏到托盘", + "closeBtnAction": "关闭按钮的行为", + "whenCloseBtnClicked": "当点击关闭按钮时", + "closeApp": "关闭应用", + "closeMusicPod": "关闭 MusicPod?", + "confirmCloseOrHideTip": "请确定您想要退出应用还是隐藏到托盘?", + "doNotAskAgain": "不再询问" } diff --git a/lib/l10n/app_zh_CN.arb b/lib/l10n/app_zh_CN.arb index 2a9329cf3..9f8505650 100644 --- a/lib/l10n/app_zh_CN.arb +++ b/lib/l10n/app_zh_CN.arb @@ -91,7 +91,7 @@ "cryptocurrencyXXXPodcastIndexOnly": "加密货币", "cultureXXXPodcastIndexOnly": "文化", "dailyXXXPodcastIndexOnly": "每日", - "dark": "暗色", + "dark": "浅色", "decreaseSearchLimit": "请减少搜索限制", "deletePlaylist": "删除播放列表", "dependencies": "依赖", @@ -330,5 +330,14 @@ "writeMetadata": "写入元数据", "year": "年份", "years": "年份", - "playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。" + "playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。", + "schedulePlaybackStopTimer": "计划停止播放的时间", + "alwaysAsk": "总是询问", + "hideToTray": "隐藏到托盘", + "closeBtnAction": "关闭按钮的行为", + "whenCloseBtnClicked": "当点击关闭按钮时", + "closeApp": "关闭应用", + "closeMusicPod": "关闭 MusicPod?", + "confirmCloseOrHideTip": "请确定您想要退出应用还是隐藏到托盘?", + "doNotAskAgain": "不再询问" } diff --git a/lib/main.dart b/lib/main.dart index 42001802c..e70ba2cad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import '../../library/library_model.dart'; import 'app/app_model.dart'; import 'app/connectivity_model.dart'; import 'app/view/app.dart'; +import 'app/view/system_tray.dart'; import 'library/library_service.dart'; import 'local_audio/local_audio_model.dart'; import 'local_audio/local_audio_service.dart'; @@ -133,6 +134,15 @@ Future main(List args) async { final gitHub = GitHub(); di.registerSingleton(gitHub); + if (!isMobile) { + final systemTray = SystemTray(); + await systemTray.init(); + di.registerSingleton( + systemTray, + dispose: (s) async => s.dispose(), + ); + } + // Register ViewModels di.registerLazySingleton( () => SettingsModel( diff --git a/lib/settings/settings_model.dart b/lib/settings/settings_model.dart index 7e1b60af7..af29808a0 100644 --- a/lib/settings/settings_model.dart +++ b/lib/settings/settings_model.dart @@ -4,6 +4,7 @@ import 'package:github/github.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../../external_path/external_path_service.dart'; +import '../common/data/close_btn_action.dart'; import 'settings_service.dart'; class SettingsModel extends SafeChangeNotifier { @@ -25,6 +26,7 @@ class SettingsModel extends SafeChangeNotifier { StreamSubscription? _themeIndexChangedSub; StreamSubscription? _recentPatchNotesDisposedChangedSub; StreamSubscription? _useArtistGridViewChangedSub; + StreamSubscription? _closeBtnActionIndexChangedSub; String? get directory => _service.directory; Future setDirectory(String? value) async { @@ -57,6 +59,10 @@ class SettingsModel extends SafeChangeNotifier { Future getPathOfDirectory() async => _externalPathService.getPathOfDirectory(); + CloseBtnAction get closeBtnActionIndex => _service.closeBtnActionIndex; + void setCloseBtnActionIndex(CloseBtnAction value) => + _service.setCloseBtnActionIndex(value); + void init() { _themeIndexChangedSub ??= _service.themeIndexChanged.listen((_) => notifyListeners()); @@ -73,6 +79,8 @@ class SettingsModel extends SafeChangeNotifier { _recentPatchNotesDisposedChangedSub ??= _service .recentPatchNotesDisposedChanged .listen((_) => notifyListeners()); + _closeBtnActionIndexChangedSub ??= + _service.closeBtnActionChanged.listen((_) => notifyListeners()); } @override @@ -85,6 +93,7 @@ class SettingsModel extends SafeChangeNotifier { await _directoryChangedSub?.cancel(); await _recentPatchNotesDisposedChangedSub?.cancel(); await _useArtistGridViewChangedSub?.cancel(); + await _closeBtnActionIndexChangedSub?.cancel(); super.dispose(); } } diff --git a/lib/settings/settings_service.dart b/lib/settings/settings_service.dart index f085dbe00..a1096095b 100644 --- a/lib/settings/settings_service.dart +++ b/lib/settings/settings_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:shared_preferences/shared_preferences.dart'; +import '../common/data/close_btn_action.dart'; import '../constants.dart'; class SettingsService { @@ -87,6 +88,25 @@ class SettingsService { }); } + final _closeBtnActionIndexController = StreamController.broadcast(); + Stream get closeBtnActionChanged => + _closeBtnActionIndexController.stream; + CloseBtnAction get closeBtnActionIndex => + _preferences.getString(kCloseBtnAction) == null + ? CloseBtnAction.alwaysAsk + : CloseBtnAction.values.firstWhere( + (element) => + element.toString() == _preferences.getString(kCloseBtnAction), + orElse: () => CloseBtnAction.alwaysAsk, + ); + void setCloseBtnActionIndex(CloseBtnAction value) { + _preferences.setString(kCloseBtnAction, value.toString()).then( + (saved) { + if (saved) _closeBtnActionIndexController.add(true); + }, + ); + } + Future dispose() async { await _themeIndexController.close(); await _recentPatchNotesDisposedController.close(); @@ -95,5 +115,6 @@ class SettingsService { await _podcastIndexApiSecretController.close(); await _usePodcastIndexController.close(); await _podcastIndexApiKeyController.close(); + await _closeBtnActionIndexController.close(); } } diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 07723e30a..110623326 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -6,7 +6,9 @@ import 'package:yaru/yaru.dart'; import '../../app/app_model.dart'; import '../../app/connectivity_model.dart'; +import '../../common/data/close_btn_action.dart'; import '../../common/view/common_widgets.dart'; +import '../../common/view/drop_down_arrow.dart'; import '../../common/view/global_keys.dart'; import '../../common/view/icons.dart'; import '../../common/view/progress.dart'; @@ -38,11 +40,12 @@ class SettingsPage extends StatelessWidget { ), Expanded( child: ListView( - children: const [ - _ThemeSection(), - _PodcastSection(), - _LocalAudioSection(), - _AboutSection(), + children: [ + const _ThemeSection(), + if (!isMobile) const _CloseActionSection(), + const _PodcastSection(), + const _LocalAudioSection(), + const _AboutSection(), ], ), ), @@ -98,6 +101,51 @@ class _ThemeSection extends StatelessWidget with WatchItMixin { } } +class _CloseActionSection extends StatelessWidget with WatchItMixin { + const _CloseActionSection(); + + @override + Widget build(BuildContext context) { + final model = di(); + + final closeBtnAction = + watchPropertyValue((SettingsModel m) => m.closeBtnActionIndex); + return YaruSection( + margin: const EdgeInsets.only( + left: kYaruPagePadding, + top: kYaruPagePadding, + right: kYaruPagePadding, + ), + headline: Text(context.l10n.closeBtnAction), + child: Column( + children: [ + YaruTile( + title: Text(context.l10n.whenCloseBtnClicked), + trailing: YaruPopupMenuButton( + icon: const DropDownArrow(), + initialValue: closeBtnAction, + child: Text(closeBtnAction.localize(context.l10n)), + onSelected: (value) { + model.setCloseBtnActionIndex(value); + }, + itemBuilder: (context) { + return [ + for (var i = 0; i < CloseBtnAction.values.length; ++i) + PopupMenuItem( + value: CloseBtnAction.values[i], + child: + Text(CloseBtnAction.values[i].localize(context.l10n)), + ), + ]; + }, + ), + ), + ], + ), + ); + } +} + class _PodcastSection extends StatefulWidget with WatchItStatefulWidgetMixin { const _PodcastSection(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 1af123b9b..6bc70a71e 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) system_theme_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin"); system_theme_plugin_register_with_registrar(system_theme_registrar); + g_autoptr(FlPluginRegistrar) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d52676c1e..a5d30a295 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever super_native_extensions system_theme + tray_manager url_launcher_linux window_manager yaru_window_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9e16b13bf..712af2624 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -21,6 +21,7 @@ import shared_preferences_foundation import sqflite import super_native_extensions import system_theme +import tray_manager import url_launcher_macos import wakelock_plus import window_manager @@ -42,6 +43,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/needs_translation.json b/needs_translation.json index ca20363cf..400f62e14 100644 --- a/needs_translation.json +++ b/needs_translation.json @@ -8,7 +8,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "da": [ @@ -22,13 +30,29 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "de": [ "gitHubClientConnectError", "replayEpisode", - "replayAllEpisodes" + "replayAllEpisodes", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "es": [ @@ -232,7 +256,37 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" + ], + + "fr": [ + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" + ], + + "it": [ + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "nl": [ @@ -464,7 +518,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "pl": [ @@ -516,7 +578,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "pt": [ @@ -719,7 +789,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "pt_BR": [ @@ -922,7 +1000,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "ru": [ @@ -940,7 +1026,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "sk": [ @@ -952,7 +1046,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "sv": [ @@ -1145,7 +1247,15 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "tr": [ @@ -1157,22 +1267,38 @@ "replayAllEpisodes", "checkForUpdates", "playbackWillStopIn", - "schedulePlaybackStopTimer" - ], - - "zh": [ - "schedulePlaybackStopTimer" - ], - - "zh_CN": [ - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "zh_HK": [ - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "zh_TW": [ - "schedulePlaybackStopTimer" + "schedulePlaybackStopTimer", + "alwaysAsk", + "hideToTray", + "closeBtnAction", + "whenCloseBtnClicked", + "closeApp", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ] } diff --git a/pubspec.lock b/pubspec.lock index 54fd059d6..643bdecd8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -901,6 +901,14 @@ packages: url: "https://github.com/media-kit/media-kit" source: git version: "1.2.4" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -1342,6 +1350,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -1507,6 +1523,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: c9a63fd88bd3546287a7eb8ccc978d707eef82c775397af17dda3a4f4c039e64 + url: "https://pub.dev" + source: hosted + version: "0.2.3" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c30d6c349..715ef3c7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: smtc_windows: ^0.1.3 super_drag_and_drop: ^0.8.18 system_theme: ^3.0.0 + tray_manager: ^0.2.3 url_launcher: ^6.3.0 watch_it: ^1.4.2 win32: ^5.5.4 @@ -92,6 +93,8 @@ flutter: assets: - snap/gui/musicpod.png - assets/images/media-optical.png + - assets/images/tray_icon.ico + - assets/images/tray_icon.png - CHANGELOG.md dependency_overrides: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b34aa2b54..820f3252e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8b941b22e..f2b18ea3c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever super_native_extensions system_theme + tray_manager url_launcher_windows window_manager )