Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): add system tray functionality #824

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added assets/images/tray_icon.ico
Binary file not shown.
Binary file added assets/images/tray_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 35 additions & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'dart:io';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:phoenix_theme/phoenix_theme.dart' hide ColorX, isMobile;
import 'package:system_theme/system_theme.dart';
import 'package:tray_manager/tray_manager.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 All @@ -14,6 +17,7 @@ import '../../library/library_model.dart';
import '../../settings/settings_model.dart';
import 'scaffold.dart';
import 'splash_screen.dart';
import 'system_tray.dart';

class YaruMusicPodApp extends StatelessWidget {
const YaruMusicPodApp({
Expand Down Expand Up @@ -105,7 +109,8 @@ class _MusicPodApp extends StatefulWidget with WatchItStatefulWidgetMixin {
State<_MusicPodApp> createState() => _MusicPodAppState();
}

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

@override
Expand All @@ -118,9 +123,38 @@ class _MusicPodAppState extends State<_MusicPodApp> {
await di<LibraryModel>().init();
if (!mounted) return false;
di<ExternalPathService>().init();
if (Platform.isLinux) {
windowManager.addListener(this);
trayManager.addListener(this);
}
return true;
}

@override
void dispose() {
if (Platform.isLinux) {
windowManager.removeListener(this);
trayManager.removeListener(this);
}
super.dispose();
}

@override
void onTrayIconMouseDown() {
trayManager.popUpContextMenu();
}

@override
void onWindowEvent(String eventName) {
if ('show' == eventName || 'hide' == eventName) {
updateTrayItems(context);
}
super.onWindowEvent(eventName);
}

@override
void onTrayMenuItemClick(MenuItem menuItem) => reactToTray(menuItem);

@override
Widget build(BuildContext context) {
final themeIndex = watchPropertyValue((SettingsModel m) => m.themeIndex);
Expand Down
65 changes: 65 additions & 0 deletions lib/app/view/system_tray.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'dart:io';
dongfengweixiao marked this conversation as resolved.
Show resolved Hide resolved

import 'package:flutter/material.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';

import '../../l10n/l10n.dart';

const _exitAppMenuKey = 'exit_app';
const _showHideWindowMenuKey = 'show_hide_window';

Future<void> initTray() async {
await trayManager.setIcon(
(Platform.isWindows)
? 'assets/images/tray_icon.ico'
: 'assets/images/tray_icon.png',
);
Menu menu = Menu(
items: [
MenuItem(
key: _showHideWindowMenuKey,
label: 'Show Window',
),
MenuItem.separator(),
MenuItem(
key: _exitAppMenuKey,
label: 'Exit App',
),
],
);
await trayManager.setContextMenu(menu);
}

Future<void> updateTrayItems(BuildContext context) async {
bool isVisible = await windowManager.isVisible();
if (!context.mounted) return;
final trayMenuItems = [
MenuItem(
key: _showHideWindowMenuKey,
label: isVisible ? context.l10n.hideToTray : context.l10n.closeApp,
),
MenuItem.separator(),
MenuItem(
key: _exitAppMenuKey,
label: context.l10n.closeApp,
),
];

await trayManager.setContextMenu(Menu(items: trayMenuItems));
}

void reactToTray(MenuItem menuItem) {
switch (menuItem.key) {
case _showHideWindowMenuKey:
windowManager.isVisible().then((value) {
if (value) {
windowManager.hide();
} else {
windowManager.show();
}
});
case _exitAppMenuKey:
windowManager.close();
}
}
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' 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 {
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: Platform.isLinux
? (context) {
switch (closeBtnAction) {
case CloseBtnAction.alwaysAsk:
showDialog(
context: context,
builder: (_) => const CloseWindowActionConfirmDialog(),
);
case CloseBtnAction.hideToTray:
YaruWindow.hide(context);
case CloseBtnAction.close:
YaruWindow.close(context);
}
}
: null,
);
}

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 @@ -161,6 +161,7 @@ const kFavCountryCodes = 'favCountryCodes';
const kFavLanguageCodes = 'favLanguageCodes';
const kAscendingFeeds = 'ascendingfeed:::';
const kPatchNotesDisposed = 'kPatchNotesDisposed';
const kCloseBtnAction = 'closeBtnAction';

const shops = <String, String>{
'https://us.7digital.com/': '7digital',
Expand Down
10 changes: 9 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
13 changes: 11 additions & 2 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"cryptocurrencyXXXPodcastIndexOnly": "加密货币",
"cultureXXXPodcastIndexOnly": "文化",
"dailyXXXPodcastIndexOnly": "每日",
"dark": "暗色",
"dark": "浅色",
"decreaseSearchLimit": "请减少搜索限制",
"deletePlaylist": "删除播放列表",
"dependencies": "依赖",
Expand Down Expand Up @@ -330,5 +330,14 @@
"writeMetadata": "写入元数据",
"year": "年份",
"years": "年份",
"playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。"
"playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。",
"schedulePlaybackStopTimer": "计划停止播放的时间",
"alwaysAsk": "总是询问",
"hideToTray": "隐藏到托盘",
"closeBtnAction": "关闭按钮的行为",
"whenCloseBtnClicked": "当点击关闭按钮时",
"closeApp": "关闭应用",
"closeMusicPod": "关闭 MusicPod?",
"confirmCloseOrHideTip": "请确定您想要退出应用还是隐藏到托盘?",
"doNotAskAgain": "不再询问"
}
13 changes: 11 additions & 2 deletions lib/l10n/app_zh_CN.arb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"cryptocurrencyXXXPodcastIndexOnly": "加密货币",
"cultureXXXPodcastIndexOnly": "文化",
"dailyXXXPodcastIndexOnly": "每日",
"dark": "暗色",
"dark": "浅色",
"decreaseSearchLimit": "请减少搜索限制",
"deletePlaylist": "删除播放列表",
"dependencies": "依赖",
Expand Down Expand Up @@ -330,5 +330,14 @@
"writeMetadata": "写入元数据",
"year": "年份",
"years": "年份",
"playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。"
"playbackWillStopIn": "播放将在 {duration} ({timeOfDay})后停止。",
"schedulePlaybackStopTimer": "计划停止播放的时间",
"alwaysAsk": "总是询问",
"hideToTray": "隐藏到托盘",
"closeBtnAction": "关闭按钮的行为",
"whenCloseBtnClicked": "当点击关闭按钮时",
"closeApp": "关闭应用",
"closeMusicPod": "关闭 MusicPod?",
"confirmCloseOrHideTip": "请确定您想要退出应用还是隐藏到托盘?",
"doNotAskAgain": "不再询问"
}
5 changes: 5 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -133,6 +134,10 @@ Future<void> main(List<String> args) async {
final gitHub = GitHub();
di.registerSingleton<GitHub>(gitHub);

if (Platform.isLinux) {
await initTray();
}

// Register ViewModels
di.registerLazySingleton<SettingsModel>(
() => SettingsModel(
Expand Down
Loading