Skip to content

Commit

Permalink
feat: add close button action settings
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
dongfengweixiao committed Aug 4, 2024
1 parent 5e3c96b commit 3b5f9fd
Show file tree
Hide file tree
Showing 13 changed files with 396 additions and 59 deletions.
16 changes: 12 additions & 4 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -105,16 +106,13 @@ class _MusicPodApp extends StatefulWidget with WatchItStatefulWidgetMixin {
}

class _MusicPodAppState extends State<_MusicPodApp>
with WidgetsBindingObserver {
with WidgetsBindingObserver, WindowListener {
late Future<bool> _initFuture;
late SystemTray _tray;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_tray = SystemTray();
_tray.init();
_initFuture = _init();
}

Expand All @@ -137,6 +135,8 @@ class _MusicPodAppState extends State<_MusicPodApp>

if (!mounted) return false;
di<ExternalPathService>().init();
di<SystemTray>().updateTrayMenuItems(context);
windowManager.addListener(this);

return true;
}
Expand All @@ -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<SystemTray>().updateTrayMenuItems(context);
}
}

@override
Widget build(BuildContext context) {
final themeIndex = watchPropertyValue((SettingsModel m) => m.themeIndex);
Expand Down
48 changes: 35 additions & 13 deletions lib/app/view/system_tray.dart
Original file line number Diff line number Diff line change
@@ -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<MenuItem> trayMenuItems;

Future<void> 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<void> dispose() async {
trayManager.removeListener(this);
}

Future<void> 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();
Expand Down
16 changes: 16 additions & 0 deletions lib/common/data/close_btn_action.dart
Original file line number Diff line number Diff line change
@@ -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,
};
}
93 changes: 93 additions & 0 deletions lib/common/view/header_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
},
);
}

Expand All @@ -101,6 +123,77 @@ class HeaderBar extends StatelessWidget
);
}

class CloseWindowActionConfirmDialog extends StatefulWidget {
const CloseWindowActionConfirmDialog({super.key});

@override
State<CloseWindowActionConfirmDialog> createState() =>
_CloseWindowActionConfirmDialogState();
}

class _CloseWindowActionConfirmDialogState
extends State<CloseWindowActionConfirmDialog> {
bool rememberChoice = false;
@override
Widget build(BuildContext context) {
final model = di<SettingsModel>();
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});

Expand Down
1 change: 1 addition & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ const kPodcastIndexApiKey = 'podcastIndexApiKey';
const kPodcastIndexApiSecret = 'podcastIndexApiSecret';
const kUseArtistGridView = 'useArtistGridView';
const kSearchPageId = 'searchPageId';
const kCloseBtnAction = 'closeBtnAction';

const shops = <String, String>{
'https://us.7digital.com/': '7digital',
Expand Down
11 changes: 9 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
12 changes: 11 additions & 1 deletion lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -322,5 +322,15 @@
"wrestlingXXXPodcastIndexOnly": "摔跤",
"writeMetadata": "写入元数据",
"year": "年份",
"years": "年份"
"years": "年份",
"radioTagDisclaimerTitle": "这个电台发送了很多标签。",
"radioTagDisclaimerSubTitle": "有时电台发送的标签与音乐类型不匹配。MusicPod 不对内容负责!",
"alwaysAsk": "总是询问",
"hideToTray": "隐藏到托盘",
"closeApp": "关闭应用",
"closeBtnAction": "关闭按钮行为",
"whenCloseBtnClicked": "当点击关闭按钮时",
"closeMusicPod": "关闭 MusicPod?",
"confirmCloseOrHideTip": "请确认您想要关闭应用还是想将应用隐藏到系统托盘。",
"doNotAskAgain": "不再询问"
}
8 changes: 8 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -131,6 +132,13 @@ Future<void> main(List<String> args) async {
final gitHub = GitHub();
di.registerSingleton<GitHub>(gitHub);

final systemTray = SystemTray();
await systemTray.init();
di.registerSingleton<SystemTray>(
systemTray,
dispose: (s) async => s.dispose(),
);

// Register ViewModels
di.registerLazySingleton<SettingsModel>(
() => SettingsModel(
Expand Down
21 changes: 0 additions & 21 deletions lib/persistence_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -280,23 +279,3 @@ Future<Map<String, Uint8List?>?> readUint8ListMap(String fileName) async {
return theMap;
}

/// TODO: how to l10n labels?
List<MenuItem> 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';
}
}
Loading

0 comments on commit 3b5f9fd

Please sign in to comment.