Skip to content

Commit

Permalink
feat: system tray support (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Apr 10, 2023
1 parent 5e47faa commit 06a0437
Show file tree
Hide file tree
Showing 14 changed files with 304 additions and 95 deletions.
Binary file added assets/spotube-logo.ico
Binary file not shown.
1 change: 1 addition & 0 deletions lib/collections/spotube_icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ abstract class SpotubeIcons {
static const genres = FeatherIcons.music;
static const zoomIn = FeatherIcons.zoomIn;
static const zoomOut = FeatherIcons.zoomOut;
static const tray = FeatherIcons.chevronDown;
}
20 changes: 16 additions & 4 deletions lib/components/shared/page_window_title_bar.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:titlebar_buttons/titlebar_buttons.dart';
import 'package:window_manager/window_manager.dart';
Expand Down Expand Up @@ -85,18 +87,28 @@ class _PageWindowTitleBarState extends State<PageWindowTitleBar> {
}
}

class WindowTitleBarButtons extends HookWidget {
class WindowTitleBarButtons extends HookConsumerWidget {
final Color? foregroundColor;
const WindowTitleBarButtons({
Key? key,
this.foregroundColor,
}) : super(key: key);

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
final closeBehavior =
ref.watch(userPreferencesProvider.select((s) => s.closeBehavior));
final isMaximized = useState<bool?>(null);
const type = ThemeType.auto;

void onClose() {
if (closeBehavior == CloseBehavior.close) {
windowManager.close();
} else {
windowManager.hide();
}
}

useEffect(() {
if (kIsDesktop) {
windowManager.isMaximized().then((value) {
Expand Down Expand Up @@ -157,7 +169,7 @@ class WindowTitleBarButtons extends HookWidget {
),
CloseWindowButton(
colors: closeColors,
onPressed: windowManager.close,
onPressed: onClose,
),
],
),
Expand Down Expand Up @@ -187,7 +199,7 @@ class WindowTitleBarButtons extends HookWidget {
),
DecoratedCloseButton(
type: type,
onPressed: windowManager.close,
onPressed: onClose,
),
],
),
Expand Down
125 changes: 125 additions & 0 deletions lib/hooks/use_init_sys_tray.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';

void useInitSysTray(WidgetRef ref) {
final context = useContext();
final systemTray = useRef<SystemTray?>(null);

final initializeMenu = useCallback(() async {
systemTray.value?.destroy();
final playlistQueue = ref.read(PlaylistQueueNotifier.notifier);
final preferences = ref.read(userPreferencesProvider);
if (!preferences.showSystemTrayIcon) {
await systemTray.value?.destroy();
systemTray.value = null;
return;
}
final enabled =
playlistQueue.isLoaded && playlistQueue.state?.isLoading != true;
systemTray.value = await DesktopTools.createSystemTrayMenu(
title: "Spotube",
iconPath: "assets/spotube-logo.png",
windowsIconPath: "assets/spotube-logo.ico",
items: [
MenuItemLabel(
label: "Show/Hide",
name: "show-hide",
onClicked: (item) async {
if (await DesktopTools.window.isVisible()) {
await DesktopTools.window.hide();
} else {
await DesktopTools.window.show();
}
},
),
MenuSeparator(),
MenuItemLabel(
label: "Play/Pause",
name: "play-pause",
enabled: enabled,
onClicked: (_) async {
Actions.maybeInvoke<PlayPauseIntent>(
context, PlayPauseIntent(ref)) ??
PlayPauseAction().invoke(PlayPauseIntent(ref));
},
),
MenuItemLabel(
label: "Next",
name: "next",
enabled: enabled && (playlistQueue.state?.tracks.length ?? 0) > 1,
onClicked: (p0) async {
await playlistQueue.next();
},
),
MenuItemLabel(
label: "Previous",
name: "previous",
enabled: enabled && (playlistQueue.state?.tracks.length ?? 0) > 1,
onClicked: (p0) async {
await playlistQueue.previous();
},
),
MenuSeparator(),
MenuItemLabel(
label: "Quit",
name: "quit",
onClicked: (item) async {
await DesktopTools.window.close();
},
),
],
onEvent: (event, tray) async {
if (DesktopTools.platform.isWindows) {
switch (event) {
case SystemTrayEvent.click:
await DesktopTools.window.show();
break;
case SystemTrayEvent.rightClick:
await tray.popUpContextMenu();
break;
default:
}
} else {
switch (event) {
case SystemTrayEvent.rightClick:
await DesktopTools.window.show();
break;
case SystemTrayEvent.click:
await tray.popUpContextMenu();
break;
default:
}
}
},
);
}, [ref]);

useReassemble(initializeMenu);

ref.listen<PlaylistQueue?>(
PlaylistQueueNotifier.provider,
(previous, next) {
initializeMenu();
},
);
ref.listen(
userPreferencesProvider.select((s) => s.showSystemTrayIcon),
(previous, next) {
initializeMenu();
},
);

useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
initializeMenu();
});
return () async {
await systemTray.value?.destroy();
};
}, [initializeMenu]);
}
77 changes: 15 additions & 62 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
Expand All @@ -7,13 +6,13 @@ import 'package:fl_query/fl_query.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:metadata_god/metadata_god.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/cache_keys.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/entities/cache_track.dart';
Expand All @@ -26,11 +25,10 @@ import 'package:spotube/services/audio_player.dart';
import 'package:spotube/services/pocketbase.dart';
import 'package:spotube/services/youtube.dart';
import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart';
import 'package:system_theme/system_theme.dart';

import 'hooks/use_init_sys_tray.dart';

Future<void> main(List<String> rawArgs) async {
final parser = ArgParser();

Expand Down Expand Up @@ -69,6 +67,15 @@ Future<void> main(List<String> rawArgs) async {
}

WidgetsFlutterBinding.ensureInitialized();
await DesktopTools.ensureInitialized(
DesktopWindowOptions(
hideTitleBar: true,
title: "Spotube",
backgroundColor: Colors.transparent,
minimumSize: const Size(300, 700),
),
);

await SystemTheme.accentColor.load();
MetadataGod.initialize();
await QueryClient.initialize(cachePrefix: "oss.krtirtho.spotube");
Expand All @@ -77,32 +84,6 @@ Future<void> main(List<String> rawArgs) async {
Hive.registerAdapter(CacheTrackSkipSegmentAdapter());
await Env.configure();

if (kIsDesktop) {
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
center: true,
backgroundColor: Colors.transparent,
titleBarStyle: TitleBarStyle.hidden,
title: "Spotube",
);
setWindowMinSize(const Size(kReleaseMode ? 1020 : 300, 700));
await windowManager.waitUntilReadyToShow(windowOptions, () async {
final localStorage = await SharedPreferences.getInstance();
final rawSize = localStorage.getString(LocalStorageKeys.windowSizeInfo);
final savedSize = rawSize != null ? json.decode(rawSize) : null;
final wasMaximized = savedSize?["maximized"] ?? false;
final double? height = savedSize?["height"];
final double? width = savedSize?["width"];
await windowManager.setResizable(true);
if (wasMaximized) {
await windowManager.maximize();
} else if (height != null && width != null) {
await windowManager.setSize(Size(width, height));
}
await windowManager.show();
});
}

Catcher(
enableLogger: arguments["verbose"],
debugConfig: CatcherOptions(
Expand Down Expand Up @@ -188,44 +169,14 @@ class Spotube extends StatefulHookConsumerWidget {
context.findAncestorStateOfType<SpotubeState>()!;
}

class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
class SpotubeState extends ConsumerState<Spotube> {
final logger = getLogger(Spotube);
SharedPreferences? localStorage;

Size? prevSize;

@override
void initState() {
super.initState();
SharedPreferences.getInstance().then(((value) => localStorage = value));
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
void didChangeMetrics() async {
super.didChangeMetrics();
if (kIsMobile) return;
final size = await windowManager.getSize();
final windowSameDimension =
prevSize?.width == size.width && prevSize?.height == size.height;

if (localStorage == null || windowSameDimension) return;
final isMaximized = await windowManager.isMaximized();
localStorage!.setString(
LocalStorageKeys.windowSizeInfo,
jsonEncode({
'maximized': isMaximized,
'width': size.width,
'height': size.height,
}),
);
prevSize = size;
}

@override
Expand All @@ -235,6 +186,8 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
final accentMaterialColor =
ref.watch(userPreferencesProvider.select((s) => s.accentColorScheme));

useInitSysTray(ref);

/// For enabling hot reload for audio player
useEffect(() {
return () {
Expand Down
46 changes: 38 additions & 8 deletions lib/pages/settings/settings.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
Expand Down Expand Up @@ -198,14 +199,6 @@ class SettingsPage extends HookConsumerWidget {
),
onTap: pickColorScheme(),
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.album),
title: const Text("Rotating Album Art"),
value: preferences.rotatingAlbumArt,
onChanged: (state) {
preferences.setRotatingAlbumArt(state);
},
),
Text(
" Playback",
style: theme.textTheme.headlineSmall
Expand Down Expand Up @@ -321,6 +314,43 @@ class SettingsPage extends HookConsumerWidget {
preferences.setSaveTrackLyrics(state);
},
),
if (DesktopTools.platform.isDesktop) ...[
Text(
" Desktop",
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
AdaptiveListTile(
leading: const Icon(SpotubeIcons.close),
title: const Text("Close Behavior"),
trailing: (context, update) =>
DropdownButton<CloseBehavior>(
value: preferences.closeBehavior,
items: const [
DropdownMenuItem(
value: CloseBehavior.close,
child: Text("Close"),
),
DropdownMenuItem(
value: CloseBehavior.minimizeToTray,
child: Text("Minimize to Tray"),
),
],
onChanged: (value) {
if (value != null) {
preferences.setCloseBehavior(value);
update?.call(() {});
}
},
),
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.tray),
title: const Text("Show System Tray Icon"),
value: preferences.showSystemTrayIcon,
onChanged: preferences.setShowSystemTrayIcon,
),
],
Text(
" About",
style: theme.textTheme.headlineSmall
Expand Down

0 comments on commit 06a0437

Please sign in to comment.