From ed44a5c5b9bcb15bd7948efdf7ed3e538871b889 Mon Sep 17 00:00:00 2001 From: "Dee.H.Y" Date: Thu, 1 Aug 2024 11:36:30 +0800 Subject: [PATCH] feat: add close button action settings - Add new settings for close button actions: always ask, hide to tray, and close app. - Implement confirmation dialog for closing or hiding the app when the close button is clicked. - Save user preference for close button action and remember the choice. --- lib/app/view/app.dart | 16 ++- lib/app/view/system_tray.dart | 48 ++++++--- lib/common/data/close_btn_action.dart | 16 +++ lib/common/view/header_bar.dart | 93 ++++++++++++++++ lib/constants.dart | 1 + lib/l10n/app_en.arb | 11 +- lib/l10n/app_zh.arb | 12 ++- lib/main.dart | 10 ++ lib/persistence_utils.dart | 22 ---- lib/settings/settings_model.dart | 9 ++ lib/settings/settings_service.dart | 26 +++++ lib/settings/view/settings_page.dart | 58 +++++++++- needs_translation.json | 146 ++++++++++++++++++++++---- 13 files changed, 403 insertions(+), 65 deletions(-) create mode 100644 lib/common/data/close_btn_action.dart diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index f9655bcc3..01f38c7cd 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:phoenix_theme/phoenix_theme.dart' hide ColorX; import 'package:system_theme/system_theme.dart'; import 'package:watch_it/watch_it.dart'; +import 'package:window_manager/window_manager.dart'; import 'package:yaru/yaru.dart'; import '../../common/view/icons.dart'; @@ -105,16 +106,13 @@ class _MusicPodApp extends StatefulWidget with WatchItStatefulWidgetMixin { } class _MusicPodAppState extends State<_MusicPodApp> - with WidgetsBindingObserver { + with WidgetsBindingObserver, WindowListener { late Future _initFuture; - late SystemTray _tray; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - _tray = SystemTray(); - _tray.init(); _initFuture = _init(); } @@ -137,6 +135,8 @@ class _MusicPodAppState extends State<_MusicPodApp> if (!mounted) return false; di().init(); + di().updateTrayMenuItems(context); + windowManager.addListener(this); return true; } @@ -151,9 +151,17 @@ class _MusicPodAppState extends State<_MusicPodApp> @override void dispose() { WidgetsBinding.instance.removeObserver(this); + windowManager.removeListener(this); super.dispose(); } + @override + void onWindowEvent(String eventName) { + if ('show' == eventName || 'hide' == eventName) { + di().updateTrayMenuItems(context); + } + } + @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 index edc5f167c..bffb54342 100644 --- a/lib/app/view/system_tray.dart +++ b/lib/app/view/system_tray.dart @@ -1,31 +1,53 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; -import '../../persistence_utils.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()); - await trayManager.setContextMenu(Menu(items: trayMenuItems)); } - // KDE@Arch Linux, this feature does not work. - // @override - // void onTrayIconMouseDown() { - // print('onTrayIconMouseDown'); - // } + 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', + ), + ]; - // KDE@Arch Linux, this feature does not work. - // @override - // void onTrayIconRightMouseDown() { - // print('onTrayIconRightMouseDown'); - // } + await trayManager.setContextMenu(Menu(items: trayMenuItems)); + } @override void onTrayMenuItemClick(MenuItem menuItem) { switch (menuItem.key) { - case 'restore_window': + case 'show_hide_window': windowManager.isVisible().then((value) { if (value) { windowManager.hide(); 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 894d54871..dac26d02c 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'; 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 9605db878..8d558c7e5 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -155,6 +155,7 @@ const kPodcastIndexApiKey = 'podcastIndexApiKey'; const kPodcastIndexApiSecret = 'podcastIndexApiSecret'; const kUseArtistGridView = 'useArtistGridView'; const kSearchPageId = 'searchPageId'; +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 541cd37b6..1258be7aa 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -324,6 +324,13 @@ "language": "Language", "duration": "Duration", "radioTagDisclaimerTitle": "This station sends a lot of tags.", - "radioTagDisclaimerSubTitle": "Sometimes stations send tags that do not match music genres. MusicPod is not responsible for the content!" - + "radioTagDisclaimerSubTitle": "Sometimes stations send tags that do not match music genres. MusicPod is not responsible for the content!", + "alwaysAsk": "Always ask", + "hideToTray": "Hide to tray", + "closeApp": "Close Application", + "closeBtnAction": "Close Button Action", + "whenCloseBtnClicked": "When close button is clicked", + "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 c31d060f1..f3d2c6b5e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -322,5 +322,15 @@ "wrestlingXXXPodcastIndexOnly": "摔跤", "writeMetadata": "写入元数据", "year": "年份", - "years": "年份" + "years": "年份", + "radioTagDisclaimerTitle": "这个电台发送了很多标签。", + "radioTagDisclaimerSubTitle": "有时电台发送的标签与音乐类型不匹配。MusicPod 不对内容负责!", + "alwaysAsk": "总是询问", + "hideToTray": "隐藏到托盘", + "closeApp": "关闭应用", + "closeBtnAction": "关闭按钮行为", + "whenCloseBtnClicked": "当点击关闭按钮时", + "closeMusicPod": "关闭 MusicPod?", + "confirmCloseOrHideTip": "请确认您想要关闭应用还是想将应用隐藏到系统托盘。", + "doNotAskAgain": "不再询问" } diff --git a/lib/main.dart b/lib/main.dart index a346fa31b..35f7c0c7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,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'; @@ -131,6 +132,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/persistence_utils.dart b/lib/persistence_utils.dart index 47c53b01c..d8373df50 100644 --- a/lib/persistence_utils.dart +++ b/lib/persistence_utils.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; -import 'package:tray_manager/tray_manager.dart'; import 'package:xdg_directories/xdg_directories.dart'; import 'constants.dart'; @@ -279,24 +278,3 @@ Future?> readUint8ListMap(String fileName) async { return theMap; } - -/// TODO: how to l10n labels? -List trayMenuItems = [ - MenuItem( - key: 'restore_window', - label: 'Hide/Restore', - ), - MenuItem.separator(), - MenuItem( - key: 'close_application', - label: 'Close Application', - ), -]; - -String trayIcon() { - if (Platform.isWindows) { - return 'assets/images/tray_icon.ico'; - } else { - return 'assets/images/tray_icon.png'; - } -} diff --git a/lib/settings/settings_model.dart b/lib/settings/settings_model.dart index aa598e8e8..bd08af6f6 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 '../constants.dart'; import 'settings_service.dart'; @@ -28,6 +29,7 @@ class SettingsModel extends SafeChangeNotifier { StreamSubscription? _themeIndexChangedSub; StreamSubscription? _recentPatchNotesDisposedChangedSub; StreamSubscription? _useArtistGridViewChangedSub; + StreamSubscription? _closeBtnActionIndexChangedSub; bool get allowManualUpdate => _service.allowManualUpdates; String? get appName => _service.appName; @@ -68,6 +70,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()); @@ -84,6 +90,8 @@ class SettingsModel extends SafeChangeNotifier { _recentPatchNotesDisposedChangedSub ??= _service .recentPatchNotesDisposedChanged .listen((_) => notifyListeners()); + _closeBtnActionIndexChangedSub ??= + _service.closeBtnActionChanged.listen((_) => notifyListeners()); } @override @@ -96,6 +104,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 db59d1bd4..8c62b59fa 100644 --- a/lib/settings/settings_service.dart +++ b/lib/settings/settings_service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import '../common/data/close_btn_action.dart'; import '../constants.dart'; import '../patch_notes/patch_notes.dart'; import '../persistence_utils.dart'; @@ -170,6 +171,29 @@ class SettingsService { } } + final _closeBtnActionIndexController = StreamController.broadcast(); + Stream get closeBtnActionChanged => + _closeBtnActionIndexController.stream; + CloseBtnAction _closeBtnActionIndex = CloseBtnAction.alwaysAsk; + CloseBtnAction get closeBtnActionIndex => _closeBtnActionIndex; + void setCloseBtnActionIndex(CloseBtnAction value) { + if (value == _closeBtnActionIndex) return; + writeSetting(kCloseBtnAction, value.toString()).then((_) { + _closeBtnActionIndex = value; + _closeBtnActionIndexController.add(true); + }); + } + + Future _initCloseBtnActionIndex() async { + final closeBtnActionIndexOrNull = await readSetting(kCloseBtnAction); + _closeBtnActionIndex = closeBtnActionIndexOrNull == null + ? CloseBtnAction.alwaysAsk + : CloseBtnAction.values.firstWhere( + (value) => value.toString() == closeBtnActionIndexOrNull, + orElse: () => CloseBtnAction.alwaysAsk, + ); + } + Future init({@visibleForTesting String? testDir}) async { await _initPackageInfo(); await _initSettings(testDir); @@ -183,6 +207,7 @@ class SettingsService { await _initPodcastIndexApiSecret(); await _initRecentPatchNotesDisposed(); await _initNeverShowImports(); + await _initCloseBtnActionIndex(); } Future dispose() async { @@ -193,5 +218,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 adb76e534..87888e9e6 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -5,7 +5,9 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.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'; @@ -37,11 +39,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(), ], ), ), @@ -97,6 +100,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/needs_translation.json b/needs_translation.json index 85d698892..582f0a3ee 100644 --- a/needs_translation.json +++ b/needs_translation.json @@ -1,14 +1,41 @@ { "cs": [ "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "da": [ "insertedIntoQueue", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" + ], + + "de": [ + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "es": [ @@ -205,7 +232,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "fr": [ @@ -433,7 +468,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "it": [ @@ -681,7 +724,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "nl": [ @@ -906,7 +957,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "pl": [ @@ -951,7 +1010,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "pt": [ @@ -1147,7 +1214,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "pt_BR": [ @@ -1343,7 +1418,15 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "ru": [ @@ -1354,12 +1437,28 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "sk": [ "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "sv": [ @@ -1545,16 +1644,27 @@ "language", "duration", "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ], "tr": [ "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" - ], - - "zh": [ - "radioTagDisclaimerTitle", - "radioTagDisclaimerSubTitle" + "radioTagDisclaimerSubTitle", + "alwaysAsk", + "hideToTray", + "closeApp", + "closeBtnAction", + "whenCloseBtnClicked", + "closeMusicPod", + "confirmCloseOrHideTip", + "doNotAskAgain" ] }