From 930539ca483a9fbedd40a241ee133e28a9076a94 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 15 Apr 2024 19:47:15 +0600 Subject: [PATCH 01/57] chore: fix analyzer issues --- lib/components/desktop_login/login_form.dart | 3 +-- lib/components/shared/waypoint.dart | 8 +++----- lib/hooks/configurators/use_get_storage_perms.dart | 6 +++--- lib/hooks/utils/use_palette_color.dart | 7 +++---- lib/pages/root/root_app.dart | 3 +-- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart index 2949fbae7..6091829c0 100644 --- a/lib/components/desktop_login/login_form.dart +++ b/lib/components/desktop_login/login_form.dart @@ -16,7 +16,6 @@ class TokenLoginForm extends HookConsumerWidget { Widget build(BuildContext context, ref) { final authenticationNotifier = ref.watch(authenticationProvider.notifier); final directCodeController = useTextEditingController(); - final mounted = useIsMounted(); final isLoading = useState(false); @@ -57,7 +56,7 @@ class TokenLoginForm extends HookConsumerWidget { await AuthenticationCredentials.fromCookie( cookieHeader), ); - if (mounted()) { + if (context.mounted) { onDone?.call(); } } finally { diff --git a/lib/components/shared/waypoint.dart b/lib/components/shared/waypoint.dart index 08e9088a1..cf00e29b6 100644 --- a/lib/components/shared/waypoint.dart +++ b/lib/components/shared/waypoint.dart @@ -20,8 +20,6 @@ class Waypoint extends HookWidget { @override Widget build(BuildContext context) { - final isMounted = useIsMounted(); - useEffect(() { if (isGrid) { return null; @@ -32,19 +30,19 @@ class Waypoint extends HookWidget { // scrollController fetches the next paginated data when the current // position of the user on the screen has surpassed - if (controller.position.pixels >= nextPageTrigger && isMounted()) { + if (controller.position.pixels >= nextPageTrigger && context.mounted) { await onTouchEdge?.call(); } } WidgetsBinding.instance.addPostFrameCallback((_) { - if (controller.hasClients && isMounted()) { + if (controller.hasClients && context.mounted) { listener(); controller.addListener(listener); } }); return () => controller.removeListener(listener); - }, [controller, onTouchEdge, isMounted]); + }, [controller, onTouchEdge]); if (isGrid) { return VisibilityDetector( diff --git a/lib/hooks/configurators/use_get_storage_perms.dart b/lib/hooks/configurators/use_get_storage_perms.dart index 86b495c40..db51af146 100644 --- a/lib/hooks/configurators/use_get_storage_perms.dart +++ b/lib/hooks/configurators/use_get_storage_perms.dart @@ -7,7 +7,7 @@ import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/hooks/utils/use_async_effect.dart'; void useGetStoragePermissions(WidgetRef ref) { - final isMounted = useIsMounted(); + final context = useContext(); useAsyncEffect( () async { @@ -25,11 +25,11 @@ void useGetStoragePermissions(WidgetRef ref) { if (hasNoStoragePerm) { await Permission.storage.request(); - if (isMounted()) ref.invalidate(localTracksProvider); + if (context.mounted) ref.invalidate(localTracksProvider); } if (hasNoAudioPerm) { await Permission.audio.request(); - if (isMounted()) ref.invalidate(localTracksProvider); + if (context.mounted) ref.invalidate(localTracksProvider); } }, null, diff --git a/lib/hooks/utils/use_palette_color.dart b/lib/hooks/utils/use_palette_color.dart index 9269edd79..e6d8b3988 100644 --- a/lib/hooks/utils/use_palette_color.dart +++ b/lib/hooks/utils/use_palette_color.dart @@ -14,7 +14,6 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) { final context = useContext(); final theme = Theme.of(context); final paletteColor = ref.watch(_paletteColorState); - final mounted = useIsMounted(); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { @@ -25,7 +24,7 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) { width: 50, ), ); - if (!mounted()) return; + if (!context.mounted) return; final color = theme.brightness == Brightness.light ? palette.lightMutedColor ?? palette.lightVibrantColor : palette.darkMutedColor ?? palette.darkVibrantColor; @@ -41,7 +40,7 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) { PaletteGenerator usePaletteGenerator(String imageUrl) { final palette = useState(PaletteGenerator.fromColors([])); - final mounted = useIsMounted(); + final context = useContext(); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { @@ -52,7 +51,7 @@ PaletteGenerator usePaletteGenerator(String imageUrl) { width: 50, ), ); - if (!mounted()) return; + if (!context.mounted) return; palette.value = newPalette; }); diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 56ea43a64..5ac0689a1 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -38,7 +38,6 @@ class RootApp extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final isMounted = useIsMounted(); final showingDialogCompleter = useRef(Completer()..complete()); final downloader = ref.watch(downloadManagerProvider); final scaffoldMessenger = ScaffoldMessenger.of(context); @@ -129,7 +128,7 @@ class RootApp extends HookConsumerWidget { useEffect(() { downloader.onFileExists = (track) async { - if (!isMounted()) return false; + if (!context.mounted) return false; if (!showingDialogCompleter.value.isCompleted) { await showingDialogCompleter.value.future; From 6907f9c756d8f49aadb1b23a2a1dc8bf7d658dc0 Mon Sep 17 00:00:00 2001 From: Kshamendra Date: Wed, 17 Apr 2024 18:25:06 +0530 Subject: [PATCH 02/57] fix(updater): dead link (#1408) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * Update use_update_checker.dart --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho --- README.md | 7 +------ lib/hooks/configurators/use_update_checker.dart | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8b8a62143..f2666fbc2 100644 --- a/README.md +++ b/README.md @@ -97,12 +97,7 @@ This handy table lists all the methods you can use to install Spotube: AppImage - - - Download AppImage - -

Note: AppimageLauncher is required!

- + AppImage's lacking stability led to it's temporal removal. More information at https://github.com/KRTirtho/spotube/issues/1082 Debian/Ubuntu diff --git a/lib/hooks/configurators/use_update_checker.dart b/lib/hooks/configurators/use_update_checker.dart index 1a6a5be52..7b937efbf 100644 --- a/lib/hooks/configurators/use_update_checker.dart +++ b/lib/hooks/configurators/use_update_checker.dart @@ -62,7 +62,7 @@ void useUpdateChecker(WidgetRef ref) { barrierColor: Colors.black26, builder: (context) { const url = - "https://spotube.krtirtho.dev/other-downloads/stable-downloads"; + "https://spotube.krtirtho.dev/downloads"; return AlertDialog( title: const Text("Spotube has an update"), actions: [ From 7ac791757abb30f40374c169c4211916287bb3f3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 17 Apr 2024 22:20:13 +0600 Subject: [PATCH 03/57] fix(linux): tray icon not showing #541 upgrade old packages --- .fvm/fvm_config.json | 2 +- .github/workflows/spotube-publish-binary.yml | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- lib/collections/env.dart | 4 +- lib/collections/initializers.dart | 5 +- lib/components/root/bottom_player.dart | 18 +- .../inter_scrollbar/inter_scrollbar.dart | 4 +- .../shared/page_window_title_bar.dart | 35 +- .../sections/header/flexible_header.dart | 5 +- .../shared/tracks_view/track_view.dart | 5 +- .../configurators/use_close_behavior.dart | 26 +- lib/hooks/configurators/use_deep_linking.dart | 4 +- .../use_disable_battery_optimizations.dart | 6 +- .../configurators/use_get_storage_perms.dart | 5 +- .../configurators/use_init_sys_tray.dart | 128 ---- .../configurators/use_window_listener.dart | 10 +- lib/main.dart | 33 +- lib/models/connect/connect.freezed.dart | 2 +- lib/models/spotify/home_feed.freezed.dart | 2 +- .../spotify/recommendation_seeds.freezed.dart | 2 +- lib/pages/home/genres/genre_playlists.dart | 8 +- lib/pages/lyrics/mini_lyrics.dart | 92 +-- lib/pages/root/root_app.dart | 4 +- lib/pages/settings/sections/desktop.dart | 4 +- lib/pages/settings/sections/downloads.dart | 4 +- lib/pages/settings/settings.dart | 5 +- lib/provider/discord_provider.dart | 6 +- lib/provider/tray_manager/tray_manager.dart | 79 +++ lib/provider/tray_manager/tray_menu.dart | 108 +++ .../user_preferences_provider.dart | 10 +- .../user_preferences_state.dart | 4 +- .../user_preferences_state.freezed.dart | 6 +- .../user_preferences_state.g.dart | 4 +- lib/services/audio_player/audio_player.dart | 2 +- lib/services/audio_player/custom_player.dart | 6 +- .../audio_services/audio_services.dart | 10 +- lib/services/kv_store/kv_store.dart | 20 + lib/services/song_link/song_link.freezed.dart | 2 +- lib/services/sourced_track/sources/piped.dart | 2 +- lib/services/wm_tools/wm_tools.dart | 88 +++ linux/flutter/generated_plugin_registrant.cc | 8 +- linux/flutter/generated_plugins.cmake | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 6 +- macos/Podfile.lock | 20 +- pubspec.lock | 658 ++++++++---------- pubspec.yaml | 96 ++- .../flutter/generated_plugin_registrant.cc | 6 +- windows/flutter/generated_plugins.cmake | 2 +- 48 files changed, 840 insertions(+), 722 deletions(-) delete mode 100644 lib/hooks/configurators/use_init_sys_tray.dart create mode 100644 lib/provider/tray_manager/tray_manager.dart create mode 100644 lib/provider/tray_manager/tray_menu.dart create mode 100644 lib/services/wm_tools/wm_tools.dart diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 7ca74200a..d42a42fae 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.19.1", + "flutterSdkVersion": "3.19.5", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 805a89ac3..960507f9b 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to publish (x.x.x) - default: 3.1.0 + default: 3.6.0 required: true dry_run: description: Dry run diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index d9fbd0c7d..969e1b77c 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -26,7 +26,7 @@ on: default: true env: - FLUTTER_VERSION: '3.19.1' + FLUTTER_VERSION: '3.19.5' jobs: windows: diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 50fe1e6a7..14f33b806 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -1,5 +1,5 @@ import 'package:envied/envied.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:spotube/utils/platform.dart'; part 'env.g.dart'; @@ -26,7 +26,7 @@ abstract class Env { static final String _enableUpdateChecker = _Env._enableUpdateChecker; static bool get enableUpdateChecker => - DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1"; + kIsFlatpak || _enableUpdateChecker == "1"; static String discordAppId = "1176718791388975124"; } diff --git a/lib/collections/initializers.dart b/lib/collections/initializers.dart index 9627de1c3..976661fc2 100644 --- a/lib/collections/initializers.dart +++ b/lib/collections/initializers.dart @@ -1,9 +1,10 @@ import 'dart:io'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; + +import 'package:spotube/utils/platform.dart'; import 'package:win32_registry/win32_registry.dart'; Future registerWindowsScheme(String scheme) async { - if (!DesktopTools.platform.isWindows) return; + if (!kIsWindows) return; String appPath = Platform.resolvedExecutable; String protocolRegKey = 'Software\\Classes\\$scheme'; diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 06250131e..5429e172b 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -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'; @@ -24,6 +23,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; class BottomPlayer extends HookConsumerWidget { BottomPlayer({super.key}); @@ -95,19 +95,19 @@ class BottomPlayer extends HookConsumerWidget { tooltip: context.l10n.mini_player, icon: const Icon(SpotubeIcons.miniPlayer), onPressed: () async { - final prevSize = - await DesktopTools.window.getSize(); - await DesktopTools.window.setMinimumSize( + if (!kIsDesktop) return; + + final prevSize = await windowManager.getSize(); + await windowManager.setMinimumSize( const Size(300, 300), ); - await DesktopTools.window.setAlwaysOnTop(true); + await windowManager.setAlwaysOnTop(true); if (!kIsLinux) { - await DesktopTools.window.setHasShadow(false); + await windowManager.setHasShadow(false); } - await DesktopTools.window + await windowManager .setAlignment(Alignment.topRight); - await DesktopTools.window - .setSize(const Size(400, 500)); + await windowManager.setSize(const Size(400, 500)); await Future.delayed( const Duration(milliseconds: 100), () async { diff --git a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart b/lib/components/shared/inter_scrollbar/inter_scrollbar.dart index 2b3ce3192..8a86b6439 100644 --- a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart +++ b/lib/components/shared/inter_scrollbar/inter_scrollbar.dart @@ -1,7 +1,7 @@ import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/utils/platform.dart'; class InterScrollbar extends HookWidget { final Widget child; @@ -15,7 +15,7 @@ class InterScrollbar extends HookWidget { @override Widget build(BuildContext context) { - if (DesktopTools.platform.isDesktop) return child; + if (kIsDesktop) return child; return DraggableScrollbar.semicircle( controller: controller, diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index 37daefa95..f19757f3b 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -7,7 +7,8 @@ import 'package:titlebar_buttons/titlebar_buttons.dart'; import 'dart:math'; import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:io' show Platform; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; + +import 'package:window_manager/window_manager.dart'; class PageWindowTitleBar extends StatefulHookConsumerWidget implements PreferredSizeWidget { @@ -89,7 +90,7 @@ class _PageWindowTitleBarState extends ConsumerState { final systemTitleBar = ref.read(userPreferencesProvider.select((s) => s.systemTitleBar)); if (kIsDesktop && !systemTitleBar) { - DesktopTools.window.startDragging(); + windowManager.startDragging(); } } @@ -107,11 +108,7 @@ class _PageWindowTitleBarState extends ConsumerState { return SliverPadding( padding: EdgeInsets.only( - left: DesktopTools.platform.isMacOS && - hasFullscreen && - hasLeadingOrCanPop - ? 65 - : 0, + left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0, ), sliver: SliverAppBar( leading: widget.leading, @@ -149,11 +146,7 @@ class _PageWindowTitleBarState extends ConsumerState { onVerticalDragStart: onDrag, child: Padding( padding: EdgeInsets.only( - left: DesktopTools.platform.isMacOS && - hasFullscreen && - hasLeadingOrCanPop - ? 65 - : 0, + left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0, ), child: AppBar( leading: widget.leading, @@ -193,12 +186,12 @@ class WindowTitleBarButtons extends HookConsumerWidget { const type = ThemeType.auto; Future onClose() async { - await DesktopTools.window.close(); + await windowManager.close(); } useEffect(() { if (kIsDesktop) { - DesktopTools.window.isMaximized().then((value) { + windowManager.isMaximized().then((value) { isMaximized.value = value; }); } @@ -235,14 +228,14 @@ class WindowTitleBarButtons extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ MinimizeWindowButton( - onPressed: DesktopTools.window.minimize, + onPressed: windowManager.minimize, colors: colors, ), if (isMaximized.value != true) MaximizeWindowButton( colors: colors, onPressed: () { - DesktopTools.window.maximize(); + windowManager.maximize(); isMaximized.value = true; }, ) @@ -250,7 +243,7 @@ class WindowTitleBarButtons extends HookConsumerWidget { RestoreWindowButton( colors: colors, onPressed: () { - DesktopTools.window.unmaximize(); + windowManager.unmaximize(); isMaximized.value = false; }, ), @@ -270,16 +263,16 @@ class WindowTitleBarButtons extends HookConsumerWidget { children: [ DecoratedMinimizeButton( type: type, - onPressed: DesktopTools.window.minimize, + onPressed: windowManager.minimize, ), DecoratedMaximizeButton( type: type, onPressed: () async { - if (await DesktopTools.window.isMaximized()) { - await DesktopTools.window.unmaximize(); + if (await windowManager.isMaximized()) { + await windowManager.unmaximize(); isMaximized.value = false; } else { - await DesktopTools.window.maximize(); + await windowManager.maximize(); isMaximized.value = true; } }, diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart index 4a7043023..d6e71e8f8 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -12,6 +12,7 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:gap/gap.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; +import 'package:spotube/utils/platform.dart'; class TrackViewFlexHeader extends HookConsumerWidget { const TrackViewFlexHeader({super.key}); @@ -53,7 +54,7 @@ class TrackViewFlexHeader extends HookConsumerWidget { floating: false, pinned: true, expandedHeight: 450, - automaticallyImplyLeading: DesktopTools.platform.isMobile, + automaticallyImplyLeading: kIsMobile, backgroundColor: palette.color, title: isExpanded ? null : Text(props.title, style: headingStyle), flexibleSpace: FlexibleSpaceBar( diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/shared/tracks_view/track_view.dart index eb8f68712..03d628a8a 100644 --- a/lib/components/shared/tracks_view/track_view.dart +++ b/lib/components/shared/tracks_view/track_view.dart @@ -1,5 +1,5 @@ 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:sliver_tools/sliver_tools.dart'; @@ -8,6 +8,7 @@ import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/utils/platform.dart'; class TrackView extends HookConsumerWidget { const TrackView({super.key}); @@ -18,7 +19,7 @@ class TrackView extends HookConsumerWidget { final controller = useScrollController(); return Scaffold( - appBar: DesktopTools.platform.isDesktop + appBar: kIsDesktop ? const PageWindowTitleBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, diff --git a/lib/hooks/configurators/use_close_behavior.dart b/lib/hooks/configurators/use_close_behavior.dart index 79b14fa96..3df6a5288 100644 --- a/lib/hooks/configurators/use_close_behavior.dart +++ b/lib/hooks/configurators/use_close_behavior.dart @@ -1,29 +1,31 @@ import 'dart:io'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/configurators/use_window_listener.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -// ignore: depend_on_referenced_packages import 'package:local_notifier/local_notifier.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; -final closeNotification = DesktopTools.createNotification( - title: 'Spotube', - message: 'Running in background. Minimized to System Tray', - actions: [ - LocalNotificationAction(text: 'Close The App'), - ], -)?..onClickAction = (value) { - exit(0); - }; +final closeNotification = !kIsDesktop + ? null + : (LocalNotification( + title: 'Spotube', + body: 'Running in background. Minimized to System Tray', + actions: [ + LocalNotificationAction(text: 'Close The App'), + ], + )..onClickAction = (value) { + exit(0); + }); void useCloseBehavior(WidgetRef ref) { useWindowListener( onWindowClose: () async { final preferences = ref.read(userPreferencesProvider); if (preferences.closeBehavior == CloseBehavior.minimizeToTray) { - await DesktopTools.window.hide(); + await windowManager.hide(); closeNotification?.show(); } else { exit(0); diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index 2650b05ca..90d062dc7 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/routes.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:spotube/utils/platform.dart'; final appLinks = AppLinks(); final linkStream = appLinks.allStringLinkStream.asBroadcastStream(); @@ -53,7 +53,7 @@ void useDeepLinking(WidgetRef ref) { StreamSubscription? mediaStream; - if (DesktopTools.platform.isMobile) { + if (kIsMobile) { FlutterSharingIntent.instance.getInitialSharing().then(uriListener); mediaStream = diff --git a/lib/hooks/configurators/use_disable_battery_optimizations.dart b/lib/hooks/configurators/use_disable_battery_optimizations.dart index a9afef456..4aa51b741 100644 --- a/lib/hooks/configurators/use_disable_battery_optimizations.dart +++ b/lib/hooks/configurators/use_disable_battery_optimizations.dart @@ -1,12 +1,12 @@ import 'package:disable_battery_optimization/disable_battery_optimization.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; + import 'package:spotube/hooks/utils/use_async_effect.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/utils/platform.dart'; void useDisableBatteryOptimizations() { useAsyncEffect(() async { - if (!DesktopTools.platform.isAndroid || - KVStoreService.askedForBatteryOptimization) return; + if (!kIsAndroid || KVStoreService.askedForBatteryOptimization) return; await DisableBatteryOptimization.showDisableBatteryOptimizationSettings(); diff --git a/lib/hooks/configurators/use_get_storage_perms.dart b/lib/hooks/configurators/use_get_storage_perms.dart index db51af146..bcc340422 100644 --- a/lib/hooks/configurators/use_get_storage_perms.dart +++ b/lib/hooks/configurators/use_get_storage_perms.dart @@ -1,17 +1,18 @@ import 'package:device_info_plus/device_info_plus.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:permission_handler/permission_handler.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/hooks/utils/use_async_effect.dart'; +import 'package:spotube/utils/platform.dart'; void useGetStoragePermissions(WidgetRef ref) { final context = useContext(); useAsyncEffect( () async { - if (!DesktopTools.platform.isMobile) return; + if (!kIsMobile) return; final androidInfo = await DeviceInfoPlugin().androidInfo; diff --git a/lib/hooks/configurators/use_init_sys_tray.dart b/lib/hooks/configurators/use_init_sys_tray.dart deleted file mode 100644 index 0bce67270..000000000 --- a/lib/hooks/configurators/use_init_sys_tray.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:io'; - -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/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -void useInitSysTray(WidgetRef ref) { - final context = useContext(); - final systemTray = useRef(null); - - final initializeMenu = useCallback(() async { - systemTray.value?.destroy(); - final playlist = ref.read(proxyPlaylistProvider); - final playlistQueue = ref.read(proxyPlaylistProvider.notifier); - final preferences = ref.read(userPreferencesProvider); - if (!preferences.showSystemTrayIcon) { - await systemTray.value?.destroy(); - systemTray.value = null; - return; - } - final enabled = !playlist.isFetching; - systemTray.value = await DesktopTools.createSystemTrayMenu( - title: DesktopTools.platform.isWindows ? "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( - context, PlayPauseIntent(ref)) ?? - PlayPauseAction().invoke(PlayPauseIntent(ref)); - }, - ), - MenuItemLabel( - label: "Next", - name: "next", - enabled: enabled && (playlist.tracks.length) > 1, - onClicked: (p0) async { - await playlistQueue.next(); - }, - ), - MenuItemLabel( - label: "Previous", - name: "previous", - enabled: enabled && (playlist.tracks.length) > 1, - onClicked: (p0) async { - await playlistQueue.previous(); - }, - ), - MenuSeparator(), - MenuItemLabel( - label: "Quit", - name: "quit", - onClicked: (item) async { - exit(0); - }, - ), - ], - 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( - proxyPlaylistProvider, - (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]); -} diff --git a/lib/hooks/configurators/use_window_listener.dart b/lib/hooks/configurators/use_window_listener.dart index b91ad413f..5977ea8e8 100644 --- a/lib/hooks/configurators/use_window_listener.dart +++ b/lib/hooks/configurators/use_window_listener.dart @@ -1,6 +1,8 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; + import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; class CallbackWindowListener implements WindowListener { final VoidCallback? _onWindowClose; @@ -154,6 +156,8 @@ void useWindowListener({ VoidCallback? onWindowEvent, }) { useEffect(() { + if (!kIsDesktop) return null; + final listener = CallbackWindowListener( onWindowClose: onWindowClose, onWindowFocus: onWindowFocus, @@ -172,9 +176,9 @@ void useWindowListener({ onWindowUndocked: onWindowUndocked, onWindowEvent: onWindowEvent, ); - DesktopTools.window.addListener(listener); + windowManager.addListener(listener); return () { - DesktopTools.window.removeListener(listener); + windowManager.removeListener(listener); }; }, [ onWindowClose, diff --git a/lib/main.dart b/lib/main.dart index 0bb72932f..7123b0d0a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,11 +4,11 @@ import 'package:device_preview/device_preview.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:flutter_localizations/flutter_localizations.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:local_notifier/local_notifier.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -19,6 +19,7 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart'; import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; +import 'package:spotube/provider/tray_manager/tray_manager.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/skip_segment.dart'; @@ -31,15 +32,17 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/cli/cli.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/themes/theme.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:system_theme/system_theme.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:spotube/hooks/configurators/use_init_sys_tray.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:timezone/data/latest.dart' as tz; +import 'package:window_manager/window_manager.dart'; Future main(List rawArgs) async { final arguments = await startCLI(rawArgs); @@ -55,12 +58,12 @@ Future main(List rawArgs) async { MediaKit.ensureInitialized(); // force High Refresh Rate on some Android devices (like One Plus) - if (DesktopTools.platform.isAndroid) { + if (kIsAndroid) { await FlutterDisplayMode.setHighRefreshRate(); } - if (DesktopTools.platform.isDesktop) { - await DesktopTools.window.setPreventClose(true); + if (kIsDesktop) { + await windowManager.setPreventClose(true); } await SystemTheme.accentColor.load(); @@ -69,7 +72,7 @@ Future main(List rawArgs) async { MetadataGod.initialize(); } - if (DesktopTools.platform.isWindows || DesktopTools.platform.isLinux) { + if (kIsWindows || kIsLinux) { DiscordRPC.initialize(); } @@ -101,14 +104,10 @@ Future main(List rawArgs) async { path: hiveCacheDir, ); - await DesktopTools.ensureInitialized( - DesktopWindowOptions( - hideTitleBar: true, - title: "Spotube", - backgroundColor: Colors.transparent, - minimumSize: const Size(300, 700), - ), - ); + if (kIsDesktop) { + await localNotifier.setup(appName: "Spotube"); + await WindowManagerTools.initialize(); + } Catcher2( enableLogger: arguments["verbose"], @@ -189,9 +188,9 @@ class SpotubeState extends ConsumerState { ref.listen(playbackServerProvider, (_, __) {}); ref.listen(connectServerProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); + ref.listen(trayManagerProvider, (_, __) {}); useDisableBatteryOptimizations(); - useInitSysTray(ref); useDeepLinking(ref); useCloseBehavior(ref); useGetStoragePermissions(ref); @@ -233,9 +232,7 @@ class SpotubeState extends ConsumerState { builder: (context, child) { return DevicePreview.appBuilder( context, - DesktopTools.platform.isDesktop && !DesktopTools.platform.isMacOS - ? DragToResizeArea(child: child!) - : child, + kIsDesktop && !kIsMacOS ? DragToResizeArea(child: child!) : child, ); }, themeMode: themeMode, diff --git a/lib/models/connect/connect.freezed.dart b/lib/models/connect/connect.freezed.dart index dcbd783dc..face800e6 100644 --- a/lib/models/connect/connect.freezed.dart +++ b/lib/models/connect/connect.freezed.dart @@ -12,7 +12,7 @@ part of 'connect.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); WebSocketLoadEventData _$WebSocketLoadEventDataFromJson( Map json) { diff --git a/lib/models/spotify/home_feed.freezed.dart b/lib/models/spotify/home_feed.freezed.dart index 97c4ffc77..c2bb2aba1 100644 --- a/lib/models/spotify/home_feed.freezed.dart +++ b/lib/models/spotify/home_feed.freezed.dart @@ -12,7 +12,7 @@ part of 'home_feed.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); SpotifySectionPlaylist _$SpotifySectionPlaylistFromJson( Map json) { diff --git a/lib/models/spotify/recommendation_seeds.freezed.dart b/lib/models/spotify/recommendation_seeds.freezed.dart index 4cfcce12a..adf4aab85 100644 --- a/lib/models/spotify/recommendation_seeds.freezed.dart +++ b/lib/models/spotify/recommendation_seeds.freezed.dart @@ -12,7 +12,7 @@ part of 'recommendation_seeds.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$GeneratePlaylistProviderInput { diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index d80b4513a..ca4e7238d 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -12,7 +12,7 @@ import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:spotube/utils/platform.dart'; class GenrePlaylistsPage extends HookConsumerWidget { final Category category; @@ -27,7 +27,7 @@ class GenrePlaylistsPage extends HookConsumerWidget { final scrollController = useScrollController(); return Scaffold( - appBar: DesktopTools.platform.isDesktop + appBar: kIsDesktop ? const PageWindowTitleBar( leading: BackButton(color: Colors.white), backgroundColor: Colors.transparent, @@ -53,12 +53,12 @@ class GenrePlaylistsPage extends HookConsumerWidget { controller: scrollController, slivers: [ SliverAppBar( - automaticallyImplyLeading: DesktopTools.platform.isMobile, + automaticallyImplyLeading: kIsMobile, expandedHeight: mediaQuery.mdAndDown ? 200 : 150, title: const Text(""), backgroundColor: Colors.transparent, flexibleSpace: FlexibleSpaceBar( - centerTitle: DesktopTools.platform.isDesktop, + centerTitle: kIsDesktop, title: Text( category.name!, style: Theme.of(context).textTheme.headlineMedium?.copyWith( diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index 1e4d46416..6d6f75a92 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; @@ -18,6 +17,7 @@ import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; class MiniLyricsPage extends HookConsumerWidget { final Size prevSize; @@ -36,9 +36,11 @@ class MiniLyricsPage extends HookConsumerWidget { final showLyrics = useState(true); useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) async { - wasMaximized.value = await DesktopTools.window.isMaximized(); - }); + if (kIsDesktop) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + wasMaximized.value = await windowManager.isMaximized(); + }); + } return null; }, []); @@ -112,11 +114,13 @@ class MiniLyricsPage extends HookConsumerWidget { areaActive.value = true; hoverMode.value = false; - await DesktopTools.window.setSize( - showLyrics.value - ? const Size(400, 500) - : const Size(400, 150), - ); + if (kIsDesktop) { + await windowManager.setSize( + showLyrics.value + ? const Size(400, 500) + : const Size(400, 150), + ); + } }, ), IconButton( @@ -135,33 +139,34 @@ class MiniLyricsPage extends HookConsumerWidget { hoverMode.value = !hoverMode.value; }, ), - FutureBuilder( - future: DesktopTools.window.isAlwaysOnTop(), - builder: (context, snapshot) { - return IconButton( - tooltip: context.l10n.always_on_top, - icon: Icon( - snapshot.data == true - ? SpotubeIcons.pinOn - : SpotubeIcons.pinOff, - ), - style: ButtonStyle( - foregroundColor: snapshot.data == true - ? MaterialStateProperty.all( - theme.colorScheme.primary) - : null, - ), - onPressed: snapshot.data == null - ? null - : () async { - await DesktopTools.window.setAlwaysOnTop( - snapshot.data == true ? false : true, - ); - update(); - }, - ); - }, - ), + if (kIsDesktop) + FutureBuilder( + future: windowManager.isAlwaysOnTop(), + builder: (context, snapshot) { + return IconButton( + tooltip: context.l10n.always_on_top, + icon: Icon( + snapshot.data == true + ? SpotubeIcons.pinOn + : SpotubeIcons.pinOff, + ), + style: ButtonStyle( + foregroundColor: snapshot.data == true + ? MaterialStateProperty.all( + theme.colorScheme.primary) + : null, + ), + onPressed: snapshot.data == null + ? null + : () async { + await windowManager.setAlwaysOnTop( + snapshot.data == true ? false : true, + ); + update(); + }, + ); + }, + ), ], ), ), @@ -243,19 +248,20 @@ class MiniLyricsPage extends HookConsumerWidget { tooltip: context.l10n.exit_mini_player, icon: const Icon(SpotubeIcons.maximize), onPressed: () async { + if (!kIsDesktop) return; + try { - await DesktopTools.window + await windowManager .setMinimumSize(const Size(300, 700)); - await DesktopTools.window.setAlwaysOnTop(false); + await windowManager.setAlwaysOnTop(false); if (wasMaximized.value) { - await DesktopTools.window.maximize(); + await windowManager.maximize(); } else { - await DesktopTools.window.setSize(prevSize); + await windowManager.setSize(prevSize); } - await DesktopTools.window - .setAlignment(Alignment.center); + await windowManager.setAlignment(Alignment.center); if (!kIsLinux) { - await DesktopTools.window.setHasShadow(true); + await windowManager.setHasShadow(true); } await Future.delayed( const Duration(milliseconds: 200)); diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 5ac0689a1..f3ed6571e 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -2,7 +2,6 @@ import 'dart:async'; 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:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -21,6 +20,7 @@ import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/utils/platform.dart'; const rootPaths = { "/": 0, @@ -206,7 +206,7 @@ class RootApp extends HookConsumerWidget { ), extendBody: true, drawerScrimColor: Colors.transparent, - endDrawer: DesktopTools.platform.isDesktop + endDrawer: kIsDesktop ? Container( constraints: const BoxConstraints(maxWidth: 800), decoration: BoxDecoration( diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 4e4408d9b..563068686 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -8,6 +7,7 @@ import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/utils/platform.dart'; class SettingsDesktopSection extends HookConsumerWidget { const SettingsDesktopSection({super.key}); @@ -53,7 +53,7 @@ class SettingsDesktopSection extends HookConsumerWidget { value: preferences.systemTitleBar, onChanged: preferencesNotifier.setSystemTitleBar, ), - if (!DesktopTools.platform.isMacOS) + if (!kIsMacOS) SwitchListTile( secondary: const Icon(SpotubeIcons.discord), title: Text(context.l10n.discord_rich_presence), diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index 1f25028ec..76ef8e3e9 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -1,13 +1,13 @@ import 'package:file_picker/file_picker.dart'; import 'package:file_selector/file_selector.dart'; 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/spotube_icons.dart'; import 'package:spotube/components/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; class SettingsDownloadsSection extends HookConsumerWidget { const SettingsDownloadsSection({super.key}); @@ -18,7 +18,7 @@ class SettingsDownloadsSection extends HookConsumerWidget { final preferences = ref.watch(userPreferencesProvider); final pickDownloadLocation = useCallback(() async { - if (DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS) { + if (kIsMobile || kIsMacOS) { final dirStr = await FilePicker.platform.getDirectoryPath( initialDirectory: preferences.downloadLocation, ); diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index d2a750573..d293518d8 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; 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/components/shared/page_window_title_bar.dart'; @@ -14,6 +13,7 @@ import 'package:spotube/pages/settings/sections/downloads.dart'; import 'package:spotube/pages/settings/sections/language_region.dart'; import 'package:spotube/pages/settings/sections/playback.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; class SettingsPage extends HookConsumerWidget { const SettingsPage({super.key}); @@ -45,8 +45,7 @@ class SettingsPage extends HookConsumerWidget { const SettingsAppearanceSection(), const SettingsPlaybackSection(), const SettingsDownloadsSection(), - if (DesktopTools.platform.isDesktop) - const SettingsDesktopSection(), + if (kIsDesktop) const SettingsDesktopSection(), if (!kIsWeb) const SettingsDevelopersSection(), const SettingsAboutSection(), Center( diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index ca8eecfa7..f90db54ae 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -1,21 +1,19 @@ import 'package:dart_discord_rpc/dart_discord_rpc.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; class Discord extends ChangeNotifier { final DiscordRPC? discordRPC; final bool isEnabled; Discord(this.isEnabled) - : discordRPC = (DesktopTools.platform.isWindows || - DesktopTools.platform.isLinux) && - isEnabled + : discordRPC = (kIsWindows || kIsLinux) && isEnabled ? DiscordRPC(applicationId: Env.discordAppId) : null { discordRPC?.start(autoRegister: true); diff --git a/lib/provider/tray_manager/tray_manager.dart b/lib/provider/tray_manager/tray_manager.dart new file mode 100644 index 000000000..2145cbefc --- /dev/null +++ b/lib/provider/tray_manager/tray_manager.dart @@ -0,0 +1,79 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/tray_manager/tray_menu.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; + +class SystemTrayManager with TrayListener { + final Ref ref; + final bool enabled; + + SystemTrayManager( + this.ref, { + required this.enabled, + }) { + initialize(); + } + + Future initialize() async { + if (!kIsDesktop) return; + + if (enabled) { + await trayManager.setIcon( + kIsWindows + ? 'assets/spotube-logo.ico' + : kIsFlatpak + ? 'com.github.KRTirtho.Spotube.png' + : 'assets/spotube-logo.png', + ); + trayManager.addListener(this); + } else { + await trayManager.destroy(); + } + } + + void dispose() { + trayManager.removeListener(this); + } + + @override + onTrayIconMouseDown() { + if (kIsWindows) { + windowManager.show(); + } else { + trayManager.popUpContextMenu(); + } + } + + @override + onTrayIconRightMouseDown() { + if (!kIsWindows) { + windowManager.show(); + } else { + trayManager.popUpContextMenu(); + } + } +} + +final trayManagerProvider = Provider( + (ref) { + final enabled = ref.watch( + userPreferencesProvider.select((s) => s.showSystemTrayIcon), + ); + + ref.listen(trayMenuProvider, (_, menu) { + if (!enabled || !kIsDesktop) return; + trayManager.setContextMenu(menu); + }); + + final manager = SystemTrayManager( + ref, + enabled: enabled, + ); + + ref.onDispose(manager.dispose); + + return manager; + }, +); diff --git a/lib/provider/tray_manager/tray_menu.dart b/lib/provider/tray_manager/tray_menu.dart new file mode 100644 index 000000000..cb793707e --- /dev/null +++ b/lib/provider/tray_manager/tray_menu.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/loop_mode.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; + +final audioPlayerLoopMode = StreamProvider((ref) { + return audioPlayer.loopModeStream; +}); + +final audioPlayerShuffleMode = StreamProvider((ref) { + return audioPlayer.shuffledStream; +}); +final audioPlayerPlaying = StreamProvider((ref) { + return audioPlayer.playingStream; +}); + +final trayMenuProvider = Provider((ref) { + final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final isPlaybackPlaying = + ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null)); + final isLoopOne = + ref.watch(audioPlayerLoopMode).asData?.value == PlaybackLoopMode.one; + final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false; + final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false; + + return Menu( + items: [ + MenuItem( + label: "Show/Hide Window", + onClick: (menuItem) async { + if (await windowManager.isVisible()) { + await windowManager.hide(); + } else { + await windowManager.focus(); + await windowManager.show(); + } + }, + ), + MenuItem.separator(), + MenuItem( + label: isPlaying ? "Pause" : "Play", + disabled: !isPlaybackPlaying, + onClick: (menuItem) async { + if (audioPlayer.isPlaying) { + await audioPlayer.pause(); + } else { + await audioPlayer.resume(); + } + }, + ), + MenuItem( + label: "Next", + disabled: !isPlaybackPlaying, + onClick: (menuItem) { + playlistNotifier.next(); + }, + ), + MenuItem( + label: "Previous", + disabled: !isPlaybackPlaying, + onClick: (menuItem) { + playlistNotifier.previous(); + }, + ), + MenuItem.submenu( + label: "Playback", + submenu: Menu( + items: [ + MenuItem( + label: "Repeat", + checked: isLoopOne, + onClick: (menuItem) { + audioPlayer.setLoopMode( + isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one, + ); + }, + ), + MenuItem( + label: "Shuffle", + checked: isShuffled, + onClick: (menuItem) { + audioPlayer.setShuffle(!isShuffled); + }, + ), + MenuItem.separator(), + MenuItem( + label: "Stop", + onClick: (menuItem) { + playlistNotifier.stop(); + }, + ), + ], + ), + ), + MenuItem.separator(), + MenuItem( + label: "Quit", + onClick: (menuItem) { + exit(0); + }, + ), + ], + ); +}); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index a1e247b22..a537038e2 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; @@ -15,6 +14,7 @@ import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; import 'package:path/path.dart' as path; +import 'package:window_manager/window_manager.dart'; class UserPreferencesNotifier extends PersistedStateNotifier { final Ref ref; @@ -103,8 +103,8 @@ class UserPreferencesNotifier extends PersistedStateNotifier { void setSystemTitleBar(bool isSystemTitleBar) { state = state.copyWith(systemTitleBar: isSystemTitleBar); - if (DesktopTools.platform.isDesktop) { - DesktopTools.window.setTitleBarStyle( + if (kIsDesktop) { + windowManager.setTitleBarStyle( isSystemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, ); } @@ -151,8 +151,8 @@ class UserPreferencesNotifier extends PersistedStateNotifier { ); } - if (DesktopTools.platform.isDesktop) { - await DesktopTools.window.setTitleBarStyle( + if (kIsDesktop) { + await windowManager.setTitleBarStyle( state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, ); } diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index e35c73b5e..67eb18a24 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -62,10 +62,10 @@ class UserPreferences with _$UserPreferences { @Default(false) bool amoledDarkTheme, @Default(true) bool checkUpdate, @Default(false) bool normalizeAudio, - @Default(true) bool showSystemTrayIcon, + @Default(false) bool showSystemTrayIcon, @Default(false) bool skipNonMusic, @Default(false) bool systemTitleBar, - @Default(CloseBehavior.minimizeToTray) CloseBehavior closeBehavior, + @Default(CloseBehavior.close) CloseBehavior closeBehavior, @Default(SpotubeColor(0xFF2196F3, name: "Blue")) @JsonKey( fromJson: UserPreferences._accentColorSchemeFromJson, diff --git a/lib/provider/user_preferences/user_preferences_state.freezed.dart b/lib/provider/user_preferences/user_preferences_state.freezed.dart index a5b076bb6..94015d37d 100644 --- a/lib/provider/user_preferences/user_preferences_state.freezed.dart +++ b/lib/provider/user_preferences/user_preferences_state.freezed.dart @@ -12,7 +12,7 @@ part of 'user_preferences_state.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); UserPreferences _$UserPreferencesFromJson(Map json) { return _UserPreferences.fromJson(json); @@ -415,10 +415,10 @@ class _$UserPreferencesImpl implements _UserPreferences { this.amoledDarkTheme = false, this.checkUpdate = true, this.normalizeAudio = false, - this.showSystemTrayIcon = true, + this.showSystemTrayIcon = false, this.skipNonMusic = false, this.systemTitleBar = false, - this.closeBehavior = CloseBehavior.minimizeToTray, + this.closeBehavior = CloseBehavior.close, @JsonKey( fromJson: UserPreferences._accentColorSchemeFromJson, toJson: UserPreferences._accentColorSchemeToJson, diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index 8bdd12cc6..930b1dd17 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -16,12 +16,12 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson( amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, checkUpdate: json['checkUpdate'] as bool? ?? true, normalizeAudio: json['normalizeAudio'] as bool? ?? false, - showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? true, + showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? false, skipNonMusic: json['skipNonMusic'] as bool? ?? false, systemTitleBar: json['systemTitleBar'] as bool? ?? false, closeBehavior: $enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ?? - CloseBehavior.minimizeToTray, + CloseBehavior.close, accentColorScheme: UserPreferences._accentColorSchemeReadValue( json, 'accentColorScheme') == null diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index a81c6c958..92de192ba 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -101,7 +101,7 @@ abstract class AudioPlayerInterface { return _mkPlayer.state.completed; } - Future get isShuffled async { + bool get isShuffled { return _mkPlayer.shuffled; } diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index 916a983f4..e32a0d144 100644 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:catcher_2/catcher_2.dart'; import 'package:media_kit/media_kit.dart'; import 'package:flutter_broadcasts/flutter_broadcasts.dart'; @@ -7,6 +6,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:audio_session/audio_session.dart'; // ignore: implementation_imports import 'package:spotube/services/audio_player/playback_state.dart'; +import 'package:spotube/utils/platform.dart'; /// MediaKit [Player] by default doesn't have a state stream. /// This class adds a state stream to the [Player] class. @@ -54,7 +54,7 @@ class CustomPlayer extends Player { PackageInfo.fromPlatform().then((packageInfo) { _packageName = packageInfo.packageName; }); - if (DesktopTools.platform.isAndroid) { + if (kIsAndroid) { _androidAudioManager = AndroidAudioManager(); AudioSession.instance.then((s) async { _androidAudioSessionId = @@ -71,7 +71,7 @@ class CustomPlayer extends Player { } Future notifyAudioSessionUpdate(bool active) async { - if (DesktopTools.platform.isAndroid) { + if (kIsAndroid) { sendBroadcast( BroadcastMessage( name: active diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index 338427aa1..f42d6c4bf 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -1,5 +1,4 @@ import 'package:audio_service/audio_service.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; @@ -8,6 +7,7 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/utils/platform.dart'; class AudioServices { final MobileAudioService? mobile; @@ -19,9 +19,7 @@ class AudioServices { Ref ref, ProxyPlaylistNotifier playback, ) async { - final mobile = DesktopTools.platform.isMobile || - DesktopTools.platform.isMacOS || - DesktopTools.platform.isLinux + final mobile = kIsMobile || kIsMacOS || kIsLinux ? await AudioService.init( builder: () => MobileAudioService(playback), config: const AudioServiceConfig( @@ -31,9 +29,7 @@ class AudioServices { ), ) : null; - final smtc = DesktopTools.platform.isWindows - ? WindowsAudioService(ref, playback) - : null; + final smtc = kIsWindows ? WindowsAudioService(ref, playback) : null; return AudioServices( mobile, diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index f94ec4ee6..ae62a0554 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/services/wm_tools/wm_tools.dart'; abstract class KVStoreService { static SharedPreferences? _sharedPreferences; @@ -23,4 +26,21 @@ abstract class KVStoreService { static Future setRecentSearches(List value) async => await sharedPreferences.setStringList('recentSearches', value); + + static WindowSize? get windowSize { + final raw = sharedPreferences.getString('windowSize'); + + if (raw == null) { + return null; + } + return WindowSize.fromJson(jsonDecode(raw)); + } + + static Future setWindowSize(WindowSize value) async => + await sharedPreferences.setString( + 'windowSize', + jsonEncode( + value.toJson(), + ), + ); } diff --git a/lib/services/song_link/song_link.freezed.dart b/lib/services/song_link/song_link.freezed.dart index a8230eebf..0a1af8a9b 100644 --- a/lib/services/song_link/song_link.freezed.dart +++ b/lib/services/song_link/song_link.freezed.dart @@ -12,7 +12,7 @@ part of 'song_link.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); SongLink _$SongLinkFromJson(Map json) { return _SongLink.fromJson(json); diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 75f831256..8444db537 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -163,7 +163,7 @@ class PipedSourcedTrack extends SourcedTrack { final PipedSearchResult(items: searchResults) = await pipedClient.search( query, preference.searchMode == SearchMode.youtube - ? PipedFilter.videos + ? PipedFilter.video : PipedFilter.musicSongs, ); diff --git a/lib/services/wm_tools/wm_tools.dart b/lib/services/wm_tools/wm_tools.dart new file mode 100644 index 000000000..4572a8b43 --- /dev/null +++ b/lib/services/wm_tools/wm_tools.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowSize { + final double height; + final double width; + final bool maximized; + + WindowSize({ + required this.height, + required this.width, + required this.maximized, + }); + + factory WindowSize.fromJson(Map json) => WindowSize( + height: json["height"], + width: json["width"], + maximized: json["maximized"], + ); + + Map toJson() => { + "height": height, + "width": width, + "maximized": maximized, + }; +} + +class WindowManagerTools with WidgetsBindingObserver { + static WindowManagerTools? _instance; + static WindowManagerTools get instance => _instance!; + + WindowManagerTools._(); + + static Future initialize() async { + await windowManager.ensureInitialized(); + _instance = WindowManagerTools._(); + WidgetsBinding.instance.addObserver(instance); + + await windowManager.waitUntilReadyToShow( + const WindowOptions( + title: "Spotube", + backgroundColor: Colors.transparent, + minimumSize: Size(300, 700), + titleBarStyle: TitleBarStyle.hidden, + ), + () async { + final savedSize = KVStoreService.windowSize; + await windowManager.setResizable(true); + if (savedSize?.maximized == true && + !(await windowManager.isMaximized())) { + await windowManager.maximize(); + } else if (savedSize != null) { + await windowManager.setSize(Size(savedSize.width, savedSize.height)); + } + + await windowManager.focus(); + await windowManager.show(); + }, + ); + } + + Size? _prevSize; + + @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 (windowSameDimension || _prevSize == null) { + _prevSize = size; + return; + } + final isMaximized = await windowManager.isMaximized(); + await KVStoreService.setWindowSize( + WindowSize( + height: size.height, + width: size.width, + maximized: isMaximized, + ), + ); + _prevSize = size; + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c69c17c01..6dfdd7409 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -44,9 +44,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) system_tray_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin"); - system_tray_plugin_register_with_registrar(system_tray_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 a4487f4d0..93ffd3e95 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -11,7 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_linux screen_retriever system_theme - system_tray + tray_manager url_launcher_linux window_manager window_size diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a9f6650ff..84f393411 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -21,7 +21,7 @@ import screen_retriever import shared_preferences_foundation import sqflite import system_theme -import system_tray +import tray_manager import url_launcher_macos import window_manager import window_size @@ -37,13 +37,13 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) - SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 317de385f..c1cf630ca 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -44,7 +44,7 @@ PODS: - FMDB (>= 2.7.5) - system_theme (0.0.1): - FlutterMacOS - - system_tray (0.0.1): + - tray_manager (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS @@ -73,7 +73,7 @@ DEPENDENCIES: - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) - - system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`) + - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) @@ -122,8 +122,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos system_theme: :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos - system_tray: - :path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos + tray_manager: + :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_manager: @@ -132,11 +132,11 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: - app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea @@ -147,13 +147,13 @@ SPEC CHECKSUMS: media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5 metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc - system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d + tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/pubspec.lock b/pubspec.lock index 8d19f604c..1532bcf75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" url: "https://pub.dev" source: hosted - version: "0.11.2" + version: "0.11.3" ansicolor: dependency: transitive description: @@ -37,90 +37,26 @@ packages: dependency: "direct main" description: name: app_links - sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" + sha256: "42dc15aecf2618ace4ffb74a2e58a50e45cd1b9f2c17c8f0cafe4c297f08c815" url: "https://pub.dev" source: hosted - version: "3.5.0" - app_package_maker: - dependency: transitive - description: - name: app_package_maker - sha256: "0dc1949e09a60ec2d0b79b43c5237734e6d03f7a022919bdfe1b4789f4c3bfb0" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_aab: - dependency: transitive - description: - name: app_package_maker_aab - sha256: "44810e77dff3b3b54011270b01a42876e838766d6e85c98f9a33bfe61db51651" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_apk: - dependency: transitive - description: - name: app_package_maker_apk - sha256: "974e639cda26c2e18fffaba18f88797523731f5b5457056b25e92243c9191f61" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_deb: - dependency: transitive - description: - name: app_package_maker_deb - sha256: dcd4047cb67648e53afd61079a8baa3c8ea383668f068e3ce8da841f3728eb29 - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_dmg: - dependency: transitive - description: - name: app_package_maker_dmg - sha256: e0410a51304f3fff3e3850696c8e56f53f71c990e097f1c325126ebe90d242c4 - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_exe: - dependency: transitive - description: - name: app_package_maker_exe - sha256: "07e3899a3ae12e8b6cd80efc7281ccca6c9050d2810e0fdc0e7e614cf4bd8a02" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_ipa: - dependency: transitive - description: - name: app_package_maker_ipa - sha256: "1a11498506ba975d02a4715650701981a382a2161c81481911517b50b378cd65" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - app_package_maker_zip: - dependency: transitive - description: - name: app_package_maker_zip - sha256: cef07a47c589036a4762fdc9e61b9022f0a2a2a9f69538109a0a952a7e668306 - url: "https://pub.dev" - source: hosted - version: "0.0.9" + version: "4.0.1" archive: dependency: transitive description: name: archive - sha256: ca12e6c9ac022f33fd89128e7007fb5e97ab6e814d4fa05dd8d4f2db1e3c69cb + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.5" + version: "3.4.10" args: dependency: "direct main" description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: "direct main" description: @@ -133,18 +69,18 @@ packages: dependency: "direct main" description: name: audio_service - sha256: a4d989f1225ea9621898d60f23236dcbfc04876fa316086c23c5c4af075dbac4 + sha256: "4547c312a94f9cb2c48b60823fb190767cbd63454a83c73049384d5d3cba4650" url: "https://pub.dev" source: hosted - version: "0.18.12" + version: "0.18.13" audio_service_mpris: dependency: "direct main" description: name: audio_service_mpris - sha256: "31be5de2db0c71b217157afce1974ac6d0ad329bd91deb1f19ad094d29340d8e" + sha256: a8d1583f9143d17b2facc994a99bd1ea257cec43adcb8d7349458555c62b570f url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.3" audio_service_platform_interface: dependency: transitive description: @@ -157,18 +93,18 @@ packages: dependency: transitive description: name: audio_service_web - sha256: "523e64ddc914c714d53eec2da85bba1074f08cf26c786d4efb322de510815ea7" + sha256: "9d7d5ae5f98a5727f2580fad73062f2484f400eef6cef42919413268e62a363e" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" audio_session: dependency: "direct main" description: name: audio_session - sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" + sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e url: "https://pub.dev" source: hosted - version: "0.1.18" + version: "0.1.19" auto_size_text: dependency: "direct main" description: @@ -261,18 +197,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -285,10 +221,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.0" built_collection: dependency: transitive description: @@ -301,18 +237,18 @@ packages: dependency: transitive description: name: built_value - sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.2" + version: "8.9.2" buttons_tabbar: dependency: "direct main" description: name: buttons_tabbar - sha256: "781128180f3e76cf93c093183f10395c664983dbee20bc4da2025be70085c2da" + sha256: "3f0969c26574ef15c0c9ff1dee42c3c4b0d3563d2c8607804372490fb8b76896" url: "https://pub.dev" source: hosted - version: "1.3.7+1" + version: "1.3.8" cached_network_image: dependency: "direct main" description: @@ -341,10 +277,10 @@ packages: dependency: "direct main" description: name: catcher_2 - sha256: "73e251057b1b59442d58b5109d26401cfed850df21da44b028338d4f85f69105" + sha256: "3c8f6cedc8c5eab61192830096d4f303900a5d0bddbf96a07ff9f7a8d5ff8fcd" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.4" change_case: dependency: transitive description: @@ -381,10 +317,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -397,10 +333,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -429,10 +365,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+5" + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -449,14 +385,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.dev" - source: hosted - version: "1.0.6" curved_navigation_bar: dependency: "direct main" description: @@ -469,26 +397,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d" + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" url: "https://pub.dev" source: hosted - version: "0.5.11" + version: "0.6.4" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "0d48e002438950f9582e574ef806b2bea5719d8d14c0f9f754fbad729bcf3b19" + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 url: "https://pub.dev" source: hosted - version: "0.5.14" + version: "0.6.4" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "2952837953022de610dacb464f045594854ced6506ac7f76af28d4a6490e189b" + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 url: "https://pub.dev" source: hosted - version: "0.5.14" + version: "0.6.3" dart_des: dependency: transitive description: @@ -506,14 +434,22 @@ packages: url: "https://github.com/Tommypop2/dart_discord_rpc.git" source: git version: "0.0.3" + dart_mappable: + dependency: transitive + description: + name: dart_mappable + sha256: "47269caf2060533c29b823ff7fa9706502355ffcb61e7f2a374e3a0fb2f2c3f0" + url: "https://pub.dev" + source: hosted + version: "4.2.2" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dartx: dependency: transitive description: @@ -542,10 +478,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -566,18 +502,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "5.4.3+1" disable_battery_optimization: dependency: "direct main" description: name: disable_battery_optimization - sha256: b3441975ab2a3ab0c19ed78e909a88d245ce689d43d17f9b23582b1ed41c047b + sha256: "6b2ba802f984af141faf1b6b5fb956d5ef01f9cd555597c35b9cc335a03185ba" url: "https://pub.dev" source: hosted - version: "1.1.0+1" + version: "1.1.1" dots_indicator: dependency: transitive description: @@ -607,18 +543,26 @@ packages: dependency: "direct main" description: name: envied - sha256: "60d3f5606c7b35bc6ef493e650d916b34351d8af2e58b7ac45881ba59dfcf039" + sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 url: "https://pub.dev" source: hosted - version: "0.3.0+3" + version: "0.5.4+1" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: dfdbe5dc52863e54c036a4c4042afbdf1bd528cb4c1e638ecba26228ba72e9e5 + sha256: "517b70de08d13dcd40e97b4e5347e216a0b1c75c99e704f3c85c0474a392d14a" url: "https://pub.dev" source: hosted - version: "0.3.0+3" + version: "0.5.4+1" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -631,10 +575,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -647,34 +591,34 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03 url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "8.0.0+1" file_selector: dependency: "direct main" description: name: file_selector - sha256: "84eaf3e034d647859167d1f01cfe7b6352488f34c1b4932635012b202014c25b" + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" file_selector_android: dependency: transitive description: name: file_selector_android - sha256: d41e165d6f798ca941d536e5dc93494d50e78c571c28ad60cfe0b0fefeb9f1e7 + sha256: "1cd66575f063b689e041aec836905ba7be18d76c9f0634d0d75daec825f67095" url: "https://pub.dev" source: hosted - version: "0.5.0+3" + version: "0.5.0+7" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: b3fbdda64aa2e335df6e111f6b0f1bb968402ed81d2dd1fa4274267999aa32c2 + sha256: "0a1196a9c5795858aa315332da2fb5c4bcfdcb312d8a4e27651f765b87904431" url: "https://pub.dev" source: hosted - version: "0.5.1+6" + version: "0.5.1+9" file_selector_linux: dependency: transitive description: @@ -687,26 +631,26 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72" + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" file_selector_web: dependency: transitive description: name: file_selector_web - sha256: dc6622c4d66cb1bee623ddcc029036603c6cc45c85e4a775bb06008d61c809c1 + sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.4+1" file_selector_windows: dependency: transitive description: @@ -727,31 +671,15 @@ packages: dependency: "direct main" description: name: fluentui_system_icons - sha256: "7637cab80bd8d1ba762144cd85df79a7318c12ed5a66d166a9e4acbf24a4c412" + sha256: "1c860f10a0e74c5788ff8a650ae6074d9a544463ae269714f1044b32df52b978" url: "https://pub.dev" source: hosted - version: "1.1.214" + version: "1.1.234" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_app_builder: - dependency: transitive - description: - name: flutter_app_builder - sha256: "9e5527919f62424f0fafaa3e8dfda8469caf63e465862e9866a0d60a37c00fcf" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - flutter_app_packager: - dependency: transitive - description: - name: flutter_app_packager - sha256: b5bfb7113b49710c004c5f1ab6f08ac121418540d49e14825dd75e99810fa695 - url: "https://pub.dev" - source: hosted - version: "0.0.9" flutter_broadcasts: dependency: "direct main" description: @@ -768,15 +696,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" - flutter_desktop_tools: - dependency: "direct main" - description: - path: "." - ref: "1f0bec3283626dcbd8ee2f54e238d096d8dea50e" - resolved-ref: "1f0bec3283626dcbd8ee2f54e238d096d8dea50e" - url: "https://github.com/KRTirtho/flutter_desktop_tools.git" - source: git - version: "0.0.1" flutter_displaymode: dependency: "direct main" description: @@ -785,14 +704,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" - flutter_distributor: - dependency: "direct dev" - description: - name: flutter_distributor - sha256: "50d56df265e97396427ec42cc02374b72d08c71b3442d662b97fc089bd1705ea" - url: "https://pub.dev" - source: hosted - version: "0.0.2" flutter_driver: dependency: transitive description: flutter @@ -890,10 +801,10 @@ packages: dependency: transitive description: name: flutter_keyboard_visibility - sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "6.0.0" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -946,10 +857,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_localizations: dependency: "direct main" description: flutter @@ -959,42 +870,42 @@ packages: dependency: transitive description: name: flutter_mailer - sha256: a935e9caa842877e8ed56109afb75b86e6488edbcd4696a5ac02b327a48fcd8a + sha256: "4fffaa35e911ff5ec2e5a4ebbca62c372e99a154eb3bb2c0bf79f09adf6ecf4c" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" flutter_native_splash: dependency: "direct main" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.19" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" flutter_rust_bridge: dependency: transitive description: name: flutter_rust_bridge - sha256: e12415c3bce49bcbc3fed383f0ea41ad7d828f6cf0eccba0588ffa5a812fe522 + sha256: "02720226035257ad0b571c1256f43df3e1556a499f6bcb004849a0faaa0e87f0" url: "https://pub.dev" source: hosted - version: "1.82.1" + version: "1.82.6" flutter_secure_storage: dependency: "direct main" description: @@ -1047,10 +958,10 @@ packages: dependency: "direct main" description: name: flutter_sharing_intent - sha256: "6eb896e6523b735e8230eeb206fd3b9f220f11ce879c2400a90b443147036ff9" + sha256: "785ffc391822641457f930eb477c91c2f598a888f50b8fbb40d481ee01c7e719" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter_svg: dependency: "direct main" description: @@ -1073,10 +984,10 @@ packages: dependency: transitive description: name: fluttertoast - sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" + sha256: "81b68579e23fcbcada2db3d50302813d2371664afe6165bc78148050ab94bf66" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "8.2.5" form_validator: dependency: "direct main" description: @@ -1089,10 +1000,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -1105,10 +1016,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -1150,10 +1061,10 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: f0b8d115a13ecf827013ec9fc883390ccc0e87a96ed5347a3114cac177ef18e8 + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.1" graphs: dependency: transitive description: @@ -1206,10 +1117,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" hotreloader: dependency: transitive description: @@ -1270,42 +1181,42 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.7" image_picker: dependency: "direct main" description: name: image_picker - sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + sha256: fe9ee64ccb8d599a5dfb0e21cc6652232c610bcf667af4e79b9eb175cc30a7a5 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "47da2161c2e9f8f8a9cbbd89d466d174333fbdd769aeed848912e0b16d9cb369" + sha256: "8e75431a62b7feb4fd55cb4a5c6f0ac4564460ec5dc09f9c4a0d50a5ce7c4cb9" url: "https://pub.dev" source: hosted - version: "0.8.8" + version: "0.8.10" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 + sha256: f4a6f62be96d6fd268f32a6bf8ef444cd8e3fff64d16923c6e6fe55e0c84a761 url: "https://pub.dev" source: hosted - version: "0.8.8+2" + version: "0.8.10" image_picker_linux: dependency: transitive description: @@ -1326,10 +1237,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.10.0" image_picker_windows: dependency: transitive description: @@ -1355,10 +1266,10 @@ packages: dependency: "direct main" description: name: introduction_screen - sha256: ef5a5479a8e06a84b9a7eff16c698b9b82f70cd1b6203b264bc3686f9bfb77e2 + sha256: "325f26e86fa3c3e86e6ab2bbc1fda860c9e6eae5ff29166fc2a3cab8f710d5b5" url: "https://pub.dev" source: hosted - version: "3.1.11" + version: "3.1.14" io: dependency: transitive description: @@ -1432,21 +1343,21 @@ packages: source: hosted version: "3.0.0" local_notifier: - dependency: transitive + dependency: "direct main" description: name: local_notifier - sha256: cc855aa6362c8840e3d3b35b1c3b058a3a8becdb2b03d5a9aa3f3a1e861f0a03 + sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025 url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" logger: dependency: "direct main" description: name: logger - sha256: ba3bc83117b2b49bdd723c0ea7848e8285a0fbc597ba09203b20d329d020c24a + sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" logging: dependency: transitive description: @@ -1467,10 +1378,10 @@ packages: dependency: transitive description: name: mailer - sha256: "57f6dd1496699999a7bfd0aa6be0645384f477f4823e16d4321c40a434346382" + sha256: d25d89555c1031abacb448f07b801d7c01b4c21d4558e944b12b64394c84a3cb url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.1.0" matcher: dependency: transitive description: @@ -1491,26 +1402,26 @@ packages: dependency: "direct main" description: name: media_kit - sha256: "92c7f59e075d74471b31e703f81ccc1d7102739ebcce945b30a6417fa2f751d5" + sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.10+1" media_kit_libs_android_audio: dependency: transitive description: name: media_kit_libs_android_audio - sha256: "1d9ba8814e58a12ae69014884abf96893d38e444abfb806ff38896dfe089dd15" + sha256: "3d2df5c09d3f3ff7c55b53bf955e46712f76483e77562a5a017439a3ea85ce88" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.6" media_kit_libs_audio: dependency: "direct main" description: name: media_kit_libs_audio - sha256: "9ccf9019edc220b8d0bfa1f53a91aa2cf2506ac860b4d5774c9d6bbdd49796ca" + sha256: f3f91df69848005363b3ae0ef7971a90edbd80a9365195684ef26c9a6ac8833f url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" media_kit_libs_ios_audio: dependency: transitive description: @@ -1551,6 +1462,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -1571,10 +1490,10 @@ packages: dependency: "direct main" description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" nested: dependency: transitive description: @@ -1611,10 +1530,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: cb44f49b6e690fa766f023d5b22cac6b9affe741dd792b6ac7ad4fabe0d7b097 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "6.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1659,26 +1578,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -1691,10 +1610,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -1707,42 +1626,50 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.3.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: ace7d15a3d1a4a0b91c041d01e5405df221edb9de9116525efc773c74e6fc790 + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" url: "https://pub.dev" source: hosted - version: "11.0.5" + version: "12.0.5" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + url: "https://pub.dev" + source: hosted + version: "9.4.4" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "0.1.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2 + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" url: "https://pub.dev" source: hosted - version: "3.11.5" + version: "4.2.1" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.1" petitparser: dependency: transitive description: @@ -1754,11 +1681,10 @@ packages: piped_client: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "64631732eefe3d93889756dc2e4ff5c8523ed763" - url: "https://github.com/KRTirtho/piped_client.git" - source: git + name: piped_client + sha256: "87b04b2ebf4e008cfbb0ac85e9920ab3741f5aa697be2dd44919658a3297a4bc" + url: "https://pub.dev" + source: hosted version: "0.1.1" platform: dependency: transitive @@ -1772,18 +1698,18 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.8.0" pool: dependency: transitive description: @@ -1812,18 +1738,18 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_api_client: dependency: "direct main" description: name: pub_api_client - sha256: d456816ef5142906a22dc56e37be6bef6cb0276f0a26c11d1f7d277868202e71 + sha256: cc3d2c93df3823553de6a3e7d3ac09a3f43f8c271af4f43c2795266090ac9625 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" pub_semver: dependency: transitive description: @@ -1852,10 +1778,10 @@ packages: dependency: transitive description: name: puppeteer - sha256: eedeaae6ec5d2e54f9ae22ab4d6b3dda2e8791c356cc783046d06c287ffe11d8 + sha256: "6833edca01b1e9dcdd9a6e41bad84b706dfba4366d095c4edff64b00c02ac472" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.8.0" quiver: dependency: transitive description: @@ -1864,30 +1790,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" riverpod: dependency: transitive description: name: riverpod - sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: d72d7096964baf288b55619fe48100001fc4564ab7923ed0a7f5c7650e03c0d6 + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.5.1" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "70198738c3047ae4f6517ef1a2011a8514a980a52576c7f629a3a08810319a02" + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.10" rxdart: dependency: transitive description: @@ -1933,34 +1867,34 @@ packages: dependency: transitive description: name: sentry - sha256: "39c23342fc96105da449914f7774139a17a0ca8a4e70d9ad5200171f7e47d6ba" + sha256: "19a267774906ca3a3c4677fc7e9582ea9da79ae9a28f84bbe4885dac2c269b70" url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.20.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: @@ -1973,18 +1907,18 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -2025,22 +1959,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sidebarx: dependency: "direct main" description: name: sidebarx - sha256: "7042d64844b8e64ca5c17e70d89b49df35b54a26c015b90000da9741eab70bc0" + sha256: abe39d6db237fb8e25c600e8039ffab80fa7fe71acab03e9c378c31f912d2766 url: "https://pub.dev" source: hosted - version: "0.16.3" + version: "0.17.1" simple_icons: dependency: "direct main" description: name: simple_icons - sha256: "8aa6832dc7a263a3213e40ecbf1328a392308c809d534a3b860693625890483b" + sha256: "30067d70a9d72923fbc80e142e17fa46085dfa970e66bc4bede3be4819d05901" url: "https://pub.dev" source: hosted - version: "7.10.0" + version: "10.1.3" skeleton_text: dependency: "direct main" description: @@ -2053,10 +1995,10 @@ packages: dependency: "direct main" description: name: skeletonizer - sha256: ff4c36e826efd5288d7a84e7619a6e9be8185d3064cecf101a9133762f3b401b + sha256: "9a3ae2f4ee4349bdbed3292d04586a1315a44745d2c454684f82f0c46dbeabf9" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "1.1.1" sky_engine: dependency: transitive description: flutter @@ -2074,18 +2016,18 @@ packages: dependency: "direct main" description: name: smtc_windows - sha256: aba2bad5ddfaf595496db04df3d9fdb54fb128fc1f39c8f024945a67455388fe + sha256: "799bbe0f8e4436da852c5dcc0be482c97b8ae0f504f65c6b750cd239b4835aa0" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" source_gen: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -2106,26 +2048,34 @@ packages: dependency: "direct main" description: name: spotify - sha256: "2308a84511c18ec1e72515a57e28abb1467389549d571c460732b4538c2e34de" + sha256: "50bd5a07b580ee441d0b4d81227185ada768332c353671aa7555ea47cc68eb9e" url: "https://pub.dev" source: hosted - version: "0.13.3" + version: "0.13.5" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.3" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.4" stack_trace: dependency: transitive description: @@ -2138,10 +2088,10 @@ packages: dependency: transitive description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.0.0" stream_channel: dependency: transitive description: @@ -2186,10 +2136,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" system_theme: dependency: "direct main" description: @@ -2206,14 +2156,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" - system_tray: - dependency: "direct overridden" - description: - name: system_tray - sha256: "087edb877f22f286d82d42f330fa640138c192e98aa9d20c2b83aa4e406bb432" - url: "https://pub.dev" - source: hosted - version: "2.0.2" term_glyph: dependency: transitive description: @@ -2234,10 +2176,10 @@ packages: dependency: transitive description: name: time - sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" timezone: dependency: "direct main" description: @@ -2262,6 +2204,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: e0ac9a88b2700f366b8629b97e8663b6ef450a2f169560a685dc167bfe9c9c29 + url: "https://pub.dev" + source: hosted + version: "0.2.2" tuple: dependency: transitive description: @@ -2270,6 +2220,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + type_plus: + dependency: transitive + description: + name: type_plus + sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 + url: "https://pub.dev" + source: hosted + version: "2.1.1" typed_data: dependency: transitive description: @@ -2314,74 +2272,74 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.3.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.4.0" vector_math: dependency: transitive description: @@ -2434,18 +2392,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" web_socket_channel: dependency: "direct main" description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" webdriver: dependency: transitive description: @@ -2466,26 +2424,26 @@ packages: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.4.0" win32_registry: dependency: "direct main" description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" window_manager: dependency: "direct main" description: name: window_manager - sha256: "6ee795be9124f90660ea9d05e581a466de19e1c89ee74fc4bf528f60c8600edd" + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 url: "https://pub.dev" source: hosted - version: "0.3.6" + version: "0.3.8" window_size: dependency: "direct main" description: @@ -2499,10 +2457,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: @@ -2523,10 +2481,10 @@ packages: dependency: "direct main" description: name: youtube_explode_dart - sha256: "98fd11b51adbbca76cbdb17f560168f1d7a9835cecceea965f49eb1e5eed155c" + sha256: "12d32dffd8c85927eb46f7cf7a9dfce690edfe82134c08a90529c51eba58a85c" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index 3f4c22af3..62c20c354 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,96 +13,89 @@ environment: flutter: ">=3.10.0" dependencies: - args: ^2.3.2 + args: ^2.5.0 async: ^2.9.0 - audio_service: ^0.18.9 - audio_session: ^0.1.18 + audio_service: ^0.18.13 + audio_service_mpris: ^0.1.3 + audio_session: ^0.1.19 auto_size_text: ^3.0.0 - buttons_tabbar: ^1.3.6 + buttons_tabbar: ^1.3.8 cached_network_image: ^3.3.1 - catcher_2: 1.0.0 + catcher_2: ^1.2.4 collection: ^1.15.0 - cupertino_icons: ^1.0.5 curved_navigation_bar: ^1.0.3 dbus: ^0.7.8 - device_info_plus: ^9.1.2 + device_info_plus: ^10.1.0 device_preview: ^1.1.0 - dio: ^5.4.1 - disable_battery_optimization: ^1.1.0+1 + dio: ^5.4.3+1 + disable_battery_optimization: ^1.1.1 duration: ^3.0.12 - envied: ^0.3.0 - file_selector: ^1.0.1 - fluentui_system_icons: ^1.1.189 + envied: ^0.5.4+1 + file_picker: ^8.0.0+1 + file_selector: ^1.0.3 + fluentui_system_icons: ^1.1.234 flutter: sdk: flutter flutter_cache_manager: ^3.3.0 - flutter_desktop_tools: - git: - url: https://github.com/KRTirtho/flutter_desktop_tools.git - ref: 1f0bec3283626dcbd8ee2f54e238d096d8dea50e flutter_displaymode: ^0.6.0 flutter_feather_icons: ^2.0.0+1 flutter_hooks: ^0.20.5 flutter_inappwebview: ^6.0.0 flutter_localizations: sdk: flutter - flutter_native_splash: ^2.3.10 - flutter_riverpod: ^2.4.10 + flutter_native_splash: ^2.4.0 + flutter_riverpod: ^2.5.1 flutter_secure_storage: ^9.0.0 flutter_svg: ^1.1.6 form_validator: ^2.1.1 fuzzywuzzy: ^1.1.6 go_router: 12.1.3 # Stuck on this https://github.com/flutter/flutter/issues/140869 - google_fonts: ^6.1.0 + google_fonts: ^6.2.1 hive: ^2.2.3 hive_flutter: ^1.1.0 - hooks_riverpod: ^2.4.3 + hooks_riverpod: ^2.5.1 html: ^0.15.1 http: ^1.2.0 - image_picker: ^1.0.4 + image_picker: ^1.1.0 intl: ^0.18.0 - introduction_screen: ^3.0.2 + introduction_screen: ^3.1.14 json_annotation: ^4.8.1 logger: ^2.0.2 - media_kit: ^1.1.3 - media_kit_libs_audio: ^1.0.3 + media_kit: ^1.1.10+1 + media_kit_libs_audio: ^1.0.4 metadata_god: ^0.5.2+1 mime: ^1.0.2 - package_info_plus: ^4.1.0 + package_info_plus: ^6.0.0 palette_generator: ^0.3.3 path: ^1.8.0 - path_provider: ^2.0.8 - permission_handler: ^11.0.1 - piped_client: - git: - url: https://github.com/KRTirtho/piped_client.git + path_provider: ^2.1.3 + permission_handler: ^11.3.1 + piped_client: ^0.1.1 popover: ^0.3.0 scrobblenaut: git: url: https://github.com/KRTirtho/scrobblenaut.git ref: dart-3-support scroll_to_index: ^3.0.1 - sidebarx: ^0.16.3 - shared_preferences: ^2.2.2 + sidebarx: ^0.17.1 + shared_preferences: ^2.2.3 skeleton_text: ^3.0.1 - smtc_windows: ^0.1.1 + smtc_windows: ^0.1.2 stroke_text: ^0.0.2 system_theme: ^2.1.0 titlebar_buttons: ^1.0.0 - url_launcher: ^6.1.7 - uuid: ^3.0.7 + url_launcher: ^6.2.6 + uuid: ^4.4.0 version: ^3.0.2 visibility_detector: ^0.4.0+2 - window_manager: ^0.3.1 + window_manager: ^0.3.8 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git ref: a738913c8ce2c9f47515382d40827e794a334274 path: plugins/window_size - youtube_explode_dart: ^2.0.1 - simple_icons: ^7.10.0 - audio_service_mpris: ^0.1.0 - file_picker: ^6.0.0 + youtube_explode_dart: ^2.2.0 + simple_icons: ^10.1.3 jiosaavn: ^0.1.0 draggable_scrollbar: git: @@ -116,28 +109,29 @@ dependencies: url: https://github.com/Tommypop2/dart_discord_rpc.git html_unescape: ^2.0.0 wikipedia_api: ^0.1.0 - skeletonizer: ^0.8.0 - app_links: ^3.5.0 - win32_registry: ^1.1.2 + skeletonizer: ^1.1.1 + app_links: ^4.0.1 + win32_registry: ^1.1.3 flutter_sharing_intent: ^1.1.0 flutter_broadcasts: ^0.4.0 freezed_annotation: ^2.4.1 - spotify: ^0.13.3 + spotify: ^0.13.5 bonsoir: ^5.1.9 shelf: ^1.4.1 shelf_router: ^1.1.4 shelf_web_socket: ^1.0.4 - web_socket_channel: ^2.4.4 + web_socket_channel: ^2.4.5 lrc: ^1.0.2 pub_api_client: ^2.4.0 pubspec_parse: ^1.2.2 timezone: ^0.9.2 crypto: ^3.0.3 + local_notifier: ^0.1.6 + tray_manager: ^0.2.2 dev_dependencies: build_runner: ^2.4.9 - envied_generator: ^0.3.0+3 - flutter_distributor: ^0.0.2 + envied_generator: ^0.5.4+1 flutter_gen_runner: ^5.4.0 flutter_launcher_icons: ^0.13.1 flutter_lints: ^3.0.1 @@ -147,12 +141,12 @@ dev_dependencies: sdk: flutter hive_generator: ^2.0.0 json_serializable: ^6.6.2 - freezed: ^2.4.6 - custom_lint: ^0.5.11 - riverpod_lint: ^2.1.1 + freezed: ^2.5.2 + custom_lint: ^0.6.4 + riverpod_lint: ^2.3.10 dependency_overrides: - system_tray: 2.0.2 + uuid: ^4.4.0 flutter: generate: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d8a9db298..57542decb 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include #include #include @@ -42,8 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); - SystemTrayPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SystemTrayPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 902927444..6a0c77230 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,7 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows screen_retriever system_theme - system_tray + tray_manager url_launcher_windows window_manager window_size From 7e07c2e1985da7ccb96b1fac2ecd703720068d26 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 18 Apr 2024 00:05:47 +0600 Subject: [PATCH 04/57] fix(search): load more button not working #1417 --- lib/pages/search/sections/tracks.dart | 2 +- macos/Podfile.lock | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 48dabc139..7fb58759f 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -113,7 +113,7 @@ class SearchTracksSection extends HookConsumerWidget { child: TextButton( onPressed: searchTrack.isLoadingNextPage ? null - : () => searchTrackNotifier.fetchMore, + : searchTrackNotifier.fetchMore, child: searchTrack.isLoadingNextPage ? const CircularProgressIndicator() : Text(context.l10n.load_more), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index c1cf630ca..ce2ef2339 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -18,9 +18,6 @@ PODS: - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - local_notifier (0.1.0): - FlutterMacOS - media_kit_libs_macos_audio (1.0.4): @@ -39,9 +36,9 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): + - sqflite (0.0.3): + - Flutter - FlutterMacOS - - FMDB (>= 2.7.5) - system_theme (0.0.1): - FlutterMacOS - tray_manager (0.0.1): @@ -71,7 +68,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -80,7 +77,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - FMDB - OrderedSet EXTERNAL SOURCES: @@ -119,7 +115,7 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin system_theme: :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos tray_manager: @@ -141,7 +137,6 @@ SPEC CHECKSUMS: flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff media_kit_libs_macos_audio: 3871782a4f3f84c77f04d7666c87800a781c24da media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5 @@ -151,7 +146,7 @@ SPEC CHECKSUMS: path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 From 9bccbc93c63dd34f6e15ff68c276976ecd1d9a33 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 18 Apr 2024 00:19:47 +0600 Subject: [PATCH 05/57] fix: spotify friends and user profile icon (mobile) showing when not authenticated #1410 --- .vscode/settings.json | 1 + lib/components/home/sections/friends.dart | 47 +++++++++++++---------- lib/pages/home/home.dart | 6 +++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 29c5ba4e0..de5fbd69a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,5 +24,6 @@ "explorer.fileNesting.patterns": { "pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml", "README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md", + "*.dart": "${capture}.g.dart,${capture}.freezed.dart", } } \ No newline at end of file diff --git a/lib/components/home/sections/friends.dart b/lib/components/home/sections/friends.dart index 35ec09b0f..4ae802e62 100644 --- a/lib/components/home/sections/friends.dart +++ b/lib/components/home/sections/friends.dart @@ -1,12 +1,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/home/sections/friends/friend_item.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/spotify_friends.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomePageFriendsSection extends HookConsumerWidget { @@ -14,6 +16,7 @@ class HomePageFriendsSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final auth = ref.watch(authenticationProvider); final friendsQuery = ref.watch(friendsProvider); final friends = friendsQuery.asData?.value.friends ?? FakeData.friends.friends; @@ -27,32 +30,36 @@ class HomePageFriendsSection extends HookConsumerWidget { xxl: 7, ); - final friendGroup = friends.fold>>( - [], - (previousValue, element) { - if (previousValue.isEmpty) { - return [ - [element] - ]; - } + final friendGroup = useMemoized( + () => friends.fold>>( + [], + (previousValue, element) { + if (previousValue.isEmpty) { + return [ + [element] + ]; + } + + final lastGroup = previousValue.last; + if (lastGroup.length < groupCount) { + return [ + ...previousValue.sublist(0, previousValue.length - 1), + [...lastGroup, element] + ]; + } - final lastGroup = previousValue.last; - if (lastGroup.length < groupCount) { return [ - ...previousValue.sublist(0, previousValue.length - 1), - [...lastGroup, element] + ...previousValue, + [element] ]; - } - - return [ - ...previousValue, - [element] - ]; - }, + }, + ), + [friends, groupCount], ); if (friendsQuery.isLoading || - friendsQuery.asData?.value.friends.isEmpty == true) { + friendsQuery.asData?.value.friends.isEmpty == true || + auth == null) { return const SliverToBoxAdapter( child: SizedBox.shrink(), ); diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 31f26bee3..a4a71146b 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -14,6 +14,7 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -41,9 +42,14 @@ class HomePage extends HookConsumerWidget { const ConnectDeviceButton(), const Gap(10), Consumer(builder: (context, ref, _) { + final auth = ref.watch(authenticationProvider); final me = ref.watch(meProvider); final meData = me.asData?.value; + if (auth == null) { + return const SizedBox(); + } + return IconButton( icon: CircleAvatar( backgroundImage: UniversalImage.imageProvider( From 2da5d786d277ee8ba05685c4f98ae22e9c27d023 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Apr 2024 16:05:01 +0600 Subject: [PATCH 06/57] chore: add docker and m1 based linux arm build --- .dockerignore | 4 ++ .github/Dockerfile | 32 +++++++++ .github/workflows/spotube-release-binary.yml | 74 ++++++++++++++++++-- 3 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..55fee41a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +build +dist +.dart_tool +.idea diff --git a/.github/Dockerfile b/.github/Dockerfile new file mode 100644 index 000000000..e4dacb0e9 --- /dev/null +++ b/.github/Dockerfile @@ -0,0 +1,32 @@ +ARG FLUTTER_VERSION +ARG BUILD_VERSION + +FROM --platform=arm64 fischerscode/flutter-sudo:${FLUTTER_VERSION} + +WORKDIR /app + +# Install dependencies +RUN sudo apt-get update &&\ + sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev rpm &&\ + sudo rm -rf /var/lib/apt/lists/* + +COPY . . + +RUN sudo chown -R $(whoami) /app + +RUN flutter pub get &&\ + flutter config --enable-linux-desktop &&\ + flutter pub get &&\ + dart run build_runner build --delete-conflicting-outputs + +RUN dart pub global activate flutter_distributor &&\ + alias dpkg-deb="dpkg-deb --Zxz" &&\ + flutter_distributor package --platform=linux --targets=deb &&\ + flutter_distributor package --platform=linux --targets=rpm + + +RUN make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64 + +RUN mv build/spotube-linux-*-aarch64.tar.xz dist/ &&\ + mv dist/**/spotube-*-linux.deb dist/Spotube-linux-aarch64.deb &&\ + mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-aarch64.rpm \ No newline at end of file diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 969e1b77c..044738c95 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -70,7 +70,7 @@ jobs: run: | flutter config --enable-windows-desktop flutter pub get - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + dart run build_runner build --delete-conflicting-outputs - name: Build Windows Executable run: | @@ -156,7 +156,7 @@ jobs: run: | flutter config --enable-linux-desktop flutter pub get - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + dart run build_runner build --delete-conflicting-outputs - name: Build Linux Packages run: | @@ -206,6 +206,66 @@ jobs: with: limit-access-to-actor: true + linux_arm: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - name: Install Docker + run: brew install docker + + - name: Replace pubspec version and BUILD_VERSION Env (nightly) + if: ${{ inputs.channel == 'nightly' }} + run: | + brew install yq + yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml + yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml + echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV + + + - name: BUILD_VERSION Env (stable) + if: ${{ inputs.channel == 'stable' }} + run: | + echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV + + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + + - name: Replace Version in files + run: | + sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml + + - name: Create Stable .env + if: ${{ inputs.channel == 'stable' }} + run: echo '${{ secrets.DOTENV_RELEASE }}' > .env + + - name: Create Nightly .env + if: ${{ inputs.channel == 'nightly' }} + run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env + + - name: Build Linux Arm + run: | + docker build -t spotube-linux-arm -f .github/Dockerfile . --build-arg BUILD_VERSION=${{ env.BUILD_VERSION }} --build-arg FLUTTER_VERSION=${{ env.FLUTTER_VERSION }} + docker create --name spotube-linux-arm spotube-linux-arm + docker cp spotube-linux-arm:/app/dist . + docker rm -f spotube-linux-arm + + - uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: Spotube-Release-Binaries + path: | + dist/Spotube-linux-aarch64.deb + dist/Spotube-linux-aarch64.rpm + dist/spotube-linux-nightly-aarch64.tar.xz + + - name: Debug With SSH When fails + if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} + uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: true + android: runs-on: ubuntu-latest @@ -245,7 +305,7 @@ jobs: - name: Generate Secrets run: | flutter pub get - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + dart run build_runner build --delete-conflicting-outputs - name: Sign Apk run: | @@ -260,7 +320,7 @@ jobs: - name: Build Playstore AppBundle run: | echo 'ENABLE_UPDATE_CHECK=0' >> .env - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + dart run build_runner build --delete-conflicting-outputs export MANIFEST=android/app/src/main/AndroidManifest.xml xmlstarlet ed -d '//meta-data[@android:name="com.google.android.gms.car.application"]' $MANIFEST > $MANIFEST.tmp mv $MANIFEST.tmp $MANIFEST @@ -283,7 +343,6 @@ jobs: limit-access-to-actor: true macos: - runs-on: macos-14 steps: - uses: actions/checkout@v4 @@ -317,7 +376,7 @@ jobs: run: | dart pub global activate flutter_distributor flutter pub get - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + dart run build_runner build --delete-conflicting-outputs - name: Build Macos App run: | @@ -381,7 +440,7 @@ jobs: - name: Generate Secrets run: | flutter pub get - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + dart run build_runner build --delete-conflicting-outputs - name: Build iOS iPA run: | @@ -408,6 +467,7 @@ jobs: needs: - windows - linux + - linux_arm - android - macos - iOS From ef7833eb672feb591b424ece900e0b3b199fe036 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Apr 2024 16:10:28 +0600 Subject: [PATCH 07/57] cd: fix sed failing us --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 044738c95..4979c21a0 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -234,7 +234,7 @@ jobs: - name: Replace Version in files run: | - sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml + sed -i '' 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml - name: Create Stable .env if: ${{ inputs.channel == 'stable' }} From 88fea7ecf9ea426d26b6c8ad44e9b872136e8eb5 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Apr 2024 16:14:17 +0600 Subject: [PATCH 08/57] cd: use docker cask --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 4979c21a0..c77531550 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -212,7 +212,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Docker - run: brew install docker + run: brew install --cask docker - name: Replace pubspec version and BUILD_VERSION Env (nightly) if: ${{ inputs.channel == 'nightly' }} From 937a706ac9c0e59943b2609e5cc398dcdbed2344 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 May 2024 20:10:19 +0600 Subject: [PATCH 09/57] fix: windows SSL Certificate error breaking login #905 (#1474) * fix: certificate error by using custom ssl certificate * Cd/docker linux ar (#1468) * cd: use docker buildx * cd: use linux host for linux arm instead of macos m1 m1 doesn't support nested virtualization. (Apple truly sucks) * cd: don't specify arch in Dockerfile * cd: use custom Dockerfile from ubuntu instead of flutter image * cd: add setup java for android * cd: add flutter distributor pre-built docker image for arm * cd: save me from this cursed arm build * cd: ?? * cd: ?? * cd: use docker build * fix: windows SSL Exception for Signing in * refactor: extract update checker as a basic function instead of a hook --- .dockerignore | 2 + .github/Dockerfile | 23 ++-- .github/Dockerfile.flutter_distributor | 23 ++++ .github/workflows/spotube-release-binary.yml | 69 ++++++++---- lib/components/root/update_dialog.dart | 46 ++++++++ .../configurators/use_update_checker.dart | 100 ------------------ lib/pages/root/root_app.dart | 5 +- lib/provider/authentication_provider.dart | 40 ++++--- lib/utils/service_utils.dart | 52 ++++++++- pubspec.yaml | 1 + 10 files changed, 211 insertions(+), 150 deletions(-) create mode 100644 .github/Dockerfile.flutter_distributor create mode 100644 lib/components/root/update_dialog.dart delete mode 100644 lib/hooks/configurators/use_update_checker.dart diff --git a/.dockerignore b/.dockerignore index 55fee41a5..ddfd15179 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,5 @@ build dist .dart_tool .idea +.github +.git \ No newline at end of file diff --git a/.github/Dockerfile b/.github/Dockerfile index e4dacb0e9..007d1a6e7 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -1,32 +1,27 @@ ARG FLUTTER_VERSION -ARG BUILD_VERSION -FROM --platform=arm64 fischerscode/flutter-sudo:${FLUTTER_VERSION} +FROM --platform=linux/arm64 krtirtho/flutter_distributor_arm64:${FLUTTER_VERSION} -WORKDIR /app +ARG BUILD_VERSION -# Install dependencies -RUN sudo apt-get update &&\ - sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev rpm &&\ - sudo rm -rf /var/lib/apt/lists/* +WORKDIR /app COPY . . -RUN sudo chown -R $(whoami) /app +RUN chown -R $(whoami) /app RUN flutter pub get &&\ flutter config --enable-linux-desktop &&\ flutter pub get &&\ dart run build_runner build --delete-conflicting-outputs -RUN dart pub global activate flutter_distributor &&\ - alias dpkg-deb="dpkg-deb --Zxz" &&\ - flutter_distributor package --platform=linux --targets=deb &&\ - flutter_distributor package --platform=linux --targets=rpm +RUN alias dpkg-deb="dpkg-deb --Zxz" &&\ + flutter_distributor package --platform=linux --targets=deb RUN make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64 RUN mv build/spotube-linux-*-aarch64.tar.xz dist/ &&\ - mv dist/**/spotube-*-linux.deb dist/Spotube-linux-aarch64.deb &&\ - mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-aarch64.rpm \ No newline at end of file + mv dist/**/spotube-*-linux.deb dist/Spotube-linux-aarch64.deb + +CMD [ "sleep", "5000000" ] \ No newline at end of file diff --git a/.github/Dockerfile.flutter_distributor b/.github/Dockerfile.flutter_distributor new file mode 100644 index 000000000..952b9158e --- /dev/null +++ b/.github/Dockerfile.flutter_distributor @@ -0,0 +1,23 @@ +FROM --platform=linux/arm64 ubuntu:22.04 + +ARG FLUTTER_VERSION + +RUN apt-get clean &&\ + apt-get update &&\ + apt-get install -y bash curl file git unzip xz-utils zip libglu1-mesa cmake tar clang ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev rpm && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /home/flutter + +RUN git clone https://github.com/flutter/flutter.git -b ${FLUTTER_VERSION} --single-branch flutter-sdk + +RUN flutter-sdk/bin/flutter precache + +RUN flutter-sdk/bin/flutter config --no-analytics + +ENV PATH="$PATH:/home/flutter/flutter-sdk/bin" +ENV PATH="$PATH:/home/flutter/flutter-sdk/bin/cache/dart-sdk/bin" +ENV PATH="$PATH:/home/flutter/.pub-cache/bin" +ENV PUB_CACHE="/home/flutter/.pub-cache" + +RUN dart pub global activate flutter_distributor \ No newline at end of file diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index c77531550..c7fcbf446 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -207,35 +207,36 @@ jobs: limit-access-to-actor: true linux_arm: - runs-on: macos-14 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Install Docker - run: brew install --cask docker + - name: Install Dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y pkg-config make python3-pip python3-setuptools - name: Replace pubspec version and BUILD_VERSION Env (nightly) if: ${{ inputs.channel == 'nightly' }} run: | - brew install yq + curl -sS https://webi.sh/yq | sh yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - - name: BUILD_VERSION Env (stable) if: ${{ inputs.channel == 'stable' }} run: | echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV - - name: Get current date - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - - name: Replace Version in files - run: | - sed -i '' 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml - - name: Create Stable .env if: ${{ inputs.channel == 'stable' }} run: echo '${{ secrets.DOTENV_RELEASE }}' > .env @@ -244,20 +245,42 @@ jobs: if: ${{ inputs.channel == 'nightly' }} run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - name: Build Linux Arm + - name: Replace Version in files + run: | + sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml + + - name: Build Binaries (stable) + if: ${{ inputs.channel == 'stable' }} + run: | + docker buildx build --platform=linux/arm64 -f .github/Dockerfile . --build-arg FLUTTER_VERSION=${{env.FLUTTER_VERSION}} --build-arg BUILD_VERSION=${{env.BUILD_VERSION}} -t krtirtho/spotube_linux_arm:latest --load + + - name: Build Binaries (nightly) + if: ${{ inputs.channel == 'nightly' }} run: | - docker build -t spotube-linux-arm -f .github/Dockerfile . --build-arg BUILD_VERSION=${{ env.BUILD_VERSION }} --build-arg FLUTTER_VERSION=${{ env.FLUTTER_VERSION }} - docker create --name spotube-linux-arm spotube-linux-arm - docker cp spotube-linux-arm:/app/dist . - docker rm -f spotube-linux-arm + docker buildx build --platform=linux/arm64 -f .github/Dockerfile . --build-arg FLUTTER_VERSION=${{env.FLUTTER_VERSION}} --build-arg BUILD_VERSION=nightly -t krtirtho/spotube_linux_arm:latest --load + - name: Copy the built packages + run: | + docker images ls + docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest + docker cp spotube_linux_arm:/app/dist/ dist/ + + - uses: actions/upload-artifact@v3 + if: ${{ inputs.channel == 'stable' }} + with: + if-no-files-found: error + name: Spotube-Release-Binaries + path: | + dist/Spotube-linux-aarch64.deb + dist/spotube-linux-${{ env.BUILD_VERSION }}-aarch64.tar.xz + - uses: actions/upload-artifact@v3 + if: ${{ inputs.channel == 'nightly' }} with: if-no-files-found: error name: Spotube-Release-Binaries path: | dist/Spotube-linux-aarch64.deb - dist/Spotube-linux-aarch64.rpm dist/spotube-linux-nightly-aarch64.tar.xz - name: Debug With SSH When fails @@ -266,7 +289,6 @@ jobs: with: limit-access-to-actor: true - android: runs-on: ubuntu-latest steps: @@ -275,6 +297,13 @@ jobs: with: cache: true flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + check-latest: true - name: Install Dependencies run: | diff --git a/lib/components/root/update_dialog.dart b/lib/components/root/update_dialog.dart new file mode 100644 index 000000000..f5388aa1f --- /dev/null +++ b/lib/components/root/update_dialog.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:spotube/components/shared/links/anchor_button.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:version/version.dart'; + +class RootAppUpdateDialog extends StatelessWidget { + final Version? version; + const RootAppUpdateDialog({super.key, this.version}); + + @override + Widget build(BuildContext context) { + const url = "https://spotube.krtirtho.dev/downloads"; + return AlertDialog( + title: const Text("Spotube has an update"), + actions: [ + FilledButton( + child: const Text("Download Now"), + onPressed: () => launchUrlString( + url, + mode: LaunchMode.externalApplication, + ), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Spotube v$version has been released"), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Read the latest "), + AnchorButton( + "release notes", + style: const TextStyle(color: Colors.blue), + onTap: () => launchUrlString( + url, + mode: LaunchMode.externalApplication, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/hooks/configurators/use_update_checker.dart b/lib/hooks/configurators/use_update_checker.dart deleted file mode 100644 index 7b937efbf..000000000 --- a/lib/hooks/configurators/use_update_checker.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:http/http.dart' as http; -import 'package:spotube/collections/env.dart'; - -import 'package:spotube/components/shared/links/anchor_button.dart'; -import 'package:spotube/hooks/controllers/use_package_info.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:version/version.dart'; - -void useUpdateChecker(WidgetRef ref) { - final isCheckUpdateEnabled = - ref.watch(userPreferencesProvider.select((s) => s.checkUpdate)); - final packageInfo = usePackageInfo( - appName: 'Spotube', - packageName: 'spotube', - ); - final Future> Function() checkUpdate = useCallback( - () async { - final value = await http.get( - Uri.parse( - "https://api.github.com/repos/KRTirtho/spotube/releases/latest"), - ); - final tagName = - (jsonDecode(value.body)["tag_name"] as String).replaceAll("v", ""); - final currentVersion = packageInfo.version == "Unknown" - ? null - : Version.parse(packageInfo.version); - final latestVersion = - tagName == "nightly" ? null : Version.parse(tagName); - return [currentVersion, latestVersion]; - }, - [packageInfo.version], - ); - - final context = useContext(); - - download(String url) => launchUrlString( - url, - mode: LaunchMode.externalApplication, - ); - - useEffect(() { - if (!Env.enableUpdateChecker) return; - if (!isCheckUpdateEnabled) return null; - checkUpdate().then((value) { - final currentVersion = value.first; - final latestVersion = value.last; - if (currentVersion == null || - latestVersion == null || - (latestVersion.isPreRelease && !currentVersion.isPreRelease) || - (!latestVersion.isPreRelease && currentVersion.isPreRelease)) return; - if (latestVersion <= currentVersion) return; - showDialog( - context: context, - barrierDismissible: true, - barrierColor: Colors.black26, - builder: (context) { - const url = - "https://spotube.krtirtho.dev/downloads"; - return AlertDialog( - title: const Text("Spotube has an update"), - actions: [ - FilledButton( - child: const Text("Download Now"), - onPressed: () => download(url), - ), - ], - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Spotube v${value.last} has been released"), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Read the latest "), - AnchorButton( - "release notes", - style: const TextStyle(color: Colors.blue), - onTap: () => launchUrlString( - url, - mode: LaunchMode.externalApplication, - ), - ), - ], - ), - ], - ), - ); - }, - ); - }); - return null; - }, [packageInfo, isCheckUpdateEnabled]); -} diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index f3ed6571e..42bf3f695 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -14,13 +14,13 @@ import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart'; -import 'package:spotube/hooks/configurators/use_update_checker.dart'; import 'package:spotube/provider/connect/server.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/service_utils.dart'; const rootPaths = { "/": 0, @@ -46,6 +46,8 @@ class RootApp extends HookConsumerWidget { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { + ServiceUtils.checkForUpdates(context, ref); + final sharedPreferences = await SharedPreferences.getInstance(); if (sharedPreferences.getBool(kIsUsingEncryption) == false && @@ -160,7 +162,6 @@ class RootApp extends HookConsumerWidget { }, [downloader]); // checks for latest version of the application - useUpdateChecker(ref); useEndlessPlayback(ref); diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication_provider.dart index a82f82c0c..c94f4f3ee 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication_provider.dart @@ -1,10 +1,12 @@ import 'dart:async'; -import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart' + hide X509Certificate; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; import 'package:spotube/extensions/context.dart'; @@ -18,6 +20,18 @@ class AuthenticationCredentials { bool get isExpired => DateTime.now().isAfter(expiration); + static final Dio dio = () { + final dio = Dio(); + + (dio.httpClientAdapter as IOHttpClientAdapter) + .createHttpClient = () => HttpClient() + ..badCertificateCallback = (X509Certificate cert, String host, int port) { + return host.endsWith("spotify.com") && port == 443; + }; + + return dio; + }(); + AuthenticationCredentials({ required this.cookie, required this.accessToken, @@ -30,21 +44,23 @@ class AuthenticationCredentials { .split("; ") .firstWhereOrNull((c) => c.trim().startsWith("sp_dc=")) ?.trim(); - final res = await get( + final res = await dio.getUri( Uri.parse( "https://open.spotify.com/get_access_token?reason=transport&productType=web_player", ), - headers: { - "Cookie": spDc ?? "", - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" - }, + options: Options( + headers: { + "Cookie": spDc ?? "", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" + }, + ), ); - final body = jsonDecode(res.body); + final body = res.data; - if (res.statusCode >= 400) { + if ((res.statusCode ?? 500) >= 400) { throw Exception( - "Failed to get access token: ${body['error'] ?? res.reasonPhrase}", + "Failed to get access token: ${body['error'] ?? res.statusMessage}", ); } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 88c528966..30c92e1d7 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:flutter/widgets.dart' hide Element; import 'package:go_router/go_router.dart'; -import 'package:html/dom.dart'; +import 'package:html/dom.dart' hide Text; import 'package:spotify/spotify.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/components/root/update_dialog.dart'; import 'package:spotube/models/logger.dart'; import 'package:http/http.dart' as http; import 'package:spotube/models/lyrics.dart'; @@ -14,6 +14,16 @@ import 'package:spotube/utils/primitive_utils.dart'; import 'package:collection/collection.dart'; import 'package:html/parser.dart' as parser; +import 'dart:async'; + +import 'package:flutter/material.dart' hide Element; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:spotube/collections/env.dart'; + +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:version/version.dart'; + abstract class ServiceUtils { static final logger = getLogger("ServiceUtils"); @@ -318,4 +328,42 @@ abstract class ServiceUtils { } }); } + + static Future checkForUpdates( + BuildContext context, + WidgetRef ref, + ) async { + if (!Env.enableUpdateChecker) return; + if (!ref.read(userPreferencesProvider.select((s) => s.checkUpdate))) return; + + final packageInfo = await PackageInfo.fromPlatform(); + + final value = await http.get( + Uri.parse( + "https://api.github.com/repos/KRTirtho/spotube/releases/latest", + ), + ); + final tagName = + (jsonDecode(value.body)["tag_name"] as String).replaceAll("v", ""); + final currentVersion = packageInfo.version == "Unknown" + ? null + : Version.parse(packageInfo.version); + final latestVersion = tagName == "nightly" ? null : Version.parse(tagName); + + if (currentVersion == null || + latestVersion == null || + (latestVersion.isPreRelease && !currentVersion.isPreRelease) || + (!latestVersion.isPreRelease && currentVersion.isPreRelease)) return; + + if (latestVersion <= currentVersion || !context.mounted) return; + + showDialog( + context: context, + barrierDismissible: true, + barrierColor: Colors.black26, + builder: (context) { + return RootAppUpdateDialog(version: latestVersion); + }, + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 62c20c354..20acd3d45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -153,6 +153,7 @@ flutter: uses-material-design: true assets: - assets/ + - assets/ca/ - assets/tutorial/ - assets/logos/ - LICENSE From 7ad67fa3fa6cb44b926bedf2f682f589a9b1b206 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 May 2024 20:39:36 +0600 Subject: [PATCH 10/57] cd: fix windows build error due to nightly version format --- .github/workflows/spotube-release-binary.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index c7fcbf446..6139bacb2 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -42,9 +42,10 @@ jobs: if: ${{ inputs.channel == 'nightly' }} run: | choco install sed make yq -y - yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml + yq -i '.version |= sub("\+\d+", "-${{ inputs.channel }}+")' pubspec.yaml yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $env:GITHUB_ENV + "BUILD_VERSION=${{ inputs.version }}-${{ inputs.channel }}+${{ github.run_number }}" >> $env:GITHUB_ENV + shell: bash - name: BUILD_VERSION Env (stable) if: ${{ inputs.channel == 'stable' }} From c1a105a1ffed7207120cffed812b7a890ec63368 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 May 2024 21:00:06 +0600 Subject: [PATCH 11/57] cd: fix github versioning scheme --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 6139bacb2..cabe2dbf6 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -44,7 +44,7 @@ jobs: choco install sed make yq -y yq -i '.version |= sub("\+\d+", "-${{ inputs.channel }}+")' pubspec.yaml yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - "BUILD_VERSION=${{ inputs.version }}-${{ inputs.channel }}+${{ github.run_number }}" >> $env:GITHUB_ENV + echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV shell: bash - name: BUILD_VERSION Env (stable) From 2286277a062833e541fde625376acd6a0a03b48e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 May 2024 21:26:45 +0600 Subject: [PATCH 12/57] chore: remove assets/ca entry in pubspec.yaml --- pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 20acd3d45..62c20c354 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -153,7 +153,6 @@ flutter: uses-material-design: true assets: - assets/ - - assets/ca/ - assets/tutorial/ - assets/logos/ - LICENSE From 4ca893950b07f678acf7db690112c47d21e54782 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 5 May 2024 09:15:52 +0600 Subject: [PATCH 13/57] fix(macos): Logs directory not created by default #1353 --- lib/models/logger.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/logger.dart b/lib/models/logger.dart index 4f687d093..3236028dc 100644 --- a/lib/models/logger.dart +++ b/lib/models/logger.dart @@ -27,7 +27,7 @@ Future getLogsPath() async { } final file = File(path.join(dir, ".spotube_logs")); if (!await file.exists()) { - await file.create(); + await file.create(recursive: true); } return file; } From a77b6776e81d88d665a7368fa0fb71b65933afb8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 9 May 2024 15:26:58 +0600 Subject: [PATCH 14/57] refactor: Dart based Github Workflow CLI (#1490) * feat: add build dart script for windows * feat: add android build support * feat: add linux build support * feat: add macos build support * feat: add ios build support * feat: add deps install command and workflow file * cd: what? * cd: what? * cd: what? * cd: update workflow inputs * cd: replace release binary * cd: run flutter pub get * cd: use dpkg zstd instead of xz, windows disable innoInstall, fix channel enum.name and reset pubspec after changing build no for nightly * cd: fix tar copy path * cd: fix copy linux command * cd: fix windows inno depend and fix android aab path * cd: idk * cd: linux why??? * cd: windows choco copy failed * cd: use dart tar archive for creating tar file * cd: fix linux file copy error * cd: use tar command directly * feat: add linux_arm platform * cd: add linux_arm platform * cd: don't know what? * feat: notification about nightly channel update * chore: fix some errors parsing nightly version info --- .env.example | 3 + .github/Dockerfile | 8 +- .github/workflows/spotube-release-binary.yml | 523 +++---------------- .metadata | 16 +- cli/README.md | 4 + cli/cli.dart | 16 + cli/commands/build.dart | 25 + cli/commands/build/android.dart | 90 ++++ cli/commands/build/common.dart | 66 +++ cli/commands/build/ios.dart | 29 + cli/commands/build/linux.dart | 106 ++++ cli/commands/build/linux_arm.dart | 37 ++ cli/commands/build/macos.dart | 42 ++ cli/commands/build/windows.dart | 100 ++++ cli/commands/install-dependencies.dart | 74 +++ cli/core/env.dart | 24 + lib/collections/env.dart | 12 + lib/components/root/update_dialog.dart | 42 +- lib/pages/settings/about.dart | 8 + lib/utils/service_utils.dart | 80 ++- pubspec.lock | 24 +- pubspec.yaml | 3 + windows/CMakeLists.txt | 29 +- windows/runner/Runner.rc | 14 +- 24 files changed, 840 insertions(+), 535 deletions(-) create mode 100644 cli/README.md create mode 100644 cli/cli.dart create mode 100644 cli/commands/build.dart create mode 100644 cli/commands/build/android.dart create mode 100644 cli/commands/build/common.dart create mode 100644 cli/commands/build/ios.dart create mode 100644 cli/commands/build/linux.dart create mode 100644 cli/commands/build/linux_arm.dart create mode 100644 cli/commands/build/macos.dart create mode 100644 cli/commands/build/windows.dart create mode 100644 cli/commands/install-dependencies.dart create mode 100644 cli/core/env.dart diff --git a/.env.example b/.env.example index 22abd24bd..566656632 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,6 @@ ENABLE_UPDATE_CHECK= LASTFM_API_KEY= LASTFM_API_SECRET= + +# Release channel. Can be: nightly, stable +RELEASE_CHANNEL= diff --git a/.github/Dockerfile b/.github/Dockerfile index 007d1a6e7..2e3934499 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -10,14 +10,10 @@ COPY . . RUN chown -R $(whoami) /app -RUN flutter pub get &&\ - flutter config --enable-linux-desktop &&\ - flutter pub get &&\ - dart run build_runner build --delete-conflicting-outputs +RUN flutter pub get RUN alias dpkg-deb="dpkg-deb --Zxz" &&\ - flutter_distributor package --platform=linux --targets=deb - + flutter_distributor package --platform=linux --targets=deb --skip-clean RUN make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64 diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index cabe2dbf6..0fe1f1bac 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -2,296 +2,65 @@ name: Spotube Release Binary on: workflow_dispatch: inputs: - version: - description: Version to release (x.x.x) - default: 3.6.0 - required: true channel: type: choice - description: Release Channel - required: true options: - stable - nightly default: nightly + description: The release channel debug: - description: Debug on failed when channel is nightly - required: true type: boolean default: false + description: Debug with SSH toggle + required: false dry_run: - description: Dry run - required: true type: boolean - default: true + default: false + description: Dry run without uploading to release env: - FLUTTER_VERSION: '3.19.5' - -jobs: - windows: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.12.0 - with: - cache: true - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Replace pubspec version and BUILD_VERSION Env (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - choco install sed make yq -y - yq -i '.version |= sub("\+\d+", "-${{ inputs.channel }}+")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - shell: bash - - - name: BUILD_VERSION Env (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - "BUILD_VERSION=${{ inputs.version }}" >> $env:GITHUB_ENV - - - name: Replace version in files - run: | - choco install sed make -y - sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" windows/runner/Runner.rc - sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/tools/VERIFICATION.txt - sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/spotube.nuspec - - - name: Create Stable .env - if: ${{ inputs.channel == 'stable' }} - run: echo '${{ secrets.DOTENV_RELEASE }}' > .env - - - name: Create Nightly .env - if: ${{ inputs.channel == 'nightly' }} - run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - - name: Generating Secrets - run: | - flutter config --enable-windows-desktop - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - - name: Build Windows Executable - run: | - dart pub global activate flutter_distributor - make innoinstall - flutter_distributor package --platform=windows --targets=exe --skip-clean - mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe - - - name: Create Chocolatey Package and set hash - if: ${{ inputs.channel == 'stable' }} - run: | - Set-Variable -Name HASH -Value (Get-FileHash dist\Spotube-windows-x86_64-setup.exe).Hash - sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt - make choco - mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg - - - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - dist/Spotube-windows-x86_64.nupkg - dist/Spotube-windows-x86_64-setup.exe - - - name: Debug With SSH When fails - if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - - linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.12.0 - with: - cache: true - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Get current date - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - - name: Install Dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev - - - name: Install AppImage Tool - run: | - wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x appimagetool - mv appimagetool /usr/local/bin/ - - - name: Replace pubspec version and BUILD_VERSION Env (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - curl -sS https://webi.sh/yq | sh - yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - - - name: BUILD_VERSION Env (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV - - - name: Create Stable .env - if: ${{ inputs.channel == 'stable' }} - run: echo '${{ secrets.DOTENV_RELEASE }}' > .env - - - name: Create Nightly .env - if: ${{ inputs.channel == 'nightly' }} - run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - - name: Replace Version in files - run: | - sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml - - - name: Generate Secrets - run: | - flutter config --enable-linux-desktop - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - - name: Build Linux Packages - run: | - dart pub global activate flutter_distributor - alias dpkg-deb="dpkg-deb --Zxz" - flutter_distributor package --platform=linux --targets=deb - flutter_distributor package --platform=linux --targets=rpm - - - name: Create tar.xz (stable) - if: ${{ inputs.channel == 'stable' }} - run: make tar VERSION=${{ env.BUILD_VERSION }} ARCH=x64 PKG_ARCH=x86_64 - - - name: Create tar.xz (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: make tar VERSION=nightly ARCH=x64 PKG_ARCH=x86_64 - - - name: Move Files to dist - run: | - mv build/spotube-linux-*-x86_64.tar.xz dist/ - mv dist/**/spotube-*-linux.deb dist/Spotube-linux-x86_64.deb - mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm - - - - uses: actions/upload-artifact@v3 - if: ${{ inputs.channel == 'stable' }} - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - dist/Spotube-linux-x86_64.deb - dist/Spotube-linux-x86_64.rpm - dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz - - - uses: actions/upload-artifact@v3 - if: ${{ inputs.channel == 'nightly' }} - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - dist/Spotube-linux-x86_64.deb - dist/Spotube-linux-x86_64.rpm - dist/spotube-linux-nightly-x86_64.tar.xz - - - name: Debug With SSH When fails - if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - - linux_arm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Get current date - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - - name: Install Dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y pkg-config make python3-pip python3-setuptools - - - name: Replace pubspec version and BUILD_VERSION Env (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - curl -sS https://webi.sh/yq | sh - yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - - - name: BUILD_VERSION Env (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV - - - name: Create Stable .env - if: ${{ inputs.channel == 'stable' }} - run: echo '${{ secrets.DOTENV_RELEASE }}' > .env - - - name: Create Nightly .env - if: ${{ inputs.channel == 'nightly' }} - run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - - name: Replace Version in files - run: | - sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml - - - name: Build Binaries (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - docker buildx build --platform=linux/arm64 -f .github/Dockerfile . --build-arg FLUTTER_VERSION=${{env.FLUTTER_VERSION}} --build-arg BUILD_VERSION=${{env.BUILD_VERSION}} -t krtirtho/spotube_linux_arm:latest --load - - - name: Build Binaries (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - docker buildx build --platform=linux/arm64 -f .github/Dockerfile . --build-arg FLUTTER_VERSION=${{env.FLUTTER_VERSION}} --build-arg BUILD_VERSION=nightly -t krtirtho/spotube_linux_arm:latest --load - - - name: Copy the built packages - run: | - docker images ls - docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest - docker cp spotube_linux_arm:/app/dist/ dist/ + FLUTTER_VERSION: 3.19.5 - - uses: actions/upload-artifact@v3 - if: ${{ inputs.channel == 'stable' }} - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - dist/Spotube-linux-aarch64.deb - dist/spotube-linux-${{ env.BUILD_VERSION }}-aarch64.tar.xz - - - uses: actions/upload-artifact@v3 - if: ${{ inputs.channel == 'nightly' }} - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - dist/Spotube-linux-aarch64.deb - dist/spotube-linux-nightly-aarch64.tar.xz +permissions: + contents: write - - name: Debug With SSH When fails - if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - - android: - runs-on: ubuntu-latest +jobs: + build_platform: + strategy: + matrix: + include: + - os: ubuntu-latest + platform: linux + files: | + dist/Spotube-linux-x86_64.deb + dist/Spotube-linux-x86_64.rpm + dist/spotube-linux-*-x86_64.tar.xz + - os: ubuntu-latest + platform: linux_arm + files: | + dist/Spotube-linux-aarch64.deb + dist/spotube-linux-*-aarch64.tar.xz + - os: ubuntu-latest + platform: android + files: | + build/Spotube-android-all-arch.apk + build/Spotube-playstore-all-arch.aab + - os: windows-latest + platform: windows + files: | + dist/Spotube-windows-x86_64.nupkg + dist/Spotube-windows-x86_64-setup.exe + - os: macos-latest + platform: ios + files: | + Spotube-iOS.ipa + - os: macos-14 + platform: macos + files: | + build/Spotube-macos-universal.dmg + build/Spotube-macos-universal.pkg + runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.12.0 @@ -299,137 +68,42 @@ jobs: cache: true flutter-version: ${{ env.FLUTTER_VERSION }} - name: Setup Java + if: ${{matrix.platform == 'android'}} uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' cache: 'gradle' check-latest: true + - name: Set up QEMU + if: ${{matrix.platform == 'linux_arm'}} + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + if: ${{matrix.platform == 'linux_arm'}} + uses: docker/setup-buildx-action@v3 - - name: Install Dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse xmlstarlet - - - name: Replace pubspec version and BUILD_VERSION Env (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - curl -sS https://webi.sh/yq | sh - yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - - - name: BUILD_VERSION Env (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV - - - name: Create Stable .env - if: ${{ inputs.channel == 'stable' }} - run: echo '${{ secrets.DOTENV_RELEASE }}' > .env - - - name: Create Nightly .env - if: ${{ inputs.channel == 'nightly' }} - run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - - name: Generate Secrets + - name: Install ${{matrix.platform}} dependencies run: | flutter pub get - dart run build_runner build --delete-conflicting-outputs + dart cli/cli.dart install-dependencies --platform=${{matrix.platform}} - name: Sign Apk + if: ${{matrix.platform == 'android'}} run: | echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties - - - name: Build Apk - run: | - flutter build apk --flavor ${{ inputs.channel }} - mv build/app/outputs/flutter-apk/app-${{ inputs.channel }}-release.apk build/Spotube-android-all-arch.apk - - - name: Build Playstore AppBundle - run: | - echo 'ENABLE_UPDATE_CHECK=0' >> .env - dart run build_runner build --delete-conflicting-outputs - export MANIFEST=android/app/src/main/AndroidManifest.xml - xmlstarlet ed -d '//meta-data[@android:name="com.google.android.gms.car.application"]' $MANIFEST > $MANIFEST.tmp - mv $MANIFEST.tmp $MANIFEST - flutter build appbundle --flavor ${{ inputs.channel }} - mv build/app/outputs/bundle/${{ inputs.channel }}Release/app-${{ inputs.channel }}-release.aab build/Spotube-playstore-all-arch.aab - - - - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - build/Spotube-android-all-arch.apk - build/Spotube-playstore-all-arch.aab - - - name: Debug With SSH When fails - if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - - macos: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.12.0 - with: - cache: true - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Replace pubspec version and BUILD_VERSION Env (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - brew install yq - yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - - - name: BUILD_VERSION Env (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV - - - name: Create Stable .env - if: ${{ inputs.channel == 'stable' }} - run: echo '${{ secrets.DOTENV_RELEASE }}' > .env - - - name: Create Nightly .env - if: ${{ inputs.channel == 'nightly' }} - run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - - name: Generate Secrets - run: | - dart pub global activate flutter_distributor - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - - name: Build Macos App - run: | - flutter config --enable-macos-desktop - flutter build macos - du -sh build/macos/Build/Products/Release/spotube.app - - - name: Package Macos App - run: | - brew install python-setuptools - npm install -g appdmg - mkdir -p build/${{ env.BUILD_VERSION }} - appdmg appdmg.json build/Spotube-macos-universal.dmg - flutter_distributor package --platform=macos --targets pkg --skip-clean - mv dist/**/spotube-*-macos.pkg build/Spotube-macos-universal.pkg - + + - name: Build ${{matrix.platform}} binaries + run: dart cli/cli.dart build ${{matrix.platform}} + env: + CHANNEL: ${{inputs.channel}} + DOTENV: ${{secrets.DOTENV_RELEASE}} + - uses: actions/upload-artifact@v3 with: if-no-files-found: error name: Spotube-Release-Binaries - path: | - build/Spotube-macos-universal.dmg - build/Spotube-macos-universal.pkg + path: ${{matrix.files}} - name: Debug With SSH When fails if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} @@ -437,70 +111,10 @@ jobs: with: limit-access-to-actor: true - iOS: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2.10.0 - with: - cache: true - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Replace pubspec version and BUILD_VERSION Env (nightly) - if: ${{ inputs.channel == 'nightly' }} - run: | - brew install yq - yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV - - - name: BUILD_VERSION Env (stable) - if: ${{ inputs.channel == 'stable' }} - run: | - echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV - - - name: Create Stable .env - if: ${{ inputs.channel == 'stable' }} - run: echo '${{ secrets.DOTENV_RELEASE }}' > .env - - - name: Create Nightly .env - if: ${{ inputs.channel == 'nightly' }} - run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env - - - name: Generate Secrets - run: | - flutter pub get - dart run build_runner build --delete-conflicting-outputs - - - name: Build iOS iPA - run: | - flutter build ios --release --no-codesign --flavor ${{ inputs.channel }} - ln -sf ./build/ios/iphoneos Payload - zip -r9 Spotube-iOS.ipa Payload/${{ inputs.channel }}.app - - - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: Spotube-Release-Binaries - path: | - Spotube-iOS.ipa - - - name: Debug With SSH When fails - if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - upload: runs-on: ubuntu-latest - needs: - - windows - - linux - - linux_arm - - android - - macos - - iOS + - build_platform steps: - uses: actions/download-artifact@v3 with: @@ -516,6 +130,10 @@ jobs: md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum + + - name: Extract pubspec version + run: | + echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV - uses: actions/upload-artifact@v3 with: @@ -530,7 +148,7 @@ jobs: uses: ncipollo/release-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - tag: v${{ inputs.version }} # mind the "v" prefix + tag: v${{ env.PUBSPEC_VERSION }} # mind the "v" prefix omitBodyDuringUpdate: true omitNameDuringUpdate: true omitPrereleaseDuringUpdate: true @@ -548,3 +166,8 @@ jobs: omitPrereleaseDuringUpdate: true allowUpdates: true artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum + body: | + Build Number: ${{github.run_number}} + + Nightly release includes newest features but may contain bugs + It is preferred to use the stable version unless you know what you're doing diff --git a/.metadata b/.metadata index 082985ad4..828f2c0ad 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: eb6d86ee27deecba4a83536aa20f366a6044895c - channel: stable + revision: "300451adae589accbece3490f4396f10bdf15e6e" + channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c - base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c - - platform: macos - create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c - base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c + create_revision: 300451adae589accbece3490f4396f10bdf15e6e + base_revision: 300451adae589accbece3490f4396f10bdf15e6e + - platform: windows + create_revision: 300451adae589accbece3490f4396f10bdf15e6e + base_revision: 300451adae589accbece3490f4396f10bdf15e6e # User provided section diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 000000000..b2ba8ebda --- /dev/null +++ b/cli/README.md @@ -0,0 +1,4 @@ +## Spotube Configuration CLI + +This is used for building the project for multiple platforms and having utilities specific for the project. +Written in Dart diff --git a/cli/cli.dart b/cli/cli.dart new file mode 100644 index 000000000..3210f5574 --- /dev/null +++ b/cli/cli.dart @@ -0,0 +1,16 @@ +import 'package:args/command_runner.dart'; + +import 'commands/build.dart'; +import 'commands/install-dependencies.dart'; + +void main(List args) { + final commandRunner = CommandRunner( + "cli", + "Configuration CLI for Spotube", + ); + + commandRunner.addCommand(InstallDependenciesCommand()); + commandRunner.addCommand(BuildCommand()); + + commandRunner.run(args); +} diff --git a/cli/commands/build.dart b/cli/commands/build.dart new file mode 100644 index 000000000..fdf35a952 --- /dev/null +++ b/cli/commands/build.dart @@ -0,0 +1,25 @@ +import 'package:args/command_runner.dart'; + +import 'build/android.dart'; +import 'build/ios.dart'; +import 'build/linux.dart'; +import 'build/linux_arm.dart'; +import 'build/macos.dart'; +import 'build/windows.dart'; + +class BuildCommand extends Command { + @override + String get description => "Build for different platforms"; + + @override + String get name => "build"; + + BuildCommand() { + addSubcommand(AndroidBuildCommand()); + addSubcommand(IosBuildCommand()); + addSubcommand(LinuxBuildCommand()); + addSubcommand(LinuxArmBuildCommand()); + addSubcommand(MacosBuildCommand()); + addSubcommand(WindowsBuildCommand()); + } +} diff --git a/cli/commands/build/android.dart b/cli/commands/build/android.dart new file mode 100644 index 000000000..800522b8a --- /dev/null +++ b/cli/commands/build/android.dart @@ -0,0 +1,90 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart'; +import 'package:xml/xml.dart'; + +import '../../core/env.dart'; +import 'common.dart'; + +class AndroidBuildCommand extends Command with BuildCommandCommonSteps { + @override + String get description => "Build for android"; + + @override + String get name => "android"; + + @override + FutureOr? run() async { + await bootstrap(); + + await shell.run( + "flutter build apk --flavor ${CliEnv.channel.name}", + ); + + await dotEnvFile.writeAsString( + "\nENABLE_UPDATE_CHECK=0", + mode: FileMode.append, + ); + + final androidManifestFile = File( + join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml")); + + final androidManifestXml = + XmlDocument.parse(await androidManifestFile.readAsString()); + + final deletingElement = + androidManifestXml.findAllElements("meta-data").firstWhereOrNull( + (el) => + el.getAttribute("android:name") == + "com.google.android.gms.car.application", + ); + + deletingElement?.parent?.children.remove(deletingElement); + + await androidManifestFile.writeAsString( + androidManifestXml.toXmlString(pretty: true), + ); + + await shell.run( + """ + dart run build_runner build --delete-conflicting-outputs + flutter build appbundle --flavor ${CliEnv.channel.name} + """, + ); + + final ogApkFile = File( + join( + "build", + "app", + "outputs", + "flutter-apk", + "app-${CliEnv.channel.name}-release.apk", + ), + ); + + await ogApkFile.copy( + join(cwd.path, "build", "Spotube-android-all-arch.apk"), + ); + + final ogAppbundleFile = File( + join( + cwd.path, + "build", + "app", + "outputs", + "bundle", + "${CliEnv.channel.name}Release", + "app-${CliEnv.channel.name}-release.aab", + ), + ); + + await ogAppbundleFile.copy( + join(cwd.path, "build", "Spotube-playstore-all-arch.aab"), + ); + + stdout.writeln("✅ Built Android Apk and Appbundle"); + } +} diff --git a/cli/commands/build/common.dart b/cli/commands/build/common.dart new file mode 100644 index 000000000..4c7e3e510 --- /dev/null +++ b/cli/commands/build/common.dart @@ -0,0 +1,66 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; +import 'package:process_run/shell_run.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import '../../core/env.dart'; + +mixin BuildCommandCommonSteps on Command { + final shell = Shell(); + Directory get cwd => Directory.current; + + Pubspec? _pubspec; + + Pubspec get pubspec { + if (_pubspec != null) { + return _pubspec!; + } + + final pubspecFile = File(join(cwd.path, "pubspec.yaml")); + _pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + return _pubspec!; + } + + String get versionWithoutBuildNumber { + return "${pubspec.version!.major}.${pubspec.version!.minor}.${pubspec.version!.patch}"; + } + + RegExp get versionVarRegExp => + RegExp(r"\%\{\{SPOTUBE_VERSION\}\}\%", multiLine: true); + + File get dotEnvFile => File(join(cwd.path, ".env")); + + Future bootstrap() async { + await dotEnvFile.create(recursive: true); + + await dotEnvFile.writeAsString( + "${CliEnv.dotenv}\n" + "RELEASE_CHANNEL=${CliEnv.channel.name}\n", + ); + + if (CliEnv.channel == BuildChannel.nightly) { + final pubspecFile = File(join(cwd.path, "pubspec.yaml")); + + pubspecFile.writeAsStringSync( + pubspecFile.readAsStringSync().replaceAll( + "version: ${pubspec.version!.canonicalizedVersion}", + "version: $versionWithoutBuildNumber+${CliEnv.ghRunNumber}", + ), + ); + + _pubspec = null; + pubspec; + } + + await shell.run( + """ + flutter pub get + dart run build_runner build --delete-conflicting-outputs + dart pub global activate flutter_distributor + """, + ); + } +} diff --git a/cli/commands/build/ios.dart b/cli/commands/build/ios.dart new file mode 100644 index 000000000..6460f9edb --- /dev/null +++ b/cli/commands/build/ios.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; + +import '../../core/env.dart'; +import 'common.dart'; + +class IosBuildCommand extends Command with BuildCommandCommonSteps { + @override + String get description => "iOS build command"; + + @override + String get name => "ios"; + + @override + FutureOr? run() async { + await bootstrap(); + + final buildDirPath = join(cwd.path, "build", "ios", "iphoneos"); + await shell.run( + """ + flutter build ios --release --no-codesign --flavor ${CliEnv.channel.name} + ln -sf $buildDirPath Payload + zip -r9 Spotube-iOS.ipa ${join("Payload", "${CliEnv.channel.name}.app")} + """, + ); + } +} diff --git a/cli/commands/build/linux.dart b/cli/commands/build/linux.dart new file mode 100644 index 000000000..a218720ce --- /dev/null +++ b/cli/commands/build/linux.dart @@ -0,0 +1,106 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:io/io.dart'; +import 'package:args/command_runner.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart'; + +import '../../core/env.dart'; +import 'common.dart'; + +class LinuxBuildCommand extends Command with BuildCommandCommonSteps { + @override + String get description => "Linux build command"; + + @override + String get name => "linux"; + + @override + FutureOr? run() async { + stdout.writeln("Replacing versions"); + + final appDataFile = File( + join(cwd.path, "linux", "com.github.KRTirtho.Spotube.appdata.xml"), + ); + + appDataFile.writeAsStringSync( + appDataFile.readAsStringSync().replaceAll( + versionVarRegExp, + '', + ), + ); + + await bootstrap(); + + await shell.run( + """ + flutter_distributor package --platform=linux --targets=deb + flutter_distributor package --platform=linux --targets=rpm + """, + ); + + final tempDir = join(Directory.systemTemp.path, "spotube-tar"); + + final bundleDirPath = + join(cwd.path, "build", "linux", "x64", "release", "bundle"); + + final tarFile = File(join( + cwd.path, + "dist", + "spotube-linux-" + "${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}" + "-x86_64.tar.xz", + )); + + await copyPath(bundleDirPath, tempDir); + await File(join(cwd.path, "linux", "spotube.desktop")).copy( + join(tempDir, "spotube.desktop"), + ); + await File( + join(cwd.path, "linux", "com.github.KRTirtho.Spotube.appdata.xml"), + ).copy( + join(tempDir, "com.github.KRTirtho.Spotube.appdata.xml"), + ); + await File(join(cwd.path, "assets", "spotube-logo.png")).copy( + join(tempDir, "spotube-logo.png"), + ); + + await shell.run( + "tar -cJf ${tarFile.path} -C $tempDir .", + ); + + final ogDeb = File( + join( + cwd.path, + "dist", + pubspec.version.toString(), + "spotube-${pubspec.version}-linux.deb", + ), + ); + + final ogRpm = File( + join( + cwd.path, + "dist", + pubspec.version.toString(), + "spotube-${pubspec.version}-linux.rpm", + ), + ); + + await ogDeb.copy( + join(cwd.path, "dist", "Spotube-linux-x86_64.deb"), + ); + await ogRpm.copy( + join(cwd.path, "dist", "Spotube-linux-x86_64.rpm"), + ); + + await ogDeb.delete(); + await ogRpm.delete(); + + stdout.writeln("✅ Linux building done"); + } +} diff --git a/cli/commands/build/linux_arm.dart b/cli/commands/build/linux_arm.dart new file mode 100644 index 000000000..a09f09808 --- /dev/null +++ b/cli/commands/build/linux_arm.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; + +import '../../core/env.dart'; +import 'common.dart'; + +class LinuxArmBuildCommand extends Command with BuildCommandCommonSteps { + @override + String get description => "Build Linux Arm"; + + @override + String get name => "linux_arm"; + + @override + FutureOr? run() async { + await bootstrap(); + + await shell.run( + "docker buildx build --platform=linux/arm64 " + "-f ${join(cwd.path, ".github", "Dockerfile")} ${cwd.path} " + "--build-arg FLUTTER_VERSION=${CliEnv.flutterVersion} " + "--build-arg BUILD_VERSION=${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber} " + "-t krtirtho/spotube_linux_arm:latest " + "--load", + ); + + await shell.run( + """ + docker images ls + docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest + docker cp spotube_linux_arm:/app/dist/ dist/ + """, + ); + } +} diff --git a/cli/commands/build/macos.dart b/cli/commands/build/macos.dart new file mode 100644 index 000000000..e8f34b775 --- /dev/null +++ b/cli/commands/build/macos.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; + +import 'common.dart'; + +class MacosBuildCommand extends Command with BuildCommandCommonSteps { + @override + String get description => "Macos Build command"; + + @override + String get name => "macos"; + + @override + FutureOr? run() async { + await bootstrap(); + + await shell.run( + """ + flutter build macos + appdmg appdmg.json ${join(cwd.path, "build", "Spotube-macos-universal.dmg")} + flutter_distributor package --platform=macos --targets pkg --skip-clean + """, + ); + + final ogPkg = File( + join( + cwd.path, + "dist", + pubspec.version.toString(), + "spotube-${pubspec.version}-macos.pkg", + ), + ); + + await ogPkg.copy( + join(cwd.path, "build", "Spotube-macos-universal.pkg"), + ); + await ogPkg.delete(); + } +} diff --git a/cli/commands/build/windows.dart b/cli/commands/build/windows.dart new file mode 100644 index 000000000..15e0bf170 --- /dev/null +++ b/cli/commands/build/windows.dart @@ -0,0 +1,100 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; +import 'package:crypto/crypto.dart'; +import 'common.dart'; + +class WindowsBuildCommand extends Command with BuildCommandCommonSteps { + @override + String get description => "Build Windows exe"; + + @override + String get name => "windows"; + + Future innoDependInstall() async { + final innoDependencyPath = join(cwd.path, "build", "inno-depend"); + + await shell.run( + "git clone https://github.com/DomGries/InnoDependencyInstaller.git $innoDependencyPath", + ); + } + + @override + void run() async { + stdout.writeln("Replace versions"); + + final chocoFiles = [ + join(cwd.path, "choco-struct", "tools", "VERIFICATION.txt"), + join(cwd.path, "choco-struct", "spotube.nuspec"), + ]; + + for (final filePath in chocoFiles) { + final file = File(filePath); + final content = file.readAsStringSync(); + final newContent = + content.replaceAll(versionVarRegExp, versionWithoutBuildNumber); + + file.writeAsStringSync(newContent); + } + + await bootstrap(); + await innoDependInstall(); + + await shell.run( + "flutter_distributor package --platform=windows --targets=exe --skip-clean", + ); + + final ogExe = File( + join( + cwd.path, + "dist", + pubspec.version.toString(), + "spotube-${pubspec.version}-windows-setup.exe", + ), + ); + + final exePath = join(cwd.path, "dist", "Spotube-windows-x86_64-setup.exe"); + + await ogExe.copy(exePath); + await ogExe.delete(); + + stdout.writeln("✅ Windows exe built at $exePath"); + + final exeFile = File(exePath); + + final hash = sha256.convert(await exeFile.readAsBytes()).toString(); + + final chocoVerificationFile = File(chocoFiles.first); + + chocoVerificationFile.writeAsStringSync( + chocoVerificationFile.readAsStringSync().replaceAll( + RegExp(r"\%\{\{WIN_SHA256\}\}\%"), + hash, + ), + ); + + await exeFile.copy( + join(cwd.path, "choco-struct", "tools", basename(exeFile.path)), + ); + + await shell.run( + "choco pack ${chocoFiles[1]} --outputdirectory ${join(cwd.path, "dist")}", + ); + + final chocoNupkg = File( + join(cwd.path, "dist", "spotube.$versionWithoutBuildNumber.nupkg"), + ); + + final distNupkgPath = join( + cwd.path, + "dist", + "Spotube-windows-x86_64.nupkg", + ); + + await chocoNupkg.copy(distNupkgPath); + await chocoNupkg.delete(); + + stdout.writeln("✅ Windows nupkg built at $distNupkgPath"); + } +} diff --git a/cli/commands/install-dependencies.dart b/cli/commands/install-dependencies.dart new file mode 100644 index 000000000..75df28dfa --- /dev/null +++ b/cli/commands/install-dependencies.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:process_run/shell_run.dart'; + +class InstallDependenciesCommand extends Command { + @override + String get description => "Install platform dependencies"; + + @override + String get name => "install-dependencies"; + + InstallDependenciesCommand() { + argParser.addOption( + "platform", + abbr: "p", + allowed: [ + "windows", + "linux", + "linux_arm", + "macos", + "ios", + "android", + ], + mandatory: true, + ); + } + + @override + FutureOr? run() async { + final shell = Shell(); + + switch (argResults!.option("platform")) { + case "windows": + break; + case "linux": + await shell.run( + """ + sudo apt-get update -y + sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev + """, + ); + break; + case "linux_arm": + await shell.run( + """ + sudo apt-get update -y + sudo apt-get install -y pkg-config make python3-pip python3-setuptools + """, + ); + break; + case "macos": + await shell.run( + """ + brew install python-setuptools + npm install -g appdmg + """, + ); + break; + case "ios": + break; + case "android": + await shell.run( + """ + sudo apt-get update -y + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse + """, + ); + break; + default: + break; + } + } +} diff --git a/cli/core/env.dart b/cli/core/env.dart new file mode 100644 index 000000000..33cc5df13 --- /dev/null +++ b/cli/core/env.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +enum BuildChannel { + stable, + nightly; + + factory BuildChannel.fromEnvironment(String name) { + final channel = Platform.environment[name]!; + if (channel == "stable") { + return BuildChannel.stable; + } else if (channel == "nightly") { + return BuildChannel.nightly; + } else { + throw Exception("Invalid channel: $channel"); + } + } +} + +class CliEnv { + static final channel = BuildChannel.fromEnvironment("CHANNEL"); + static final dotenv = Platform.environment["DOTENV"]!; + static final ghRunNumber = Platform.environment["GITHUB_RUN_NUMBER"]; + static final flutterVersion = Platform.environment["FLUTTER_VERSION"]!; +} diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 14f33b806..89a777b64 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -3,6 +3,11 @@ import 'package:spotube/utils/platform.dart'; part 'env.g.dart'; +enum ReleaseChannel { + nightly, + stable, +} + @Envied(obfuscate: true, requireEnvFile: true, path: ".env") abstract class Env { @EnviedField(varName: 'SPOTIFY_SECRETS') @@ -25,6 +30,13 @@ abstract class Env { @EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1") static final String _enableUpdateChecker = _Env._enableUpdateChecker; + @EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly") + static final String _releaseChannel = _Env._releaseChannel; + + static ReleaseChannel get releaseChannel => _releaseChannel == "stable" + ? ReleaseChannel.stable + : ReleaseChannel.nightly; + static bool get enableUpdateChecker => kIsFlatpak || _enableUpdateChecker == "1"; diff --git a/lib/components/root/update_dialog.dart b/lib/components/root/update_dialog.dart index f5388aa1f..e15903c63 100644 --- a/lib/components/root/update_dialog.dart +++ b/lib/components/root/update_dialog.dart @@ -5,18 +5,23 @@ import 'package:version/version.dart'; class RootAppUpdateDialog extends StatelessWidget { final Version? version; - const RootAppUpdateDialog({super.key, this.version}); + final int? nightlyBuildNum; + + const RootAppUpdateDialog({super.key, this.version}) : nightlyBuildNum = null; + const RootAppUpdateDialog.nightly({super.key, required this.nightlyBuildNum}) + : version = null; @override Widget build(BuildContext context) { const url = "https://spotube.krtirtho.dev/downloads"; + const nightlyUrl = "https://spotube.krtirtho.dev/downloads/nightly"; return AlertDialog( title: const Text("Spotube has an update"), actions: [ FilledButton( child: const Text("Download Now"), onPressed: () => launchUrlString( - url, + nightlyBuildNum != null ? nightlyUrl : url, mode: LaunchMode.externalApplication, ), ), @@ -24,21 +29,26 @@ class RootAppUpdateDialog extends StatelessWidget { content: Column( mainAxisSize: MainAxisSize.min, children: [ - Text("Spotube v$version has been released"), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Read the latest "), - AnchorButton( - "release notes", - style: const TextStyle(color: Colors.blue), - onTap: () => launchUrlString( - url, - mode: LaunchMode.externalApplication, - ), - ), - ], + Text( + nightlyBuildNum != null + ? "Spotube Nightly $nightlyBuildNum has been released" + : "Spotube v$version has been released", ), + if (nightlyBuildNum == null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Read the latest "), + AnchorButton( + "release notes", + style: const TextStyle(color: Colors.blue), + onTap: () => launchUrlString( + url, + mode: LaunchMode.externalApplication, + ), + ), + ], + ), ], ), ); diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 21b8117b1..505eecb94 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/collections/env.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; @@ -72,6 +73,13 @@ class AboutSpotube extends HookConsumerWidget { Text("v${packageInfo.version}") ], ), + TableRow( + children: [ + Text(context.l10n.channel), + colon, + Text(Env.releaseChannel.name) + ], + ), TableRow( children: [ Text(context.l10n.build_number), diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 30c92e1d7..ec3bb0cbf 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -335,35 +335,59 @@ abstract class ServiceUtils { ) async { if (!Env.enableUpdateChecker) return; if (!ref.read(userPreferencesProvider.select((s) => s.checkUpdate))) return; - final packageInfo = await PackageInfo.fromPlatform(); - final value = await http.get( - Uri.parse( - "https://api.github.com/repos/KRTirtho/spotube/releases/latest", - ), - ); - final tagName = - (jsonDecode(value.body)["tag_name"] as String).replaceAll("v", ""); - final currentVersion = packageInfo.version == "Unknown" - ? null - : Version.parse(packageInfo.version); - final latestVersion = tagName == "nightly" ? null : Version.parse(tagName); - - if (currentVersion == null || - latestVersion == null || - (latestVersion.isPreRelease && !currentVersion.isPreRelease) || - (!latestVersion.isPreRelease && currentVersion.isPreRelease)) return; - - if (latestVersion <= currentVersion || !context.mounted) return; - - showDialog( - context: context, - barrierDismissible: true, - barrierColor: Colors.black26, - builder: (context) { - return RootAppUpdateDialog(version: latestVersion); - }, - ); + if (Env.releaseChannel == ReleaseChannel.nightly) { + final value = await http.get( + Uri.parse( + "https://api.github.com/repos/KRTirtho/spotube/actions/workflows/spotube-release-binary.yml/runs?status=success&per_page=1", + ), + ); + + final buildNum = + jsonDecode(value.body)["workflow_runs"][0]["run_number"] as int; + + if (buildNum <= int.parse(packageInfo.buildNumber) || !context.mounted) { + return; + } + + await showDialog( + context: context, + barrierDismissible: true, + barrierColor: Colors.black26, + builder: (context) { + return RootAppUpdateDialog.nightly(nightlyBuildNum: buildNum); + }, + ); + } else { + final value = await http.get( + Uri.parse( + "https://api.github.com/repos/KRTirtho/spotube/releases/latest", + ), + ); + final tagName = + (jsonDecode(value.body)["tag_name"] as String).replaceAll("v", ""); + final currentVersion = packageInfo.version == "Unknown" + ? null + : Version.parse(packageInfo.version); + final latestVersion = + tagName == "nightly" ? null : Version.parse(tagName); + + if (currentVersion == null || + latestVersion == null || + (latestVersion.isPreRelease && !currentVersion.isPreRelease) || + (!latestVersion.isPreRelease && currentVersion.isPreRelease)) return; + + if (latestVersion <= currentVersion || !context.mounted) return; + + showDialog( + context: context, + barrierDismissible: true, + barrierColor: Colors.black26, + builder: (context) { + return RootAppUpdateDialog(version: latestVersion); + }, + ); + } } } diff --git a/pubspec.lock b/pubspec.lock index 1532bcf75..df623b9e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.1" args: dependency: "direct main" description: @@ -1271,7 +1271,7 @@ packages: source: hosted version: "3.1.14" io: - dependency: transitive + dependency: "direct dev" description: name: io sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" @@ -1702,14 +1702,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" - url: "https://pub.dev" - source: hosted - version: "3.8.0" pool: dependency: transitive description: @@ -1734,6 +1726,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" + process_run: + dependency: "direct dev" + description: + name: process_run + sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e" + url: "https://pub.dev" + source: hosted + version: "0.14.2" provider: dependency: transitive description: @@ -2462,7 +2462,7 @@ packages: source: hosted version: "1.0.4" xml: - dependency: transitive + dependency: "direct dev" description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 diff --git a/pubspec.yaml b/pubspec.yaml index 62c20c354..7435e077c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -144,6 +144,9 @@ dev_dependencies: freezed: ^2.5.2 custom_lint: ^0.6.4 riverpod_lint: ^2.3.10 + process_run: ^0.14.2 + xml: ^6.5.0 + io: ^1.0.4 dependency_overrides: uuid: ^4.4.0 diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 6e1e3cb37..0c638eb75 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,13 +1,16 @@ +# Project-level configuration. cmake_minimum_required(VERSION 3.14) project(spotube LANGUAGES CXX) +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. set(BINARY_NAME "spotube") -cmake_policy(SET CMP0063 NEW) +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -30,6 +33,10 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,14 +45,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -80,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index e8fccc8a6..0b586d339 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -60,16 +60,16 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else -#define VERSION_AS_STRING "%{{SPOTUBE_VERSION}}%" +#define VERSION_AS_STRING "3.6.0" #endif VS_VERSION_INFO VERSIONINFO @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "Spotube" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "spotube" "\0" - VALUE "LegalCopyright", "Copyright (C) 2022 oss.krtirtho. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 oss.krtirtho. All rights reserved." "\0" VALUE "OriginalFilename", "spotube.exe" "\0" VALUE "ProductName", "spotube" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" From a838eadc12a4c4acc8a3d1d76b547515e1b6d5e0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 9 May 2024 16:47:28 +0600 Subject: [PATCH 15/57] refactor: move dart scripts as commands under CLI --- bin/gen-credits.dart | 103 ----------------------------- bin/translated_messages.dart | 28 -------- bin/untranslated_messages.dart | 50 --------------- bin/verify-pkgbuild.dart | 22 ------- cli/cli.dart | 4 ++ cli/commands/credits.dart | 114 +++++++++++++++++++++++++++++++++ cli/commands/translated.dart | 39 +++++++++++ cli/commands/untranslated.dart | 48 ++++++++++++++ 8 files changed, 205 insertions(+), 203 deletions(-) delete mode 100644 bin/gen-credits.dart delete mode 100644 bin/translated_messages.dart delete mode 100644 bin/untranslated_messages.dart delete mode 100644 bin/verify-pkgbuild.dart create mode 100644 cli/commands/credits.dart create mode 100644 cli/commands/translated.dart create mode 100644 cli/commands/untranslated.dart diff --git a/bin/gen-credits.dart b/bin/gen-credits.dart deleted file mode 100644 index f8975335f..000000000 --- a/bin/gen-credits.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:http/http.dart'; -import 'package:html/parser.dart'; -import 'package:pub_api_client/pub_api_client.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; - -void main() async { - final client = PubClient(); - - final pubspec = Pubspec.parse(File('pubspec.yaml').readAsStringSync()); - - final allDeps = [ - ...pubspec.dependencies.entries, - ...pubspec.devDependencies.entries, - ]; - - final dependencies = allDeps - .where((d) => d.value is HostedDependency) - .map((d) => d.key) - .toSet(); - final packageInfo = await Future.wait(dependencies.map(client.packageInfo)); - - final gitDepsList = List.castFrom, - MapEntry>( - allDeps - .where((d) => d.value is GitDependency) - .map((d) => MapEntry(d.key, d.value as GitDependency)) - .toList(), - ); - - final gitDeps = gitDepsList.map( - (d) { - final uri = Uri.parse( - d.value.url.toString().replaceAll('.git', ''), - ); - return MapEntry( - d.key, - uri.replace( - pathSegments: [ - ...uri.pathSegments, - 'raw', - d.value.ref ?? 'main', - d.value.path ?? '', - 'pubspec.yaml', - ], - ).toString(), - ); - }, - ).toList(); - - final gitPubspecs = await Future.wait( - gitDeps.map( - (d) { - Pubspec parser(res) { - try { - return Pubspec.parse(res.body); - } catch (e) { - final document = parse(res.body); - final pre = document.querySelector('pre'); - if (pre == null) { - log(d.toString()); - rethrow; - } - return Pubspec.parse(pre.text); - } - } - - return get(Uri.parse(d.value)).then(parser).catchError( - (_) => get(Uri.parse(d.value.replaceFirst('/main', '/master'))) - .then(parser), - ); - }, - ), - ); - - // ignore: avoid_print - print( - packageInfo - .map( - (package) => - '1. [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}', - ) - .join('\n'), - ); - // ignore: avoid_print - print( - gitPubspecs.map( - (package) { - final packageUrl = package.homepage ?? - gitDepsList - .firstWhereOrNull((dep) => dep.key == package.name) - ?.value - .url - .toString(); - return '1. [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}'; - }, - ).join('\n'), - ); - exit(0); -} diff --git a/bin/translated_messages.dart b/bin/translated_messages.dart deleted file mode 100644 index 1ac8f148f..000000000 --- a/bin/translated_messages.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:convert'; -import 'dart:io'; - -void main(List args) async { - final translatedFile = - jsonDecode(await File('tm.json').readAsString()) as Map; - - for (final MapEntry(:key, :value) in translatedFile.entries) { - print('Updating locale: $key'); - final file = File('lib/l10n/app_$key.arb'); - - final fileContent = - jsonDecode(await file.readAsString()) as Map; - - final newContent = { - ...fileContent, - ...value, - }; - - await file.writeAsString( - const JsonEncoder.withIndent(' ').convert(newContent), - ); - - print('✅ Updated locale: $key'); - } -} diff --git a/bin/untranslated_messages.dart b/bin/untranslated_messages.dart deleted file mode 100644 index 0b3485a7b..000000000 --- a/bin/untranslated_messages.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:convert'; -import 'dart:io'; - -/// Generate JSON output for untranslated messages with English values -/// for quick translation in ChatGPT -/// -/// Usage: dart bin/untranslated_messages.dart [locale?] -/// -/// Example: dart bin/untranslated_messages.dart -/// -/// or with specific locale (e.g. bn (Bengali)) -/// -/// Example: dart bin/untranslated_messages.dart bn - -void main(List args) { - final file = jsonDecode( - File('untranslated_messages.json').readAsStringSync(), - ) as Map; - - final englishMessages = - jsonDecode(File('lib/l10n/app_en.arb').readAsStringSync()) - as Map; - - final messagesWithValues = {}; - - for (final MapEntry(key: locale, value: messages) in file.entries) { - messagesWithValues[locale] = Map.fromEntries( - messages - .map( - (message) => - MapEntry(message, englishMessages[message]), - ) - .toList() - .cast>(), - ); - } - - print( - "Prompt:\n" - "Translate following to their appropriate locale for flutter arb translations files." - " Put the respective new translations in a map of their corresponding locale.", - ); - print( - const JsonEncoder.withIndent(' ').convert( - args.isNotEmpty ? messagesWithValues[args.first] : messagesWithValues, - ), - ); -} diff --git a/bin/verify-pkgbuild.dart b/bin/verify-pkgbuild.dart deleted file mode 100644 index 587e63d07..000000000 --- a/bin/verify-pkgbuild.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -void main() { - Process.run("sh", ["-c", '"./scripts/pkgbuild2json.sh aur-struct/PKGBUILD"']) - .then((result) { - try { - final pkgbuild = jsonDecode(result.stdout); - if (pkgbuild["version"] != - Platform.environment["RELEASE_VERSION"]?.substring(1)) { - throw Exception( - "PKGBUILD version doesn't match current RELEASE_VERSION"); - } - if (pkgbuild["release"] != "1") { - throw Exception("In new releases pkgrel should be 1"); - } - } catch (e) { - // ignore: avoid_print - print("[Failed to parse PKGBUILD] $e"); - } - }); -} diff --git a/cli/cli.dart b/cli/cli.dart index 3210f5574..074c5b12f 100644 --- a/cli/cli.dart +++ b/cli/cli.dart @@ -1,7 +1,9 @@ import 'package:args/command_runner.dart'; import 'commands/build.dart'; +import 'commands/credits.dart'; import 'commands/install-dependencies.dart'; +import 'commands/untranslated.dart'; void main(List args) { final commandRunner = CommandRunner( @@ -11,6 +13,8 @@ void main(List args) { commandRunner.addCommand(InstallDependenciesCommand()); commandRunner.addCommand(BuildCommand()); + commandRunner.addCommand(CreditsCommand()); + commandRunner.addCommand(UntranslatedCommand()); commandRunner.run(args); } diff --git a/cli/commands/credits.dart b/cli/commands/credits.dart new file mode 100644 index 000000000..66ec1172b --- /dev/null +++ b/cli/commands/credits.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:collection/collection.dart'; +import 'package:http/http.dart'; +import 'package:html/parser.dart'; +import 'package:path/path.dart'; +import 'package:pub_api_client/pub_api_client.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +class CreditsCommand extends Command { + @override + String get description => "Generate credits for used Library's authors"; + + @override + String get name => "credits"; + + @override + run() async { + final client = PubClient(); + final cwd = Directory.current; + + final pubspec = Pubspec.parse( + File(join(cwd.path, 'pubspec.yaml')).readAsStringSync(), + ); + + final allDeps = [ + ...pubspec.dependencies.entries, + ...pubspec.devDependencies.entries, + ]; + + final dependencies = allDeps + .where((d) => d.value is HostedDependency) + .map((d) => d.key) + .toSet(); + final packageInfo = await Future.wait(dependencies.map(client.packageInfo)); + + final gitDepsList = List.castFrom, + MapEntry>( + allDeps + .where((d) => d.value is GitDependency) + .map((d) => MapEntry(d.key, d.value as GitDependency)) + .toList(), + ); + + final gitDeps = gitDepsList.map( + (d) { + final uri = Uri.parse( + d.value.url.toString().replaceAll('.git', ''), + ); + return MapEntry( + d.key, + uri.replace( + pathSegments: [ + ...uri.pathSegments, + 'raw', + d.value.ref ?? 'main', + d.value.path ?? '', + 'pubspec.yaml', + ], + ).toString(), + ); + }, + ).toList(); + + final gitPubspecs = await Future.wait( + gitDeps.map( + (d) { + Pubspec parser(res) { + try { + return Pubspec.parse(res.body); + } catch (e) { + final document = parse(res.body); + final pre = document.querySelector('pre'); + if (pre == null) { + stdout.writeln(d.toString()); + rethrow; + } + return Pubspec.parse(pre.text); + } + } + + return get(Uri.parse(d.value)).then(parser).catchError( + (_) => get(Uri.parse(d.value.replaceFirst('/main', '/master'))) + .then(parser), + ); + }, + ), + ); + + stdout.writeln( + packageInfo + .map( + (package) => + '1. [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}', + ) + .join('\n'), + ); + + stdout.writeln( + gitPubspecs.map( + (package) { + final packageUrl = package.homepage ?? + gitDepsList + .firstWhereOrNull((dep) => dep.key == package.name) + ?.value + .url + .toString(); + return '1. [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}'; + }, + ).join('\n'), + ); + } +} diff --git a/cli/commands/translated.dart b/cli/commands/translated.dart new file mode 100644 index 000000000..43c4ea49d --- /dev/null +++ b/cli/commands/translated.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'dart:convert'; +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:path/path.dart'; + +class TranslatedCommand extends Command { + @override + String get description => + "Update translation based on generated translated messages"; + + @override + String get name => "translated"; + + @override + FutureOr? run() async { + final cwd = Directory.current; + final translatedFile = jsonDecode( + await File(join(cwd.path, 'tm.json')).readAsString(), + ) as Map; + + for (final MapEntry(:key, :value) in translatedFile.entries) { + stdout.writeln('Updating locale: $key'); + final file = File(join(cwd.path, 'lib', 'l10n', 'app_$key.arb')); + + final fileContent = + jsonDecode(await file.readAsString()) as Map; + + final newContent = {...fileContent, ...value}; + + await file.writeAsString( + const JsonEncoder.withIndent(' ').convert(newContent), + ); + + stdout.writeln('✅ Updated locale: $key'); + } + } +} diff --git a/cli/commands/untranslated.dart b/cli/commands/untranslated.dart new file mode 100644 index 000000000..dadcd8b5b --- /dev/null +++ b/cli/commands/untranslated.dart @@ -0,0 +1,48 @@ +import 'package:args/command_runner.dart'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart'; + +class UntranslatedCommand extends Command { + @override + get name => "untranslated"; + @override + get description => + "Generate Untranslated Messages for ChatGPT based Translation"; + + @override + run() async { + final cwd = Directory.current; + final file = jsonDecode( + File(join(cwd.path, 'untranslated_messages.json')).readAsStringSync(), + ) as Map; + + final englishMessages = jsonDecode( + File(join(cwd.path, 'lib', 'l10n', 'app_en.arb')).readAsStringSync(), + ) as Map; + + final messagesWithValues = {}; + + for (final MapEntry(key: locale, value: messages) in file.entries) { + messagesWithValues[locale] = Map.fromEntries( + messages + .map( + (message) => + MapEntry(message, englishMessages[message]), + ) + .toList() + .cast>(), + ); + } + + stdout.writeln( + "Prompt:\n" + "Translate following to their appropriate locale for flutter arb translations files." + " Put the respective new translations in a map of their corresponding locale.", + ); + stdout.writeln( + const JsonEncoder.withIndent(' ').convert(messagesWithValues), + ); + } +} From 2b01e4fb4d816f98581ff3b6e2330008caa1273e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 9 May 2024 16:50:42 +0600 Subject: [PATCH 16/57] chore: add translated message command to command list --- cli/cli.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/cli.dart b/cli/cli.dart index 074c5b12f..26190d4c4 100644 --- a/cli/cli.dart +++ b/cli/cli.dart @@ -3,6 +3,7 @@ import 'package:args/command_runner.dart'; import 'commands/build.dart'; import 'commands/credits.dart'; import 'commands/install-dependencies.dart'; +import 'commands/translated.dart'; import 'commands/untranslated.dart'; void main(List args) { @@ -14,6 +15,7 @@ void main(List args) { commandRunner.addCommand(InstallDependenciesCommand()); commandRunner.addCommand(BuildCommand()); commandRunner.addCommand(CreditsCommand()); + commandRunner.addCommand(TranslatedCommand()); commandRunner.addCommand(UntranslatedCommand()); commandRunner.run(args); From dbc1c452dd53153c61589f956ea9836cea7bf2bb Mon Sep 17 00:00:00 2001 From: Josu Igoa Date: Fri, 10 May 2024 18:22:56 +0200 Subject: [PATCH 17/57] feat(translations): add Basque translation (#1493) * added Basque translation * chore: fix country codes and language native name --------- Co-authored-by: Kingkor Roy Tirtho --- lib/collections/language_codes.dart | 8 +- lib/l10n/app_eu.arb | 324 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 1 + 3 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 lib/l10n/app_eu.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index 45456d697..dcc42657c 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -81,10 +81,10 @@ abstract class LanguageLocals { // name: "Bashkir", // nativeName: "башҡорт теле", // ), - // "eu": const ISOLanguageName( - // name: "Basque", - // nativeName: "euskara,", - // ), + "eu": const ISOLanguageName( + name: "Basque", + nativeName: "euskara", + ), // "be": const ISOLanguageName( // name: "Belarusian", // nativeName: "Беларуская", diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb new file mode 100644 index 000000000..9a4ebb463 --- /dev/null +++ b/lib/l10n/app_eu.arb @@ -0,0 +1,324 @@ +{ + "guest": "Gonbidatua", + "browse": "Arakatu", + "search": "Bilatu", + "library": "Liburutegia", + "lyrics": "Hitzak", + "settings": "Ezarpenak", + "genre_categories_filter": "Kategoria edo generoak filtratu...", + "genre": "Generoa", + "personalized": "Pertsonalizatua", + "featured": "Nabarmenduak", + "new_releases": "Argitaratze berriak", + "songs": "Abestiak", + "playing_track": "{track} erreproduzitzen", + "queue_clear_alert": "Uneko zerrenda ezabatuko da. {track_length} abesti ezabatuko dira.\nJarraitu nahi duzu?", + "load_more": "Gehiago kargatu", + "playlists": "Zerrendak", + "artists": "Artistak", + "albums": "Albumak", + "tracks": "Kantak", + "downloads": "Deskargak", + "filter_playlists": "Zure zerrendak filtratu...", + "liked_tracks": "Gustuko Kantak", + "liked_tracks_description": "Zure gustuko kanta guztiak", + "create_playlist": "Sortu zerrenda", + "create_a_playlist": "Sortu zerrenda bat", + "update_playlist": "Eguneratu zerrenda", + "create": "Sortu", + "cancel": "Ezeztatu", + "update": "Eguneratu", + "playlist_name": "Zerrenda Izena", + "name_of_playlist": "Zerrendaren izena", + "description": "Deskribapena", + "public": "Publikoa", + "collaborative": "Kolaboratiboa", + "search_local_tracks": "Bilatu kanta lokalak...", + "play": "Erreproduzitu", + "delete": "Ezabatu", + "none": "Batere ez", + "sort_a_z": "Ordenatu A-Z", + "sort_z_a": "Ordenatu Z-A", + "sort_artist": "Ordenatu Artistaren arabera", + "sort_album": "Ordenatu Albumaren arabera", + "sort_duration": "Ordenar Iraupenaren arabera", + "sort_tracks": "Ordenatu Kantak", + "currently_downloading": "Oraintxe ({tracks_length}) deskargatzen", + "cancel_all": "Ezeztatu dena", + "filter_artist": "Filtratu artistak...", + "followers": "{followers} Jarraitzaile", + "add_artist_to_blacklist": "Gehitu artista zerrenda beltzera", + "top_tracks": "Top Kantak", + "fans_also_like": "Fan-ek hau ere gustuko dute", + "loading": "Kargatzen...", + "artist": "Artista", + "blacklisted": "Zerrenda beltzean", + "following": "Jarraitzen", + "follow": "Jarraitu", + "artist_url_copied": "Artistaren URL-a arbelera kopiatua", + "added_to_queue": "{tracks} kanta zerrendara gehituak", + "filter_albums": "Albumak filtratu...", + "synced": "Sinkronizatuta", + "plain": "Arrunta", + "shuffle": "Ausaz", + "search_tracks": "Bilatu kantak...", + "released": "Argitaratua", + "error": "Errorea: {error}", + "title": "Izenburua", + "time": "Iraupena", + "more_actions": "Ekintza gehiago", + "download_count": "({count}) deskarga", + "add_count_to_playlist": "Gehitu ({count}) zerrendara", + "add_count_to_queue": "Gehitu ({count}) ilarara", + "play_count_next": "Erreproduzitu hurrengo ({count})-ak", + "album": "Albuma", + "copied_to_clipboard": "{data} arbelean kopiatua", + "add_to_following_playlists": "Gehitu {track} hurrengo erreprodukzio-zerrendetara", + "add": "Gehitu", + "added_track_to_queue": "{track} zerrendan gehitua", + "add_to_queue": "Gehitu zerrendan", + "track_will_play_next": "{track} erreproduzituko da ondoren", + "play_next": "Hurrengo erreprodukzioa", + "removed_track_from_queue": "{track} zerrendatik ezabatua", + "remove_from_queue": "Ezabatu ilaratik", + "remove_from_favorites": "Ezabatu gogokoetatik", + "save_as_favorite": "Gorde gogokoetan", + "add_to_playlist": "Gehitu zerrendara", + "remove_from_playlist": "Ezabatu zerrendatik", + "add_to_blacklist": "Gehitu zerrenda beltzera", + "remove_from_blacklist": "Ezabatu zerrenda beltzetik", + "share": "Elkarbanatu", + "mini_player": "Mini Erreproduzitzailea", + "slide_to_seek": "Arrastatu aurrerantz edo atzearantz bilatzeko", + "shuffle_playlist": "Erreproduzitu zerrenda ausazko ordenean", + "unshuffle_playlist": "Desgaitu ausazko erreprodukzioa", + "previous_track": "Aurreko pista", + "next_track": "Hurrengo pista", + "pause_playback": "Pausatu erreprodukzioa", + "resume_playback": "Berrabiarazi erreprodukzioa", + "loop_track": "Kanta begiztan", + "repeat_playlist": "Errepikatu lista", + "queue": "Ilara", + "alternative_track_sources": "Kanten iturri alternatiboak", + "download_track": "Deskargatu kanta", + "tracks_in_queue": "{tracks} kanta zerrendan", + "clear_all": "Garbitu dena", + "show_hide_ui_on_hover": "Erakutsi/Ezkutatu interfazea kurtsorea pasatzean", + "always_on_top": "Beti ikusgai", + "exit_mini_player": "Irten mini erreproduzitzailetik", + "download_location": "Deskargen kokapena", + "account": "Kontua", + "login_with_spotify": "Hasi saioa zure Spotify kontuarekin", + "connect_with_spotify": "Spotify-rekin konektatu", + "logout": "Itxi saioa", + "logout_of_this_account": "Itxi kontu honen saioa", + "language_region": "Hizkuntza eta Herrialdea", + "language": "Hizkuntza", + "system_default": "Sisteman lehenetsia", + "market_place_region": "Dendaren herrialdea", + "recommendation_country": "Gomendio herrialdea", + "appearance": "Itxura", + "layout_mode": "Diseinu modua", + "override_layout_settings": "Responsive diseinu moduaren ezarpenak ezeztatu", + "adaptive": "Moldagarria", + "compact": "Trinkoa", + "extended": "Hedatua", + "theme": "Gaia", + "dark": "Iluna", + "light": "Argia", + "system": "Sistema", + "accent_color": "Azentu kolorea", + "sync_album_color": "Sinkronizatu albumaren kolorea", + "sync_album_color_description": "Albumaren artearen kolore nagusia erabili azentu kolore bezala", + "playback": "Erreprodukzioa", + "audio_quality": "Audioaren kalitatea", + "high": "Altua", + "low": "Baxua", + "pre_download_play": "Aurre-deskargatu eta erreproduzitu", + "pre_download_play_description": "Streaming egin beharrean, byte-ak deskargatu eta erreproduzitu (banda-zabalera handia duten erabiltzaileentzat gomendagarria)", + "skip_non_music": "Musika ez diren segmentuak baztertu (SponsorBlock)", + "blacklist_description": "Zerrenda beltzeko abesti eta artistak", + "wait_for_download_to_finish": "Mesedez, itxaron uneko deskarga bukatu arte", + "desktop": "Mahaigaina", + "close_behavior": "Ixterako Portaera", + "close": "Itxi", + "minimize_to_tray": "Sistemako erretilura minimizatu", + "show_tray_icon": "Erakutsi ikonoa sistemaren erretiluan", + "about": "Honi buruz", + "u_love_spotube": "Badakigu Spotube maite duzula", + "check_for_updates": "Bilatu eguneraketak", + "about_spotube": "Spotube-ri buruz", + "blacklist": "Zerrenda beltza", + "please_sponsor": "Mesedez, babestu/diruz lagundu", + "spotube_description": "Spotube, arina, plataforma-anitza eta doakoa den Spotify-ren bezeroa", + "version": "Bertsioa", + "build_number": "Konpilazio zenbakia", + "founder": "Sortzailea", + "repository": "Errepositorioa", + "bug_issues": "Erroreak eta arazoak", + "made_with": "Bangladesh🇧🇩-en ❤️-z egina", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Lizentzia", + "add_spotify_credentials": "Gehitu zure Spotify kredentzialak hasi ahal izateko", + "credentials_will_not_be_shared_disclaimer": "Ez arduratu, zure kredentzialak ez ditugu bilduko edo inorekin elkarbanatuko", + "know_how_to_login": "Ez dakizu nola egin?", + "follow_step_by_step_guide": "Jarraitu pausoz-pausoko gida", + "spotify_cookie": "Spotify-ren {name} cookiea", + "cookie_name_cookie": "{name} cookiea", + "fill_in_all_fields": "Mesedez, osatu eremu guztiak", + "submit": "Bidali", + "exit": "Irten", + "previous": "Aurrekoa", + "next": "Hurrengoa", + "done": "Eginda", + "step_1": "1. pausua", + "first_go_to": "Hasteko, joan hona", + "login_if_not_logged_in": "eta hasi saioa/sortu kontua lehendik ez baduzu eginda", + "step_2": "2. pausua", + "step_2_steps": "1. Saioa hasita duzularik, sakatu F12 edo saguaren eskuineko botoia klikatu > Ikuskatu nabigatzaileko garapen tresnak irekitzeko.\n2. Joan \"Aplikazio\" (Chrome, Edge, Brave, etab.) edo \"Biltegiratzea\" (Firefox, Palemoon, etab.)\n3. Joan \"Cookieak\" atalera eta gero \"https://accounts.spotify.com\" azpiatalera", + "step_3": "3. pausua", + "step_3_steps": "Kopiatu \"sp_dc\" cookiearen balioa", + "success_emoji": "Eginda! 🥳", + "success_message": "Ongi hasi duzu zure Spotify kontua. Lan bikaina, lagun!", + "step_4": "4. pausua", + "step_4_steps": "Itsatsi \"sp_dc\"-tik kopiatutako balioa", + "something_went_wrong": "Zerbaitek huts egin du", + "piped_instance": "Piped zerbitzariaren instantzia", + "piped_description": "Kanten koizidentzietan erabiltzeko Piped zerbitzariaren instantzia", + "piped_warning": "Batzuk agian ez dute ongi funtzionatuko, zure ardurapean erabili", + "generate_playlist": "Sortu Zerrenda", + "track_exists": "{track} kanta dagoeneko badago", + "replace_downloaded_tracks": "Ordezkatu deskargatutako kanta guztiak", + "skip_download_tracks": "Deskargatutako kanta guztien deskarga baztertu", + "do_you_want_to_replace": "Dagoen kanta ordezkatu nahi duzu??", + "replace": "Ordezkatu", + "skip": "Baztertu", + "select_up_to_count_type": "Aukertu {count} {type}", + "select_genres": "Aukeratu Generoak", + "add_genres": "Gehitu Generoak", + "country": "Herrialdea", + "number_of_tracks_generate": "Sortzeko kanta kopurua", + "acousticness": "Akustikotasuna", + "danceability": "Dantzagarritasuna", + "energy": "Energia", + "instrumentalness": "Instrumentaltasuna", + "liveness": "Zuzenean", + "loudness": "Ozentasuna", + "speechiness": "Hitzaldia", + "valence": "Balentzia", + "popularity": "Populartasuna", + "key": "Tonua", + "duration": "Iraupena (s)", + "tempo": "Tenpoa (BPM)", + "mode": "Modua", + "time_signature": "Konpasa", + "short": "Motza", + "medium": "Ertaina", + "long": "Luzea", + "min": "Min.", + "max": "Max.", + "target": "Helburua", + "moderate": "Moderatua", + "deselect_all": "Desaukeratu dena", + "select_all": "Aukeratu dena", + "are_you_sure": "Ziur zaude?", + "generating_playlist": "Zure pertsonalizatutako zerrenda sortzen...", + "selected_count_tracks": "{count} kanta aukeratuta", + "download_warning": "Abesti guztiak aldi berean deskargatuz gero, argi dago musika pirateatzen ari zarela eta musikaren gizarte sortzaileari kalte egiten diozula. Honen jakitun izan eta artisten lan gogorra errespetatu eta babestea espero dut", + "download_ip_ban_warning": "Bidenabar, baliteke zure IPa YouTuben blokeatzea deskarga eskera gehiegi egiten badituzu. IPa blokeatzeak esan nahi du ezin izango duzula YouTube erabili (nahiz eta saioa hasia izan) gutxienez 2-3 hilabetez IP helbide horretatik. Eta Spotube ez da erantzule izango hori gertatzen bazaizu", + "by_clicking_accept_terms": "'Onartu' klikatzean, ondorengo baldintzak onartzen dituzu:", + "download_agreement_1": "Badakit musika pirateatzen ari naizela. Gaiztoa naiz", + "download_agreement_2": "Ahal dudanean lagunduko diot artistari baina oraingoz ez dut bere artea erosteko dirurik", + "download_agreement_3": "Erabat jakitun naiz YouTubek nire IPa blokea dezakeela eta ez diot Spotube-ri edo bere jabe/laguntzaileei erantzukizunik eskatuko nire oraingo jokaerak ekar ditzakeen arazoengatik", + "decline": "Baztertu", + "accept": "Onartu", + "details": "Xehetasunak", + "youtube": "YouTube", + "channel": "Kanala", + "likes": "Gustukoak", + "dislikes": "Ez gustukoak", + "views": "Ikuspenak", + "streamUrl": "Streaming-aren URLa", + "stop": "Gelditu", + "sort_newest": "Ordenatu gehitu berrienetik", + "sort_oldest": "Ordenatu gehitu zaharrenetik", + "sleep_timer": "Itzaltzeko tenporizadorea", + "mins": "{minutes} minutu", + "hours": "{hours} ordu", + "hour": "{hours} ordu", + "custom_hours": "Ordu pertsonalizatuak", + "logs": "Log-ak", + "developers": "Garatzaileak", + "not_logged_in": "Ez duzu saioa hasi", + "search_mode": "Bilaketa modua", + "audio_source": "Audio Iturria", + "ok": "OK", + "failed_to_encrypt": "Errorea zifratzean", + "encryption_failed_warning": "Spotube-ek zifratzea darabil datuak modu seguruan biltegiratzeko. Baina huts egin du. Hori dela eta, biltegiratzea ez da segurua izango\nLinux erabiltzen ari bazara, ziurtatu edozein sekretu-zerbitzu (gnome-keyring, kde-wallet, keepassxc etab.) instalatuta duzula", + "querying_info": "Informazioa egiaztatzen...", + "piped_api_down": "Piped-en APIa ez dago eskuragarri", + "piped_down_error_instructions": "Piped-en {pipedInstance} instantzia ez dago martxan une honetan\n\nAldatu instantzia edo aldatu 'API mota' YouTuberen API ofizialera\n\nZiurtatu aplikazioa berrabiarazten duzula aldaketa eta gero", + "you_are_offline": "Une honetan konexiorik gabe zaude", + "connection_restored": "Internet konexioa berrezarri egin da", + "use_system_title_bar": "Erabili sistemako izenburu barra", + "crunching_results": "Emaitzak prozesatzen...", + "search_to_get_results": "Bilatu emaitzak lortzeko", + "use_amoled_mode": "Erabili AMOLED modua", + "pitch_dark_theme": "Dart-en gai iluna", + "normalize_audio": "Normalizatu audioa", + "change_cover": "Aldatu azala", + "add_cover": "Gehitu azala", + "restore_defaults": "Berrezarri berezko balioak", + "download_music_codec": "Deskargatutako musikaren codec-a", + "streaming_music_codec": "Streaming musikaren codec-a", + "login_with_lastfm": "Hasi saioa Last.fm-n", + "connect": "Konektatu", + "disconnect_lastfm": "Deskonektatu Last.fm-tik", + "disconnect": "Deskonektatu", + "username": "Erabiltzaile izena", + "password": "Pasahitza", + "login": "Hasi saioa", + "login_with_your_lastfm": "Hasi saioa Last.fm-ko zure kontuarekin", + "scrobble_to_lastfm": "Scrobble Last.fm-ra", + "go_to_album": "Albumera joan", + "discord_rich_presence": "Discord-en presentzia aberatsa", + "browse_all": "Esploratu dena", + "genres": "Generoak", + "explore_genres": "Esploratu generoak", + "friends": "Lagunak", + "no_lyrics_available": "Sentitzen dut, ezin dira kanta honen hitzak aurkitu", + "start_a_radio": "Hasi Irrati bat", + "how_to_start_radio": "Nola hasi nahi duzu irratia?", + "replace_queue_question": "Uneko zerrenda ordezkatu nahi duzu edo bertan gehitu?", + "endless_playback": "Amaigabeko erreprodukzioa", + "delete_playlist": "Ezabatu zerrenda", + "delete_playlist_confirmation": "Ziur zaude zerrenda ezabatu nahi duzula?", + "local_tracks": "Kanta lokalak", + "song_link": "Kantaren lotura", + "skip_this_nonsense": "Utzi txorakeria hau", + "freedom_of_music": "“Musika Askatasuna”", + "freedom_of_music_palm": "“Musika Askatasuna zure eskuetan”", + "get_started": "Has gaitezen", + "youtube_source_description": "Gomendatua eta hobekien dabilena.", + "piped_source_description": "Aske zara? YouTube bezala, baino askeago.", + "jiosaavn_source_description": "Asia hegoaldeko herrialdeetarako hoberena.", + "highest_quality": "Kalitate Onena: {quality}", + "select_audio_source": "Aukeratu Audio Iturria", + "endless_playback_description": "Gehitu automatikoki kanta berriak\n ilararen bukaeran", + "choose_your_region": "Aukeratu zure herrialdea", + "choose_your_region_description": "Honekin Spotube-k zure kokalerakuari dagokion edukia\neskeiniko dizu.", + "choose_your_language": "Aukeratu zure hizkuntza", + "help_project_grow": "Lagundu proiektu honi hazten", + "help_project_grow_description": "Spotube kode irekiko proiektu bat da. Proiektu hau hazten lagundu dezakezu, erroreak jakinaraziz edo ezaugarri berriak proposatuz.", + "contribute_on_github": "GitHub-en lagundu", + "donate_on_open_collective": "Open Collective-en diruz lagundu", + "browse_anonymously": "Nabigatu Anonimoki", + "enable_connect": "Gaitu konexioa", + "enable_connect_description": "Kontrolatu Spotube beste gailu batzuetatik", + "devices": "Gailuak", + "select": "Aukeratu", + "connect_client_alert": "{client} gailuak kontrolatzen zaitu", + "this_device": "Gailu hau", + "remote": "Urrunekoa" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index ef3685fa3..29ededde3 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -43,5 +43,6 @@ class L10n { const Locale('tr', 'TR'), const Locale('zh', 'CN'), const Locale('vi', 'VN'), + const Locale('eu', 'ES'), ]; } From 1e7f0e1fe71e0a8d86614fc884861f8791469112 Mon Sep 17 00:00:00 2001 From: Omari Sopromadze Date: Fri, 10 May 2024 18:37:22 +0200 Subject: [PATCH 18/57] feat(translations): add georgian language (#1450) * feat: add georgian language * feat: translate more georgian words --- lib/collections/language_codes.dart | 8 +- lib/components/home/sections/genres.dart | 2 +- lib/l10n/app_ka.arb | 324 +++++++++++++++++++++++ lib/l10n/l10n.dart | 1 + 4 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 lib/l10n/app_ka.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index dcc42657c..ae75433a8 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -213,10 +213,10 @@ abstract class LanguageLocals { // name: "Galician", // nativeName: "Galego", // ), - // "ka": const ISOLanguageName( - // name: "Georgian", - // nativeName: "ქართული", - // ), + "ka": const ISOLanguageName( + name: "Georgian", + nativeName: "ქართული", + ), "de": const ISOLanguageName( name: "German", nativeName: "Deutsch", diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index ac2644f0b..8fbc8bf9a 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -54,7 +54,7 @@ class HomeGenresSection extends HookConsumerWidget { }, icon: const Icon(SpotubeIcons.angleRight), label: Text( - "Browse All", + context.l10n.browse_all, style: textTheme.bodyMedium?.copyWith( color: colorScheme.secondary, ), diff --git a/lib/l10n/app_ka.arb b/lib/l10n/app_ka.arb new file mode 100644 index 000000000..3da064447 --- /dev/null +++ b/lib/l10n/app_ka.arb @@ -0,0 +1,324 @@ +{ + "guest": "სტუმარი", + "browse": "ნახვა", + "search": "ძებნა", + "library": "ბიბლიოთეკა", + "lyrics": "ტექსტები", + "settings": "კონფიგურაციები", + "genre_categories_filter": "კატეგორიების ან ჟანრების ფილტრი...", + "genre": "ჟანრი", + "personalized": "პეერსონალიზებული", + "featured": "გამორჩეული", + "new_releases": "ახალი გამოცემები", + "songs": "სიმღერები", + "playing_track": "უკრავს {track}", + "queue_clear_alert": "ეს გაასუფთავებს მიმდინარე რიგს. {track_length} ტრეკი წაიშლება\nᲒინდა გააგრძელო?", + "load_more": "მეტის ჩატვირთვა", + "playlists": "ფლეილისტები", + "artists": "არტისტები", + "albums": "ალბომები", + "tracks": "ტრეკები", + "downloads": "ჩამოტვირთვები", + "filter_playlists": "ფლეილისტების გაფილტვრა...", + "liked_tracks": "მოწონებული ტრეკები", + "liked_tracks_description": "ყველა შენი მოწონებული ტრეკი", + "create_playlist": "ფლეილისტის შექმნა", + "create_a_playlist": "ფლეილისტის შექმნა", + "update_playlist": "ფლეილისტის განახლება", + "create": "შექმნა", + "cancel": "გაუქმება", + "update": "განახლება", + "playlist_name": "ფლეილისტის სახელი", + "name_of_playlist": "ფლეილისტის სახელი", + "description": "აღწერა", + "public": "საჯარო", + "collaborative": "კოლაბორაციული", + "search_local_tracks": "ლოცალური ტრეკების ძებნა...", + "play": "დაკვრა", + "delete": "წაშლა", + "none": "არცერთი", + "sort_a_z": "დალაგება A-Z-ს მიხედვით", + "sort_z_a": "დალაგება Z-A-ს მიხედვით", + "sort_artist": "დალაგება არტისტის მიხედვით", + "sort_album": "დალაგება ალბომის მიხედვით", + "sort_duration": "დალაგება ხანგრძლივობის მიხედვით", + "sort_tracks": "ტრეკების დალაგება", + "currently_downloading": "მიმდინარეობს ჩამოტვირთვა ({tracks_length})", + "cancel_all": "ყველას გაუქმება", + "filter_artist": "არტისტების ფილტრი...", + "followers": "{followers} ფოლოვერები", + "add_artist_to_blacklist": "არტისტის შავ სიაში დამატება", + "top_tracks": "ტოპ ტრეკები", + "fans_also_like": "ფანებს ასევე მოსწონთ", + "loading": "იტვირთება...", + "artist": "არტისტი", + "blacklisted": "შავ სიაში მყოფი", + "following": "ფოლოვინგი", + "follow": "დაფოლოვება", + "artist_url_copied": "არტისტის ლინკი დაკოპირებულია", + "added_to_queue": "{tracks} ტრეკი დაემატა რიგში", + "filter_albums": "ალბომების გაფილტვრა...", + "synced": "სინქრონიზებული", + "plain": "Plain", + "shuffle": "რიგის არევა", + "search_tracks": "ტრეკების ძებნა...", + "released": "გამოშვებული", + "error": "შეცდომა {error}", + "title": "სათაური", + "time": "დრო", + "more_actions": "მეტი მოქმედებები", + "download_count": "გადმოწერა ({count})", + "add_count_to_playlist": "ფლეილისტში ({count})-ის დამატება", + "add_count_to_queue": "რიგში ({count})-ის დამატება", + "play_count_next": "შემდეგი ({count})-ის დაკვრა", + "album": "ალბომი", + "copied_to_clipboard": "{data} დაკოპირებულია", + "add_to_following_playlists": "დაამატე {track} ამ ფლეილისტებში", + "add": "დამატება", + "added_track_to_queue": "რიგში დაემატა {track}", + "add_to_queue": "რიგში დამატება", + "track_will_play_next": "{track} დაუკრავს შემდეგს", + "play_next": "შემდეგის დაკვრა", + "removed_track_from_queue": "რიგიდან წაიშალა {track}", + "remove_from_queue": "რიგიდან წაშლა", + "remove_from_favorites": "ფავორიტებიდან წაშლა", + "save_as_favorite": "ფავორიტებში დამატება", + "add_to_playlist": "ფლეილისტში დამატება", + "remove_from_playlist": "ფლეილისტიდან წაშლა", + "add_to_blacklist": "შავ სიაში დამატება", + "remove_from_blacklist": "შავი სიიდან წაშლა", + "share": "გაზიარება", + "mini_player": "მინი დამკვრელი", + "slide_to_seek": "გადახვევისთვის გაასრიალეთ წინ ან უკან", + "shuffle_playlist": "ფლეილისტის არევა", + "unshuffle_playlist": "ფლეილისტის დალაგება", + "previous_track": "წინა ტრეკი", + "next_track": "შემდეგი ტრეკი", + "pause_playback": "დაკვრის გაჩერება", + "resume_playback": "დაკვრის გაგრძელება", + "loop_track": "ტრეკის ლუპზე დაკვრა", + "repeat_playlist": "ფლეილისტის გამეორება", + "queue": "რიგი", + "alternative_track_sources": "ალტერნატიული ტრეკების წყაროები", + "download_track": "გადმოწერე ტრეკი", + "tracks_in_queue": "{tracks} ტრეკი რიგში", + "clear_all": "ყველას წაშლა", + "show_hide_ui_on_hover": "UI-ის ჩვენება/დამალვა ჰოვერზე", + "always_on_top": "ტოველთვის ზემოდან", + "exit_mini_player": "მინი დამკვრელიდან გამოსვლა", + "download_location": "ჩამოტვირთვის მდებარეობა", + "account": "ანგარიში", + "login_with_spotify": "შედით თქვენი Spotify ანგარიშით", + "connect_with_spotify": "დაუკავშირდით Spotify-ს", + "logout": "გასვლა", + "logout_of_this_account": "ანგარიშიდან გასვლა", + "language_region": "ენა და რეგიონი", + "language": "ენა", + "system_default": "სისტემის ნაგულისხმევი", + "market_place_region": "მარკეტფლეისის რეგიონი", + "recommendation_country": "რეკომენდირებული ქვეყანა", + "appearance": "გარეგნობა", + "layout_mode": "განლაგების რეჟიმი", + "override_layout_settings": "რესფონსივ განლაგების რეჟიმის კონფიგურაციაზე გადაწერა", + "adaptive": "ადაპტირებული", + "compact": "კომპაქტური", + "extended": "გაფართოებული", + "theme": "თემა", + "dark": "ბნელი", + "light": "ღია", + "system": "სისტემის", + "accent_color": "აქცენტის ფერი", + "sync_album_color": "ალბომის ფერის სინქრონიზაცია", + "sync_album_color_description": "დომინანტური ალბომის ფერის აქცენტის ფერად გამოყენება", + "playback": "დაკვრა", + "audio_quality": "აუდიოს ხარისხი", + "high": "მაღალი", + "low": "დაბალი", + "pre_download_play": "წინასწარ ჩამოტვირთვა და დაკვრა", + "pre_download_play_description": "აუდიოს სტრიმინგის ნაცვლად, ბაიტების ჩამოტვირთვა და დაკვრა (რეკომენდებულია უფრო მაღალი გამტარუნარიანობის მომხმარებლებისთვის)", + "skip_non_music": "არა მუსიკალური ნაწილის გამოტოვება (სპონსორის ბლოკი)", + "blacklist_description": "შავ სიაში მყოფი არტისტები და ტრეკები", + "wait_for_download_to_finish": "გთხოვთ, დაელოდოთ მიმდინარე ჩამოტვირთვის დასრულებას", + "desktop": "დესკტოპი", + "close_behavior": "დახურვის ქცევა", + "close": "დახურვა", + "minimize_to_tray": "მინიმიზაცია", + "show_tray_icon": "სისტემის აიკონის ჩვენება", + "about": "ჩვენს შესახებ", + "u_love_spotube": "We know you love Spotube", + "check_for_updates": "განახლებების შემოწმება", + "about_spotube": "Spotube-ს შესახებ", + "blacklist": "შავი სია", + "please_sponsor": "გთხოვთ დაგვასპონსოროთ", + "spotube_description": "Spotube, a lightweight, cross-platform, free-for-all spotify client", + "version": "ვერსია", + "build_number": "Build Number", + "founder": "დამფუძნებელი", + "repository": "რეპოზიტორია", + "bug_issues": "Bug+Issues", + "made_with": "Made with ❤️ in Bangladesh🇧🇩", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "ლიცენზია", + "add_spotify_credentials": "დასაწყებად დაამატეთ თქვენი Spotify მონაცემები", + "credentials_will_not_be_shared_disclaimer": "არ ინერვიულოთ, თქვენი მონაცემები არ იქნება შეგროვებული ან გაზიარებული ვინმესთან", + "know_how_to_login": "არ იცით როგორ გააკეთოთ ეს?", + "follow_step_by_step_guide": "მიჰყევით ნაბიჯ-ნაბიჯ სახელმძღვანელოს", + "spotify_cookie": "Spotify {name} ქუქი", + "cookie_name_cookie": "{name} ქუქი", + "fill_in_all_fields": "გთხოვთ შეავსოთ ყველა ველი", + "submit": "გაგზავნა", + "exit": "გამოსვლა", + "previous": "წინა", + "next": "შემდეგი", + "done": "მზადაა", + "step_1": "ნაბიჯი 1", + "first_go_to": "პირველი, გადადით", + "login_if_not_logged_in": "და შესვლა/რეგისტრაცია, თუ არ ხართ შესული", + "step_2": "ნაბიჯი 2", + "step_2_steps": "1. როცა შეხვალთ, დააჭირეთ F12-ს ან მაუსის მარჯვენა ღილაკს > Inspect to Open the Browser devtools.\n2. შემდეგ გახსენით \"Application\" განყოფილება (Chrome, Edge, Brave etc..) ან \"Storage\" განყოფილება (Firefox, Palemoon etc..)\n3. შედით \"Cookies\" სექციაში და შემდეგ \"https://accounts.spotify.com\" სუბსექციაში", + "step_3": "ნაბიჯი 3", + "step_3_steps": "დააკოპირეთ \"sp_dc\" ქუქი-ფაილის მნიშვნელობა", + "success_emoji": "წარმატება🥳", + "success_message": "თქვენ წარმატებით შეხვედით თქვენი Spotify ანგარიშით.", + "step_4": "ნაბიჯი 4", + "step_4_steps": "ჩასვით კოპირებული \"sp_dc\" მნიშვნელობა", + "something_went_wrong": "Რაღაც არასწორად წავიდა", + "piped_instance": "Piped Server Instance", + "piped_description": "The Piped server instance to use for track matching", + "piped_warning": "ზოგიერთი მათგანმა შეიძლება კარგად არ იმუშაოს. ", + "generate_playlist": "ფლეილისტის დაგენერირება", + "track_exists": "ტრეკი {track} უკვე არსებობს", + "replace_downloaded_tracks": "ყველა ჩამოტვირთული ტრეკის შეცვლა", + "skip_download_tracks": "ყველა ჩამოტვირთული ტრეკის გამოტოვება", + "do_you_want_to_replace": "გსურთ შეცვალოთ არსებული ტრეკი??", + "replace": "შეცვლა", + "skip": "გამოტოვება", + "select_up_to_count_type": "აირჩიე {count}-მდე {type}", + "select_genres": "ჟანრების არჩევა", + "add_genres": "ჟანრების დამატება", + "country": "ქვეყანა", + "number_of_tracks_generate": "დასაგენერირებელი ტრეკების რაოდენობა", + "acousticness": "Acousticness", + "danceability": "Danceability", + "energy": "Energy", + "instrumentalness": "Instrumentalness", + "liveness": "Liveness", + "loudness": "Loudness", + "speechiness": "Speechiness", + "valence": "Valence", + "popularity": "Popularity", + "key": "Key", + "duration": "Duration (s)", + "tempo": "Tempo (BPM)", + "mode": "Mode", + "time_signature": "Time Signature", + "short": "Short", + "medium": "საშუალო", + "long": "გრძელი", + "min": "მინიმალური", + "max": "მაქსიმალური", + "target": "სამიზნე", + "moderate": "საშუალო", + "deselect_all": "ყველა მონიშვნის გაუქმება", + "select_all": "ყველას მონიშვნა", + "are_you_sure": "Დარწმუნებული ხართ?", + "generating_playlist": "მიმდინარეობს თქვენი მორგებული ფლეილისტის გენერირება...", + "selected_count_tracks": "არჩეულია {count} ტრეკი", + "download_warning": "If you download all Tracks at bulk you're clearly pirating Music & causing damage to the creative society of Music. I hope you are aware of this. Always, try respecting & supporting Artist's hard work", + "download_ip_ban_warning": "BTW, your IP can get blocked on YouTube due excessive download requests than usual. IP block means you can't use YouTube (even if you're logged in) for at least 2-3 months from that IP device. And Spotube doesn't hold any responsibility if this ever happens", + "by_clicking_accept_terms": "By clicking 'accept' you agree to following terms:", + "download_agreement_1": "I know I'm pirating Music. I'm bad", + "download_agreement_2": "I'll support the Artist wherever I can and I'm only doing this because I don't have money to buy their art", + "download_agreement_3": "I'm completely aware that my IP can get blocked on YouTube & I don't hold Spotube or his owners/contributors responsible for any accidents caused by my current action", + "decline": "უარყოფა", + "accept": "დათანხმება", + "details": "დეტალები", + "youtube": "YouTube", + "channel": "Channel", + "likes": "მოწონებები", + "dislikes": "არ მოწონებები", + "views": "ნახვები", + "streamUrl": "სტრიმის ლინკი", + "stop": "გაჩერება", + "sort_newest": "ფალაგება სიახლის მიხედიტ", + "sort_oldest": "დალაგება სიძველის მიხედვით", + "sleep_timer": "ძილის ტაიმერი", + "mins": "{minutes} წუთი", + "hours": "{hours} საათი", + "hour": "{hours} საათი", + "custom_hours": "მორგებული საათები", + "logs": "ლოგები", + "developers": "დეველოპერები", + "not_logged_in": "არ ხარ დალოგინებული", + "search_mode": "ძებნის რეჟიმი", + "audio_source": "აუდიოს წყარო", + "ok": "ოკ", + "failed_to_encrypt": "დაშიფვრა ვერ მოხერხდა", + "encryption_failed_warning": "Spotube uses encryption to securely store your data. But failed to do so. So it'll fallback to insecure storage\nIf you're using linux, please make sure you've any secret-service (gnome-keyring, kde-wallet, keepassxc etc) installed", + "querying_info": "Querying info...", + "piped_api_down": "Piped API is down", + "piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change", + "you_are_offline": "ამჟამად ხაზგარეშე ხართ", + "connection_restored": "თქვენი ინტერნეტ კავშირი აღდგა", + "use_system_title_bar": "სისტემის სათაურის ზოლის გამოყენება", + "crunching_results": "იტვირთება შედეგები...", + "search_to_get_results": "მოძებნეთ შედეგების მისაღებად", + "use_amoled_mode": "Pitch black dark theme", + "pitch_dark_theme": "AMOLED Mode", + "normalize_audio": "აუდიოს ნორმალიზება", + "change_cover": "Ქავერის შეცვლა", + "add_cover": "Ქავერის ფოტოს დამატება", + "restore_defaults": "ნაგულისხმევი პარამეტრების აღდგენა", + "download_music_codec": "მუსიკის კოდეკის გადმოწერა", + "streaming_music_codec": "სტრიმინგ მუსიკის კოდეკი", + "login_with_lastfm": "Last.fm-ით შესვლა", + "connect": "დაკავშირება", + "disconnect_lastfm": "Last.fm-იდან გამოსვლა", + "disconnect": "გამოსვლა", + "username": "მომხმარებელი", + "password": "პაროლი", + "login": "შესვლა", + "login_with_your_lastfm": "Last.fm ანგარიშით შესვლა", + "scrobble_to_lastfm": "Scrobble to Last.fm", + "go_to_album": "ალბომზე გადასვლა", + "discord_rich_presence": "Discord Rich Presence", + "browse_all": "ყველას ნახვა", + "genres": "ჟანრები", + "explore_genres": "შეისწავლეთ ჟანრები", + "friends": "მეგობრები", + "no_lyrics_available": "უკაცრავად, ამ ტრეკისთვის ტექსტის პოვნა შეუძლებელია", + "start_a_radio": "რადიოს ჩართვა", + "how_to_start_radio": "როგორ გნებავთ რადიოს ჩართვა?", + "replace_queue_question": "გნებავთ ჩაანაცვლოთ არსებული რიგი თუ დაამატოთ მასზე?", + "endless_playback": "დაუსრულებელი დაკვრა", + "delete_playlist": "ფლეილისტის წაშლა", + "delete_playlist_confirmation": "დარწმუნებული ხართ რომ გნებავთ ფლეილისტის წაშლა?", + "local_tracks": "ლოკალური ტრეკები", + "song_link": "ტრეკის ლინკი", + "skip_this_nonsense": "ამ სისულელის გამოტოვება", + "freedom_of_music": "“მუსიკის თავისუფლება”", + "freedom_of_music_palm": "“მუსიკის თავისუფლება შენს ხელის გულზე”", + "get_started": "დავიწყოთ", + "youtube_source_description": "რეკომენდებულია და მუშაობს საუკეთესოდ.", + "piped_source_description": "თავისუფლად გრძნობთ თავს? იგივეა, რაც YouTube, მაგრამ ბევრი თავისუფალი.", + "jiosaavn_source_description": "საუკეთესოა სამხრეთ აზიის რეგიონისთვის.", + "highest_quality": "საუკეთესო ხარისხი: {quality}", + "select_audio_source": "აუდიოს წყაროს არჩევა", + "endless_playback_description": "ახალი სიმთერების ავტომატურად რიგის ბოლოში დამატება", + "choose_your_region": "აირჩიე შენი რეგიონი", + "choose_your_region_description": "This will help Spotube show you the right content\nfor your location.", + "choose_your_language": "აირჩიე ენა", + "help_project_grow": "დაეხმარეთ ამ პროექტს განვითარებაში", + "help_project_grow_description": "Spotube is an open-source project. You can help this project grow by contributing to the project, reporting bugs, or suggesting new features.", + "contribute_on_github": "GitHub-ზე კონტრიბუცია", + "donate_on_open_collective": "Open Collective-ზე დონაცია", + "browse_anonymously": "ანონიმურად ნახვა", + "enable_connect": "დაკავშირების ჩართვა", + "enable_connect_description": "აკონტროლე Spotube სხვა მოწყობილობებიდან", + "devices": "მოწყობილობები", + "select": "არჩევა", + "connect_client_alert": "თქვენ კონტროლირებული ხართ {client} მოწყობილობით", + "this_device": "ეს მოწყობილობა", + "remote": "დისტანციური" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 29ededde3..7d1e995b3 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -33,6 +33,7 @@ class L10n { const Locale('hi', 'IN'), const Locale('it', 'IT'), const Locale('ja', 'JP'), + const Locale('ka', 'GE'), const Locale('ko', 'KR'), const Locale('nl', 'NL'), const Locale('pl', 'PL'), From edc997e7470ce17f60c96b8198dc8851cbf21f18 Mon Sep 17 00:00:00 2001 From: ctih <78687256+ctih1@users.noreply.github.com> Date: Fri, 10 May 2024 19:49:38 +0300 Subject: [PATCH 19/57] feat(translations): add Finnish translations (#1449) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * added finnish translation * chore: fix arb syntax errors and language in l10n entries --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho Co-authored-by: Onni Nevala --- lib/collections/language_codes.dart | 8 +- lib/l10n/app_fi.arb | 324 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 1 + 3 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 lib/l10n/app_fi.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index ae75433a8..099b1a6e7 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -197,10 +197,10 @@ abstract class LanguageLocals { // name: "Fijian", // nativeName: "vosa Vakaviti", // ), - // "fi": const ISOLanguageName( - // name: "Finnish", - // nativeName: "suomi", - // ), + "fi": const ISOLanguageName( + name: "Finnish", + nativeName: "suomi", + ), "fr": const ISOLanguageName( name: "French", nativeName: "français", diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb new file mode 100644 index 000000000..35470791e --- /dev/null +++ b/lib/l10n/app_fi.arb @@ -0,0 +1,324 @@ +{ + "guest": "Vieras", + "browse": "Selaa", + "search": "Hae", + "library": "Kirjasto", + "lyrics": "Lyriikat", + "settings": "Asetukset", + "genre_categories_filter": "Suodata kategorioita tai genrejä", + "genre": "Genre", + "personalized": "Personoidut", + "featured": "Esittelyssä", + "new_releases": "Uusi julkaisu", + "songs": "Laulut", + "playing_track": "Soitetaan {track}", + "queue_clear_alert": "Tämä tulee tyhjentämään jonon. {track_length} Kappaleita poistetaan\nHaluatko jatkaa?", + "load_more": "Lataa lisää", + "playlists": "Soittolistat", + "artists": "Artistit", + "albums": "Albumit", + "tracks": "Kappaleet", + "downloads": "Lataukset", + "filter_playlists": "Suodata soittolistasi...", + "liked_tracks": "Tykätyt kappaleet", + "liked_tracks_description": "Kaikki tykättysi kappaleet", + "create_playlist": "Luo soittolista", + "create_a_playlist": "Luo soittolista", + "update_playlist": "Päivitä soittolista", + "create": "Luo", + "cancel": "Peruuta", + "update": "Päivitä", + "playlist_name": "Soittolistan nimi", + "name_of_playlist": "Soittolistan nimi", + "description": "Kuvaus", + "public": "Julkinen", + "collaborative": "Collaborative", + "search_local_tracks": "Hae paikallisia lauluja...", + "play": "Soita", + "delete": "Poista", + "none": "Ei mitään", + "sort_a_z": "Suodata A-Z", + "sort_z_a": "Suodata Z-A", + "sort_artist": "Suodata Artistilta", + "sort_album": "Suodata Albumilta", + "sort_duration": "Suodata Pituudelta", + "sort_tracks": "Suodata Kappaleet", + "currently_downloading": "Ladataan ({tracks_length})", + "cancel_all": "Peru kaikki", + "filter_artist": "Suodata artistit...", + "followers": "{followers} Seuraajaa", + "add_artist_to_blacklist": "Lisää artisti mustalle listalle", + "top_tracks": "Suosituimmat kappaleet", + "fans_also_like": "Fanit myös tykkäsivät", + "loading": "Ladataan...", + "artist": "Artisti", + "blacklisted": "Mustalistattu", + "following": "Seurataan", + "follow": "Seuraa", + "artist_url_copied": "Aristin URL kopioitiin leikepöytään", + "added_to_queue": "Lisättiin {tracks} kappaletta jonoon", + "filter_albums": "Suodata albumit...", + "synced": "Synkronoitu", + "plain": "Tavallinen", + "shuffle": "Sekoita", + "search_tracks": "Hae kappaleita...", + "released": "Julkaistu", + "error": "Virhe {error}", + "title": "Otsikko", + "time": "Aika", + "more_actions": "Lisää toimintoja", + "download_count": "Lataa ({count})", + "add_count_to_playlist": "Lisää ({count}) Soittolistaasi", + "add_count_to_queue": "Lisää ({count}) Jonoon", + "play_count_next": "Soita ({count}) seuraavaksi", + "album": "Albumi", + "copied_to_clipboard": "Kopioitiin {data} leikepöytään", + "add_to_following_playlists": "Lisää {track} seuraaviin soittolistoihin", + "add": "Lisää", + "added_track_to_queue": "Lisättiin {track} jonoon", + "add_to_queue": "Lisää jonoon", + "track_will_play_next": "{track} Soitetaan seuraavaksi", + "play_next": "Soita seuraavaksi", + "removed_track_from_queue": "Poistettiin {track} jonosta", + "remove_from_queue": "Poista jonosta", + "remove_from_favorites": "Poista suosikeista", + "save_as_favorite": "Tallenna soittolistana", + "add_to_playlist": "Lisää soittolistaan", + "remove_from_playlist": "Poista soittolistasta", + "add_to_blacklist": "Lisää mustalle listalle", + "remove_from_blacklist": "Poista mustalistalta", + "share": "Jaa", + "mini_player": "Minisoitin", + "slide_to_seek": "Liu'uta mennäkseen eteenpäin tai taaksepäin", + "shuffle_playlist": "Sekoita soittolista", + "unshuffle_playlist": "Poista sekoitus soittolistasta", + "previous_track": "Äskeinen kappale", + "next_track": "Seuraava kappale", + "pause_playback": "Pysäytä soittolistan toisto", + "resume_playback": "Jatka soittolistan toistoa", + "loop_track": "Uudelleentoista kappale", + "repeat_playlist": "Toista soittolista uudelleen", + "queue": "Jono", + "alternative_track_sources": "Toinen kappale lähde", + "download_track": "Lataa kappale", + "tracks_in_queue": "{tracks} kappaletta jonossa", + "clear_all": "Tyhjennä kaikki", + "show_hide_ui_on_hover": "Näytä/Piilota UI leijumalla", + "always_on_top": "Aina päällimmäisenä", + "exit_mini_player": "Lähde minisoittimesta", + "download_location": "Lataus sijainti", + "account": "Käyttäjä", + "login_with_spotify": "Kirjaudu Spotify-käyttäjällä", + "connect_with_spotify": "Yhdistä Spotify:lla", + "logout": "Kirjaudu ulos", + "logout_of_this_account": "Kirjaudu ulos tältä käyttäjältä", + "language_region": "Kieli ja Maa", + "language": "Kieli", + "system_default": "Järjestelmän oletus", + "market_place_region": "Markkina-alue", + "recommendation_country": "Suositeltu maa", + "appearance": "Ulkomuto", + "layout_mode": "Asettelutila", + "override_layout_settings": "Jätä reagoiva asettelutila huomioimatta", + "adaptive": "Mukautuva", + "compact": "Kompakti", + "extended": "Laajennettu", + "theme": "Teema", + "dark": "Tumma", + "light": "Vaalea", + "system": "Järjestelmä", + "accent_color": "Korostusväri", + "sync_album_color": "Synkronoi albumin väri", + "sync_album_color_description": "Käyttää albumin kansitaiteen vallitsevaa väirä korostuvärinä", + "playback": "Toisto", + "audio_quality": "Äänenlaatu", + "high": "Korkea", + "low": "Matala", + "pre_download_play": "Esilataa ja soita", + "pre_download_play_description": "Audion suoratoiston sijaan, lataa tavut ja soita ne (Suositeltu korkeamman kaistanleveyden käyttäjille)", + "skip_non_music": "Ohita ei-musiikki kohdat (SponsorBlock)", + "blacklist_description": "Mustalistat kappaleet aja artistit", + "wait_for_download_to_finish": "Odota nykyisen latauksen lopetteluun", + "desktop": "Työpöytä", + "close_behavior": "Sulkemisen käyttäytyminen", + "close": "Sulje", + "minimize_to_tray": "Minimisoi tehtäväpalkkiin", + "show_tray_icon": "Näytä järjestelmäkuvake", + "about": "Tietoa", + "u_love_spotube": "Tiedämme että rakastat Spotubea", + "check_for_updates": "Tarkista päivitykset", + "about_spotube": "Tietoa Spotube:sta", + "blacklist": "Mustalista", + "please_sponsor": "Sponsoroi/Lahjoita, kiitos", + "spotube_description": "Spotube, kevyt, cross-platform, vapaa-kaikille spotify clientti", + "version": "Versio", + "build_number": "Rakennusnumero", + "founder": "Perustaja", + "repository": "Arkisto", + "bug_issues": "Bugit+Ongelmat", + "made_with": "Tehty ❤️ Bangladeshista 🇧🇩", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Lisenssi", + "add_spotify_credentials": "Lisää Spotify-tunnuksesi aloittaaksesi", + "credentials_will_not_be_shared_disclaimer": "Älä huoli, tunnuksiasi ei talleteta tai jaeta kenenkään kanssa", + "know_how_to_login": "Etkö tiedä miten tehdä tämä?", + "follow_step_by_step_guide": "Seuraa askel askeleelta opasta", + "spotify_cookie": "Spotify {name} Keksi", + "cookie_name_cookie": "{name} Keksi", + "fill_in_all_fields": "Täytä kaikki kentät", + "submit": "Lähetä", + "exit": "Poistu", + "previous": "Edellinen", + "next": "Seuraava", + "done": "Tehty", + "step_1": "Vaihe 1", + "first_go_to": "Ensiksi, mene", + "login_if_not_logged_in": "ja Kirjaudu/Tee tili jos et ole kirjautunut sisään", + "step_2": "Vaihe 2", + "step_2_steps": "1. Kun olet kirjautunut, paina F12 tai oikeaa hiiren näppäintä > Tarkista ja avaa selaimen kehittäjä työkalut.\n2. Mene sitten \"Application\"-välilehteen (Chrome, Edge, Brave jne..) tai \"Storage\"-välilehteen (Firefox, Palemoon jne..)\n3. Mene \"Cookies\"-osastoon, sitten \"https://accounts.spotify.com\" alakohtaan.", + "step_3": "Vaihe 3", + "step_3_steps": "Kopioi Keksin \"sp_dc\" arvo", + "success_emoji": "Onnistuit🥳", + "success_message": "Olet nyt kirjautunut sisään Spotify-käyttäjällesi. Hyvää työtä toveri!", + "step_4": "Vaihe 4", + "step_4_steps": "Liitä kopioitu \"sp_dc\" arvo", + "something_went_wrong": "Jotain meni pieleen", + "piped_instance": "Johdettu palvelinesiintymä", + "piped_description": "Johdettu palvelinesiintymä Kappale täsmäyksiin", + "piped_warning": "Jotkut niistä eivät toimi hyvin, käytä siis omalla vastuullasi", + "generate_playlist": "Tuota soittolista", + "track_exists": "Kappale {track} on jo olemassa!", + "replace_downloaded_tracks": "Korvaa kaikki ladatut kappaleet", + "skip_download_tracks": "Ohita ladattujen laulujen lataaminen", + "do_you_want_to_replace": "Haluatko korvata olemassa olevan kappaleen??", + "replace": "Korvaa", + "skip": "Ohita", + "select_up_to_count_type": "Valitse enintään {count} {type}", + "select_genres": "Valitse Genret", + "add_genres": "Lisää Genrejä", + "country": "Maa", + "number_of_tracks_generate": "Numero tuotettavia kappaleita", + "acousticness": "Akustisuus", + "danceability": "Tanssittavuus", + "energy": "Energia", + "instrumentalness": "Instrumentaalisuus", + "liveness": "Elävyyttä", + "loudness": "Äänekkyys", + "speechiness": "Puheisuus", + "valence": "Valenssi", + "popularity": "Suosio", + "key": "Sävellaji", + "duration": "Pituus (s)", + "tempo": "Tempo (BPM)", + "mode": "Tila", + "time_signature": "Aikamerkki", + "short": "Lyhyt", + "medium": "Keskikokoinen", + "long": "Pitkä", + "min": "Minimi", + "max": "Maximi", + "target": "Kohde", + "moderate": "Kohtalainen", + "deselect_all": "Poista kaikki valinnat", + "select_all": "Valitse kaikki", + "are_you_sure": "Oletko varma?", + "generating_playlist": "Luodaan mukautettua soittolistoa...", + "selected_count_tracks": "Valittu {count} kappaletta", + "download_warning": "Jos lataat kaikki laulut kerrällä olet selkeästi Piratoimassa ja aiheuttamassa vahinkoa musiikin luovaan yhteiskuntaan. Toivottavasti olet tietoinen tästä. Yritä aina kunnioittaa ja tukea Artistin kovaa työtä.", + "download_ip_ban_warning": "BTW, YouTube voi estää IP-Osoitteesi tavallista liiallisten latauspyyntöjen takia. IP-Osoitteen esto tarkoittaa sitä, ettet voi käyttää YouTubea (vaikka olisit kirjautunut) vähintään 2-3kk aikana kyseiseltä laitteelta. Spotube ei kanna yhtään vastuuta jos se tapahtuu.", + "by_clicking_accept_terms": "Painamalla 'hyväksy' hyväksyt seuraaviin ehtoihin:", + "download_agreement_1": "Tiedän että Piratoin musiikkia. Olen paha.", + "download_agreement_2": "Tuen Artisteja silloin kun pystyn, ja teen tämän vain koska minulla ei ole rahaa ostaa heidän taidetta", + "download_agreement_3": "Ymmärrän että minun YouTube voi estää IP-Osoitteeni ja en pidä Spotubea tai omistajiinsa/avustajia vastuullisena mistään omista teoistsani", + "decline": "Hylkää", + "accept": "Hyväksy", + "details": "Yksityiskohdat", + "youtube": "YouTube", + "channel": "Kanava", + "likes": "Tykkäykset", + "dislikes": "Epä-tykkäykset", + "views": "Näyttökerrat", + "streamUrl": "Suoratoiston URL", + "stop": "Lopeta", + "sort_newest": "Suodata uusimmista", + "sort_oldest": "Suodata vanhimmista", + "sleep_timer": "Uniajastin", + "mins": "{minutes} Minuuttia", + "hours": "{hours} Tuntia", + "hour": "{hours} Tunti", + "custom_hours": "Mukautetut tunnit", + "logs": "Lokit", + "developers": "Kehittäjät", + "not_logged_in": "Et ole kirjautunut sisään.", + "search_mode": "Hakutila", + "audio_source": "Äänilähde", + "ok": "Ok", + "failed_to_encrypt": "Salaaminen epäonnistui", + "encryption_failed_warning": "Spotube käyttää salausta tallentaakseen tietosi, mutta epäonnistui, joten se palaa epäturvalliseen tallennukseen\nJos käytät Linuxia, varmista että sinulla on turvallisuuspalvelu (gnome-keyring, kde-wallet, keepassxc jne) asennettu", + "querying_info": "Hankitaan tietoa...", + "piped_api_down": "Johdettu palvelinesiintymä on alhaalla", + "piped_down_error_instructions": "Johdettu palvelinesiintymä {pipedInstance} on alhaalla.\n\nVaihda joko ilmeytymä tia vahda 'API tyyppi' YouTuben viralliseen API\n\nKäynnistä sovellus uudestaan vaihdon jälkeen", + "you_are_offline": "Et ole yhdistetty verkkoon", + "connection_restored": "Verkkoyhteys palautettu", + "use_system_title_bar": "Käytä järjestelmäpalkkia", + "crunching_results": "Paloitellaan tuloksia...", + "search_to_get_results": "Hae saadakseen tuloksia", + "use_amoled_mode": "Pilkkopimeä tumma teema", + "pitch_dark_theme": "AMOLED Tila", + "normalize_audio": "Normalisoi audio", + "change_cover": "Vaihda koveri", + "add_cover": "Lisää koveri", + "restore_defaults": "Palauta oletukset", + "download_music_codec": "Ladatun musiikin codefc", + "streaming_music_codec": "Suoratoistetun musiikin codec", + "login_with_lastfm": "Kirjaudu sisään Last.fm:llä", + "connect": "Yhdistä", + "disconnect_lastfm": "Katkaise Last.fm", + "disconnect": "Katkaise", + "username": "Käyttäjänimi", + "password": "Salasana", + "login": "Kirjaudu", + "login_with_your_lastfm": "Kirjaudu Last.fm käyttäjälläsi", + "scrobble_to_lastfm": "Scrobble Last.fm:ään", + "go_to_album": "Mene albumiin", + "discord_rich_presence": "Discord Rich Presence", + "browse_all": "Selaa kaikki", + "genres": "Genret", + "explore_genres": "Seikkaile genrejä", + "friends": "Kaverit", + "no_lyrics_available": "Anteeksi, emme löytäneet lyriikoita tälle laululle", + "start_a_radio": "Aloita Radio", + "how_to_start_radio": "Kuinka haluat aloittaa radion?", + "replace_queue_question": "Haluatko korvata nykyisen jonon vai lisätä siihen?", + "endless_playback": "Loputon toisto", + "delete_playlist": "Poista soittolista", + "delete_playlist_confirmation": "Oletko varma että haluat poistaa tämän soittolistan?", + "local_tracks": "Paikalliset kappaleet", + "song_link": "Laulun linkki", + "skip_this_nonsense": "Ohita tämä hölynpöly", + "freedom_of_music": "“Musiikin vapaus”", + "freedom_of_music_palm": "“Musiikin vapaus käsissäsi”", + "get_started": "Aloitetaan", + "youtube_source_description": "Suositeltu ja toimii parhaiten.", + "piped_source_description": "Tuntuuko vapaalta? Sama kuin YouTube mutta paljon vapautta", + "jiosaavn_source_description": "Paras Etelä-Aasian alueelle.", + "highest_quality": "Korkein laatu: {quality}", + "select_audio_source": "Valitse äänilähde", + "endless_playback_description": "Lisää automaattisesti uusia lauluja\njonon perään", + "choose_your_region": "Valitse alueesi", + "choose_your_region_description": "Tämä auttaa Spotube näyttämään sinulle oikeaa sisältöä\nsijaintiasi varten.", + "choose_your_language": "Valitse kielesi", + "help_project_grow": "Auta tätä projektia kasvamaan", + "help_project_grow_description": "Spotube projekti minkä lähdekoodi on julkisesti saatavilla. Voit autta tätä projektia kasvamaan muutoksilla, ilmoittamalla bugeista, tai ehdottamalla uusia ominaisuuksia.", + "contribute_on_github": "Auta GitHub:ssa", + "donate_on_open_collective": "Lahjoita avoimessa kollektiivissa", + "browse_anonymously": "Selaa anonyyminä", + "enable_connect": "Ota käyttöön yhdistäminen", + "enable_connect_description": "Ohjaa Spotubea toiselta laitteelta", + "devices": "Laitteet", + "select": "Valitse", + "connect_client_alert": "{client} ohjaa sinua", + "this_device": "Tämä laite", + "remote": "Etä" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 7d1e995b3..d96a93720 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -28,6 +28,7 @@ class L10n { const Locale('de', 'GE'), const Locale('es', 'ES'), const Locale('fa', 'IR'), + const Locale('fi', 'FI'), const Locale('fr', 'FR'), const Locale('ne', 'NP'), const Locale('hi', 'IN'), From 0280654bb6bad373aee521f5a866228d2d38f038 Mon Sep 17 00:00:00 2001 From: Yusril Rapsanjani Date: Sat, 11 May 2024 00:00:24 +0700 Subject: [PATCH 20/57] feat(translations): add Indonesian translation (#1426) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * Add Indonesia translation --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho --- lib/collections/language_codes.dart | 8 +- lib/l10n/app_id.arb | 324 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 1 + 3 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 lib/l10n/app_id.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index 099b1a6e7..f46e0efe9 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -265,10 +265,10 @@ abstract class LanguageLocals { // name: "Interlingua", // nativeName: "Interlingua", // ), - // "id": const ISOLanguageName( - // name: "Indonesian", - // nativeName: "Bahasa Indonesia", - // ), + "id": const ISOLanguageName( + name: "Indonesian", + nativeName: "Bahasa Indonesia", + ), // "ie": const ISOLanguageName( // name: "Interlingue", // nativeName: "Occidental", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb new file mode 100644 index 000000000..b94cdd281 --- /dev/null +++ b/lib/l10n/app_id.arb @@ -0,0 +1,324 @@ +{ + "guest": "Tamu", + "browse": "Jelajahi", + "search": "Cari", + "library": "Pustaka", + "lyrics": "Lirik", + "settings": "Pengaturan", + "genre_categories_filter": "Urutkan kategori atau genre...", + "genre": "Genre", + "personalized": "Dipersonalisasi", + "featured": "Unggulan", + "new_releases": "Rilis Terbaru", + "songs": "Lagu", + "playing_track": "Memutar {track}", + "queue_clear_alert": "Ini akan menghapus antrian saat ini This will clear the current queue. {track_length} trek akan dihapus\nAnda ingin melanjutkan?", + "load_more": "Lebih Banyak", + "playlists": "Daftar Putar", + "artists": "Artis", + "albums": "Album", + "tracks": "Trek", + "downloads": "Unduhan", + "filter_playlists": "Urutkan daftar putar Anda...", + "liked_tracks": "Lagu Yang Disukai", + "liked_tracks_description": "Semua lagu yang Anda sukai", + "create_playlist": "Buat Daftar Putar", + "create_a_playlist": "Buat daftar putar", + "update_playlist": "Ubah daftar putar", + "create": "Buat", + "cancel": "Batal", + "update": "Ubah", + "playlist_name": "Nama Daftar Putar", + "name_of_playlist": "Nama daftar putar", + "description": "Deskripsi", + "public": "Publik", + "collaborative": "Kolaboratif", + "search_local_tracks": "Cari trek lokal...", + "play": "Putar", + "delete": "Hapus", + "none": "Tidak Ada", + "sort_a_z": "Urutkan berdasarkan A-Z", + "sort_z_a": "Urutkan berdasarkan Z-A", + "sort_artist": "Urutkan berdasarkan Artis", + "sort_album": "Urutkan berdasarkan Album", + "sort_duration": "Urutkan berdasarkan Durasi", + "sort_tracks": "Urutkan trek", + "currently_downloading": "Sedang Mengunduh ({tracks_length})", + "cancel_all": "Batalkan Semua", + "filter_artist": "Urutkan artis...", + "followers": "{followers} Pengikut", + "add_artist_to_blacklist": "Tambah artis ke daftar hitam", + "top_tracks": "Lagu Teratas", + "fans_also_like": "Penggemar juga menyukainya", + "loading": "Memuat...", + "artist": "Artis", + "blacklisted": "Masuk Daftar Hitam", + "following": "Mengikuti", + "follow": "Ikuti", + "artist_url_copied": "URL artis telah disalin", + "added_to_queue": "Menambah trek {tracks} ke antrean", + "filter_albums": "Urutkan album...", + "synced": "Disinkronkan", + "plain": "Normal", + "shuffle": "Acak", + "search_tracks": "Cari trek...", + "released": "Dirilis", + "error": "Kesalahan {error}", + "title": "Judul", + "time": "Waktu", + "more_actions": "Tindakan Lainnya", + "download_count": "Unduhan ({count})", + "add_count_to_playlist": "Menambah ({count}) ke Daftar Putar", + "add_count_to_queue": "Menambah ({count}) ke Antrian", + "play_count_next": "Mainkan ({count}) selanjutnya", + "album": "Album", + "copied_to_clipboard": "{data} telah disalin", + "add_to_following_playlists": "Menambah {track} ke Daftar Putar berikut", + "add": "Tambah", + "added_track_to_queue": "Menambah {track} ke antrian", + "add_to_queue": "Tambah ke antrian", + "track_will_play_next": "{track} akan diputar berikutnya", + "play_next": "Mainkan selanjutnya", + "removed_track_from_queue": "Menghapus {track} dari antrian", + "remove_from_queue": "Hapus dari antrian", + "remove_from_favorites": "Hapus dari favorit", + "save_as_favorite": "Simpan sebagai favorit", + "add_to_playlist": "Tambah ke daftar putar", + "remove_from_playlist": "Hapus dari daftar putar", + "add_to_blacklist": "Tambah ke daftar hitam", + "remove_from_blacklist": "Hapus dari daftar hitam", + "share": "Bagikan", + "mini_player": "Pemutar Mini", + "slide_to_seek": "Geser untuk maju atau mundur", + "shuffle_playlist": "Acak daftar putar", + "unshuffle_playlist": "Batalkan pengacakan daftar putar", + "previous_track": "Lagu sebelumnya", + "next_track": "Lagu berikutnya", + "pause_playback": "Jeda Pemutaran", + "resume_playback": "Lanjutkan Pemutaran", + "loop_track": "Ulangi Pemutaran", + "repeat_playlist": "Ulangi daftar putar", + "queue": "Antrian", + "alternative_track_sources": "Sumber trek alternatif", + "download_track": "Unduh lagu", + "tracks_in_queue": "{tracks} trek dalam antrian", + "clear_all": "Bersihkan semua", + "show_hide_ui_on_hover": "Tampil/Sembunyikan UI saat mengarahkan kursor", + "always_on_top": "Selalu di atas", + "exit_mini_player": "Keluar Pemutar Mini", + "download_location": "Lokasi unduhan", + "account": "Akun", + "login_with_spotify": "Masuk dengan Spotify", + "connect_with_spotify": "Hubungkan dengan Spotify", + "logout": "Keluar", + "logout_of_this_account": "Keluar dari akun", + "language_region": "Bahasa & Wilayah", + "language": "Bahasa", + "system_default": "Bawaan Sistem", + "market_place_region": "Wilayah Pasar", + "recommendation_country": "Negara Rekomendasi", + "appearance": "Tampilan", + "layout_mode": "Mode Tata Letak", + "override_layout_settings": "Ganti pengaturan mode tata letak responsif", + "adaptive": "Adaptif", + "compact": "Ringkas", + "extended": "Diperluas", + "theme": "Tema", + "dark": "Gelap", + "light": "Terang", + "system": "Sistem", + "accent_color": "Warna Aksen", + "sync_album_color": "Sinkronkan warna album", + "sync_album_color_description": "Menggunakan warna dominan sampul album sebagai warna aksen", + "playback": "Pemutaran", + "audio_quality": "Kualitas Suara", + "high": "Tinggi", + "low": "Rendah", + "pre_download_play": "Unduh dan putar", + "pre_download_play_description": "Daripada streaming audio, unduh byte dan mainkan (Direkomendasikan untuk pengguna bandwidth yang lebih tinggi)", + "skip_non_music": "Lewati segmen non-musik (SponsorBlock)", + "blacklist_description": "Lagu dan artis di daftar hitam", + "wait_for_download_to_finish": "Tunggu hingga unduhan saat ini selesai", + "desktop": "Desktop", + "close_behavior": "Tutup Perilaku", + "close": "Tutup", + "minimize_to_tray": "Perkecil ke tray", + "show_tray_icon": "Tampilkan tray ikon sistem", + "about": "Tentang", + "u_love_spotube": "Kami tahu Anda menyukai Spotube", + "check_for_updates": "Periksa pembaruan", + "about_spotube": "Tentang Spotube", + "blacklist": "Daftar Hitam", + "please_sponsor": "Silakan Sponsor/Menyumbang", + "spotube_description": "Spotube, klien Spotify yang ringan, lintas platform, dan gratis untuk semua", + "version": "Versi", + "build_number": "Nomor Pembuatan", + "founder": "Pendiri", + "repository": "Repositori", + "bug_issues": "Bug+Masalah", + "made_with": "Dibuat dengan ❤️ di Bangladesh🇧🇩", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Lisensi", + "add_spotify_credentials": "Tambahkan kredensial Spotify Anda untuk memulai", + "credentials_will_not_be_shared_disclaimer": "Jangan khawatir, kredensial Anda tidak akan dikumpulkan atau dibagikan kepada siapa pun", + "know_how_to_login": "Tidak tahu bagaimana melakukan ini?", + "follow_step_by_step_guide": "Ikuti panduan Langkah demi Langkah", + "spotify_cookie": "Spotify {name} Cookie", + "cookie_name_cookie": "{name} Cookie", + "fill_in_all_fields": "Silakan isi semua kolom", + "submit": "Kirim", + "exit": "Keluar", + "previous": "Sebelumnya", + "next": "Berikutnya", + "done": "Selesai", + "step_1": "Langkah 1", + "first_go_to": "Pertama, Pergi ke", + "login_if_not_logged_in": "dan Masuk/Daftar jika Anda belum masuk", + "step_2": "Langkah 2", + "step_2_steps": "1. Setelah Anda masuk, tekan F12 atau Klik Kanan Mouse > Buka Browser Devtools.\n2. Lalu buka Tab \"Aplikasi\" (Chrome, Edge, Brave, dll.) atau Tab \"Penyimpanan\" (Firefox, Palemoon, dll.)\n3. Buka bagian \"Cookie\" lalu subbagian \"https://accounts.spotify.com\"", + "step_3": "Langkah 3", + "step_3_steps": "Salin nilai Cookie \"sp_dc\" ", + "success_emoji": "Berhasil🥳", + "success_message": "Sekarang Anda telah berhasil Masuk dengan akun Spotify Anda. Kerja bagus, sobat!", + "step_4": "Langkah 4", + "step_4_steps": "Tempel nilai \"sp_dc\" yang disalin", + "something_went_wrong": "Terjadi kesalahan", + "piped_instance": "Piped Server Instance", + "piped_description": "The Piped server instance untuk digunakan sebagai pencocokan trek", + "piped_warning": "Beberapa di antaranya mungkin tidak berfungsi dengan baik. Jadi gunakan dengan risiko Anda sendiri", + "generate_playlist": "Hasilkan Daftar Putar", + "track_exists": "Lagu {track} sudah ada", + "replace_downloaded_tracks": "Ganti semua trek yang diunduh", + "skip_download_tracks": "Lewati pengunduhan semua trek yang diunduh", + "do_you_want_to_replace": "Apakah Anda ingin mengganti track yang ada?", + "replace": "Ganti", + "skip": "Lewati", + "select_up_to_count_type": "Pilih hingga {count} {type}", + "select_genres": "Pilih Genre", + "add_genres": "Tambah Genre", + "country": "Negara", + "number_of_tracks_generate": "Jumlah trek yang akan dihasilkan", + "acousticness": "Akustik", + "danceability": "Menari", + "energy": "Energi", + "instrumentalness": "Instrumentalitas", + "liveness": "Kehidupan", + "loudness": "Kekerasan", + "speechiness": "Berbicara", + "valence": "Valensi", + "popularity": "Popularitas", + "key": "Kunci", + "duration": "Durasi (s)", + "tempo": "Tempo (BPM)", + "mode": "Mode", + "time_signature": "Tanda Tangan Waktu", + "short": "Pendek", + "medium": "Sedang", + "long": "Panjang", + "min": "Minimal", + "max": "Maksimal", + "target": "Target", + "moderate": "Sedang", + "deselect_all": "Batalkan Semua", + "select_all": "Pilih Semua", + "are_you_sure": "Anda yakin?", + "generating_playlist": "Menghasilkan daftar putar khusus Anda...", + "selected_count_tracks": "{count} lagu yang dipilih", + "download_warning": "Jika Anda mengunduh semua Lagu secara massal, Anda jelas membajak Musik & menyebabkan kerusakan pada masyarakat kreatif Musik. Saya harap Anda menyadari hal ini. Selalu berusaha menghormati & mendukung kerja keras Artis", + "download_ip_ban_warning": "BTW, IP Anda bisa diblokir di YouTube karena permintaan unduhan yang berlebihan dari biasanya. Blokir IP berarti Anda tidak dapat menggunakan YouTube (meskipun Anda masuk) setidaknya selama 2-3 bulan dari perangkat IP tersebut. Dan Spotube tidak bertanggung jawab jika hal ini terjadi", + "by_clicking_accept_terms": "Dengan mengklik 'terima' Anda menyetujui ketentuan berikut:", + "download_agreement_1": "Saya tahu saya membajak Musik. Saya buruk", + "download_agreement_2": "Saya akan mendukung Artis di mana pun saya bisa dan saya melakukan ini hanya karena saya tidak punya uang untuk membeli karya seni mereka", + "download_agreement_3": "Saya sepenuhnya menyadari bahwa IP saya dapat diblokir di YouTube & saya tidak menganggap Spotube atau pemilik/kontributornya bertanggung jawab atas kecelakaan apa pun yang disebabkan oleh tindakan saya saat ini", + "decline": "Menolak", + "accept": "Setuju", + "details": "Detail", + "youtube": "YouTube", + "channel": "Channel", + "likes": "Suka", + "dislikes": "Tidak Suka", + "views": "Dilihat", + "streamUrl": "URL Stream", + "stop": "Berhenti", + "sort_newest": "Urutkan yang baru ditambah", + "sort_oldest": "Urutkan yang paling lama ditambah", + "sleep_timer": "Pengatur Waktu Tidur", + "mins": "{minutes} Menit", + "hours": "{hours} Jam", + "hour": "{hours} Jam", + "custom_hours": "Jam Kostum", + "logs": "Log", + "developers": "Pengembang", + "not_logged_in": "Anda belum masuk", + "search_mode": "Mode Pencarian", + "audio_source": "Sumber Suara", + "ok": "OK", + "failed_to_encrypt": "Gagal mengenkripsi", + "encryption_failed_warning": "Spotube menggunakan enkripsi untuk menyimpan data Anda dengan aman. Namun gagal melakukannya. Jadi itu akan kembali ke penyimpanan yang tidak aman\nJika Anda menggunakan linux, pastikan Anda telah menginstal layanan rahasia (gnome-keyring, kde-wallet, keepassxc, dll)", + "querying_info": "Mencari informasi...", + "piped_api_down": "Piped API tidak aktif", + "piped_down_error_instructions": "Piped Instance {pipedInstance} saat ini tidak aktif\n\nUbah instance atau ubah 'jenis API' menjadi API YouTube resmi\n\nPastikan untuk memulai ulang aplikasi setelah perubahan", + "you_are_offline": "Anda sedang offline", + "connection_restored": "Koneksi internet Anda telah pulih", + "use_system_title_bar": "Gunakan bilah judul sistem", + "crunching_results": "Mengolah hasil...", + "search_to_get_results": "Cari untuk mendapatkan hasil", + "use_amoled_mode": "Tema gelap gulita", + "pitch_dark_theme": "Mode AMOLED", + "normalize_audio": "Normalisasi audio", + "change_cover": "Ganti sampul", + "add_cover": "Tambah sampul", + "restore_defaults": "Kembalikan semula", + "download_music_codec": "Unduh codec musik", + "streaming_music_codec": "Streaming codec musik", + "login_with_lastfm": "Masuk dengan Last.fm", + "connect": "Hubungkan", + "disconnect_lastfm": "Memutuskan Last.fm", + "disconnect": "Memutuskan", + "username": "Username", + "password": "Password", + "login": "Masuk", + "login_with_your_lastfm": "Masuk dengan Last.fm Anda", + "scrobble_to_lastfm": "Scrobble ke Last.fm", + "go_to_album": "Pergi ke Album", + "discord_rich_presence": "Discord Rich Presence", + "browse_all": "Lihat Semua", + "genres": "Genre", + "explore_genres": "Jelajahi Genre", + "friends": "Daftar Teman", + "no_lyrics_available": "Maaf, tidak dapat menemukan lirik untuk lagu ini", + "start_a_radio": "Putar Radio", + "how_to_start_radio": "Bagaimana Anda ingin memutar radio?", + "replace_queue_question": "Apakah Anda ingin mengganti antrean saat ini atau menambahkannya?", + "endless_playback": "Pemutaran Tanpa Akhir", + "delete_playlist": "Hapus Daftar Putar", + "delete_playlist_confirmation": "Anda yakin ingin menghapus daftar putar ini?", + "local_tracks": "Trek Lokal", + "song_link": "Tautan Lagu", + "skip_this_nonsense": "Lewati omong kosong ini", + "freedom_of_music": "“Kebebasan Musik”", + "freedom_of_music_palm": "“Kebebasan Musik di telapak tangan Anda”", + "get_started": "Mari kita mulai", + "youtube_source_description": "Direkomendasikan dan berfungsi paling baik.", + "piped_source_description": "Merasa bebas? Sama seperti YouTube tetapi banyak yang gratis.", + "jiosaavn_source_description": "Terbaik untuk wilayah Asia Selatan.", + "highest_quality": "Kualitas Terbaik: {quality}", + "select_audio_source": "Pilih Sumber Suara", + "endless_playback_description": "Tambahkan lagu baru secara otomatis\nke akhir antrean", + "choose_your_region": "Pilih wilayah Anda", + "choose_your_region_description": "Ini akan membantu Spotube menampilkan konten yang tepat\nuntuk lokasi Anda.", + "choose_your_language": "Pilih bahasa Anda", + "help_project_grow": "Bantu proyek ini berkembang", + "help_project_grow_description": "Spotube adalah proyek sumber terbuka. Anda dapat membantu proyek ini berkembang dengan berkontribusi pada proyek, melaporkan bug, atau menyarankan fitur baru.", + "contribute_on_github": "Berkontribusi di GitHub", + "donate_on_open_collective": "Donasi di Open Collective", + "browse_anonymously": "Jelajahi Secara Anonim", + "enable_connect": "Aktifkan Hubungkan", + "enable_connect_description": "Kontrol Spotube dari perangkat lain", + "devices": "Perangkat", + "select": "Pilih", + "connect_client_alert": "Anda dikendalikan oleh {client}", + "this_device": "Perangkat Ini", + "remote": "Remot" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index d96a93720..a0fca998f 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -32,6 +32,7 @@ class L10n { const Locale('fr', 'FR'), const Locale('ne', 'NP'), const Locale('hi', 'IN'), + const Locale('id', 'ID'), const Locale('it', 'IT'), const Locale('ja', 'JP'), const Locale('ka', 'GE'), From bf45681deb951c772bf6ca05e213c949c04bded1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?W=CD=8F=20I=CD=8F=20N=CD=8F=20Z=CD=8F=20O=CD=8F=20R=CD=8F?= =?UTF-8?q?=20T=CD=8F?= <75412448+mikropsoft@users.noreply.github.com> Date: Fri, 10 May 2024 20:06:02 +0300 Subject: [PATCH 21/57] feat(translations): Improve tr locales (#1419) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * Improve tr locales --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho --- lib/l10n/app_tr.arb | 196 ++++++++++++++++++++++---------------------- lib/l10n/l10n.dart | 2 +- 2 files changed, 99 insertions(+), 99 deletions(-) diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index a4050853d..aab6bc6d8 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -3,13 +3,13 @@ "browse": "Göz at", "search": "Ara", "library": "Kütüphane", - "lyrics": "Şarkı Sözleri", + "lyrics": "Şarkı sözleri", "settings": "Ayarlar", - "genre_categories_filter": "Kategorileri veya türleri filtrele...", + "genre_categories_filter": "Kategorileri veya türleri filtreleyin...", "genre": "Tür", "personalized": "Kişiselleştirilmiş", - "featured": "Öne Çıkanlar", - "new_releases": "Yeni Çıkanlar", + "featured": "Öne çıkanlar", + "new_releases": "Yeni çıkanlar", "songs": "Şarkılar", "playing_track": "{track} oynatılıyor", "queue_clear_alert": "Bu, mevcut kuyruğu temizleyecektir. {track_length} parça kaldırılacak\nDevam etmek istiyor musunuz?", @@ -20,15 +20,15 @@ "tracks": "Parçalar", "downloads": "İndirilenler", "filter_playlists": "Oynatma listelerinizi filtreleyin...", - "liked_tracks": "Beğenilen Parçalar", + "liked_tracks": "Beğenilen parçalar", "liked_tracks_description": "Beğendiğiniz tüm parçalar", - "create_playlist": "Oynatma Listesi Oluştur", - "create_a_playlist": "Bir oynatma listesi oluşturun", + "create_playlist": "Oynatma listesi oluştur", + "create_a_playlist": "Bir oynatma listesi oluştur", "update_playlist": "Oynatma listesini güncelle", "create": "Oluştur", "cancel": "İptal", "update": "Güncelle", - "playlist_name": "Oynatma Listesi Adı", + "playlist_name": "Oynatma listesi adı", "name_of_playlist": "Oynatma listesinin adı", "description": "Açıklama", "public": "Halka açık", @@ -39,16 +39,16 @@ "none": "Yok", "sort_a_z": "A - Z'ye göre sırala", "sort_z_a": "Z - A'ya göre sırala", - "sort_artist": "Sanatçıya Göre Sırala", - "sort_album": "Albüme Göre Sırala", - "sort_duration": "Süreye Göre Sırala", - "sort_tracks": "Parçaları Sırala", - "currently_downloading": "Şu An İndirilenler ({tracks_length})", - "cancel_all": "Tümünü İptal Et", - "filter_artist": "Sanatçıları filtrele...", + "sort_artist": "Sanatçıya göre sırala", + "sort_album": "Albüme göre sırala", + "sort_duration": "Süreye göre sırala", + "sort_tracks": "Parçaları sırala", + "currently_downloading": "Şu anda indirilenler ({tracks_length})", + "cancel_all": "Tümünü iptal et", + "filter_artist": "Sanatçıları filtreleyin...", "followers": "{followers} Takipçiler", "add_artist_to_blacklist": "Sanatçıyı kara listeye ekle", - "top_tracks": "En İyi Parçalar", + "top_tracks": "En iyi parçalar", "fans_also_like": "Hayranlar ayrıca şunları da beğendi", "loading": "Yükleniyor...", "artist": "Sanatçı", @@ -57,7 +57,7 @@ "follow": "Takip et", "artist_url_copied": "Sanatçı bağlantısı panoya kopyalandı", "added_to_queue": "Kuyruğa {tracks} parçası eklendi", - "filter_albums": "Albümleri filtrele...", + "filter_albums": "Albümleri filtreleyin...", "synced": "Senkronize edildi", "plain": "Sade", "shuffle": "Karıştır", @@ -68,19 +68,19 @@ "time": "Zaman", "more_actions": "Daha fazla eylem", "download_count": "İndir ({count})", - "add_count_to_playlist": "Oynatma Listesine ({count}) ekle", - "add_count_to_queue": "Kuyruğa ({count}) ekle", - "play_count_next": "({count}) sonrakini oynat", + "add_count_to_playlist": "Oynatma Listesine ekle ({count})", + "add_count_to_queue": "Kuyruğa ekle ({count})", + "play_count_next": "Sonrakini oynat ({count})", "album": "Albüm", "copied_to_clipboard": "{data} panoya kopyalandı", - "add_to_following_playlists": "{track} parçasını aşağıdaki Oynatma Listelerine ekle", + "add_to_following_playlists": "{track} parçasını aşağıdaki oynatma listelerine ekle", "add": "Ekle", "added_track_to_queue": "{track} kuyruğa eklendi", "add_to_queue": "Kuyruğa ekle", "track_will_play_next": "{track} bir sonraki çalacak", "play_next": "Sonrakini oynat", - "removed_track_from_queue": "{track} sıradan kaldırıldı", - "remove_from_queue": "Sıradan kaldır", + "removed_track_from_queue": "{track} kuyruktan kaldırıldı", + "remove_from_queue": "Kuyruktan kaldır", "remove_from_favorites": "Favorilerden kaldır", "save_as_favorite": "Favori olarak kaydet", "add_to_playlist": "Oynatma listesine ekle", @@ -88,7 +88,7 @@ "add_to_blacklist": "Kara listeye ekle", "remove_from_blacklist": "Kara listeden kaldır", "share": "Paylaş", - "mini_player": "Mini Oynatıcı", + "mini_player": "Mini oynatıcı", "slide_to_seek": "İleri veya geri arama yapmak için kaydırın", "shuffle_playlist": "Oynatma listesini karıştır", "unshuffle_playlist": "Oynatma listesinin karışıklığını kaldır", @@ -98,27 +98,27 @@ "resume_playback": "Oynatmayı sürdür", "loop_track": "Döngü parçası", "repeat_playlist": "Oynatma listesini tekrarla", - "queue": "Sıra", - "alternative_track_sources": "Alternatif yol kaynakları", + "queue": "Kuyruk", + "alternative_track_sources": "Alternatif parça kaynakları", "download_track": "Parçayı indir", - "tracks_in_queue": "{tracks} parça sırada", + "tracks_in_queue": "{tracks} parça kuyrukta", "clear_all": "Tümünü temizle", "show_hide_ui_on_hover": "Fareyle üzerine gelindiğinde kullanıcı arayüzünü göster/gizle", "always_on_top": "Her zaman üstte", "exit_mini_player": "Mini oynatıcıdan çık", "download_location": "İndirme konumu", "account": "Hesap", - "login_with_spotify": "Spotify hesabınızla giriş yapın", + "login_with_spotify": "Spotify hesabı ile giriş yap", "connect_with_spotify": "Spotify ile bağlan", - "logout": "Çıkış Yap", - "logout_of_this_account": "Bu hesaptan çıkış yap", - "language_region": "Dil ve Bölge", - "language": "Dil", - "system_default": "Sistem Varsayılanı", - "market_place_region": "Pazaryeri Bölgesi", - "recommendation_country": "Tavsiye Edilen Ülke", + "logout": "Çıkış yap", + "logout_of_this_account": "Hesaptan çıkış yap", + "language_region": "Dil ve bölge", + "language": "Tercih edilen dil", + "system_default": "Sistem varsayılanı", + "market_place_region": "Tercih edilen bölge", + "recommendation_country": "Tavsiye edilen ülke", "appearance": "Görünüm", - "layout_mode": "Düzen Modu", + "layout_mode": "Düzen modu", "override_layout_settings": "Duyarlı düzen modu ayarlarını geçersiz kıl", "adaptive": "Uyarlanabilir", "compact": "Sıkıştırılmış", @@ -127,35 +127,35 @@ "dark": "Koyu", "light": "Açık", "system": "Sistem", - "accent_color": "Vurgu Rengi", + "accent_color": "Vurgu rengi", "sync_album_color": "Albüm rengini senkronize et", "sync_album_color_description": "Vurgu rengi olarak albüm resminin baskın rengini kullanır", "playback": "Oynatma", - "audio_quality": "Ses Kalitesi", + "audio_quality": "Ses kalitesi", "high": "Yüksek", "low": "Düşük", - "pre_download_play": "Ön yükleme ve oynatma", - "pre_download_play_description": "Ses akışı yerine baytları indirin ve oynatın (Daha yüksek bant genişliğine sahip kullanıcılar için önerilir)", - "skip_non_music": "Müzik olmayan bölümleri atla (SponsorBlock)", + "pre_download_play": "Önceden indir ve oynat", + "pre_download_play_description": "Ses akışı yerine baytları indir ve oynat (Daha yüksek bant genişliğine sahip kullanıcılar için önerilir)", + "skip_non_music": "Müzik olmayan bölümleri atlat (SponsorBlock)", "blacklist_description": "Kara listeye alınan parçalar ve sanatçılar", "wait_for_download_to_finish": "Lütfen mevcut indirme işleminin tamamlanmasını bekleyin", "desktop": "Masaüstü", - "close_behavior": "Kapatma Davranışı", + "close_behavior": "Kapatma davranışı", "close": "Kapat", "minimize_to_tray": "Tepsiye küçült", "show_tray_icon": "Sistem tepsisi simgesini göster", "about": "Hakkında", "u_love_spotube": "Spotube'u sevdiğinizi biliyoruz", "check_for_updates": "Güncellemeleri kontrol et", - "about_spotube": "Spotube Hakkında", + "about_spotube": "Spotube hakkında", "blacklist": "Kara liste", "please_sponsor": "Sponsor Ol/Bağış Yap", - "spotube_description": "Spotube, hafif, platformlar arası, herkes için ücretsiz bir spotify istemcisidir", + "spotube_description": "Spotube, hafif, platformlar arası uyumlu ve herkes için ücretsiz bir Spotify istemcisidir.", "version": "Sürüm", - "build_number": "Derleme Numarası", - "founder": "Kurucu", + "build_number": "Derleme numarası", + "founder": "Geliştirici", "repository": "Depo", - "bug_issues": "Hata+Sorunlar", + "bug_issues": "Hata + Sorunlar", "made_with": "❤️ ile Bangladeş'te yapıldı", "kingkor_roy_tirtho": "Kingkor Roy Tirtho", "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", @@ -163,31 +163,31 @@ "add_spotify_credentials": "Başlamak için spotify kimlik bilgilerinizi ekleyin", "credentials_will_not_be_shared_disclaimer": "Endişelenmeyin, kimlik bilgilerinizden hiçbiri toplanmayacak veya kimseyle paylaşılmayacak", "know_how_to_login": "Bunu nasıl yapacağınızı bilmiyor musunuz?", - "follow_step_by_step_guide": "Adım Adım kılavuzu takip edin", - "spotify_cookie": "Spotify {name} Çerezi", - "cookie_name_cookie": "{name} Çerezi", + "follow_step_by_step_guide": "Adım adım kılavuzu takip edin", + "spotify_cookie": "Spotify {name} çerezi", + "cookie_name_cookie": "{name} çerezi", "fill_in_all_fields": "Lütfen tüm alanları doldurun", - "submit": "Gönder", + "submit": "Başvur", "exit": "Çık", "previous": "Önceki", "next": "Sonraki", "done": "Bitti", "step_1": "1. Adım", "first_go_to": "İlk olarak şuraya gidin:", - "login_if_not_logged_in": "ve oturum açmadıysanız Oturum Açın/Kaydolun", + "login_if_not_logged_in": "ve oturum açmadıysanız Oturum açın/Kaydolun", "step_2": "2. Adım", - "step_2_steps": "1. Giriş yaptıktan sonra, Tarayıcı geliştirme araçlarını açmak için F12 veya Fareye Sağ Tıklayın > İncele'ye basın.\n2. Ardından \"Uygulama\" Sekmesine (Chrome, Edge, Brave vb.) veya \"Depolama\" Sekmesine (Firefox, Palemoon vb.) gidin.\n3. \"Çerezler\" bölümüne ve ardından \"https://accounts.spotify.com\" alt bölümüne gidin", + "step_2_steps": "1. Oturum açtıktan sonra, tarayıcı geliştirme araçlarını açmak için F12'ye veya fareye sağ tıklayın > İncele'ye basın.\n2. Daha sonra \"Uygulama\" sekmesine (Chrome, Edge, Brave vb..) veya \"Depolama\" sekmesine (Firefox, Palemoon vb..) gidin\n3. \"Çerezler\" bölümüne, ardından \"https://accounts.spotify.com\" alt bölümüne gidin", "step_3": "3. Adım", "step_3_steps": "\"sp_dc\" Çerezinin değerini kopyalayın", "success_emoji": "Başarılı🥳", - "success_message": "Artık Spotify hesabınızla başarıyla giriş yaptınız. Aferin, dostum!", + "success_message": "Artık Spotify hesabınızla başarıyla giriş yaptınız. Tebrik ederim!", "step_4": "4. Adım", "step_4_steps": "Kopyalanan \"sp_dc\" değerini yapıştırın", "something_went_wrong": "Bir hata oluştu", - "piped_instance": "Piped Sunucu Örneği", + "piped_instance": "Piped sunucu örneği", "piped_description": "Parça eşleştirme için kullanılacak Piped sunucu örneği", "piped_warning": "Bazıları iyi çalışmayabilir. Yani riski size ait olmak üzere kullanın", - "generate_playlist": "Oynatma Listesi Oluştur", + "generate_playlist": "Oynatma listesi oluştur", "track_exists": "{track} parçası zaten var", "replace_downloaded_tracks": "İndirilen tüm parçaları değiştir", "skip_download_tracks": "İndirilen tüm parçaları indirmeyi atla", @@ -195,8 +195,8 @@ "replace": "Değiştir", "skip": "Atla", "select_up_to_count_type": "En fazla {count} {type} seçin", - "select_genres": "Türleri Seç", - "add_genres": "Tür Ekle", + "select_genres": "Türleri seç", + "add_genres": "Tür ekle", "country": "Ülke", "number_of_tracks_generate": "Oluşturulacak parça sayısı", "acousticness": "Akustiklik", @@ -212,7 +212,7 @@ "duration": "Süre (sn)", "tempo": "Tempo (BPM)", "mode": "Mod", - "time_signature": "Zaman İmzası", + "time_signature": "Zaman imzası", "short": "Kısa", "medium": "Orta", "long": "Uzun", @@ -220,29 +220,29 @@ "max": "Maks", "target": "Hedef", "moderate": "Orta", - "deselect_all": "Tüm Seçimleri Kaldır", - "select_all": "Tümünü Seç", + "deselect_all": "Tüm seçimleri kaldır", + "select_all": "Tümünü seç", "are_you_sure": "Emin misiniz?", "generating_playlist": "Özel oynatma listeniz oluşturuluyor...", "selected_count_tracks": "{count} parça seçildi", - "download_warning": "Tüm Parçaları toplu olarak indirirseniz, açıkça Müzik korsanlığı yapıyor ve Müziğin yaratıcı toplumuna zarar veriyorsunuz demektir. Umarım bunun farkındasınızdır. Her zaman, Sanatçının sıkı çalışmasına saygı duymaya ve desteklemeye çalışın", - "download_ip_ban_warning": "Bu arada, IP'niz normalden daha fazla indirme isteği nedeniyle YouTube'da engellenebilir. IP engelleme, YouTube'u (oturum açmış olsanız bile) o IP cihazından en az 2 -3 ay kullanamayacağınız anlamına gelir. Ve eğer böyle bir şey olursa Spotube'un hiçbir sorumluluğu yok", + "download_warning": "Tüm şarkıları toplu olarak indiriyorsanız, açıkça müzik korsanlığı yapıyorsunuz ve müzik dünyasının yaratıcı topluluğuna zarar veriyorsunuz demektir. Umuyorum bunun farkındasınızdır. Her zaman, sanatçıların emeğine saygı göstermeyi ve desteklemeyi deneyin.", + "download_ip_ban_warning": "Ayrıca, normalden fazla indirme istekleri nedeniyle YouTube'da IP'niz engellenebilir. IP engeli, en az 2-3 ay boyunca YouTube'u (hatta oturum açmış olsanız bile) o IP cihazından kullanamayacağınız anlamına gelir. Ve eğer böyle bir durum yaşanırsa, Spotube bundan hiçbir sorumluluk kabul etmez.", "by_clicking_accept_terms": "\"Kabul et\" e tıklayarak aşağıdaki şartları kabul etmiş olursunuz:", - "download_agreement_1": "Müzik korsanlığı yaptığımı biliyorum. Ben kötüyüm", + "download_agreement_1": "Müzik korsanlığı yaptığımı biliyorum. Ben fakir biriyim.", "download_agreement_2": "Sanatçıyı elimden geldiğince destekleyeceğim ve bunu sadece sanatını satın alacak param olmadığı için yapıyorum", - "download_agreement_3": "IP adresimin YouTube'da engellenebileceğinin tamamen farkındayım ve mevcut işlemimden kaynaklanan herhangi bir kazadan Spotube'u veya sahiplerini/katkıda bulunanlarını sorumlu tutmuyorum", + "download_agreement_3": "YouTube'da IP'min engellenebileceğinin tamamen farkındayım ve mevcut eylemlerimden kaynaklanan herhangi bir kaza için Spotube'u veya sahiplerini/katkıda bulunanları sorumlu tutmuyorum.", "decline": "Reddet", "accept": "Kabul et", "details": "Detaylar", "youtube": "YouTube", "channel": "Kanal", - "likes": "Beğeniler", + "likes": "Beğenenler", "dislikes": "Beğenmeyenler", "views": "İzlenmeler", "streamUrl": "Akış bağlantısı", "stop": "Durdur", - "sort_newest": "En yeniye göre sırala", - "sort_oldest": "Eklenen en eskiye göre sırala", + "sort_newest": "En yeni eklenene göre sırala.", + "sort_oldest": "En eski eklenene göre sırala", "sleep_timer": "Uyku Zamanlayıcısı", "mins": "{minutes} Dakika", "hours": "{hours} Saatler", @@ -251,11 +251,11 @@ "logs": "Günlükler", "developers": "Geliştiriciler", "not_logged_in": "Giriş yapmadınız", - "search_mode": "Arama Modu", - "audio_source": "Ses Kaynağı", + "search_mode": "Arama modu", + "audio_source": "Ses kaynağı", "ok": "Tamam", "failed_to_encrypt": "Şifreleme başarısız oldu", - "encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde saklamak için şifreleme kullanır. Ama başaramadı. Bu yüzden güvensiz depolamaya geri dönecek\nLinux kullanıyorsanız, lütfen herhangi bir gizli servisin (gnome - anahtarlık, kde - cüzdan, keepassxc vb.) yüklü olduğundan emin olun", + "encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde depolamak için şifreleme kullanır. Ancak bunu başaramadı. Bu nedenle, güvensiz depolamaya geri dönecektir\nLinux kullanıyorsanız, lütfen gnome-keyring, kde-wallet, keepassxc vb. herhangi bir gizli servisin yüklü olduğundan emin olun.", "querying_info": "Bilgi sorgulanıyor...", "piped_api_down": "Piped API kapalı", "piped_down_error_instructions": "Piped örneği {pipedInstance} şu anda kapalı\n\nÖrneği değiştirin veya 'API türünü' resmi YouTube API'si olarak değiştirin\n\nDeğişiklikten sonra uygulamayı yeniden başlattığınızdan emin olun", @@ -263,8 +263,8 @@ "connection_restored": "İnternet bağlantınız geri yüklendi", "use_system_title_bar": "Sistem başlık çubuğunu kullan", "crunching_results": "Sonuçlar...", - "search_to_get_results": "Sonuç almak için ara", - "use_amoled_mode": "AMOLED Modunu Kullan", + "search_to_get_results": "Sonuç almak için arayın", + "use_amoled_mode": "AMOLED modu kullan", "pitch_dark_theme": "Zifiri karanlık koyu tema", "normalize_audio": "Sesi normalleştir", "change_cover": "Kapağı değiştir", @@ -277,48 +277,48 @@ "disconnect_lastfm": "Last.fm bağlantısını kes", "disconnect": "Bağlantıyı kes", "username": "Kullanıcı adı", - "password": "Parola", - "login": "Giriş", + "password": "Şifre", + "login": "Giriş yap", "login_with_your_lastfm": "Last.fm hesabınızla giriş yapın", "scrobble_to_lastfm": "Last.fm için Scrobble", - "go_to_album": "Albüme Git", - "discord_rich_presence": "Discord Zengin Varlığı", - "browse_all": "Tümüne Göz At", - "genres": "Müzik Türleri", - "explore_genres": "Türleri Keşfet", + "go_to_album": "Albüme git", + "discord_rich_presence": "Discord zengin varlığı", + "browse_all": "Tümüne göz at", + "genres": "Müzik türleri", + "explore_genres": "Türleri keşfet", "friends": "Arkadaşlar", "no_lyrics_available": "Üzgünüz, bu parçanın sözleri bulunamıyor", - "start_a_radio": "Radyo Başlat", + "start_a_radio": "Radyo başlat", "how_to_start_radio": "Radyoyu nasıl başlatmak istersiniz?", "replace_queue_question": "Mevcut kuyruğu değiştirmek mi yoksa eklemek mi istersiniz?", - "endless_playback": "Sonsuz Olarak Oynat", - "delete_playlist": "Oynatma Listesini Sil", + "endless_playback": "Sonsuz olarak oynat", + "delete_playlist": "Oynatma listesini sil", "delete_playlist_confirmation": "Bu oynatma listesini silmek istediğinizden emin misiniz?", - "local_tracks": "Yerel Parçalar", - "song_link": "Şarkı Bağlantısı", + "local_tracks": "Yerel parçalar", + "song_link": "Şarkı bağlantısı", "skip_this_nonsense": "Bu saçmalığı atla", - "freedom_of_music": "“Müzik Özgürlüğü”", - "freedom_of_music_palm": "“Müzik Özgürlüğü avucunuzun içinde”", + "freedom_of_music": "“Müzik özgürlüğü”", + "freedom_of_music_palm": "“Müzik özgürlüğü avucunuzun içinde”", "get_started": "Haydi başlayalım", "youtube_source_description": "Tavsiye edilir ve en iyi şekilde çalışır.", - "piped_source_description": "Özgür hissediyor musunuz? YouTube ile aynı ama çok daha fazla ücretsiz.", + "piped_source_description": "Özgür hissediyor musunuz? YouTube ile aynı, ama çok daha özgür.", "jiosaavn_source_description": "Güney Asya bölgesi için en iyisi.", - "highest_quality": "En Yüksek Kalite: {quality}", - "select_audio_source": "Ses Kaynağını Seç", - "endless_playback_description": "Yeni şarkıları otomatik olarak \nkuyruğun sonuna ekle", + "highest_quality": "En yüksek kalite: {quality}", + "select_audio_source": "Ses kaynağını seçin", + "endless_playback_description": "Yeni şarkıları otomatik olarak\nkuyruğun sonuna ekle", "choose_your_region": "Bölgenizi seçin", - "choose_your_region_description": "Bu, Spotube'un size doğru içeriği göstermesine yardımcı olacaktır\nkonumunuz için.", + "choose_your_region_description": "Bu, Spotube'un konumunuza uygun içerikleri göstermesine yardımcı olacaktır.", "choose_your_language": "Dilinizi seçin", - "help_project_grow": "Bu projenin büyümesine yardımcı ol", + "help_project_grow": "Bu projenin büyümesine yardımcı olun", "help_project_grow_description": "Spotube açık kaynaklı bir projedir. Projeye katkıda bulunarak, hataları bildirerek veya yeni özellikler önererek bu projenin büyümesine yardımcı olabilirsiniz.", - "contribute_on_github": "GitHub'a katkıda bulunun", - "donate_on_open_collective": "Open Collective'e bağış yap", - "browse_anonymously": "Anonim Olarak Göz at", - "enable_connect": "Bağlantıyı Etkinleştir", + "contribute_on_github": "GitHub'da katkıda bulun", + "donate_on_open_collective": "Open Collective'de bağış yap", + "browse_anonymously": "Anonim olarak giriş yap", + "enable_connect": "Bağlanmayı etkinleştir", "enable_connect_description": "Spotube'u diğer cihazlardan kontrol edin", "devices": "Cihazlar", "select": "Seç", "connect_client_alert": "{client} tarafından kontrol ediliyorsun.", - "this_device": "Bu Cihaz", + "this_device": "Bu cihaz", "remote": "Yönet" } \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index a0fca998f..ebdc4b618 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -7,7 +7,7 @@ /// TexturedPolak@github => Polish /// yuri-val@github => Ukrainian /// energywave@github, ncvescera@github, OpenCode@github => Italian -/// mdksec@github, mikropsoft@github => Turkish +/// mikropsoft@github => Turkish /// Stephan-P@github, SecularSteve@github => Dutch /// doannc2212@github => Vietnamese /// sappho192@github => Korean From 8fad2251b3536e9468e0fb193939ead98bad3bc6 Mon Sep 17 00:00:00 2001 From: Akash Pattnaik Date: Fri, 10 May 2024 22:46:10 +0530 Subject: [PATCH 22/57] feat(player): add volume slider floating label showing percentage (#1445) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * add volume level tooltip in volume_slider --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho --- lib/collections/env.dart | 2 +- lib/components/player/volume_slider.dart | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 89a777b64..df45cee91 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -41,4 +41,4 @@ abstract class Env { kIsFlatpak || _enableUpdateChecker == "1"; static String discordAppId = "1176718791388975124"; -} +} \ No newline at end of file diff --git a/lib/components/player/volume_slider.dart b/lib/components/player/volume_slider.dart index 102bbef6e..8483143b2 100644 --- a/lib/components/player/volume_slider.dart +++ b/lib/components/player/volume_slider.dart @@ -1,6 +1,5 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; - import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -31,11 +30,17 @@ class VolumeSlider extends HookConsumerWidget { } } }, - child: Slider( - min: 0, - max: 1, - value: value, - onChanged: onChanged, + child: SliderTheme( + data: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + ), + child: Slider( + min: 0, + max: 1, + label: (value * 100).toStringAsFixed(0), + value: value, + onChanged: onChanged, + ), ), ); return Row( From 9aea35468fa7cd176ddc8810b37b90c2d8246931 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 23 May 2024 15:13:02 +0600 Subject: [PATCH 23/57] fix: fallback to LRCLIB when lyrics line less than 6 lines #1461 --- lib/provider/spotify/lyrics/synced.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index 6ce74ae79..066596a95 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -127,7 +127,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier final token = await spotify.getCredentials(); SubtitleSimple lyrics = await getSpotifyLyrics(token.accessToken); - if (lyrics.lyrics.isEmpty) { + if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) { lyrics = await getLRCLibLyrics(); } From 22caa818f4ac31626aaff6952e43512b42237d00 Mon Sep 17 00:00:00 2001 From: Blake Leonard Date: Thu, 23 May 2024 05:18:01 -0400 Subject: [PATCH 24/57] feat: Local music library (#1479) * feat: add one additional library folder This folder just doesn't get downloaded to. I think I'm going to rework it so that it can be multiple folders, but I'm going to commit my progress so far anyway. Signed-off-by: Blake Leonard * chore: update dependencies so that it builds I'm not sure if this breaks CI or something, but I couldn't build it locally to test my changes, so I made these changes and it builds again. Signed-off-by: Blake Leonard * feat: index multiple folders of local music If you used a previous commit from this branch, this is a breaking change, because it changes the type of a configuration field. but since this is still in development, it should be fine. Signed-off-by: Blake Leonard * refactor: manage local library in local tracks tab This also refactors the list to use slivers instead. That's the easiest way to have multiple scrolling lists here... The console keeps getting spammed with some intermediate layout error but I can't hold it long enough to figure out what's causing it. Signed-off-by: Blake Leonard * refactor: use folder add/remove icons in library Signed-off-by: Blake Leonard * refactor: remove redundant settings page Signed-off-by: Blake Leonard * refactor: rename "Local Tracks" to just "Local" Not sure if this would be the recommended way to do it... Signed-off-by: Blake Leonard * fix: console spam about useless Expanded Signed-off-by: Blake Leonard * chore: remove completed TODO Signed-off-by: Blake Leonard * chore: use new Platform constants; regenerate plugins Signed-off-by: Blake Leonard * refactor: put local libraries on separate pages Signed-off-by: Blake Leonard --------- Signed-off-by: Blake Leonard --- lib/collections/routes.dart | 12 + lib/collections/spotube_icons.dart | 2 + lib/components/library/user_local_tracks.dart | 356 +++++++----------- lib/l10n/app_en.arb | 6 +- lib/pages/library/library.dart | 2 +- lib/pages/library/local_folder.dart | 236 ++++++++++++ lib/pages/settings/sections/downloads.dart | 1 + .../user_preferences_provider.dart | 5 + .../user_preferences_state.dart | 1 + .../user_preferences_state.freezed.dart | 35 +- .../user_preferences_state.g.dart | 5 + linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 20 +- pubspec.yaml | 11 + untranslated_messages.json | 156 +++++++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 19 files changed, 621 insertions(+), 238 deletions(-) create mode 100644 lib/pages/library/local_folder.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 080cbd8a2..340b816ab 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -14,6 +14,7 @@ import 'package:spotube/pages/home/genres/genre_playlists.dart'; import 'package:spotube/pages/home/genres/genres.dart'; import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/lastfm_login/lastfm_login.dart'; +import 'package:spotube/pages/library/local_folder.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart'; import 'package:spotube/pages/lyrics/mini_lyrics.dart'; @@ -113,6 +114,17 @@ final routerProvider = Provider((ref) { ), ), ]), + GoRoute( + path: "local", + pageBuilder: (context, state) { + assert(state.extra is String); + return SpotubePage( + child: LocalLibraryPage(state.extra as String, + isDownloads: state.uri.queryParameters["downloads"] != null + ), + ); + }, + ), ]), GoRoute( path: "/lyrics", diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 6de212840..2da09f528 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -121,4 +121,6 @@ abstract class SpotubeIcons { static const monitor = FeatherIcons.monitor; static const power = FeatherIcons.power; static const bluetooth = FeatherIcons.bluetooth; + static const folderAdd = FeatherIcons.folderPlus; + static const folderRemove = FeatherIcons.folderMinus; } diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index a7b2102b8..d5115aaaa 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -1,11 +1,14 @@ import 'dart:io'; import 'package:catcher_2/catcher_2.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:mime/mime.dart'; @@ -27,6 +30,7 @@ import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; // ignore: depend_on_referenced_packages import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; @@ -59,116 +63,125 @@ enum SortBy { album, } -final localTracksProvider = FutureProvider>((ref) async { +final localTracksProvider = FutureProvider>>((ref) async { try { - if (kIsWeb) return []; + if (kIsWeb) return {}; + final Map> tracks = {}; + final downloadLocation = ref.watch( userPreferencesProvider.select((s) => s.downloadLocation), ); - if (downloadLocation.isEmpty) return []; final downloadDir = Directory(downloadLocation); if (!await downloadDir.exists()) { await downloadDir.create(recursive: true); - return []; } - final entities = downloadDir.listSync(recursive: true); - - final filesWithMetadata = (await Future.wait( - entities.map((e) => File(e.path)).where((file) { - final mimetype = lookupMimeType(file.path); - return mimetype != null && supportedAudioTypes.contains(mimetype); - }).map( - (file) async { - try { - final metadata = await MetadataGod.readMetadata(file: file.path); + final localLibraryLocations = ref.watch( + userPreferencesProvider.select((s) => s.localLibraryLocation), + ); - final imageFile = File(join( - (await getTemporaryDirectory()).path, - "spotube", - basenameWithoutExtension(file.path) + - imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, - )); - if (!await imageFile.exists() && metadata.picture != null) { - await imageFile.create(recursive: true); - await imageFile.writeAsBytes( - metadata.picture?.data ?? [], - mode: FileMode.writeOnly, - ); - } + for (var location in [downloadLocation, ...localLibraryLocations]) { + if (location.isEmpty) continue; + final entities = []; + final dir = Directory(location); + if (await Directory(location).exists()) { + entities.addAll(Directory(location).listSync(recursive: true)); + } + + final filesWithMetadata = (await Future.wait( + entities.map((e) => File(e.path)).where((file) { + final mimetype = lookupMimeType(file.path); + return mimetype != null && supportedAudioTypes.contains(mimetype); + }).map( + (file) async { + try { + final metadata = await MetadataGod.readMetadata(file: file.path); + + final imageFile = File(join( + (await getTemporaryDirectory()).path, + "spotube", + basenameWithoutExtension(file.path) + + imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, + )); + if (!await imageFile.exists() && metadata.picture != null) { + await imageFile.create(recursive: true); + await imageFile.writeAsBytes( + metadata.picture?.data ?? [], + mode: FileMode.writeOnly, + ); + } - return {"metadata": metadata, "file": file, "art": imageFile.path}; - } catch (e, stack) { - if (e is FfiException) { - return {"file": file}; + return {"metadata": metadata, "file": file, "art": imageFile.path}; + } catch (e, stack) { + if (e is FfiException) { + return {"file": file}; + } + Catcher2.reportCheckedError(e, stack); + return {}; } - Catcher2.reportCheckedError(e, stack); - return {}; - } - }, - ), - )) - .where((e) => e.isNotEmpty) - .toList(); - - final tracks = filesWithMetadata - .map( - (fileWithMetadata) => LocalTrack.fromTrack( - track: Track().fromFile( - fileWithMetadata["file"], - metadata: fileWithMetadata["metadata"], - art: fileWithMetadata["art"], + }, + ), + )) + .where((e) => e.isNotEmpty) + .toList(); + + // ignore: no_leading_underscores_for_local_identifiers + final _tracks = filesWithMetadata + .map( + (fileWithMetadata) => LocalTrack.fromTrack( + track: Track().fromFile( + fileWithMetadata["file"], + metadata: fileWithMetadata["metadata"], + art: fileWithMetadata["art"], + ), + path: fileWithMetadata["file"].path, ), - path: fileWithMetadata["file"].path, - ), - ) - .toList(); + ) + .toList(); + tracks[location] = _tracks; + } return tracks; } catch (e, stack) { Catcher2.reportCheckedError(e, stack); - return []; + return {}; } }); class UserLocalTracks extends HookConsumerWidget { const UserLocalTracks({super.key}); - Future playLocalTracks( - WidgetRef ref, - List tracks, { - LocalTrack? currentTrack, - }) async { - final playlist = ref.read(proxyPlaylistProvider); - final playback = ref.read(proxyPlaylistProvider.notifier); - currentTrack ??= tracks.first; - final isPlaylistPlaying = playlist.containsTracks(tracks); - if (!isPlaylistPlaying) { - await playback.load( - tracks, - initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id), - autoPlay: true, - ); - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playlist.activeTrack?.id) { - await playback.jumpToTrack(currentTrack); - } - } - @override Widget build(BuildContext context, ref) { - final sortBy = useState(SortBy.none); - final playlist = ref.watch(proxyPlaylistProvider); - final trackSnapshot = ref.watch(localTracksProvider); - final isPlaylistPlaying = - playlist.containsTracks(trackSnapshot.asData?.value ?? []); - - final searchController = useTextEditingController(); - useValueListenable(searchController); - final searchFocus = useFocusNode(); - final isFiltering = useState(false); - final controller = useScrollController(); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); + final preferences = ref.watch(userPreferencesProvider); + + final addLocalLibraryLocation = useCallback(() async { + if (kIsMobile || kIsMacOS) { + final dirStr = await FilePicker.platform.getDirectoryPath( + initialDirectory: preferences.downloadLocation, + ); + if (dirStr == null) return; + if (preferences.localLibraryLocation.contains(dirStr)) return; + preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]); + } else { + String? dirStr = await getDirectoryPath( + initialDirectory: preferences.downloadLocation, + ); + if (dirStr == null) return; + if (preferences.localLibraryLocation.contains(dirStr)) return; + preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]); + } + }, [preferences.localLibraryLocation]); + + final removeLocalLibraryLocation = useCallback((String location) { + if (!preferences.localLibraryLocation.contains(location)) return; + preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation]..remove(location)); + }, [preferences.localLibraryLocation]); + + // This is just to pre-load the tracks. + // For now, this gets all of them. + ref.watch(localTracksProvider); return Column( children: [ @@ -177,155 +190,42 @@ class UserLocalTracks extends HookConsumerWidget { child: Row( children: [ const SizedBox(width: 5), - FilledButton( - onPressed: trackSnapshot.asData?.value != null - ? () async { - if (trackSnapshot.asData?.value.isNotEmpty == true) { - if (!isPlaylistPlaying) { - await playLocalTracks( - ref, - trackSnapshot.asData!.value, - ); - } - } - } - : null, - child: Row( - children: [ - Text(context.l10n.play), - Icon( - isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, - ) - ], - ), - ), - const Spacer(), - ExpandableSearchButton( - isFiltering: isFiltering.value, - onPressed: (value) => isFiltering.value = value, - searchFocus: searchFocus, - ), - const SizedBox(width: 10), - SortTracksDropdown( - value: sortBy.value, - onChanged: (value) { - sortBy.value = value; - }, - ), - const SizedBox(width: 5), - FilledButton( - child: const Icon(SpotubeIcons.refresh), - onPressed: () { - ref.invalidate(localTracksProvider); - }, + TextButton.icon( + icon: const Icon(SpotubeIcons.folderAdd), + label: Text(context.l10n.add_library_location), + onPressed: addLocalLibraryLocation, ) - ], - ), + ] + ) ), - ExpandableSearchField( - searchController: searchController, - searchFocus: searchFocus, - isFiltering: isFiltering.value, - onChangeFiltering: (value) => isFiltering.value = value, - ), - trackSnapshot.when( - data: (tracks) { - final sortedTracks = useMemoized(() { - return ServiceUtils.sortTracks(tracks, sortBy.value); - }, [sortBy.value, tracks]); - - final filteredTracks = useMemoized(() { - if (searchController.text.isEmpty) { - return sortedTracks; + Expanded( + child: ListView.builder( + itemCount: preferences.localLibraryLocation.length+1, + itemBuilder: (context, index) { + late final String location; + if (index == 0) { + location = preferences.downloadLocation; + } else { + location = preferences.localLibraryLocation[index-1]; } - return sortedTracks - .map((e) => ( - weightedRatio( - "${e.name} - ${e.artists?.asString() ?? ""}", - searchController.text, - ), - e, - )) - .toList() - .sorted( - (a, b) => b.$1.compareTo(a.$1), - ) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList() - .toList(); - }, [searchController.text, sortedTracks]); - - if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { - return const Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [NotFound()], - ), + return ListTile( + title: preferences.downloadLocation != location ? Text(location) + : Text(context.l10n.downloads), + trailing: preferences.downloadLocation != location ? Tooltip( + message: context.l10n.remove_library_location, + child: IconButton( + icon: Icon(SpotubeIcons.folderRemove, color: Colors.red[400]), + onPressed: () => removeLocalLibraryLocation(location), + ), + ) : null, + onTap: () async { + context.go("/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", extra: location); + } ); } - - return Expanded( - child: RefreshIndicator( - onRefresh: () async { - ref.invalidate(localTracksProvider); - }, - child: InterScrollbar( - controller: controller, - child: Skeletonizer( - enabled: trackSnapshot.isLoading, - child: ListView.builder( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: - trackSnapshot.isLoading ? 5 : filteredTracks.length, - itemBuilder: (context, index) { - if (trackSnapshot.isLoading) { - return TrackTile( - playlist: playlist, - track: FakeData.track, - index: index, - ); - } - - final track = filteredTracks[index]; - return TrackTile( - index: index, - playlist: playlist, - track: track, - userPlaylist: false, - onTap: () async { - await playLocalTracks( - ref, - sortedTracks, - currentTrack: track, - ); - }, - ); - }, - ), - ), - ), - ), - ); - }, - loading: () => Expanded( - child: Skeletonizer( - enabled: true, - child: ListView.builder( - itemCount: 5, - itemBuilder: (context, index) => TrackTile( - track: FakeData.track, - index: index, - playlist: playlist, - ), - ), - ), ), - error: (error, stackTrace) => - Text(error.toString() + stackTrace.toString()), - ) - ], + ), + ] ); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 832862c01..a90fd35e9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -107,6 +107,9 @@ "always_on_top": "Always on top", "exit_mini_player": "Exit Mini player", "download_location": "Download location", + "local_library": "Local library", + "add_library_location": "Add to library", + "remove_library_location": "Remove from library", "account": "Account", "login_with_spotify": "Login with your Spotify account", "connect_with_spotify": "Connect with Spotify", @@ -295,6 +298,7 @@ "delete_playlist": "Delete Playlist", "delete_playlist_confirmation": "Are you sure you want to delete this playlist?", "local_tracks": "Local Tracks", + "local_tab": "Local", "song_link": "Song Link", "skip_this_nonsense": "Skip this nonsense", "freedom_of_music": "“Freedom of Music”", @@ -321,4 +325,4 @@ "connect_client_alert": "You're being controlled by {client}", "this_device": "This Device", "remote": "Remote" -} \ No newline at end of file +} diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index ccdb6a352..eff30348f 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -27,7 +27,7 @@ class LibraryPage extends HookConsumerWidget { leading: ThemedButtonsTabBar( tabs: [ Tab(text: " ${context.l10n.playlists} "), - Tab(text: " ${context.l10n.local_tracks} "), + Tab(text: " ${context.l10n.local_tab} "), Tab( child: Badge( isLabelVisible: downloadingCount > 0, diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart new file mode 100644 index 000000000..89d70e09c --- /dev/null +++ b/lib/pages/library/local_folder.dart @@ -0,0 +1,236 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; +import 'package:spotube/components/shared/fallbacks/not_found.dart'; +import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; +import 'package:spotube/components/shared/track_tile/track_tile.dart'; +import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/utils/service_utils.dart'; + +class LocalLibraryPage extends HookConsumerWidget { + final String location; + final bool isDownloads; + const LocalLibraryPage(this.location, {super.key, this.isDownloads = false}); + + Future playLocalTracks( + WidgetRef ref, + List tracks, { + LocalTrack? currentTrack, + }) async { + final playlist = ref.read(proxyPlaylistProvider); + final playback = ref.read(proxyPlaylistProvider.notifier); + currentTrack ??= tracks.first; + final isPlaylistPlaying = playlist.containsTracks(tracks); + if (!isPlaylistPlaying) { + await playback.load( + tracks, + initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id), + autoPlay: true, + ); + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playlist.activeTrack?.id) { + await playback.jumpToTrack(currentTrack); + } + } + + @override + Widget build(BuildContext context, ref) { + final sortBy = useState(SortBy.none); + final playlist = ref.watch(proxyPlaylistProvider); + final trackSnapshot = ref.watch(localTracksProvider); + final isPlaylistPlaying = + playlist.containsTracks(trackSnapshot.asData?.value.values.flattened.toList() ?? []); + + final searchController = useTextEditingController(); + useValueListenable(searchController); + final searchFocus = useFocusNode(); + final isFiltering = useState(false); + + final controller = useScrollController(); + + return SafeArea( + bottom: false, + child: Scaffold( + appBar: PageWindowTitleBar( + leading: const BackButton(), + centerTitle: true, + title: Text(isDownloads ? context.l10n.downloads : location), + backgroundColor: Colors.transparent, + ), + extendBodyBehindAppBar: true, + body: Column( + children: [ + const SizedBox(height: 56), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const SizedBox(width: 5), + FilledButton( + onPressed: trackSnapshot.asData?.value != null + ? () async { + if (trackSnapshot.asData?.value.isNotEmpty == true) { + if (!isPlaylistPlaying) { + await playLocalTracks( + ref, + trackSnapshot.asData!.value[location] ?? [], + ); + } + } + } + : null, + child: Row( + children: [ + Text(context.l10n.play), + Icon( + isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, + ) + ], + ), + ), + const Spacer(), + ExpandableSearchButton( + isFiltering: isFiltering.value, + onPressed: (value) => isFiltering.value = value, + searchFocus: searchFocus, + ), + const SizedBox(width: 10), + SortTracksDropdown( + value: sortBy.value, + onChanged: (value) { + sortBy.value = value; + }, + ), + const SizedBox(width: 5), + FilledButton( + child: const Icon(SpotubeIcons.refresh), + onPressed: () { + ref.invalidate(localTracksProvider); + }, + ) + ], + ), + ), + ExpandableSearchField( + searchController: searchController, + searchFocus: searchFocus, + isFiltering: isFiltering.value, + onChangeFiltering: (value) => isFiltering.value = value, + ), + trackSnapshot.when( + data: (tracks) { + final sortedTracks = useMemoized(() { + return ServiceUtils.sortTracks(tracks[location] ?? [], sortBy.value); + }, [sortBy.value, tracks]); + + final filteredTracks = useMemoized(() { + if (searchController.text.isEmpty) { + return sortedTracks; + } + return sortedTracks + .map((e) => ( + weightedRatio( + "${e.name} - ${e.artists?.asString() ?? ""}", + searchController.text, + ), + e, + )) + .toList() + .sorted( + (a, b) => b.$1.compareTo(a.$1), + ) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList() + .toList(); + }, [searchController.text, sortedTracks]); + + if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { + return const Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [NotFound()], + ), + ); + } + + return Expanded( + child: RefreshIndicator( + onRefresh: () async { + ref.invalidate(localTracksProvider); + }, + child: InterScrollbar( + controller: controller, + child: Skeletonizer( + enabled: trackSnapshot.isLoading, + child: ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: + trackSnapshot.isLoading ? 5 : filteredTracks.length, + itemBuilder: (context, index) { + if (trackSnapshot.isLoading) { + return TrackTile( + playlist: playlist, + track: FakeData.track, + index: index, + ); + } + + final track = filteredTracks[index]; + return TrackTile( + index: index, + playlist: playlist, + track: track, + userPlaylist: false, + onTap: () async { + await playLocalTracks( + ref, + sortedTracks, + currentTrack: track, + ); + }, + ); + }, + ), + ), + ), + ), + ); + }, + loading: () => Expanded( + child: Skeletonizer( + enabled: true, + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) => TrackTile( + track: FakeData.track, + index: index, + playlist: playlist, + ), + ), + ), + ), + error: (error, stackTrace) => + Text(error.toString() + stackTrace.toString()), + ) + ], + ) + ), + ); + } +} diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index 76ef8e3e9..3092ed03f 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -3,6 +3,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index a537038e2..d34586f33 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -69,6 +69,11 @@ class UserPreferencesNotifier extends PersistedStateNotifier { state = state.copyWith(downloadLocation: downloadDir); } + void setLocalLibraryLocation(List localLibraryDirs) { + //if (localLibraryDir.isEmpty) return; + state = state.copyWith(localLibraryLocation: localLibraryDirs); + } + void setLayoutMode(LayoutMode mode) { state = state.copyWith(layoutMode: mode); } diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index 67eb18a24..56f66375e 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -84,6 +84,7 @@ class UserPreferences with _$UserPreferences { @Default(Market.US) Market recommendationMarket, @Default(SearchMode.youtube) SearchMode searchMode, @Default("") String downloadLocation, + @Default([]) List localLibraryLocation, @Default("https://pipedapi.kavin.rocks") String pipedInstance, @Default(ThemeMode.system) ThemeMode themeMode, @Default(AudioSource.youtube) AudioSource audioSource, diff --git a/lib/provider/user_preferences/user_preferences_state.freezed.dart b/lib/provider/user_preferences/user_preferences_state.freezed.dart index 94015d37d..89c7210a3 100644 --- a/lib/provider/user_preferences/user_preferences_state.freezed.dart +++ b/lib/provider/user_preferences/user_preferences_state.freezed.dart @@ -43,6 +43,7 @@ mixin _$UserPreferences { Market get recommendationMarket => throw _privateConstructorUsedError; SearchMode get searchMode => throw _privateConstructorUsedError; String get downloadLocation => throw _privateConstructorUsedError; + List get localLibraryLocation => throw _privateConstructorUsedError; String get pipedInstance => throw _privateConstructorUsedError; ThemeMode get themeMode => throw _privateConstructorUsedError; AudioSource get audioSource => throw _privateConstructorUsedError; @@ -88,6 +89,7 @@ abstract class $UserPreferencesCopyWith<$Res> { Market recommendationMarket, SearchMode searchMode, String downloadLocation, + List localLibraryLocation, String pipedInstance, ThemeMode themeMode, AudioSource audioSource, @@ -126,6 +128,7 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> Object? recommendationMarket = null, Object? searchMode = null, Object? downloadLocation = null, + Object? localLibraryLocation = null, Object? pipedInstance = null, Object? themeMode = null, Object? audioSource = null, @@ -196,6 +199,10 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> ? _value.downloadLocation : downloadLocation // ignore: cast_nullable_to_non_nullable as String, + localLibraryLocation: null == localLibraryLocation + ? _value.localLibraryLocation + : localLibraryLocation // ignore: cast_nullable_to_non_nullable + as List, pipedInstance: null == pipedInstance ? _value.pipedInstance : pipedInstance // ignore: cast_nullable_to_non_nullable @@ -264,6 +271,7 @@ abstract class _$$UserPreferencesImplCopyWith<$Res> Market recommendationMarket, SearchMode searchMode, String downloadLocation, + List localLibraryLocation, String pipedInstance, ThemeMode themeMode, AudioSource audioSource, @@ -300,6 +308,7 @@ class __$$UserPreferencesImplCopyWithImpl<$Res> Object? recommendationMarket = null, Object? searchMode = null, Object? downloadLocation = null, + Object? localLibraryLocation = null, Object? pipedInstance = null, Object? themeMode = null, Object? audioSource = null, @@ -370,6 +379,10 @@ class __$$UserPreferencesImplCopyWithImpl<$Res> ? _value.downloadLocation : downloadLocation // ignore: cast_nullable_to_non_nullable as String, + localLibraryLocation: null == localLibraryLocation + ? _value._localLibraryLocation + : localLibraryLocation // ignore: cast_nullable_to_non_nullable + as List, pipedInstance: null == pipedInstance ? _value.pipedInstance : pipedInstance // ignore: cast_nullable_to_non_nullable @@ -433,6 +446,7 @@ class _$UserPreferencesImpl implements _UserPreferences { this.recommendationMarket = Market.US, this.searchMode = SearchMode.youtube, this.downloadLocation = "", + final List localLibraryLocation = const [], this.pipedInstance = "https://pipedapi.kavin.rocks", this.themeMode = ThemeMode.system, this.audioSource = AudioSource.youtube, @@ -440,7 +454,8 @@ class _$UserPreferencesImpl implements _UserPreferences { this.downloadMusicCodec = SourceCodecs.m4a, this.discordPresence = true, this.endlessPlayback = true, - this.enableConnect = false}); + this.enableConnect = false}) + : _localLibraryLocation = localLibraryLocation; factory _$UserPreferencesImpl.fromJson(Map json) => _$$UserPreferencesImplFromJson(json); @@ -496,6 +511,16 @@ class _$UserPreferencesImpl implements _UserPreferences { @override @JsonKey() final String downloadLocation; + final List _localLibraryLocation; + @override + @JsonKey() + List get localLibraryLocation { + if (_localLibraryLocation is EqualUnmodifiableListView) + return _localLibraryLocation; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_localLibraryLocation); + } + @override @JsonKey() final String pipedInstance; @@ -523,7 +548,7 @@ class _$UserPreferencesImpl implements _UserPreferences { @override String toString() { - return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; + return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; } @override @@ -560,6 +585,8 @@ class _$UserPreferencesImpl implements _UserPreferences { other.searchMode == searchMode) && (identical(other.downloadLocation, downloadLocation) || other.downloadLocation == downloadLocation) && + const DeepCollectionEquality() + .equals(other._localLibraryLocation, _localLibraryLocation) && (identical(other.pipedInstance, pipedInstance) || other.pipedInstance == pipedInstance) && (identical(other.themeMode, themeMode) || @@ -597,6 +624,7 @@ class _$UserPreferencesImpl implements _UserPreferences { recommendationMarket, searchMode, downloadLocation, + const DeepCollectionEquality().hash(_localLibraryLocation), pipedInstance, themeMode, audioSource, @@ -647,6 +675,7 @@ abstract class _UserPreferences implements UserPreferences { final Market recommendationMarket, final SearchMode searchMode, final String downloadLocation, + final List localLibraryLocation, final String pipedInstance, final ThemeMode themeMode, final AudioSource audioSource, @@ -698,6 +727,8 @@ abstract class _UserPreferences implements UserPreferences { @override String get downloadLocation; @override + List get localLibraryLocation; + @override String get pipedInstance; @override ThemeMode get themeMode; diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index 930b1dd17..95ed4b035 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -44,6 +44,10 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson( $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ?? SearchMode.youtube, downloadLocation: json['downloadLocation'] as String? ?? "", + localLibraryLocation: (json['localLibraryLocation'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], pipedInstance: json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks", themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? @@ -81,6 +85,7 @@ Map _$$UserPreferencesImplToJson( 'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!, 'searchMode': _$SearchModeEnumMap[instance.searchMode]!, 'downloadLocation': instance.downloadLocation, + 'localLibraryLocation': instance.localLibraryLocation, 'pipedInstance': instance.pipedInstance, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'audioSource': _$AudioSourceEnumMap[instance.audioSource]!, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 6dfdd7409..2f61edd64 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,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) system_tray_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin"); + system_tray_plugin_register_with_registrar(system_tray_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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 93ffd3e95..48c7e0cad 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_linux screen_retriever system_theme + system_tray tray_manager url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 84f393411..0057db144 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -21,6 +21,7 @@ import screen_retriever import shared_preferences_foundation import sqflite import system_theme +import system_tray import tray_manager import url_launcher_macos import window_manager @@ -43,6 +44,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) + SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index df623b9e1..61de3f25f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1455,12 +1455,13 @@ packages: source: hosted version: "1.0.9" media_kit_native_event_loop: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit_native_event_loop - sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e - url: "https://pub.dev" - source: hosted + path: media_kit_native_event_loop + ref: main + resolved-ref: "285f7919bbf4a7d89a62615b14a3766a171ad575" + url: "https://github.com/media-kit/media-kit" + source: git version: "1.0.8" menu_base: dependency: transitive @@ -2156,6 +2157,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + system_tray: + dependency: "direct overridden" + description: + path: "." + ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c + resolved-ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c + url: "https://github.com/antler119/system_tray" + source: git + version: "2.0.2" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7435e077c..dc60abf62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -150,6 +150,17 @@ dev_dependencies: dependency_overrides: uuid: ^4.4.0 + system_tray: + # TODO: remove this when flutter_desktop_tools gets updated + # to use [MenuItemBase] instead of [MenuItem] + git: + url: https://github.com/antler119/system_tray + ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c + media_kit_native_event_loop: # to fix "macro name must be an identifier" + git: + url: https://github.com/media-kit/media-kit + path: media_kit_native_event_loop + ref: main flutter: generate: true diff --git a/untranslated_messages.json b/untranslated_messages.json index 9e26dfeeb..91b751eb0 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1 +1,155 @@ -{} \ No newline at end of file +{ + "ar": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "bn": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "ca": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "cs": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "de": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "es": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "fa": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "fr": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "hi": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "it": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "ja": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "ko": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "ne": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "nl": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "pl": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "pt": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "ru": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "th": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "tr": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "uk": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "vi": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + + "zh": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ] +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 57542decb..f2dd97143 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); + SystemTrayPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemTrayPlugin")); TrayManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6a0c77230..f4e142808 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows screen_retriever system_theme + system_tray tray_manager url_launcher_windows window_manager From d82261cb25ece63f85af0e40216cf32dccdc9dd5 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 23 May 2024 16:56:52 +0600 Subject: [PATCH 25/57] fix: local track not showing up in queue --- lib/components/library/user_local_tracks.dart | 106 +++--- .../shared/track_tile/track_options.dart | 222 +++++++------ .../shared/track_tile/track_tile.dart | 23 +- lib/pages/library/local_folder.dart | 309 +++++++++--------- .../proxy_playlist/player_listeners.dart | 2 +- .../proxy_playlist/proxy_playlist.dart | 13 +- lib/services/audio_player/audio_player.dart | 11 +- untranslated_messages.json | 28 ++ 8 files changed, 382 insertions(+), 332 deletions(-) diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index d5115aaaa..ffaae0d92 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -6,32 +6,20 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:collection/collection.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/fallbacks/not_found.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; -import 'package:spotube/components/shared/track_tile/track_tile.dart'; -import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; -import 'package:spotube/utils/service_utils.dart'; // ignore: depend_on_referenced_packages import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; @@ -63,7 +51,8 @@ enum SortBy { album, } -final localTracksProvider = FutureProvider>>((ref) async { +final localTracksProvider = + FutureProvider>>((ref) async { try { if (kIsWeb) return {}; final Map> tracks = {}; @@ -82,7 +71,6 @@ final localTracksProvider = FutureProvider>>((ref) for (var location in [downloadLocation, ...localLibraryLocations]) { if (location.isEmpty) continue; final entities = []; - final dir = Directory(location); if (await Directory(location).exists()) { entities.addAll(Directory(location).listSync(recursive: true)); } @@ -110,7 +98,11 @@ final localTracksProvider = FutureProvider>>((ref) ); } - return {"metadata": metadata, "file": file, "art": imageFile.path}; + return { + "metadata": metadata, + "file": file, + "art": imageFile.path + }; } catch (e, stack) { if (e is FfiException) { return {"file": file}; @@ -152,7 +144,6 @@ class UserLocalTracks extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferences = ref.watch(userPreferencesProvider); @@ -163,69 +154,74 @@ class UserLocalTracks extends HookConsumerWidget { ); if (dirStr == null) return; if (preferences.localLibraryLocation.contains(dirStr)) return; - preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]); + preferencesNotifier.setLocalLibraryLocation( + [...preferences.localLibraryLocation, dirStr]); } else { String? dirStr = await getDirectoryPath( initialDirectory: preferences.downloadLocation, ); if (dirStr == null) return; if (preferences.localLibraryLocation.contains(dirStr)) return; - preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]); + preferencesNotifier.setLocalLibraryLocation( + [...preferences.localLibraryLocation, dirStr]); } }, [preferences.localLibraryLocation]); final removeLocalLibraryLocation = useCallback((String location) { if (!preferences.localLibraryLocation.contains(location)) return; - preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation]..remove(location)); + preferencesNotifier.setLocalLibraryLocation( + [...preferences.localLibraryLocation]..remove(location), + ); }, [preferences.localLibraryLocation]); // This is just to pre-load the tracks. // For now, this gets all of them. ref.watch(localTracksProvider); - return Column( - children: [ - Padding( + return Column(children: [ + Padding( padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const SizedBox(width: 5), - TextButton.icon( - icon: const Icon(SpotubeIcons.folderAdd), - label: Text(context.l10n.add_library_location), - onPressed: addLocalLibraryLocation, - ) - ] - ) - ), - Expanded( - child: ListView.builder( - itemCount: preferences.localLibraryLocation.length+1, + child: Row(children: [ + const SizedBox(width: 5), + TextButton.icon( + icon: const Icon(SpotubeIcons.folderAdd), + label: Text(context.l10n.add_library_location), + onPressed: addLocalLibraryLocation, + ) + ])), + Expanded( + child: ListView.builder( + itemCount: preferences.localLibraryLocation.length + 1, itemBuilder: (context, index) { late final String location; if (index == 0) { location = preferences.downloadLocation; } else { - location = preferences.localLibraryLocation[index-1]; + location = preferences.localLibraryLocation[index - 1]; } return ListTile( - title: preferences.downloadLocation != location ? Text(location) - : Text(context.l10n.downloads), - trailing: preferences.downloadLocation != location ? Tooltip( - message: context.l10n.remove_library_location, - child: IconButton( - icon: Icon(SpotubeIcons.folderRemove, color: Colors.red[400]), - onPressed: () => removeLocalLibraryLocation(location), - ), - ) : null, - onTap: () async { - context.go("/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", extra: location); - } - ); - } - ), - ), - ] - ); + title: preferences.downloadLocation != location + ? Text(location) + : Text(context.l10n.downloads), + trailing: preferences.downloadLocation != location + ? Tooltip( + message: context.l10n.remove_library_location, + child: IconButton( + icon: Icon(SpotubeIcons.folderRemove, + color: Colors.red[400]), + onPressed: () => + removeLocalLibraryLocation(location), + ), + ) + : null, + onTap: () async { + context.go( + "/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", + extra: location, + ); + }); + }), + ), + ]); } } diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index a9ec36b94..c917ebaa5 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -197,6 +197,8 @@ class TrackOptions extends HookConsumerWidget { return downloadManager.getProgressNotifier(spotubeTrack); }); + final isLocalTrack = track is LocalTrack; + final adaptivePopSheetList = AdaptivePopSheetList( onSelected: (value) async { switch (value) { @@ -314,118 +316,120 @@ class TrackOptions extends HookConsumerWidget { ), ), ], - children: switch (track.runtimeType) { - LocalTrack() => [ - PopSheetEntry( - value: TrackOptionValue.delete, - leading: const Icon(SpotubeIcons.trash), - title: Text(context.l10n.delete), - ) - ], - _ => [ - if (mediaQuery.smAndDown) - PopSheetEntry( - value: TrackOptionValue.album, - leading: const Icon(SpotubeIcons.album), - title: Text(context.l10n.go_to_album), - subtitle: Text(track.album!.name!), - ), - if (!playlist.containsTrack(track)) ...[ - PopSheetEntry( - value: TrackOptionValue.addToQueue, - leading: const Icon(SpotubeIcons.queueAdd), - title: Text(context.l10n.add_to_queue), - ), - PopSheetEntry( - value: TrackOptionValue.playNext, - leading: const Icon(SpotubeIcons.lightning), - title: Text(context.l10n.play_next), - ), - ] else - PopSheetEntry( - value: TrackOptionValue.removeFromQueue, - enabled: playlist.activeTrack?.id != track.id, - leading: const Icon(SpotubeIcons.queueRemove), - title: Text(context.l10n.remove_from_queue), - ), - if (me.asData?.value != null) - PopSheetEntry( - value: TrackOptionValue.favorite, - leading: favorites.isLiked - ? const Icon( - SpotubeIcons.heartFilled, - color: Colors.pink, - ) - : const Icon(SpotubeIcons.heart), - title: Text( - favorites.isLiked - ? context.l10n.remove_from_favorites - : context.l10n.save_as_favorite, - ), - ), - if (auth != null) ...[ - PopSheetEntry( - value: TrackOptionValue.startRadio, - leading: const Icon(SpotubeIcons.radio), - title: Text(context.l10n.start_a_radio), - ), - PopSheetEntry( - value: TrackOptionValue.addToPlaylist, - leading: const Icon(SpotubeIcons.playlistAdd), - title: Text(context.l10n.add_to_playlist), - ), - ], - if (userPlaylist && auth != null) - PopSheetEntry( - value: TrackOptionValue.removeFromPlaylist, - leading: const Icon(SpotubeIcons.removeFilled), - title: Text(context.l10n.remove_from_playlist), - ), - PopSheetEntry( - value: TrackOptionValue.download, - enabled: !isInQueue, - leading: isInQueue - ? HookBuilder(builder: (context) { - final progress = useListenable(progressNotifier!); - return CircularProgressIndicator( - value: progress.value, - ); - }) - : const Icon(SpotubeIcons.download), - title: Text(context.l10n.download_track), - ), - PopSheetEntry( - value: TrackOptionValue.blacklist, - leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: !isBlackListed ? Colors.red[400] : null, - textColor: !isBlackListed ? Colors.red[400] : null, - title: Text( - isBlackListed - ? context.l10n.remove_from_blacklist - : context.l10n.add_to_blacklist, - ), - ), - PopSheetEntry( - value: TrackOptionValue.share, - leading: const Icon(SpotubeIcons.share), - title: Text(context.l10n.share), + children: [ + if (isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.delete, + leading: const Icon(SpotubeIcons.trash), + title: Text(context.l10n.delete), + ), + if (mediaQuery.smAndDown) + PopSheetEntry( + value: TrackOptionValue.album, + leading: const Icon(SpotubeIcons.album), + title: Text(context.l10n.go_to_album), + subtitle: Text(track.album!.name!), + ), + if (!playlist.containsTrack(track)) ...[ + PopSheetEntry( + value: TrackOptionValue.addToQueue, + leading: const Icon(SpotubeIcons.queueAdd), + title: Text(context.l10n.add_to_queue), + ), + PopSheetEntry( + value: TrackOptionValue.playNext, + leading: const Icon(SpotubeIcons.lightning), + title: Text(context.l10n.play_next), + ), + ] else + PopSheetEntry( + value: TrackOptionValue.removeFromQueue, + enabled: playlist.activeTrack?.id != track.id, + leading: const Icon(SpotubeIcons.queueRemove), + title: Text(context.l10n.remove_from_queue), + ), + if (me.asData?.value != null && !isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.favorite, + leading: favorites.isLiked + ? const Icon( + SpotubeIcons.heartFilled, + color: Colors.pink, + ) + : const Icon(SpotubeIcons.heart), + title: Text( + favorites.isLiked + ? context.l10n.remove_from_favorites + : context.l10n.save_as_favorite, ), - PopSheetEntry( - value: TrackOptionValue.songlink, - leading: Assets.logos.songlinkTransparent.image( - width: 22, - height: 22, - color: colorScheme.onSurface.withOpacity(0.5), - ), - title: Text(context.l10n.song_link), + ), + if (auth != null && !isLocalTrack) ...[ + PopSheetEntry( + value: TrackOptionValue.startRadio, + leading: const Icon(SpotubeIcons.radio), + title: Text(context.l10n.start_a_radio), + ), + PopSheetEntry( + value: TrackOptionValue.addToPlaylist, + leading: const Icon(SpotubeIcons.playlistAdd), + title: Text(context.l10n.add_to_playlist), + ), + ], + if (userPlaylist && auth != null && !isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.removeFromPlaylist, + leading: const Icon(SpotubeIcons.removeFilled), + title: Text(context.l10n.remove_from_playlist), + ), + if (!isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.download, + enabled: !isInQueue, + leading: isInQueue + ? HookBuilder(builder: (context) { + final progress = useListenable(progressNotifier!); + return CircularProgressIndicator( + value: progress.value, + ); + }) + : const Icon(SpotubeIcons.download), + title: Text(context.l10n.download_track), + ), + if (!isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.blacklist, + leading: const Icon(SpotubeIcons.playlistRemove), + iconColor: !isBlackListed ? Colors.red[400] : null, + textColor: !isBlackListed ? Colors.red[400] : null, + title: Text( + isBlackListed + ? context.l10n.remove_from_blacklist + : context.l10n.add_to_blacklist, ), - PopSheetEntry( - value: TrackOptionValue.details, - leading: const Icon(SpotubeIcons.info), - title: Text(context.l10n.details), + ), + if (!isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.share, + leading: const Icon(SpotubeIcons.share), + title: Text(context.l10n.share), + ), + if (!isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.songlink, + leading: Assets.logos.songlinkTransparent.image( + width: 22, + height: 22, + color: colorScheme.onSurface.withOpacity(0.5), ), - ] - }, + title: Text(context.l10n.song_link), + ), + if (!isLocalTrack) + PopSheetEntry( + value: TrackOptionValue.details, + leading: const Icon(SpotubeIcons.info), + title: Text(context.l10n.details), + ), + ], ); //! This is the most ANTI pattern I've ever done, but it works diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart index 30912da22..e3aea4dec 100644 --- a/lib/components/shared/track_tile/track_tile.dart +++ b/lib/components/shared/track_tile/track_tile.dart @@ -195,19 +195,26 @@ class TrackTile extends HookConsumerWidget { children: [ Expanded( flex: 6, - child: LinkText( - track.name!, - "/track/${track.id}", - push: true, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + child: switch (track) { + LocalTrack() => Text( + track.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + _ => LinkText( + track.name!, + "/track/${track.id}", + push: true, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + }, ), if (constrains.mdAndUp) ...[ const SizedBox(width: 8), Expanded( flex: 4, - child: switch (track.runtimeType) { + child: switch (track) { LocalTrack() => Text( track.album!.name!, maxLines: 1, diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 89d70e09c..7a9759351 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; @@ -46,14 +45,14 @@ class LocalLibraryPage extends HookConsumerWidget { await playback.jumpToTrack(currentTrack); } } - + @override Widget build(BuildContext context, ref) { final sortBy = useState(SortBy.none); final playlist = ref.watch(proxyPlaylistProvider); final trackSnapshot = ref.watch(localTracksProvider); - final isPlaylistPlaying = - playlist.containsTracks(trackSnapshot.asData?.value.values.flattened.toList() ?? []); + final isPlaylistPlaying = playlist.containsTracks( + trackSnapshot.asData?.value.values.flattened.toList() ?? []); final searchController = useTextEditingController(); useValueListenable(searchController); @@ -61,176 +60,178 @@ class LocalLibraryPage extends HookConsumerWidget { final isFiltering = useState(false); final controller = useScrollController(); - + return SafeArea( bottom: false, child: Scaffold( - appBar: PageWindowTitleBar( - leading: const BackButton(), - centerTitle: true, - title: Text(isDownloads ? context.l10n.downloads : location), - backgroundColor: Colors.transparent, - ), - extendBodyBehindAppBar: true, - body: Column( - children: [ - const SizedBox(height: 56), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const SizedBox(width: 5), - FilledButton( - onPressed: trackSnapshot.asData?.value != null - ? () async { - if (trackSnapshot.asData?.value.isNotEmpty == true) { - if (!isPlaylistPlaying) { - await playLocalTracks( - ref, - trackSnapshot.asData!.value[location] ?? [], - ); + appBar: PageWindowTitleBar( + leading: const BackButton(), + centerTitle: true, + title: Text(isDownloads ? context.l10n.downloads : location), + backgroundColor: Colors.transparent, + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const SizedBox(width: 5), + FilledButton( + onPressed: trackSnapshot.asData?.value != null + ? () async { + if (trackSnapshot.asData?.value.isNotEmpty == + true) { + if (!isPlaylistPlaying) { + await playLocalTracks( + ref, + trackSnapshot.asData!.value[location] ?? [], + ); + } } } - } - : null, - child: Row( - children: [ - Text(context.l10n.play), - Icon( - isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, - ) - ], + : null, + child: Row( + children: [ + Text(context.l10n.play), + Icon( + isPlaylistPlaying + ? SpotubeIcons.stop + : SpotubeIcons.play, + ) + ], + ), ), - ), - const Spacer(), - ExpandableSearchButton( - isFiltering: isFiltering.value, - onPressed: (value) => isFiltering.value = value, - searchFocus: searchFocus, - ), - const SizedBox(width: 10), - SortTracksDropdown( - value: sortBy.value, - onChanged: (value) { - sortBy.value = value; - }, - ), - const SizedBox(width: 5), - FilledButton( - child: const Icon(SpotubeIcons.refresh), - onPressed: () { - ref.invalidate(localTracksProvider); - }, - ) - ], + const Spacer(), + ExpandableSearchButton( + isFiltering: isFiltering.value, + onPressed: (value) => isFiltering.value = value, + searchFocus: searchFocus, + ), + const SizedBox(width: 10), + SortTracksDropdown( + value: sortBy.value, + onChanged: (value) { + sortBy.value = value; + }, + ), + const SizedBox(width: 5), + FilledButton( + child: const Icon(SpotubeIcons.refresh), + onPressed: () { + ref.invalidate(localTracksProvider); + }, + ) + ], + ), ), - ), - ExpandableSearchField( - searchController: searchController, - searchFocus: searchFocus, - isFiltering: isFiltering.value, - onChangeFiltering: (value) => isFiltering.value = value, - ), - trackSnapshot.when( - data: (tracks) { - final sortedTracks = useMemoized(() { - return ServiceUtils.sortTracks(tracks[location] ?? [], sortBy.value); - }, [sortBy.value, tracks]); + ExpandableSearchField( + searchController: searchController, + searchFocus: searchFocus, + isFiltering: isFiltering.value, + onChangeFiltering: (value) => isFiltering.value = value, + ), + trackSnapshot.when( + data: (tracks) { + final sortedTracks = useMemoized(() { + return ServiceUtils.sortTracks( + tracks[location] ?? [], sortBy.value); + }, [sortBy.value, tracks]); + + final filteredTracks = useMemoized(() { + if (searchController.text.isEmpty) { + return sortedTracks; + } + return sortedTracks + .map((e) => ( + weightedRatio( + "${e.name} - ${e.artists?.asString() ?? ""}", + searchController.text, + ), + e, + )) + .toList() + .sorted( + (a, b) => b.$1.compareTo(a.$1), + ) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList() + .toList(); + }, [searchController.text, sortedTracks]); - final filteredTracks = useMemoized(() { - if (searchController.text.isEmpty) { - return sortedTracks; + if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { + return const Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [NotFound()], + ), + ); } - return sortedTracks - .map((e) => ( - weightedRatio( - "${e.name} - ${e.artists?.asString() ?? ""}", - searchController.text, - ), - e, - )) - .toList() - .sorted( - (a, b) => b.$1.compareTo(a.$1), - ) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList() - .toList(); - }, [searchController.text, sortedTracks]); - if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { - return const Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [NotFound()], - ), - ); - } + return Expanded( + child: RefreshIndicator( + onRefresh: () async { + ref.invalidate(localTracksProvider); + }, + child: InterScrollbar( + controller: controller, + child: Skeletonizer( + enabled: trackSnapshot.isLoading, + child: ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: trackSnapshot.isLoading + ? 5 + : filteredTracks.length, + itemBuilder: (context, index) { + if (trackSnapshot.isLoading) { + return TrackTile( + playlist: playlist, + track: FakeData.track, + index: index, + ); + } - return Expanded( - child: RefreshIndicator( - onRefresh: () async { - ref.invalidate(localTracksProvider); - }, - child: InterScrollbar( - controller: controller, - child: Skeletonizer( - enabled: trackSnapshot.isLoading, - child: ListView.builder( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: - trackSnapshot.isLoading ? 5 : filteredTracks.length, - itemBuilder: (context, index) { - if (trackSnapshot.isLoading) { + final track = filteredTracks[index]; return TrackTile( - playlist: playlist, - track: FakeData.track, index: index, + playlist: playlist, + track: track, + userPlaylist: false, + onTap: () async { + await playLocalTracks( + ref, + sortedTracks, + currentTrack: track, + ); + }, ); - } - - final track = filteredTracks[index]; - return TrackTile( - index: index, - playlist: playlist, - track: track, - userPlaylist: false, - onTap: () async { - await playLocalTracks( - ref, - sortedTracks, - currentTrack: track, - ); - }, - ); - }, + }, + ), ), ), ), - ), - ); - }, - loading: () => Expanded( - child: Skeletonizer( - enabled: true, - child: ListView.builder( - itemCount: 5, - itemBuilder: (context, index) => TrackTile( - track: FakeData.track, - index: index, - playlist: playlist, + ); + }, + loading: () => Expanded( + child: Skeletonizer( + enabled: true, + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) => TrackTile( + track: FakeData.track, + index: index, + playlist: playlist, + ), ), ), ), - ), - error: (error, stackTrace) => - Text(error.toString() + stackTrace.toString()), - ) - ], - ) - ), + error: (error, stackTrace) => + Text(error.toString() + stackTrace.toString()), + ) + ], + )), ); } } diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/proxy_playlist/player_listeners.dart index f86ad3d47..bf54fa909 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/proxy_playlist/player_listeners.dart @@ -1,4 +1,4 @@ -// ignore_for_file: invalid_use_of_protected_member +// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member import 'dart:async'; diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index f70301ff4..b2241ad70 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -45,7 +45,14 @@ class ProxyPlaylist { } bool containsTrack(TrackSimple track) { - return tracks.firstWhereOrNull((element) => element.id == track.id) != null; + return tracks.firstWhereOrNull((element) { + if (element is LocalTrack && track is LocalTrack) { + return element.path == track.path; + } + + return element.id == track.id; + }) != + null; } bool containsTracks(Iterable tracks) { @@ -65,8 +72,8 @@ class ProxyPlaylist { /// Otherwise default super.toJson() is used static Map _makeAppropriateTrackJson(Track track) { return switch (track.runtimeType) { - LocalTrack() => track.toJson(), - SourcedTrack() => track.toJson(), + LocalTrack() => (track as LocalTrack).toJson(), + SourcedTrack() => (track as SourcedTrack).toJson(), _ => track.toJson(), }; } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 92de192ba..d67652b49 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -13,6 +13,7 @@ import 'package:media_kit/media_kit.dart' as mk; import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; @@ -30,12 +31,18 @@ class SpotubeMedia extends mk.Media { : "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}", extras: { ...?extras, - "track": track.toJson(), + "track": switch (track) { + LocalTrack() => track.toJson(), + SourcedTrack() => track.toJson(), + _ => track.toJson(), + }, }, ); factory SpotubeMedia.fromMedia(mk.Media media) { - final track = Track.fromJson(media.extras?["track"]); + final track = media.uri.startsWith("http") + ? Track.fromJson(media.extras?["track"]) + : LocalTrack.fromJson(media.extras?["track"]); return SpotubeMedia(track); } } diff --git a/untranslated_messages.json b/untranslated_messages.json index 91b751eb0..3ea0ca23c 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -41,6 +41,13 @@ "local_tab" ], + "eu": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + "fa": [ "local_library", "add_library_location", @@ -48,6 +55,13 @@ "local_tab" ], + "fi": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + "fr": [ "local_library", "add_library_location", @@ -62,6 +76,13 @@ "local_tab" ], + "id": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + "it": [ "local_library", "add_library_location", @@ -76,6 +97,13 @@ "local_tab" ], + "ka": [ + "local_library", + "add_library_location", + "remove_library_location", + "local_tab" + ], + "ko": [ "local_library", "add_library_location", From fc5bfa089ce2f46ab786565d6750564d704ee7e0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 23 May 2024 21:27:09 +0600 Subject: [PATCH 26/57] feat: local library folder cards --- .../local_folder/local_folder_item.dart | 199 ++++++++++++++++ lib/components/library/user_local_tracks.dart | 214 ++++-------------- .../shared/track_tile/track_options.dart | 2 +- .../configurators/use_get_storage_perms.dart | 2 +- lib/pages/library/local_folder.dart | 1 + .../local_tracks/local_tracks_provider.dart | 125 ++++++++++ .../proxy_playlist/proxy_playlist.dart | 4 +- macos/Podfile.lock | 6 + 8 files changed, 380 insertions(+), 173 deletions(-) create mode 100644 lib/components/library/local_folder/local_folder_item.dart create mode 100644 lib/provider/local_tracks/local_tracks_provider.dart diff --git a/lib/components/library/local_folder/local_folder_item.dart b/lib/components/library/local_folder/local_folder_item.dart new file mode 100644 index 000000000..281cfc2cd --- /dev/null +++ b/lib/components/library/local_folder/local_folder_item.dart @@ -0,0 +1,199 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; +import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; + +class LocalFolderItem extends HookConsumerWidget { + final String folder; + const LocalFolderItem({super.key, required this.folder}); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:colorScheme) = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + final lerpValue = useBrightnessValue(.9, .7); + + final downloadFolder = + ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)); + + final isDownloadFolder = folder == downloadFolder; + + final Uri(:pathSegments) = Uri.parse( + folder + .replaceFirst(RegExp(r'^/Volumes/[^/]+/Users/'), "") + .replaceFirst(r'C:\Users\', "") + .replaceFirst(r'/home/', ""), + ); + + // if length > 5, we ... all the middle segments after 2 and the last 2 + final segments = pathSegments.length > 5 + ? [ + ...pathSegments.take(2), + "...", + ...pathSegments.skip(pathSegments.length - 3).toList() + ..removeLast(), + ] + : pathSegments.take(pathSegments.length - 1).toList(); + + final trackSnapshot = ref.watch( + localTracksProvider.select( + (s) => s.whenData((tracks) => tracks[folder]?.take(4).toList()), + ), + ); + + final tracks = trackSnapshot.value ?? []; + + return InkWell( + onTap: () { + if (isDownloadFolder) { + context.go("/library/local?downloads=1", extra: folder); + } else { + context.go( + "/library/local", + extra: folder, + ); + } + }, + borderRadius: BorderRadius.circular(8), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color.lerp( + colorScheme.surfaceVariant, + colorScheme.surface, + lerpValue, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (tracks.isEmpty) + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + SpotubeIcons.folder, + size: mediaQuery.smAndDown + ? 95 + : mediaQuery.mdAndDown + ? 100 + : 142, + ), + ), + ) + else + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: max((tracks.length / 2).ceil(), 2), + ), + itemCount: tracks.length, + itemBuilder: (context, index) { + final track = tracks[index]; + return UniversalImage( + path: (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + fit: BoxFit.cover, + ); + }, + ), + ), + const Gap(8), + Stack( + children: [ + Center( + child: Text( + isDownloadFolder + ? context.l10n.downloads + : basename(folder), + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + if (!isDownloadFolder) + Align( + alignment: Alignment.topRight, + child: PopupMenuButton( + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon(Icons.more_vert), + ), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: ListTile( + leading: const Icon(SpotubeIcons.folderRemove), + iconColor: colorScheme.error, + title: + Text(context.l10n.remove_library_location), + onTap: () { + final libraryLocations = ref + .read(userPreferencesProvider) + .localLibraryLocation; + ref + .read(userPreferencesProvider.notifier) + .setLocalLibraryLocation( + libraryLocations + .where((e) => e != folder) + .toList(), + ); + }, + ), + ) + ]; + }, + ), + ), + ], + ), + const Spacer(), + Wrap( + spacing: 2, + runSpacing: 2, + children: [ + for (final MapEntry(key: index, value: segment) + in segments.asMap().entries) + Text.rich( + TextSpan( + children: [ + if (index != 0) + TextSpan( + text: "/ ", + style: TextStyle(color: colorScheme.primary), + ), + TextSpan(text: segment), + ], + ), + style: TextStyle( + fontSize: 10, + color: colorScheme.tertiary, + ), + ), + ], + ), + const Spacer(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index ffaae0d92..c0d633803 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -1,44 +1,18 @@ -import 'dart:io'; - -import 'package:catcher_2/catcher_2.dart'; import 'package:file_picker/file_picker.dart'; import 'package:file_selector/file_selector.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:metadata_god/metadata_god.dart'; -import 'package:mime/mime.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/library/local_folder/local_folder_item.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/track.dart'; -import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; // ignore: depend_on_referenced_packages -import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; - -const supportedAudioTypes = [ - "audio/webm", - "audio/ogg", - "audio/mpeg", - "audio/mp4", - "audio/opus", - "audio/wav", - "audio/aac", -]; - -const imgMimeToExt = { - "image/png": ".png", - "image/jpeg": ".jpg", - "image/webp": ".webp", - "image/gif": ".gif", -}; enum SortBy { none, @@ -51,94 +25,6 @@ enum SortBy { album, } -final localTracksProvider = - FutureProvider>>((ref) async { - try { - if (kIsWeb) return {}; - final Map> tracks = {}; - - final downloadLocation = ref.watch( - userPreferencesProvider.select((s) => s.downloadLocation), - ); - final downloadDir = Directory(downloadLocation); - if (!await downloadDir.exists()) { - await downloadDir.create(recursive: true); - } - final localLibraryLocations = ref.watch( - userPreferencesProvider.select((s) => s.localLibraryLocation), - ); - - for (var location in [downloadLocation, ...localLibraryLocations]) { - if (location.isEmpty) continue; - final entities = []; - if (await Directory(location).exists()) { - entities.addAll(Directory(location).listSync(recursive: true)); - } - - final filesWithMetadata = (await Future.wait( - entities.map((e) => File(e.path)).where((file) { - final mimetype = lookupMimeType(file.path); - return mimetype != null && supportedAudioTypes.contains(mimetype); - }).map( - (file) async { - try { - final metadata = await MetadataGod.readMetadata(file: file.path); - - final imageFile = File(join( - (await getTemporaryDirectory()).path, - "spotube", - basenameWithoutExtension(file.path) + - imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, - )); - if (!await imageFile.exists() && metadata.picture != null) { - await imageFile.create(recursive: true); - await imageFile.writeAsBytes( - metadata.picture?.data ?? [], - mode: FileMode.writeOnly, - ); - } - - return { - "metadata": metadata, - "file": file, - "art": imageFile.path - }; - } catch (e, stack) { - if (e is FfiException) { - return {"file": file}; - } - Catcher2.reportCheckedError(e, stack); - return {}; - } - }, - ), - )) - .where((e) => e.isNotEmpty) - .toList(); - - // ignore: no_leading_underscores_for_local_identifiers - final _tracks = filesWithMetadata - .map( - (fileWithMetadata) => LocalTrack.fromTrack( - track: Track().fromFile( - fileWithMetadata["file"], - metadata: fileWithMetadata["metadata"], - art: fileWithMetadata["art"], - ), - path: fileWithMetadata["file"].path, - ), - ) - .toList(); - - tracks[location] = _tracks; - } - return tracks; - } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); - return {}; - } -}); - class UserLocalTracks extends HookConsumerWidget { const UserLocalTracks({super.key}); @@ -167,61 +53,49 @@ class UserLocalTracks extends HookConsumerWidget { } }, [preferences.localLibraryLocation]); - final removeLocalLibraryLocation = useCallback((String location) { - if (!preferences.localLibraryLocation.contains(location)) return; - preferencesNotifier.setLocalLibraryLocation( - [...preferences.localLibraryLocation]..remove(location), - ); - }, [preferences.localLibraryLocation]); - // This is just to pre-load the tracks. // For now, this gets all of them. ref.watch(localTracksProvider); - return Column(children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row(children: [ - const SizedBox(width: 5), - TextButton.icon( - icon: const Icon(SpotubeIcons.folderAdd), - label: Text(context.l10n.add_library_location), - onPressed: addLocalLibraryLocation, - ) - ])), - Expanded( - child: ListView.builder( - itemCount: preferences.localLibraryLocation.length + 1, - itemBuilder: (context, index) { - late final String location; - if (index == 0) { - location = preferences.downloadLocation; - } else { - location = preferences.localLibraryLocation[index - 1]; - } - return ListTile( - title: preferences.downloadLocation != location - ? Text(location) - : Text(context.l10n.downloads), - trailing: preferences.downloadLocation != location - ? Tooltip( - message: context.l10n.remove_library_location, - child: IconButton( - icon: Icon(SpotubeIcons.folderRemove, - color: Colors.red[400]), - onPressed: () => - removeLocalLibraryLocation(location), - ), - ) - : null, - onTap: () async { - context.go( - "/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", - extra: location, - ); - }); - }), - ), - ]); + return LayoutBuilder(builder: (context, constrains) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + children: [ + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + icon: const Icon(SpotubeIcons.folderAdd), + label: Text(context.l10n.add_library_location), + onPressed: addLocalLibraryLocation, + ), + ), + const Gap(8), + Expanded( + child: GridView.builder( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: constrains.isXs + ? 210 + : constrains.mdAndDown + ? 280 + : 250, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: preferences.localLibraryLocation.length + 1, + itemBuilder: (context, index) { + return LocalFolderItem( + folder: index == 0 + ? preferences.downloadLocation + : preferences.localLibraryLocation[index - 1], + ); + }, + ), + ), + ], + ), + ); + }); } } diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index c917ebaa5..4b383c479 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; @@ -23,6 +22,7 @@ import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart'; +import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; diff --git a/lib/hooks/configurators/use_get_storage_perms.dart b/lib/hooks/configurators/use_get_storage_perms.dart index bcc340422..9cccbfe06 100644 --- a/lib/hooks/configurators/use_get_storage_perms.dart +++ b/lib/hooks/configurators/use_get_storage_perms.dart @@ -3,8 +3,8 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/hooks/utils/use_async_effect.dart'; +import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/utils/platform.dart'; void useGetStoragePermissions(WidgetRef ref) { diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 7a9759351..6552bb5bc 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -16,6 +16,7 @@ import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart new file mode 100644 index 000000000..867774bd8 --- /dev/null +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -0,0 +1,125 @@ +import 'dart:io'; + +import 'package:catcher_2/catcher_2.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:metadata_god/metadata_god.dart'; +import 'package:mime/mime.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'package:spotify/spotify.dart'; +import 'package:spotube/extensions/track.dart'; +import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +// ignore: depend_on_referenced_packages +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; + +const supportedAudioTypes = [ + "audio/webm", + "audio/ogg", + "audio/mpeg", + "audio/mp4", + "audio/opus", + "audio/wav", + "audio/aac", +]; + +const imgMimeToExt = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/webp": ".webp", + "image/gif": ".gif", +}; + +final localTracksProvider = + FutureProvider>>((ref) async { + try { + if (kIsWeb) return {}; + final Map> tracks = {}; + + final downloadLocation = ref.watch( + userPreferencesProvider.select((s) => s.downloadLocation), + ); + final downloadDir = Directory(downloadLocation); + if (!await downloadDir.exists()) { + await downloadDir.create(recursive: true); + } + final localLibraryLocations = ref.watch( + userPreferencesProvider.select((s) => s.localLibraryLocation), + ); + + for (var location in [downloadLocation, ...localLibraryLocations]) { + if (location.isEmpty) continue; + final entities = []; + if (await Directory(location).exists()) { + try { + entities.addAll(Directory(location).listSync(recursive: true)); + } catch (e, stack) { + Catcher2.reportCheckedError(e, stack); + } + } + + final filesWithMetadata = (await Future.wait( + entities.map((e) => File(e.path)).where((file) { + final mimetype = lookupMimeType(file.path); + return mimetype != null && supportedAudioTypes.contains(mimetype); + }).map( + (file) async { + try { + final metadata = await MetadataGod.readMetadata(file: file.path); + + final imageFile = File(join( + (await getTemporaryDirectory()).path, + "spotube", + basenameWithoutExtension(file.path) + + imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, + )); + if (!await imageFile.exists() && metadata.picture != null) { + await imageFile.create(recursive: true); + await imageFile.writeAsBytes( + metadata.picture?.data ?? [], + mode: FileMode.writeOnly, + ); + } + + return { + "metadata": metadata, + "file": file, + "art": imageFile.path + }; + } catch (e, stack) { + if (e is FfiException) { + return {"file": file}; + } + Catcher2.reportCheckedError(e, stack); + return {}; + } + }, + ), + )) + .where((e) => e.isNotEmpty) + .toList(); + + // ignore: no_leading_underscores_for_local_identifiers + final _tracks = filesWithMetadata + .map( + (fileWithMetadata) => LocalTrack.fromTrack( + track: Track().fromFile( + fileWithMetadata["file"], + metadata: fileWithMetadata["metadata"], + art: fileWithMetadata["art"], + ), + path: fileWithMetadata["file"].path, + ), + ) + .toList(); + + tracks[location] = _tracks; + } + return tracks; + } catch (e, stack) { + Catcher2.reportCheckedError(e, stack); + return {}; + } +}); diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index b2241ad70..1378c589b 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -71,8 +71,10 @@ class ProxyPlaylist { /// To make sure proper instance method is used for JSON serialization /// Otherwise default super.toJson() is used static Map _makeAppropriateTrackJson(Track track) { - return switch (track.runtimeType) { + return switch (track) { + // ignore: unnecessary_cast LocalTrack() => (track as LocalTrack).toJson(), + // ignore: unnecessary_cast SourcedTrack() => (track as SourcedTrack).toJson(), _ => track.toJson(), }; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ce2ef2339..166bfa714 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -41,6 +41,8 @@ PODS: - FlutterMacOS - system_theme (0.0.1): - FlutterMacOS + - system_tray (0.0.1): + - FlutterMacOS - tray_manager (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -70,6 +72,7 @@ DEPENDENCIES: - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) + - system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -118,6 +121,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin system_theme: :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos + system_tray: + :path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos tray_manager: :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos url_launcher_macos: @@ -148,6 +153,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc + system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 From 82307bc030035b03ab1b8d8ec7b24da19a866b12 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 11:40:01 +0600 Subject: [PATCH 27/57] feat: personalized stats based on local music history (#1522) * feat: add playback history provider * feat: implement recently played section * refactor: use route names * feat: add stats summary and top tracks/artists/albums * feat: add top date based filtering * feat: add stream money calculation * refactor: place search in mobile navbar and settings in home appbar * feat: add individual minutes and streams page * feat(stats): add individual minutes and streams page * chore: default period to 1 month * feat: add text to explain user how hypothetical fees are calculated * chore: ensure usage of route names instead of direct paths * cd: add cache key * cd: remove media_kit_event_loop from git --- .github/workflows/spotube-release-binary.yml | 1 + build.yaml | 7 +- lib/collections/fake.dart | 1 - lib/collections/formatters.dart | 8 + lib/collections/intents.dart | 12 +- lib/collections/routes.dart | 114 +++- lib/collections/side_bar_tiles.dart | 71 +- lib/collections/spotube_icons.dart | 1 + lib/components/album/album_card.dart | 18 +- lib/components/artist/artist_card.dart | 9 +- lib/components/connect/connect_device.dart | 7 +- lib/components/home/sections/feed.dart | 10 +- .../home/sections/friends/friend_item.dart | 22 +- lib/components/home/sections/genres.dart | 12 +- lib/components/home/sections/recent.dart | 32 + .../local_folder/local_folder_item.dart | 16 +- lib/components/playlist/playlist_card.dart | 17 +- lib/components/root/sidebar.dart | 95 ++- .../root/spotube_navigation_bar.dart | 40 +- .../shared/fallbacks/anonymous_fallback.dart | 3 +- .../horizontal_playbutton_card_view.dart | 2 +- lib/components/shared/links/artist_link.dart | 8 +- .../shared/themed_button_tab_bar.dart | 4 +- .../sections/body/track_view_body.dart | 25 +- .../sections/body/track_view_options.dart | 15 + .../sections/header/header_actions.dart | 10 + .../sections/header/header_buttons.dart | 40 +- .../shared/tracks_view/track_view_props.dart | 12 +- lib/components/stats/common/album_item.dart | 53 ++ lib/components/stats/common/artist_item.dart | 39 ++ .../stats/common/playlist_item.dart | 46 ++ lib/components/stats/common/track_item.dart | 49 ++ lib/components/stats/summary/summary.dart | 100 +++ .../stats/summary/summary_card.dart | 86 +++ lib/components/stats/top/albums.dart | 29 + lib/components/stats/top/artists.dart | 27 + lib/components/stats/top/top.dart | 106 +++ lib/components/stats/top/tracks.dart | 31 + lib/extensions/album_simple.dart | 15 - lib/extensions/artist_simple.dart | 12 - lib/extensions/track.dart | 29 - lib/l10n/app_en.arb | 5 +- lib/models/connect/connect.dart | 1 - lib/models/connect/connect.freezed.dart | 498 ++++++++++++-- lib/models/connect/connect.g.dart | 44 +- lib/models/connect/load.dart | 19 +- lib/models/current_playlist.dart | 1 - lib/models/local_track.dart | 4 +- lib/models/source_match.g.dart | 2 +- lib/models/spotify/home_feed.g.dart | 70 +- .../spotify/recommendation_seeds.g.dart | 3 +- lib/models/spotify_friends.g.dart | 39 +- lib/pages/album/album.dart | 4 +- lib/pages/artist/artist.dart | 2 + lib/pages/artist/section/top_tracks.dart | 3 +- lib/pages/connect/connect.dart | 7 +- lib/pages/connect/control/control.dart | 11 +- lib/pages/desktop_login/desktop_login.dart | 2 + lib/pages/desktop_login/login_tutorial.dart | 4 +- .../getting_started/getting_started.dart | 2 + .../getting_started/sections/support.dart | 6 +- lib/pages/home/feed/feed_section.dart | 2 + lib/pages/home/genres/genre_playlists.dart | 2 + lib/pages/home/genres/genres.dart | 10 +- lib/pages/home/home.dart | 43 +- lib/pages/lastfm_login/lastfm_login.dart | 1 + lib/pages/library/library.dart | 2 + lib/pages/library/local_folder.dart | 2 + .../playlist_generate/playlist_generate.dart | 2 + .../playlist_generate_result.dart | 10 +- lib/pages/lyrics/lyrics.dart | 2 + lib/pages/lyrics/mini_lyrics.dart | 2 + lib/pages/mobile_login/mobile_login.dart | 1 + lib/pages/playlist/liked_playlist.dart | 5 +- lib/pages/playlist/playlist.dart | 4 +- lib/pages/profile/profile.dart | 2 + lib/pages/root/root_app.dart | 36 +- lib/pages/search/search.dart | 193 +++--- lib/pages/search/sections/tracks.dart | 2 +- lib/pages/settings/about.dart | 2 + lib/pages/settings/blacklist.dart | 2 + lib/pages/settings/logs.dart | 2 + lib/pages/settings/sections/accounts.dart | 28 +- lib/pages/settings/settings.dart | 3 + lib/pages/stats/albums/albums.dart | 38 ++ lib/pages/stats/artists/artists.dart | 38 ++ lib/pages/stats/fees/fees.dart | 65 ++ lib/pages/stats/minutes/minutes.dart | 44 ++ lib/pages/stats/playlists/playlists.dart | 39 ++ lib/pages/stats/stats.dart | 35 + lib/pages/stats/streams/streams.dart | 44 ++ lib/pages/track/track.dart | 2 + lib/provider/connect/server.dart | 15 +- lib/provider/history/history.dart | 129 ++++ lib/provider/history/recent.dart | 40 ++ lib/provider/history/state.dart | 35 + lib/provider/history/state.freezed.dart | 644 ++++++++++++++++++ lib/provider/history/state.g.dart | 55 ++ lib/provider/history/summary.dart | 62 ++ lib/provider/history/top.dart | 95 +++ .../proxy_playlist/player_listeners.dart | 55 +- .../proxy_playlist/proxy_playlist.dart | 1 - .../proxy_playlist_provider.dart | 28 +- .../user_preferences_provider.dart | 1 + .../user_preferences_state.g.dart | 3 +- lib/services/audio_player/audio_player.dart | 1 - .../audio_services/mobile_audio_service.dart | 4 +- lib/services/song_link/song_link.g.dart | 3 +- .../sourced_track/models/source_info.g.dart | 2 +- .../sourced_track/models/source_map.g.dart | 15 +- lib/utils/service_utils.dart | 46 ++ pubspec.lock | 22 +- pubspec.yaml | 15 +- untranslated_messages.json | 78 ++- 114 files changed, 3366 insertions(+), 607 deletions(-) create mode 100644 lib/collections/formatters.dart create mode 100644 lib/components/home/sections/recent.dart create mode 100644 lib/components/stats/common/album_item.dart create mode 100644 lib/components/stats/common/artist_item.dart create mode 100644 lib/components/stats/common/playlist_item.dart create mode 100644 lib/components/stats/common/track_item.dart create mode 100644 lib/components/stats/summary/summary.dart create mode 100644 lib/components/stats/summary/summary_card.dart create mode 100644 lib/components/stats/top/albums.dart create mode 100644 lib/components/stats/top/artists.dart create mode 100644 lib/components/stats/top/top.dart create mode 100644 lib/components/stats/top/tracks.dart create mode 100644 lib/pages/stats/albums/albums.dart create mode 100644 lib/pages/stats/artists/artists.dart create mode 100644 lib/pages/stats/fees/fees.dart create mode 100644 lib/pages/stats/minutes/minutes.dart create mode 100644 lib/pages/stats/playlists/playlists.dart create mode 100644 lib/pages/stats/stats.dart create mode 100644 lib/pages/stats/streams/streams.dart create mode 100644 lib/provider/history/history.dart create mode 100644 lib/provider/history/recent.dart create mode 100644 lib/provider/history/state.dart create mode 100644 lib/provider/history/state.freezed.dart create mode 100644 lib/provider/history/state.g.dart create mode 100644 lib/provider/history/summary.dart create mode 100644 lib/provider/history/top.dart diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 0fe1f1bac..694dc1ebf 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -66,6 +66,7 @@ jobs: - uses: subosito/flutter-action@v2.12.0 with: cache: true + cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }} flutter-version: ${{ env.FLUTTER_VERSION }} - name: Setup Java if: ${{matrix.platform == 'android'}} diff --git a/build.yaml b/build.yaml index f074d6e15..d83d6a202 100644 --- a/build.yaml +++ b/build.yaml @@ -2,4 +2,9 @@ targets: $default: sources: exclude: - - bin/*.dart \ No newline at end of file + - bin/*.dart + builders: + json_serializable: + options: + any_map: true + explicit_to_json: true diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index 4df19dfc9..7391d3a06 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -1,5 +1,4 @@ import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/spotify/home_feed.dart'; import 'package:spotube/models/spotify_friends.dart'; diff --git a/lib/collections/formatters.dart b/lib/collections/formatters.dart new file mode 100644 index 000000000..0aed9e9f8 --- /dev/null +++ b/lib/collections/formatters.dart @@ -0,0 +1,8 @@ +import 'package:intl/intl.dart'; + +final compactNumberFormatter = NumberFormat.compact(); +final usdFormatter = NumberFormat.compactCurrency( + locale: 'en-US', + symbol: r"$", + decimalDigits: 2, +); diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 5f60959ed..579aff185 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -7,6 +7,10 @@ import 'package:go_router/go_router.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/models/logger.dart'; +import 'package:spotube/pages/home/home.dart'; +import 'package:spotube/pages/library/library.dart'; +import 'package:spotube/pages/lyrics/lyrics.dart'; +import 'package:spotube/pages/search/search.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; @@ -67,16 +71,16 @@ class HomeTabAction extends Action { final router = intent.ref.read(routerProvider); switch (intent.tab) { case HomeTabs.browse: - router.go("/"); + router.goNamed(HomePage.name); break; case HomeTabs.search: - router.go("/search"); + router.goNamed(SearchPage.name); break; case HomeTabs.library: - router.go("/library"); + router.goNamed(LibraryPage.name); break; case HomeTabs.lyrics: - router.go("/lyrics"); + router.goNamed(LyricsPage.name); break; } return null; diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 340b816ab..dc2e4b7c1 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -25,6 +25,13 @@ import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/logs.dart'; +import 'package:spotube/pages/stats/albums/albums.dart'; +import 'package:spotube/pages/stats/artists/artists.dart'; +import 'package:spotube/pages/stats/fees/fees.dart'; +import 'package:spotube/pages/stats/minutes/minutes.dart'; +import 'package:spotube/pages/stats/playlists/playlists.dart'; +import 'package:spotube/pages/stats/stats.dart'; +import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; @@ -51,6 +58,7 @@ final routerProvider = Provider((ref) { routes: [ GoRoute( path: "/", + name: HomePage.name, redirect: (context, state) async { final authNotifier = ref.read(authenticationProvider.notifier); final json = await authNotifier.box.get(authNotifier.cacheKey); @@ -67,11 +75,13 @@ final routerProvider = Provider((ref) { routes: [ GoRoute( path: "genres", + name: GenrePage.name, pageBuilder: (context, state) => const SpotubePage(child: GenrePage()), ), GoRoute( path: "genre/:categoryId", + name: GenrePlaylistsPage.name, pageBuilder: (context, state) => SpotubePage( child: GenrePlaylistsPage( category: state.extra as Category, @@ -80,6 +90,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "feeds/:feedId", + name: HomeFeedSectionPage.name, pageBuilder: (context, state) => SpotubePage( child: HomeFeedSectionPage( sectionUri: state.pathParameters["feedId"] as String, @@ -90,56 +101,62 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/search", - name: "Search", + name: SearchPage.name, pageBuilder: (context, state) => const SpotubePage(child: SearchPage()), ), GoRoute( path: "/library", - name: "Library", + name: LibraryPage.name, pageBuilder: (context, state) => const SpotubePage(child: LibraryPage()), routes: [ GoRoute( - path: "generate", - pageBuilder: (context, state) => - const SpotubePage(child: PlaylistGeneratorPage()), - routes: [ - GoRoute( - path: "result", - pageBuilder: (context, state) => SpotubePage( - child: PlaylistGenerateResultPage( - state: state.extra as GeneratePlaylistProviderInput, - ), + path: "generate", + name: PlaylistGeneratorPage.name, + pageBuilder: (context, state) => + const SpotubePage(child: PlaylistGeneratorPage()), + routes: [ + GoRoute( + path: "result", + name: PlaylistGenerateResultPage.name, + pageBuilder: (context, state) => SpotubePage( + child: PlaylistGenerateResultPage( + state: state.extra as GeneratePlaylistProviderInput, ), ), - ]), + ) + ], + ), GoRoute( path: "local", + name: LocalLibraryPage.name, pageBuilder: (context, state) { assert(state.extra is String); return SpotubePage( child: LocalLibraryPage(state.extra as String, - isDownloads: state.uri.queryParameters["downloads"] != null - ), + isDownloads: + state.uri.queryParameters["downloads"] != null), ); }, ), ]), GoRoute( path: "/lyrics", - name: "Lyrics", + name: LyricsPage.name, pageBuilder: (context, state) => const SpotubePage(child: LyricsPage()), ), GoRoute( path: "/settings", + name: SettingsPage.name, pageBuilder: (context, state) => const SpotubePage( child: SettingsPage(), ), routes: [ GoRoute( path: "blacklist", + name: BlackListPage.name, pageBuilder: (context, state) => SpotubeSlidePage( child: const BlackListPage(), ), @@ -147,12 +164,14 @@ final routerProvider = Provider((ref) { if (!kIsWeb) GoRoute( path: "logs", + name: LogsPage.name, pageBuilder: (context, state) => SpotubeSlidePage( child: const LogsPage(), ), ), GoRoute( path: "about", + name: AboutSpotube.name, pageBuilder: (context, state) => SpotubeSlidePage( child: const AboutSpotube(), ), @@ -161,6 +180,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/album/:id", + name: AlbumPage.name, pageBuilder: (context, state) { assert(state.extra is AlbumSimple); return SpotubePage( @@ -170,6 +190,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/artist/:id", + name: ArtistPage.name, pageBuilder: (context, state) { assert(state.pathParameters["id"] != null); return SpotubePage( @@ -178,6 +199,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/playlist/:id", + name: PlaylistPage.name, pageBuilder: (context, state) { assert(state.extra is PlaylistSimple); return SpotubePage( @@ -189,6 +211,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/track/:id", + name: TrackPage.name, pageBuilder: (context, state) { final id = state.pathParameters["id"]!; return SpotubePage( @@ -198,12 +221,14 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/connect", + name: ConnectPage.name, pageBuilder: (context, state) => const SpotubePage( child: ConnectPage(), ), routes: [ GoRoute( path: "control", + name: ConnectControlPage.name, pageBuilder: (context, state) { return const SpotubePage( child: ConnectControlPage(), @@ -214,13 +239,66 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/profile", + name: ProfilePage.name, pageBuilder: (context, state) => const SpotubePage(child: ProfilePage()), + ), + GoRoute( + path: "/stats", + name: StatsPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsPage(), + ), + routes: [ + GoRoute( + path: "minutes", + name: StatsMinutesPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsMinutesPage(), + ), + ), + GoRoute( + path: "streams", + name: StatsStreamsPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsStreamsPage(), + ), + ), + GoRoute( + path: "fees", + name: StatsStreamFeesPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsStreamFeesPage(), + ), + ), + GoRoute( + path: "artists", + name: StatsArtistsPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsArtistsPage(), + ), + ), + GoRoute( + path: "albums", + name: StatsAlbumsPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsAlbumsPage(), + ), + ), + GoRoute( + path: "playlists", + name: StatsPlaylistsPage.name, + pageBuilder: (context, state) => const SpotubePage( + child: StatsPlaylistsPage(), + ), + ), + ], ) ], ), GoRoute( path: "/mini-player", + name: MiniLyricsPage.name, parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => SpotubePage( child: MiniLyricsPage(prevSize: state.extra as Size), @@ -228,6 +306,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/getting-started", + name: GettingStarting.name, parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => const SpotubePage( child: GettingStarting(), @@ -235,6 +314,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/login", + name: WebViewLogin.name, parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => SpotubePage( child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(), @@ -242,6 +322,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/login-tutorial", + name: LoginTutorial.name, parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => const SpotubePage( child: LoginTutorial(), @@ -249,6 +330,7 @@ final routerProvider = Provider((ref) { ), GoRoute( path: "/lastfm-login", + name: LastFMLoginPage.name, parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => const SpotubePage(child: LastFMLoginPage()), diff --git a/lib/collections/side_bar_tiles.dart b/lib/collections/side_bar_tiles.dart index 551d70d72..4f23c049a 100644 --- a/lib/collections/side_bar_tiles.dart +++ b/lib/collections/side_bar_tiles.dart @@ -1,33 +1,82 @@ import 'package:flutter/material.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:spotube/pages/home/home.dart'; +import 'package:spotube/pages/library/library.dart'; +import 'package:spotube/pages/lyrics/lyrics.dart'; +import 'package:spotube/pages/search/search.dart'; +import 'package:spotube/pages/stats/stats.dart'; class SideBarTiles { final IconData icon; final String title; final String id; - SideBarTiles({required this.icon, required this.title, required this.id}); + final String name; + + SideBarTiles({ + required this.icon, + required this.title, + required this.id, + required this.name, + }); } List getSidebarTileList(AppLocalizations l10n) => [ - SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse), - SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search), SideBarTiles( - id: "library", icon: SpotubeIcons.library, title: l10n.library), - SideBarTiles(id: "lyrics", icon: SpotubeIcons.music, title: l10n.lyrics), + id: "browse", + name: HomePage.name, + icon: SpotubeIcons.home, + title: l10n.browse, + ), + SideBarTiles( + id: "search", + name: SearchPage.name, + icon: SpotubeIcons.search, + title: l10n.search, + ), + SideBarTiles( + id: "library", + name: LibraryPage.name, + icon: SpotubeIcons.library, + title: l10n.library, + ), + SideBarTiles( + id: "lyrics", + name: LyricsPage.name, + icon: SpotubeIcons.music, + title: l10n.lyrics, + ), + SideBarTiles( + id: "stats", + name: StatsPage.name, + icon: SpotubeIcons.chart, + title: l10n.stats, + ), ]; List getNavbarTileList(AppLocalizations l10n) => [ - SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse), - SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search), + SideBarTiles( + id: "browse", + name: HomePage.name, + icon: SpotubeIcons.home, + title: l10n.browse, + ), + SideBarTiles( + id: "search", + name: SearchPage.name, + icon: SpotubeIcons.search, + title: l10n.search, + ), SideBarTiles( id: "library", + name: LibraryPage.name, icon: SpotubeIcons.library, title: l10n.library, ), SideBarTiles( - id: "settings", - icon: SpotubeIcons.settings, - title: l10n.settings, - ) + id: "stats", + name: StatsPage.name, + icon: SpotubeIcons.chart, + title: l10n.stats, + ), ]; diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 2da09f528..a45e581ed 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -121,6 +121,7 @@ abstract class SpotubeIcons { static const monitor = FeatherIcons.monitor; static const power = FeatherIcons.power; static const bluetooth = FeatherIcons.bluetooth; + static const chart = FeatherIcons.barChart2; static const folderAdd = FeatherIcons.folderPlus; static const folderRemove = FeatherIcons.folderMinus; } diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index a71fbf03e..7212a5741 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -9,7 +9,9 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/connect/connect.dart'; +import 'package:spotube/pages/album/album.dart'; import 'package:spotube/provider/connect/connect.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -32,6 +34,7 @@ class AlbumCard extends HookConsumerWidget { final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final historyNotifier = ref.read(playbackHistoryProvider.notifier); bool isPlaylistPlaying = useMemoized( () => playlist.containsCollection(album.id!), @@ -62,7 +65,14 @@ class AlbumCard extends HookConsumerWidget { description: "${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}", onTap: () { - ServiceUtils.push(context, "/album/${album.id}", extra: album); + ServiceUtils.pushNamed( + context, + AlbumPage.name, + pathParameters: { + "id": album.id!, + }, + extra: album, + ); }, onPlaybuttonPressed: () async { updating.value = true; @@ -79,14 +89,15 @@ class AlbumCard extends HookConsumerWidget { if (isRemoteDevice) { final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( - WebSocketLoadEventData( + WebSocketLoadEventData.album( tracks: fetchedTracks, - collectionId: album.id!, + collection: album, ), ); } else { await playlistNotifier.load(fetchedTracks, autoPlay: true); playlistNotifier.addCollection(album.id!); + historyNotifier.addAlbums([album]); } } finally { updating.value = false; @@ -104,6 +115,7 @@ class AlbumCard extends HookConsumerWidget { if (fetchedTracks.isEmpty) return; playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addCollection(album.id!); + historyNotifier.addAlbums([album]); if (context.mounted) { final snackbar = SnackBar( content: Text( diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index cc8485d5c..57971ada9 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -9,6 +9,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; +import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -63,7 +64,13 @@ class ArtistCard extends HookConsumerWidget { ), child: InkWell( onTap: () { - ServiceUtils.push(context, "/artist/${artist.id}"); + ServiceUtils.pushNamed( + context, + ArtistPage.name, + pathParameters: { + "id": artist.id!, + }, + ); }, borderRadius: radius, child: Padding( diff --git a/lib/components/connect/connect_device.dart b/lib/components/connect/connect_device.dart index 3ac585df5..f48885344 100644 --- a/lib/components/connect/connect_device.dart +++ b/lib/components/connect/connect_device.dart @@ -3,6 +3,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/connect/connect.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -22,7 +23,7 @@ class ConnectDeviceButton extends HookConsumerWidget { width: double.infinity, child: TextButton( onPressed: () { - ServiceUtils.push(context, "/connect"); + ServiceUtils.pushNamed(context, ConnectPage.name); }, style: FilledButton.styleFrom( shape: RoundedRectangleBorder( @@ -59,7 +60,7 @@ class ConnectDeviceButton extends HookConsumerWidget { clipBehavior: Clip.hardEdge, child: InkWell( onTap: () { - ServiceUtils.push(context, "/connect"); + ServiceUtils.pushNamed(context, ConnectPage.name); }, borderRadius: BorderRadius.circular(50), child: Ink( @@ -111,7 +112,7 @@ class ConnectDeviceButton extends HookConsumerWidget { foregroundColor: colorScheme.onPrimary, ), onPressed: () { - ServiceUtils.push(context, "/connect"); + ServiceUtils.pushNamed(context, ConnectPage.name); }, ), ), diff --git a/lib/components/home/sections/feed.dart b/lib/components/home/sections/feed.dart index 793cd2c3b..f3f632cee 100644 --- a/lib/components/home/sections/feed.dart +++ b/lib/components/home/sections/feed.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/pages/home/feed/feed_section.dart'; import 'package:spotube/provider/spotify/views/home.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -41,8 +42,13 @@ class HomePageFeedSection extends HookConsumerWidget { child: TextButton.icon( label: const Text("Browse More"), icon: const Icon(SpotubeIcons.angleRight), - onPressed: () => - ServiceUtils.push(context, "/feeds/${section.uri}"), + onPressed: () => ServiceUtils.pushNamed( + context, + HomeFeedSectionPage.name, + pathParameters: { + "feedId": section.uri, + }, + ), ), ), ); diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart index b883e2cc4..2b5757565 100644 --- a/lib/components/home/sections/friends/friend_item.dart +++ b/lib/components/home/sections/friends/friend_item.dart @@ -6,6 +6,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/models/spotify_friends.dart'; +import 'package:spotube/pages/album/album.dart'; +import 'package:spotube/pages/artist/artist.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/spotify_provider.dart'; class FriendItem extends HookConsumerWidget { @@ -57,7 +60,9 @@ class FriendItem extends HookConsumerWidget { text: friend.track.name, recognizer: TapGestureRecognizer() ..onTap = () { - context.push("/track/${friend.track.id}"); + context.pushNamed(TrackPage.name, pathParameters: { + "id": friend.track.id, + }); }, ), const TextSpan(text: " • "), @@ -71,8 +76,12 @@ class FriendItem extends HookConsumerWidget { text: " ${friend.track.artist.name}", recognizer: TapGestureRecognizer() ..onTap = () { - context.push( - "/artist/${friend.track.artist.id}", + context.pushNamed( + ArtistPage.name, + pathParameters: { + "id": friend.track.artist.id, + }, + extra: friend.track.artist, ); }, ), @@ -105,8 +114,11 @@ class FriendItem extends HookConsumerWidget { final album = await spotify.albums.get(friend.track.album.id); if (context.mounted) { - context.push( - "/album/${friend.track.album.id}", + context.pushNamed( + AlbumPage.name, + pathParameters: { + "id": friend.track.album.id, + }, extra: album, ); } diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index 8fbc8bf9a..7dfafd5a0 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -13,6 +13,8 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/home/genres/genre_playlists.dart'; +import 'package:spotube/pages/home/genres/genres.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomeGenresSection extends HookConsumerWidget { @@ -50,7 +52,7 @@ class HomeGenresSection extends HookConsumerWidget { textDirection: TextDirection.rtl, child: TextButton.icon( onPressed: () { - context.push('/genres'); + context.pushNamed(GenrePage.name); }, icon: const Icon(SpotubeIcons.angleRight), label: Text( @@ -110,7 +112,13 @@ class HomeGenresSection extends HookConsumerWidget { return InkWell( onTap: () { - context.push('/genre/${category.id}', extra: category); + context.pushNamed( + GenrePlaylistsPage.name, + pathParameters: { + "categoryId": category.id!, + }, + extra: category, + ); }, borderRadius: BorderRadius.circular(8), child: Ink( diff --git a/lib/components/home/sections/recent.dart b/lib/components/home/sections/recent.dart new file mode 100644 index 000000000..0fc5fadf9 --- /dev/null +++ b/lib/components/home/sections/recent.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/provider/history/recent.dart'; +import 'package:spotube/provider/history/state.dart'; + +class HomeRecentlyPlayedSection extends HookConsumerWidget { + const HomeRecentlyPlayedSection({super.key}); + + @override + Widget build(BuildContext context, ref) { + final history = ref.watch(recentlyPlayedItems); + + if (history.isEmpty) { + return const SizedBox(); + } + + return HorizontalPlaybuttonCardView( + title: const Text('Recently Played'), + items: [ + for (final item in history) + if (item is PlaybackHistoryPlaylist) + item.playlist + else if (item is PlaybackHistoryAlbum) + item.album + ], + hasNextPage: false, + isLoadingNextPage: false, + onFetchMore: () {}, + ); + } +} diff --git a/lib/components/library/local_folder/local_folder_item.dart b/lib/components/library/local_folder/local_folder_item.dart index 281cfc2cd..556f09a63 100644 --- a/lib/components/library/local_folder/local_folder_item.dart +++ b/lib/components/library/local_folder/local_folder_item.dart @@ -11,6 +11,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; +import 'package:spotube/pages/library/local_folder.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -57,14 +58,13 @@ class LocalFolderItem extends HookConsumerWidget { return InkWell( onTap: () { - if (isDownloadFolder) { - context.go("/library/local?downloads=1", extra: folder); - } else { - context.go( - "/library/local", - extra: folder, - ); - } + context.goNamed( + LocalLibraryPage.name, + queryParameters: { + if (isDownloadFolder) "downloads": 1, + }, + extra: folder, + ); }, borderRadius: BorderRadius.circular(8), child: Ink( diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index ae6f20e5f..72e13b26b 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -6,7 +6,9 @@ import 'package:spotube/components/shared/dialogs/select_device_dialog.dart'; import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/connect/connect.dart'; +import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/provider/connect/connect.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -22,6 +24,8 @@ class PlaylistCard extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlistQueue = ref.watch(proxyPlaylistProvider); final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; bool isPlaylistPlaying = useMemoized( @@ -55,9 +59,12 @@ class PlaylistCard extends HookConsumerWidget { isOwner: playlist.owner?.id == me.asData?.value.id && me.asData?.value.id != null, onTap: () { - ServiceUtils.push( + ServiceUtils.pushNamed( context, - "/playlist/${playlist.id}", + PlaylistPage.name, + pathParameters: { + "id": playlist.id!, + }, extra: playlist, ); }, @@ -78,14 +85,15 @@ class PlaylistCard extends HookConsumerWidget { if (isRemoteDevice) { final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( - WebSocketLoadEventData( + WebSocketLoadEventData.playlist( tracks: fetchedTracks, - collectionId: playlist.id!, + collection: playlist, ), ); } else { await playlistNotifier.load(fetchedTracks, autoPlay: true); playlistNotifier.addCollection(playlist.id!); + historyNotifier.addPlaylists([playlist]); } } finally { if (context.mounted) { @@ -104,6 +112,7 @@ class PlaylistCard extends HookConsumerWidget { playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addCollection(playlist.id!); + historyNotifier.addPlaylists([playlist]); if (context.mounted) { final snackbar = SnackBar( content: Text("Added ${fetchedTracks.length} tracks to queue"), diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index a100ca8e9..0e644a898 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -16,6 +16,8 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; +import 'package:spotube/pages/profile/profile.dart'; +import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -26,13 +28,9 @@ import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; class Sidebar extends HookConsumerWidget { - final int? selectedIndex; - final void Function(int) onSelectedIndexChanged; final Widget child; const Sidebar({ - required this.selectedIndex, - required this.onSelectedIndexChanged, required this.child, super.key, }); @@ -47,12 +45,9 @@ class Sidebar extends HookConsumerWidget { ); } - static void goToSettings(BuildContext context) { - GoRouter.of(context).go("/settings"); - } - @override Widget build(BuildContext context, WidgetRef ref) { + final routerState = GoRouterState.of(context); final mediaQuery = MediaQuery.of(context); final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; @@ -60,8 +55,17 @@ class Sidebar extends HookConsumerWidget { final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); + final sidebarTileList = useMemoized( + () => getSidebarTileList(context.l10n), + [context.l10n], + ); + + final selectedIndex = sidebarTileList.indexWhere( + (e) => routerState.namedLocation(e.name) == routerState.matchedLocation, + ); + final controller = useSidebarXController( - selectedIndex: selectedIndex ?? 0, + selectedIndex: selectedIndex, extended: mediaQuery.lgAndUp, ); @@ -73,29 +77,6 @@ class Sidebar extends HookConsumerWidget { Color.lerp(bg, Colors.black, 0.45)!, ); - final sidebarTileList = useMemoized( - () => getSidebarTileList(context.l10n), - [context.l10n], - ); - - useEffect(() { - if (controller.selectedIndex != selectedIndex && selectedIndex != null) { - controller.selectIndex(selectedIndex!); - } - return null; - }, [selectedIndex]); - - useEffect(() { - void listener() { - onSelectedIndexChanged(controller.selectedIndex); - } - - controller.addListener(listener); - return () { - controller.removeListener(listener); - }; - }, [controller]); - useEffect(() { if (!context.mounted) return; if (mediaQuery.lgAndUp && !controller.extended) { @@ -106,6 +87,13 @@ class Sidebar extends HookConsumerWidget { return null; }, [mediaQuery, controller]); + useEffect(() { + if (controller.selectedIndex != selectedIndex) { + controller.selectIndex(selectedIndex); + } + return null; + }, [selectedIndex]); + if (layoutMode == LayoutMode.compact || (mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) { return Scaffold(body: child); @@ -119,23 +107,28 @@ class Sidebar extends HookConsumerWidget { items: sidebarTileList.mapIndexed( (index, e) { return SidebarXItem( - iconWidget: Badge( - backgroundColor: theme.colorScheme.primary, - isLabelVisible: e.title == "Library" && downloadCount > 0, - label: Text( - downloadCount.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 10, + onTap: () { + context.goNamed(e.name); + }, + iconBuilder: (selected, hovered) { + return Badge( + backgroundColor: theme.colorScheme.primary, + isLabelVisible: e.title == "Library" && downloadCount > 0, + label: Text( + downloadCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), ), - ), - child: Icon( - e.icon, - color: selectedIndex == index - ? theme.colorScheme.primary - : null, - ), - ), + child: Icon( + e.icon, + color: selected || hovered + ? theme.colorScheme.primary + : null, + ), + ); + }, label: e.title, ); }, @@ -257,7 +250,7 @@ class SidebarFooter extends HookConsumerWidget { if (mediaQuery.mdAndDown) { return IconButton( icon: const Icon(SpotubeIcons.settings), - onPressed: () => Sidebar.goToSettings(context), + onPressed: () => ServiceUtils.navigateNamed(context, SettingsPage.name), ); } @@ -278,7 +271,7 @@ class SidebarFooter extends HookConsumerWidget { Flexible( child: InkWell( onTap: () { - ServiceUtils.push(context, "/profile"); + ServiceUtils.pushNamed(context, ProfilePage.name); }, borderRadius: BorderRadius.circular(30), child: Row( @@ -310,7 +303,7 @@ class SidebarFooter extends HookConsumerWidget { IconButton( icon: const Icon(SpotubeIcons.settings), onPressed: () { - Sidebar.goToSettings(context); + ServiceUtils.pushNamed(context, SettingsPage.name); }, ), ], diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index 489399e5d..e16ad1a89 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -3,55 +3,54 @@ import 'dart:ui'; import 'package:curved_navigation_bar/curved_navigation_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; -import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/utils/service_utils.dart'; final navigationPanelHeight = StateProvider((ref) => 50); class SpotubeNavigationBar extends HookConsumerWidget { - final int? selectedIndex; - final void Function(int) onSelectedIndexChanged; - const SpotubeNavigationBar({ - required this.selectedIndex, - required this.onSelectedIndexChanged, super.key, }); @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); + final routerState = GoRouterState.of(context); + final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; final mediaQuery = MediaQuery.of(context); final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); - final insideSelectedIndex = useState(selectedIndex ?? 0); - final buttonColor = useBrightnessValue( theme.colorScheme.inversePrimary, theme.colorScheme.primary.withOpacity(0.2), ); - final navbarTileList = - useMemoized(() => getNavbarTileList(context.l10n), [context.l10n]); + final navbarTileList = useMemoized( + () => getNavbarTileList(context.l10n), + [context.l10n], + ); final panelHeight = ref.watch(navigationPanelHeight); - useEffect(() { - if (selectedIndex != null) { - insideSelectedIndex.value = selectedIndex!; - } - return null; - }, [selectedIndex]); + final selectedIndex = useMemoized(() { + final index = navbarTileList.indexWhere( + (e) => routerState.namedLocation(e.name) == routerState.matchedLocation, + ); + + return index == -1 ? 0 : index; + }, [navbarTileList, routerState.matchedLocation]); if (layoutMode == LayoutMode.extended || (mediaQuery.mdAndUp && layoutMode == LayoutMode.adaptive) || @@ -91,14 +90,9 @@ class SpotubeNavigationBar extends HookConsumerWidget { }); }, ).toList(), - index: insideSelectedIndex.value, + index: selectedIndex, onTap: (i) { - insideSelectedIndex.value = i; - if (navbarTileList[i].id == "settings") { - Sidebar.goToSettings(context); - return; - } - onSelectedIndexChanged(i); + ServiceUtils.navigateNamed(context, navbarTileList[i].name); }, ), ), diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/shared/fallbacks/anonymous_fallback.dart index 2f06b0b6f..5ced6bb6d 100644 --- a/lib/components/shared/fallbacks/anonymous_fallback.dart +++ b/lib/components/shared/fallbacks/anonymous_fallback.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -25,7 +26,7 @@ class AnonymousFallback extends ConsumerWidget { const SizedBox(height: 10), FilledButton( child: Text(context.l10n.login_with_spotify), - onPressed: () => ServiceUtils.push(context, "/settings"), + onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name), ) ], ), diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart index e142cb35c..291950bb5 100644 --- a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart +++ b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart @@ -96,7 +96,7 @@ class HorizontalPlaybuttonCardView extends HookWidget { return switch (item) { PlaylistSimple() => PlaylistCard(item as PlaylistSimple), - AlbumSimple() => AlbumCard(item as Album), + AlbumSimple() => AlbumCard(item as AlbumSimple), Artist() => Padding( padding: const EdgeInsets.symmetric( horizontal: 12.0), diff --git a/lib/components/shared/links/artist_link.dart b/lib/components/shared/links/artist_link.dart index af8b186af..5236a0611 100644 --- a/lib/components/shared/links/artist_link.dart +++ b/lib/components/shared/links/artist_link.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/links/anchor_button.dart'; +import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/utils/service_utils.dart'; class ArtistLink extends StatelessWidget { @@ -40,9 +41,12 @@ class ArtistLink extends StatelessWidget { if (onRouteChange != null) { onRouteChange?.call("/artist/${artist.value.id}"); } else { - ServiceUtils.push( + ServiceUtils.pushNamed( context, - "/artist/${artist.value.id}", + ArtistPage.name, + pathParameters: { + "id": artist.value.id!, + }, ); } }, diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/shared/themed_button_tab_bar.dart index 017f04aa8..b21ca9924 100644 --- a/lib/components/shared/themed_button_tab_bar.dart +++ b/lib/components/shared/themed_button_tab_bar.dart @@ -5,7 +5,8 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart'; class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { final List tabs; - const ThemedButtonsTabBar({super.key, required this.tabs}); + final TabController? controller; + const ThemedButtonsTabBar({super.key, required this.tabs, this.controller}); @override Widget build(BuildContext context) { @@ -21,6 +22,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { bottom: 8, ), child: ButtonsTabBar( + controller: controller, radius: 100, decoration: BoxDecoration( color: bgColor, diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index f576ba0a1..c3605f33a 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -17,6 +17,7 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_provider.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -28,6 +29,7 @@ class TrackViewBodySection extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlist = ref.watch(proxyPlaylistProvider); final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final props = InheritedTrackView.of(context); final trackViewState = ref.watch(trackViewProvider(props.tracks)); @@ -146,11 +148,17 @@ class TrackViewBodySection extends HookConsumerWidget { } else { final tracks = await props.pagination.onFetchAll(); await remotePlayback.load( - WebSocketLoadEventData( - tracks: tracks, - collectionId: props.collectionId, - initialIndex: index, - ), + props.collection is AlbumSimple + ? WebSocketLoadEventData.album( + tracks: tracks, + collection: props.collection as AlbumSimple, + initialIndex: index, + ) + : WebSocketLoadEventData.playlist( + tracks: tracks, + collection: props.collection as PlaylistSimple, + initialIndex: index, + ), ); } } else { @@ -164,6 +172,13 @@ class TrackViewBodySection extends HookConsumerWidget { autoPlay: true, ); playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier + .addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier + .addPlaylists([props.collection as PlaylistSimple]); + } } } }, diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/shared/tracks_view/sections/body/track_view_options.dart index ff92b6638..c2adf38bb 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_options.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart'; @@ -8,6 +9,7 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_provider.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; @@ -23,6 +25,7 @@ class TrackViewBodyOptions extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final audioSource = ref.watch(userPreferencesProvider.select((s) => s.audioSource)); @@ -72,6 +75,12 @@ class TrackViewBodyOptions extends HookConsumerWidget { { playlistNotifier.addTracksAtFirst(selectedTracks); playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier.addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier + .addPlaylists([props.collection as PlaylistSimple]); + } trackViewState.deselectAll(); break; } @@ -79,6 +88,12 @@ class TrackViewBodyOptions extends HookConsumerWidget { { playlistNotifier.addTracks(selectedTracks); playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier.addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier + .addPlaylists([props.collection as PlaylistSimple]); + } trackViewState.deselectAll(); break; } diff --git a/lib/components/shared/tracks_view/sections/header/header_actions.dart b/lib/components/shared/tracks_view/sections/header/header_actions.dart index f6880485d..8c1c8e153 100644 --- a/lib/components/shared/tracks_view/sections/header/header_actions.dart +++ b/lib/components/shared/tracks_view/sections/header/header_actions.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/heart_button.dart'; @@ -9,6 +10,7 @@ import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_ import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; class TrackViewHeaderActions extends HookConsumerWidget { @@ -20,6 +22,7 @@ class TrackViewHeaderActions extends HookConsumerWidget { final playlist = ref.watch(proxyPlaylistProvider); final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final isActive = playlist.collections.contains(props.collectionId); @@ -61,6 +64,13 @@ class TrackViewHeaderActions extends HookConsumerWidget { final tracks = await props.pagination.onFetchAll(); await playlistNotifier.addTracks(tracks); playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier + .addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier + .addPlaylists([props.collection as PlaylistSimple]); + } }, ), if (props.onHeart != null && auth != null) diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/shared/tracks_view/sections/header/header_buttons.dart index 50eeb7470..5ffff5122 100644 --- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/shared/tracks_view/sections/header/header_buttons.dart @@ -5,12 +5,14 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/dialogs/select_device_dialog.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -28,6 +30,7 @@ class TrackViewHeaderButtons extends HookConsumerWidget { final props = InheritedTrackView.of(context); final playlist = ref.watch(proxyPlaylistProvider); final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final isActive = playlist.collections.contains(props.collectionId); @@ -52,10 +55,16 @@ class TrackViewHeaderButtons extends HookConsumerWidget { if (isRemoteDevice) { final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( - WebSocketLoadEventData( - tracks: allTracks, - collectionId: props.collectionId, - initialIndex: Random().nextInt(allTracks.length)), + props.collection is AlbumSimple + ? WebSocketLoadEventData.album( + tracks: allTracks, + collection: props.collection as AlbumSimple, + initialIndex: Random().nextInt(allTracks.length)) + : WebSocketLoadEventData.playlist( + tracks: allTracks, + collection: props.collection as PlaylistSimple, + initialIndex: Random().nextInt(allTracks.length), + ), ); await remotePlayback.setShuffle(true); } else { @@ -66,6 +75,11 @@ class TrackViewHeaderButtons extends HookConsumerWidget { ); await audioPlayer.setShuffle(true); playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier.addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier.addPlaylists([props.collection as PlaylistSimple]); + } } } finally { isLoading.value = false; @@ -84,14 +98,24 @@ class TrackViewHeaderButtons extends HookConsumerWidget { if (isRemoteDevice) { final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( - WebSocketLoadEventData( - tracks: allTracks, - collectionId: props.collectionId, - ), + props.collection is AlbumSimple + ? WebSocketLoadEventData.album( + tracks: allTracks, + collection: props.collection as AlbumSimple, + ) + : WebSocketLoadEventData.playlist( + tracks: allTracks, + collection: props.collection as PlaylistSimple, + ), ); } else { await playlistNotifier.load(allTracks, autoPlay: true); playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier.addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier.addPlaylists([props.collection as PlaylistSimple]); + } } } finally { isLoading.value = false; diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/shared/tracks_view/track_view_props.dart index a1a07f84d..b0a00ae29 100644 --- a/lib/components/shared/tracks_view/track_view_props.dart +++ b/lib/components/shared/tracks_view/track_view_props.dart @@ -39,7 +39,7 @@ class PaginationProps { } class InheritedTrackView extends InheritedWidget { - final String collectionId; + final Object collection; final String title; final String? description; final String image; @@ -55,7 +55,7 @@ class InheritedTrackView extends InheritedWidget { const InheritedTrackView({ super.key, required super.child, - required this.collectionId, + required this.collection, required this.title, this.description, required this.image, @@ -65,7 +65,11 @@ class InheritedTrackView extends InheritedWidget { required this.shareUrl, this.isLiked = false, this.onHeart, - }); + }) : assert(collection is AlbumSimple || collection is PlaylistSimple); + + String get collectionId => collection is AlbumSimple + ? (collection as AlbumSimple).id! + : (collection as PlaylistSimple).id!; @override bool updateShouldNotify(InheritedTrackView oldWidget) { @@ -78,7 +82,7 @@ class InheritedTrackView extends InheritedWidget { oldWidget.onHeart != onHeart || oldWidget.shareUrl != shareUrl || oldWidget.routePath != routePath || - oldWidget.collectionId != collectionId || + oldWidget.collection != collection || oldWidget.child != child; } diff --git a/lib/components/stats/common/album_item.dart b/lib/components/stats/common/album_item.dart new file mode 100644 index 000000000..ccc0fa4ec --- /dev/null +++ b/lib/components/stats/common/album_item.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/album/album_card.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/artist_link.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/album/album.dart'; +import 'package:spotube/utils/service_utils.dart'; + +class StatsAlbumItem extends StatelessWidget { + final AlbumSimple album; + final Widget info; + const StatsAlbumItem({super.key, required this.album, required this.info}); + + @override + Widget build(BuildContext context) { + return ListTile( + horizontalTitleGap: 8, + leading: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: UniversalImage( + path: (album.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + width: 40, + height: 40, + ), + ), + title: Text(album.name!), + subtitle: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${album.albumType?.formatted} • "), + Flexible( + child: ArtistLink( + artists: album.artists!, + mainAxisAlignment: WrapAlignment.start, + ), + ), + ], + ), + trailing: info, + onTap: () { + ServiceUtils.pushNamed( + context, + AlbumPage.name, + pathParameters: {"id": album.id!}, + extra: album, + ); + }, + ); + } +} diff --git a/lib/components/stats/common/artist_item.dart b/lib/components/stats/common/artist_item.dart new file mode 100644 index 000000000..9282d4e1b --- /dev/null +++ b/lib/components/stats/common/artist_item.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/artist/artist.dart'; +import 'package:spotube/utils/service_utils.dart'; + +class StatsArtistItem extends StatelessWidget { + final Artist artist; + final Widget info; + const StatsArtistItem({ + super.key, + required this.artist, + required this.info, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(artist.name!), + horizontalTitleGap: 8, + leading: CircleAvatar( + backgroundImage: UniversalImage.imageProvider( + (artist.images).asUrlString( + placeholder: ImagePlaceholder.artist, + ), + ), + ), + trailing: info, + onTap: () { + ServiceUtils.pushNamed( + context, + ArtistPage.name, + pathParameters: {"id": artist.id!}, + ); + }, + ); + } +} diff --git a/lib/components/stats/common/playlist_item.dart b/lib/components/stats/common/playlist_item.dart new file mode 100644 index 000000000..b07311ab4 --- /dev/null +++ b/lib/components/stats/common/playlist_item.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/playbutton_card.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/playlist/playlist.dart'; +import 'package:spotube/utils/service_utils.dart'; + +class StatsPlaylistItem extends StatelessWidget { + final PlaylistSimple playlist; + final Widget info; + const StatsPlaylistItem( + {super.key, required this.playlist, required this.info}); + + @override + Widget build(BuildContext context) { + return ListTile( + horizontalTitleGap: 8, + leading: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: UniversalImage( + path: (playlist.images).asUrlString( + placeholder: ImagePlaceholder.collection, + ), + width: 40, + height: 40, + ), + ), + title: Text(playlist.name!), + subtitle: Text( + playlist.description!.replaceAll(htmlTagRegexp, ''), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: info, + onTap: () { + ServiceUtils.pushNamed( + context, + PlaylistPage.name, + pathParameters: {"id": playlist.id!}, + extra: playlist, + ); + }, + ); + } +} diff --git a/lib/components/stats/common/track_item.dart b/lib/components/stats/common/track_item.dart new file mode 100644 index 000000000..6ba6b8866 --- /dev/null +++ b/lib/components/stats/common/track_item.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/artist_link.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/track/track.dart'; +import 'package:spotube/utils/service_utils.dart'; + +class StatsTrackItem extends StatelessWidget { + final Track track; + final Widget info; + const StatsTrackItem({ + super.key, + required this.track, + required this.info, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + horizontalTitleGap: 8, + leading: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: UniversalImage( + path: (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + width: 40, + height: 40, + ), + ), + title: Text(track.name!), + subtitle: ArtistLink( + artists: track.artists!, + mainAxisAlignment: WrapAlignment.start, + ), + trailing: info, + onTap: () { + ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": track.id!, + }, + ); + }, + ); + } +} diff --git a/lib/components/stats/summary/summary.dart b/lib/components/stats/summary/summary.dart new file mode 100644 index 000000000..61f3bd6c8 --- /dev/null +++ b/lib/components/stats/summary/summary.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/stats/summary/summary_card.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/pages/stats/albums/albums.dart'; +import 'package:spotube/pages/stats/artists/artists.dart'; +import 'package:spotube/pages/stats/fees/fees.dart'; +import 'package:spotube/pages/stats/minutes/minutes.dart'; +import 'package:spotube/pages/stats/playlists/playlists.dart'; +import 'package:spotube/pages/stats/streams/streams.dart'; +import 'package:spotube/provider/history/summary.dart'; +import 'package:spotube/utils/service_utils.dart'; + +class StatsPageSummarySection extends HookConsumerWidget { + const StatsPageSummarySection({super.key}); + + @override + Widget build(BuildContext context, ref) { + final summary = ref.watch(playbackHistorySummaryProvider); + + return SliverPadding( + padding: const EdgeInsets.all(10), + sliver: SliverLayoutBuilder(builder: (context, constrains) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: constrains.isXs + ? 2 + : constrains.smAndDown + ? 3 + : constrains.mdAndDown + ? 4 + : constrains.lgAndDown + ? 5 + : 6, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: constrains.isXs ? 1.3 : 1.5, + ), + delegate: SliverChildListDelegate([ + SummaryCard( + title: summary.duration.inMinutes.toDouble(), + unit: "minutes", + description: 'Listened to music', + color: Colors.purple, + onTap: () { + ServiceUtils.pushNamed(context, StatsMinutesPage.name); + }, + ), + SummaryCard( + title: summary.tracks.toDouble(), + unit: "songs", + description: 'Streamed overall', + color: Colors.lightBlue, + onTap: () { + ServiceUtils.pushNamed(context, StatsStreamsPage.name); + }, + ), + SummaryCard.unformatted( + title: usdFormatter.format(summary.fees.toDouble()), + unit: "", + description: 'Owed to artists\nthis month', + color: Colors.green, + onTap: () { + ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); + }, + ), + SummaryCard( + title: summary.artists.toDouble(), + unit: "artist's", + description: 'Music reached you', + color: Colors.yellow, + onTap: () { + ServiceUtils.pushNamed(context, StatsArtistsPage.name); + }, + ), + SummaryCard( + title: summary.albums.toDouble(), + unit: "full albums", + description: 'Got your love', + color: Colors.pink, + onTap: () { + ServiceUtils.pushNamed(context, StatsAlbumsPage.name); + }, + ), + SummaryCard( + title: summary.playlists.toDouble(), + unit: "playlists", + description: 'Were on repeat', + color: Colors.teal, + onTap: () { + ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); + }, + ), + ]), + ); + }), + ); + } +} diff --git a/lib/components/stats/summary/summary_card.dart b/lib/components/stats/summary/summary_card.dart new file mode 100644 index 000000000..243c50e87 --- /dev/null +++ b/lib/components/stats/summary/summary_card.dart @@ -0,0 +1,86 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:spotube/collections/formatters.dart'; + +class SummaryCard extends StatelessWidget { + final String title; + final String unit; + final String description; + final VoidCallback? onTap; + + final MaterialColor color; + + SummaryCard({ + super.key, + required double title, + required this.unit, + required this.description, + required this.color, + this.onTap, + }) : title = compactNumberFormatter.format(title); + + const SummaryCard.unformatted({ + super.key, + required this.title, + required this.unit, + required this.description, + required this.color, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final ThemeData(:textTheme, :brightness) = Theme.of(context); + + final descriptionNewLines = description.split("").where((s) => s == "\n"); + + return Card( + color: brightness == Brightness.dark ? color.shade100 : color.shade50, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AutoSizeText.rich( + TextSpan( + children: [ + TextSpan( + text: title, + style: textTheme.headlineLarge?.copyWith( + color: color.shade900, + ), + ), + TextSpan( + text: " $unit", + style: textTheme.titleMedium?.copyWith( + color: color.shade900, + ), + ), + ], + ), + maxLines: 1, + ), + const Gap(5), + AutoSizeText( + description, + maxLines: description.contains("\n") + ? descriptionNewLines.length + 1 + : 1, + minFontSize: 9, + style: textTheme.labelMedium!.copyWith( + color: color.shade900, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/stats/top/albums.dart b/lib/components/stats/top/albums.dart new file mode 100644 index 000000000..51bcf5b07 --- /dev/null +++ b/lib/components/stats/top/albums.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/stats/common/album_item.dart'; +import 'package:spotube/provider/history/top.dart'; + +class TopAlbums extends HookConsumerWidget { + const TopAlbums({super.key}); + + @override + Widget build(BuildContext context, ref) { + final historyDuration = ref.watch(playbackHistoryTopDurationProvider); + final albums = ref.watch(playbackHistoryTopProvider(historyDuration) + .select((value) => value.albums)); + + return SliverList.builder( + itemCount: albums.length, + itemBuilder: (context, index) { + final album = albums[index]; + return StatsAlbumItem( + album: album.album, + info: Text( + "${compactNumberFormatter.format(album.count)} plays", + ), + ); + }, + ); + } +} diff --git a/lib/components/stats/top/artists.dart b/lib/components/stats/top/artists.dart new file mode 100644 index 000000000..d6d0c98d3 --- /dev/null +++ b/lib/components/stats/top/artists.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/stats/common/artist_item.dart'; +import 'package:spotube/provider/history/top.dart'; + +class TopArtists extends HookConsumerWidget { + const TopArtists({super.key}); + + @override + Widget build(BuildContext context, ref) { + final historyDuration = ref.watch(playbackHistoryTopDurationProvider); + final artists = ref.watch(playbackHistoryTopProvider(historyDuration) + .select((value) => value.artists)); + + return SliverList.builder( + itemCount: artists.length, + itemBuilder: (context, index) { + final artist = artists[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text("${compactNumberFormatter.format(artist.count)} plays"), + ); + }, + ); + } +} diff --git a/lib/components/stats/top/top.dart b/lib/components/stats/top/top.dart new file mode 100644 index 000000000..df1275e83 --- /dev/null +++ b/lib/components/stats/top/top.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/shared/themed_button_tab_bar.dart'; +import 'package:spotube/components/stats/top/albums.dart'; +import 'package:spotube/components/stats/top/artists.dart'; +import 'package:spotube/components/stats/top/tracks.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsPageTopSection extends HookConsumerWidget { + const StatsPageTopSection({super.key}); + + @override + Widget build(BuildContext context, ref) { + final tabController = useTabController(initialLength: 3); + final historyDuration = ref.watch(playbackHistoryTopDurationProvider); + final historyDurationNotifier = + ref.watch(playbackHistoryTopDurationProvider.notifier); + + return SliverMainAxisGroup( + slivers: [ + SliverAppBar( + floating: true, + flexibleSpace: ThemedButtonsTabBar( + controller: tabController, + tabs: const [ + Tab( + child: Padding( + padding: EdgeInsets.all(5), + child: Text("Top Tracks"), + ), + ), + Tab( + child: Padding( + padding: EdgeInsets.all(5), + child: Text("Top Artists"), + ), + ), + Tab( + child: Padding( + padding: EdgeInsets.all(5), + child: Text("Top Albums"), + ), + ), + ], + ), + ), + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerRight, + child: DropdownButton( + style: Theme.of(context).textTheme.bodySmall!, + isDense: true, + padding: const EdgeInsets.all(4), + borderRadius: BorderRadius.circular(4), + underline: const SizedBox(), + value: historyDuration, + onChanged: (value) { + if (value == null) return; + historyDurationNotifier.update((_) => value); + }, + icon: const Icon(Icons.arrow_drop_down), + items: const [ + DropdownMenuItem( + value: HistoryDuration.days7, + child: Text("This week"), + ), + DropdownMenuItem( + value: HistoryDuration.days30, + child: Text("This month"), + ), + DropdownMenuItem( + value: HistoryDuration.months6, + child: Text("Last 6 months"), + ), + DropdownMenuItem( + value: HistoryDuration.year, + child: Text("This year"), + ), + DropdownMenuItem( + value: HistoryDuration.years2, + child: Text("Last 2 years"), + ), + DropdownMenuItem( + value: HistoryDuration.allTime, + child: Text("All time"), + ), + ], + ), + ), + ), + ListenableBuilder( + listenable: tabController, + builder: (context, _) { + return switch (tabController.index) { + 1 => const TopArtists(), + 2 => const TopAlbums(), + _ => const TopTracks(), + }; + }, + ), + ], + ); + } +} diff --git a/lib/components/stats/top/tracks.dart b/lib/components/stats/top/tracks.dart new file mode 100644 index 000000000..bffa4ecd5 --- /dev/null +++ b/lib/components/stats/top/tracks.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/stats/common/track_item.dart'; +import 'package:spotube/provider/history/top.dart'; + +class TopTracks extends HookConsumerWidget { + const TopTracks({super.key}); + + @override + Widget build(BuildContext context, ref) { + final historyDuration = ref.watch(playbackHistoryTopDurationProvider); + final tracks = ref.watch( + playbackHistoryTopProvider(historyDuration) + .select((value) => value.tracks), + ); + + return SliverList.builder( + itemCount: tracks.length, + itemBuilder: (context, index) { + final track = tracks[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count)} plays", + ), + ); + }, + ); + } +} diff --git a/lib/extensions/album_simple.dart b/lib/extensions/album_simple.dart index 7c8ae09e8..5678390c4 100644 --- a/lib/extensions/album_simple.dart +++ b/lib/extensions/album_simple.dart @@ -1,21 +1,6 @@ import 'package:spotify/spotify.dart'; extension AlbumExtensions on AlbumSimple { - Map toJson() { - return { - "albumType": albumType?.name, - "id": id, - "name": name, - "images": images - ?.map((image) => { - "height": image.height, - "url": image.url, - "width": image.width, - }) - .toList(), - }; - } - Album toAlbum() { Album album = Album(); album.albumType = albumType; diff --git a/lib/extensions/artist_simple.dart b/lib/extensions/artist_simple.dart index 6a80300ea..7997355d6 100644 --- a/lib/extensions/artist_simple.dart +++ b/lib/extensions/artist_simple.dart @@ -1,17 +1,5 @@ import 'package:spotify/spotify.dart'; -extension ArtistJson on ArtistSimple { - Map toJson() { - return { - "href": href, - "id": id, - "name": name, - "type": type, - "uri": uri, - }; - } -} - extension ArtistExtension on List { String asString() { return map((e) => e.name?.replaceAll(",", " ")).join(", "); diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart index 9755179db..02c0c4927 100644 --- a/lib/extensions/track.dart +++ b/lib/extensions/track.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/album_simple.dart'; -import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; extension TrackExtensions on Track { @@ -39,33 +37,6 @@ extension TrackExtensions on Track { return this; } - - Map toJson() { - return TrackExtensions.trackToJson(this); - } - - static Map trackToJson(Track track) { - return { - "album": track.album?.toJson(), - "artists": track.artists?.map((artist) => artist.toJson()).toList(), - "available_markets": track.availableMarkets?.map((e) => e.name).toList(), - "disc_number": track.discNumber, - "duration_ms": track.durationMs, - "explicit": track.explicit, - // "external_ids"track.: externalIds, - // "external_urls"track.: externalUrls, - "href": track.href, - "id": track.id, - "is_playable": track.isPlayable, - // "linked_from"track.: linkedFrom, - "name": track.name, - "popularity": track.popularity, - "preview_rrl": track.previewUrl, - "track_number": track.trackNumber, - "type": track.type, - "uri": track.uri, - }; - } } extension TrackSimpleExtensions on TrackSimple { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a90fd35e9..04fc85666 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -324,5 +324,6 @@ "select": "Select", "connect_client_alert": "You're being controlled by {client}", "this_device": "This Device", - "remote": "Remote" -} + "remote": "Remote", + "stats": "Stats" +} \ No newline at end of file diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart index efb373150..283860505 100644 --- a/lib/models/connect/connect.dart +++ b/lib/models/connect/connect.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/services/audio_player/loop_mode.dart'; diff --git a/lib/models/connect/connect.freezed.dart b/lib/models/connect/connect.freezed.dart index face800e6..088cfbd1a 100644 --- a/lib/models/connect/connect.freezed.dart +++ b/lib/models/connect/connect.freezed.dart @@ -16,16 +16,89 @@ final _privateConstructorUsedError = UnsupportedError( WebSocketLoadEventData _$WebSocketLoadEventDataFromJson( Map json) { - return _WebSocketLoadEventData.fromJson(json); + switch (json['runtimeType']) { + case 'playlist': + return WebSocketLoadEventDataPlaylist.fromJson(json); + case 'album': + return WebSocketLoadEventDataAlbum.fromJson(json); + + default: + throw CheckedFromJsonException( + json, + 'runtimeType', + 'WebSocketLoadEventData', + 'Invalid union type "${json['runtimeType']}"!'); + } } /// @nodoc mixin _$WebSocketLoadEventData { @JsonKey(name: 'tracks', toJson: _tracksJson) List get tracks => throw _privateConstructorUsedError; - String? get collectionId => throw _privateConstructorUsedError; + Object? get collection => throw _privateConstructorUsedError; int? get initialIndex => throw _privateConstructorUsedError; - + @optionalTypeArgs + TResult when({ + required TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex) + playlist, + required TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex) + album, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex)? + playlist, + TResult? Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex)? + album, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex)? + playlist, + TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex)? + album, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(WebSocketLoadEventDataPlaylist value) playlist, + required TResult Function(WebSocketLoadEventDataAlbum value) album, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(WebSocketLoadEventDataPlaylist value)? playlist, + TResult? Function(WebSocketLoadEventDataAlbum value)? album, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(WebSocketLoadEventDataPlaylist value)? playlist, + TResult Function(WebSocketLoadEventDataAlbum value)? album, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $WebSocketLoadEventDataCopyWith get copyWith => @@ -40,7 +113,6 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> { @useResult $Res call( {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - String? collectionId, int? initialIndex}); } @@ -59,7 +131,6 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res, @override $Res call({ Object? tracks = null, - Object? collectionId = freezed, Object? initialIndex = freezed, }) { return _then(_value.copyWith( @@ -67,10 +138,6 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res, ? _value.tracks : tracks // ignore: cast_nullable_to_non_nullable as List, - collectionId: freezed == collectionId - ? _value.collectionId - : collectionId // ignore: cast_nullable_to_non_nullable - as String?, initialIndex: freezed == initialIndex ? _value.initialIndex : initialIndex // ignore: cast_nullable_to_non_nullable @@ -80,46 +147,279 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res, } /// @nodoc -abstract class _$$WebSocketLoadEventDataImplCopyWith<$Res> +abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res> + implements $WebSocketLoadEventDataCopyWith<$Res> { + factory _$$WebSocketLoadEventDataPlaylistImplCopyWith( + _$WebSocketLoadEventDataPlaylistImpl value, + $Res Function(_$WebSocketLoadEventDataPlaylistImpl) then) = + __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex}); +} + +/// @nodoc +class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res> + extends _$WebSocketLoadEventDataCopyWithImpl<$Res, + _$WebSocketLoadEventDataPlaylistImpl> + implements _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res> { + __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl( + _$WebSocketLoadEventDataPlaylistImpl _value, + $Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tracks = null, + Object? collection = freezed, + Object? initialIndex = freezed, + }) { + return _then(_$WebSocketLoadEventDataPlaylistImpl( + tracks: null == tracks + ? _value._tracks + : tracks // ignore: cast_nullable_to_non_nullable + as List, + collection: freezed == collection + ? _value.collection + : collection // ignore: cast_nullable_to_non_nullable + as PlaylistSimple?, + initialIndex: freezed == initialIndex + ? _value.initialIndex + : initialIndex // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$WebSocketLoadEventDataPlaylistImpl + extends WebSocketLoadEventDataPlaylist { + _$WebSocketLoadEventDataPlaylistImpl( + {@JsonKey(name: 'tracks', toJson: _tracksJson) + required final List tracks, + this.collection, + this.initialIndex, + final String? $type}) + : _tracks = tracks, + $type = $type ?? 'playlist', + super._(); + + factory _$WebSocketLoadEventDataPlaylistImpl.fromJson( + Map json) => + _$$WebSocketLoadEventDataPlaylistImplFromJson(json); + + final List _tracks; + @override + @JsonKey(name: 'tracks', toJson: _tracksJson) + List get tracks { + if (_tracks is EqualUnmodifiableListView) return _tracks; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tracks); + } + + @override + final PlaylistSimple? collection; + @override + final int? initialIndex; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'WebSocketLoadEventData.playlist(tracks: $tracks, collection: $collection, initialIndex: $initialIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$WebSocketLoadEventDataPlaylistImpl && + const DeepCollectionEquality().equals(other._tracks, _tracks) && + (identical(other.collection, collection) || + other.collection == collection) && + (identical(other.initialIndex, initialIndex) || + other.initialIndex == initialIndex)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(_tracks), collection, initialIndex); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$WebSocketLoadEventDataPlaylistImplCopyWith< + _$WebSocketLoadEventDataPlaylistImpl> + get copyWith => __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl< + _$WebSocketLoadEventDataPlaylistImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex) + playlist, + required TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex) + album, + }) { + return playlist(tracks, collection, initialIndex); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex)? + playlist, + TResult? Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex)? + album, + }) { + return playlist?.call(tracks, collection, initialIndex); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex)? + playlist, + TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex)? + album, + required TResult orElse(), + }) { + if (playlist != null) { + return playlist(tracks, collection, initialIndex); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(WebSocketLoadEventDataPlaylist value) playlist, + required TResult Function(WebSocketLoadEventDataAlbum value) album, + }) { + return playlist(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(WebSocketLoadEventDataPlaylist value)? playlist, + TResult? Function(WebSocketLoadEventDataAlbum value)? album, + }) { + return playlist?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(WebSocketLoadEventDataPlaylist value)? playlist, + TResult Function(WebSocketLoadEventDataAlbum value)? album, + required TResult orElse(), + }) { + if (playlist != null) { + return playlist(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$WebSocketLoadEventDataPlaylistImplToJson( + this, + ); + } +} + +abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData { + factory WebSocketLoadEventDataPlaylist( + {@JsonKey(name: 'tracks', toJson: _tracksJson) + required final List tracks, + final PlaylistSimple? collection, + final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl; + WebSocketLoadEventDataPlaylist._() : super._(); + + factory WebSocketLoadEventDataPlaylist.fromJson(Map json) = + _$WebSocketLoadEventDataPlaylistImpl.fromJson; + + @override + @JsonKey(name: 'tracks', toJson: _tracksJson) + List get tracks; + @override + PlaylistSimple? get collection; + @override + int? get initialIndex; + @override + @JsonKey(ignore: true) + _$$WebSocketLoadEventDataPlaylistImplCopyWith< + _$WebSocketLoadEventDataPlaylistImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res> implements $WebSocketLoadEventDataCopyWith<$Res> { - factory _$$WebSocketLoadEventDataImplCopyWith( - _$WebSocketLoadEventDataImpl value, - $Res Function(_$WebSocketLoadEventDataImpl) then) = - __$$WebSocketLoadEventDataImplCopyWithImpl<$Res>; + factory _$$WebSocketLoadEventDataAlbumImplCopyWith( + _$WebSocketLoadEventDataAlbumImpl value, + $Res Function(_$WebSocketLoadEventDataAlbumImpl) then) = + __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>; @override @useResult $Res call( {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - String? collectionId, + AlbumSimple? collection, int? initialIndex}); } /// @nodoc -class __$$WebSocketLoadEventDataImplCopyWithImpl<$Res> +class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res> extends _$WebSocketLoadEventDataCopyWithImpl<$Res, - _$WebSocketLoadEventDataImpl> - implements _$$WebSocketLoadEventDataImplCopyWith<$Res> { - __$$WebSocketLoadEventDataImplCopyWithImpl( - _$WebSocketLoadEventDataImpl _value, - $Res Function(_$WebSocketLoadEventDataImpl) _then) + _$WebSocketLoadEventDataAlbumImpl> + implements _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res> { + __$$WebSocketLoadEventDataAlbumImplCopyWithImpl( + _$WebSocketLoadEventDataAlbumImpl _value, + $Res Function(_$WebSocketLoadEventDataAlbumImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ Object? tracks = null, - Object? collectionId = freezed, + Object? collection = freezed, Object? initialIndex = freezed, }) { - return _then(_$WebSocketLoadEventDataImpl( + return _then(_$WebSocketLoadEventDataAlbumImpl( tracks: null == tracks ? _value._tracks : tracks // ignore: cast_nullable_to_non_nullable as List, - collectionId: freezed == collectionId - ? _value.collectionId - : collectionId // ignore: cast_nullable_to_non_nullable - as String?, + collection: freezed == collection + ? _value.collection + : collection // ignore: cast_nullable_to_non_nullable + as AlbumSimple?, initialIndex: freezed == initialIndex ? _value.initialIndex : initialIndex // ignore: cast_nullable_to_non_nullable @@ -130,16 +430,20 @@ class __$$WebSocketLoadEventDataImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData { - _$WebSocketLoadEventDataImpl( +class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { + _$WebSocketLoadEventDataAlbumImpl( {@JsonKey(name: 'tracks', toJson: _tracksJson) required final List tracks, - this.collectionId, - this.initialIndex}) - : _tracks = tracks; + this.collection, + this.initialIndex, + final String? $type}) + : _tracks = tracks, + $type = $type ?? 'album', + super._(); - factory _$WebSocketLoadEventDataImpl.fromJson(Map json) => - _$$WebSocketLoadEventDataImplFromJson(json); + factory _$WebSocketLoadEventDataAlbumImpl.fromJson( + Map json) => + _$$WebSocketLoadEventDataAlbumImplFromJson(json); final List _tracks; @override @@ -151,23 +455,26 @@ class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData { } @override - final String? collectionId; + final AlbumSimple? collection; @override final int? initialIndex; + @JsonKey(name: 'runtimeType') + final String $type; + @override String toString() { - return 'WebSocketLoadEventData(tracks: $tracks, collectionId: $collectionId, initialIndex: $initialIndex)'; + return 'WebSocketLoadEventData.album(tracks: $tracks, collection: $collection, initialIndex: $initialIndex)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$WebSocketLoadEventDataImpl && + other is _$WebSocketLoadEventDataAlbumImpl && const DeepCollectionEquality().equals(other._tracks, _tracks) && - (identical(other.collectionId, collectionId) || - other.collectionId == collectionId) && + (identical(other.collection, collection) || + other.collection == collection) && (identical(other.initialIndex, initialIndex) || other.initialIndex == initialIndex)); } @@ -175,42 +482,129 @@ class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData { @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(_tracks), collectionId, initialIndex); + const DeepCollectionEquality().hash(_tracks), collection, initialIndex); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl> - get copyWith => __$$WebSocketLoadEventDataImplCopyWithImpl< - _$WebSocketLoadEventDataImpl>(this, _$identity); + _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> + get copyWith => __$$WebSocketLoadEventDataAlbumImplCopyWithImpl< + _$WebSocketLoadEventDataAlbumImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex) + playlist, + required TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex) + album, + }) { + return album(tracks, collection, initialIndex); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex)? + playlist, + TResult? Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex)? + album, + }) { + return album?.call(tracks, collection, initialIndex); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + PlaylistSimple? collection, + int? initialIndex)? + playlist, + TResult Function( + @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + AlbumSimple? collection, + int? initialIndex)? + album, + required TResult orElse(), + }) { + if (album != null) { + return album(tracks, collection, initialIndex); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(WebSocketLoadEventDataPlaylist value) playlist, + required TResult Function(WebSocketLoadEventDataAlbum value) album, + }) { + return album(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(WebSocketLoadEventDataPlaylist value)? playlist, + TResult? Function(WebSocketLoadEventDataAlbum value)? album, + }) { + return album?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(WebSocketLoadEventDataPlaylist value)? playlist, + TResult Function(WebSocketLoadEventDataAlbum value)? album, + required TResult orElse(), + }) { + if (album != null) { + return album(this); + } + return orElse(); + } @override Map toJson() { - return _$$WebSocketLoadEventDataImplToJson( + return _$$WebSocketLoadEventDataAlbumImplToJson( this, ); } } -abstract class _WebSocketLoadEventData implements WebSocketLoadEventData { - factory _WebSocketLoadEventData( +abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData { + factory WebSocketLoadEventDataAlbum( {@JsonKey(name: 'tracks', toJson: _tracksJson) required final List tracks, - final String? collectionId, - final int? initialIndex}) = _$WebSocketLoadEventDataImpl; + final AlbumSimple? collection, + final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl; + WebSocketLoadEventDataAlbum._() : super._(); - factory _WebSocketLoadEventData.fromJson(Map json) = - _$WebSocketLoadEventDataImpl.fromJson; + factory WebSocketLoadEventDataAlbum.fromJson(Map json) = + _$WebSocketLoadEventDataAlbumImpl.fromJson; @override @JsonKey(name: 'tracks', toJson: _tracksJson) List get tracks; @override - String? get collectionId; + AlbumSimple? get collection; @override int? get initialIndex; @override @JsonKey(ignore: true) - _$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl> + _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/connect/connect.g.dart b/lib/models/connect/connect.g.dart index f636e0350..f297024b9 100644 --- a/lib/models/connect/connect.g.dart +++ b/lib/models/connect/connect.g.dart @@ -6,20 +6,48 @@ part of 'connect.dart'; // JsonSerializableGenerator // ************************************************************************** -_$WebSocketLoadEventDataImpl _$$WebSocketLoadEventDataImplFromJson( - Map json) => - _$WebSocketLoadEventDataImpl( +_$WebSocketLoadEventDataPlaylistImpl + _$$WebSocketLoadEventDataPlaylistImplFromJson(Map json) => + _$WebSocketLoadEventDataPlaylistImpl( + tracks: (json['tracks'] as List) + .map((e) => Track.fromJson(Map.from(e as Map))) + .toList(), + collection: json['collection'] == null + ? null + : PlaylistSimple.fromJson( + Map.from(json['collection'] as Map)), + initialIndex: json['initialIndex'] as int?, + $type: json['runtimeType'] as String?, + ); + +Map _$$WebSocketLoadEventDataPlaylistImplToJson( + _$WebSocketLoadEventDataPlaylistImpl instance) => + { + 'tracks': _tracksJson(instance.tracks), + 'collection': instance.collection?.toJson(), + 'initialIndex': instance.initialIndex, + 'runtimeType': instance.$type, + }; + +_$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson( + Map json) => + _$WebSocketLoadEventDataAlbumImpl( tracks: (json['tracks'] as List) - .map((e) => Track.fromJson(e as Map)) + .map((e) => Track.fromJson(Map.from(e as Map))) .toList(), - collectionId: json['collectionId'] as String?, + collection: json['collection'] == null + ? null + : AlbumSimple.fromJson( + Map.from(json['collection'] as Map)), initialIndex: json['initialIndex'] as int?, + $type: json['runtimeType'] as String?, ); -Map _$$WebSocketLoadEventDataImplToJson( - _$WebSocketLoadEventDataImpl instance) => +Map _$$WebSocketLoadEventDataAlbumImplToJson( + _$WebSocketLoadEventDataAlbumImpl instance) => { 'tracks': _tracksJson(instance.tracks), - 'collectionId': instance.collectionId, + 'collection': instance.collection?.toJson(), 'initialIndex': instance.initialIndex, + 'runtimeType': instance.$type, }; diff --git a/lib/models/connect/load.dart b/lib/models/connect/load.dart index d750cddd2..bf0e164db 100644 --- a/lib/models/connect/load.dart +++ b/lib/models/connect/load.dart @@ -6,14 +6,27 @@ List> _tracksJson(List tracks) { @freezed class WebSocketLoadEventData with _$WebSocketLoadEventData { - factory WebSocketLoadEventData({ + const WebSocketLoadEventData._(); + + factory WebSocketLoadEventData.playlist({ @JsonKey(name: 'tracks', toJson: _tracksJson) required List tracks, - String? collectionId, + PlaylistSimple? collection, int? initialIndex, - }) = _WebSocketLoadEventData; + }) = WebSocketLoadEventDataPlaylist; + + factory WebSocketLoadEventData.album({ + @JsonKey(name: 'tracks', toJson: _tracksJson) required List tracks, + AlbumSimple? collection, + int? initialIndex, + }) = WebSocketLoadEventDataAlbum; factory WebSocketLoadEventData.fromJson(Map json) => _$WebSocketLoadEventDataFromJson(json); + + String? get collectionId => when( + playlist: (tracks, collection, _) => collection?.id, + album: (tracks, collection, _) => collection?.id, + ); } class WebSocketLoadEvent extends WebSocketEvent { diff --git a/lib/models/current_playlist.dart b/lib/models/current_playlist.dart index 53ea2799b..7e55e3939 100644 --- a/lib/models/current_playlist.dart +++ b/lib/models/current_playlist.dart @@ -1,6 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; class CurrentPlaylist { diff --git a/lib/models/local_track.dart b/lib/models/local_track.dart index 923f5f261..def3b64f9 100644 --- a/lib/models/local_track.dart +++ b/lib/models/local_track.dart @@ -1,5 +1,4 @@ import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; class LocalTrack extends Track { final String path; @@ -35,9 +34,10 @@ class LocalTrack extends Track { ); } + @override Map toJson() { return { - ...TrackExtensions.trackToJson(this), + ...super.toJson(), 'path': path, }; } diff --git a/lib/models/source_match.g.dart b/lib/models/source_match.g.dart index 11f34bf34..3b469694a 100644 --- a/lib/models/source_match.g.dart +++ b/lib/models/source_match.g.dart @@ -97,7 +97,7 @@ class SourceTypeAdapter extends TypeAdapter { // JsonSerializableGenerator // ************************************************************************** -SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( +SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( id: json['id'] as String, sourceId: json['sourceId'] as String, sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']), diff --git a/lib/models/spotify/home_feed.g.dart b/lib/models/spotify/home_feed.g.dart index 73a4f9093..fceb3db42 100644 --- a/lib/models/spotify/home_feed.g.dart +++ b/lib/models/spotify/home_feed.g.dart @@ -6,14 +6,13 @@ part of 'home_feed.dart'; // JsonSerializableGenerator // ************************************************************************** -_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson( - Map json) => +_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson(Map json) => _$SpotifySectionPlaylistImpl( description: json['description'] as String, format: json['format'] as String, images: (json['images'] as List) - .map((e) => - SpotifySectionItemImage.fromJson(e as Map)) + .map((e) => SpotifySectionItemImage.fromJson( + Map.from(e as Map))) .toList(), name: json['name'] as String, owner: json['owner'] as String, @@ -25,20 +24,19 @@ Map _$$SpotifySectionPlaylistImplToJson( { 'description': instance.description, 'format': instance.format, - 'images': instance.images, + 'images': instance.images.map((e) => e.toJson()).toList(), 'name': instance.name, 'owner': instance.owner, 'uri': instance.uri, }; -_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson( - Map json) => +_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson(Map json) => _$SpotifySectionArtistImpl( name: json['name'] as String, uri: json['uri'] as String, images: (json['images'] as List) - .map((e) => - SpotifySectionItemImage.fromJson(e as Map)) + .map((e) => SpotifySectionItemImage.fromJson( + Map.from(e as Map))) .toList(), ); @@ -47,19 +45,18 @@ Map _$$SpotifySectionArtistImplToJson( { 'name': instance.name, 'uri': instance.uri, - 'images': instance.images, + 'images': instance.images.map((e) => e.toJson()).toList(), }; -_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson( - Map json) => +_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(Map json) => _$SpotifySectionAlbumImpl( artists: (json['artists'] as List) - .map((e) => - SpotifySectionAlbumArtist.fromJson(e as Map)) + .map((e) => SpotifySectionAlbumArtist.fromJson( + Map.from(e as Map))) .toList(), images: (json['images'] as List) - .map((e) => - SpotifySectionItemImage.fromJson(e as Map)) + .map((e) => SpotifySectionItemImage.fromJson( + Map.from(e as Map))) .toList(), name: json['name'] as String, uri: json['uri'] as String, @@ -68,14 +65,14 @@ _$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson( Map _$$SpotifySectionAlbumImplToJson( _$SpotifySectionAlbumImpl instance) => { - 'artists': instance.artists, - 'images': instance.images, + 'artists': instance.artists.map((e) => e.toJson()).toList(), + 'images': instance.images.map((e) => e.toJson()).toList(), 'name': instance.name, 'uri': instance.uri, }; _$SpotifySectionAlbumArtistImpl _$$SpotifySectionAlbumArtistImplFromJson( - Map json) => + Map json) => _$SpotifySectionAlbumArtistImpl( name: json['name'] as String, uri: json['uri'] as String, @@ -89,7 +86,7 @@ Map _$$SpotifySectionAlbumArtistImplToJson( }; _$SpotifySectionItemImageImpl _$$SpotifySectionItemImageImplFromJson( - Map json) => + Map json) => _$SpotifySectionItemImageImpl( height: json['height'] as num?, url: json['url'] as String, @@ -105,40 +102,40 @@ Map _$$SpotifySectionItemImageImplToJson( }; _$SpotifyHomeFeedSectionItemImpl _$$SpotifyHomeFeedSectionItemImplFromJson( - Map json) => + Map json) => _$SpotifyHomeFeedSectionItemImpl( typename: json['typename'] as String, playlist: json['playlist'] == null ? null : SpotifySectionPlaylist.fromJson( - json['playlist'] as Map), + Map.from(json['playlist'] as Map)), artist: json['artist'] == null ? null : SpotifySectionArtist.fromJson( - json['artist'] as Map), + Map.from(json['artist'] as Map)), album: json['album'] == null ? null - : SpotifySectionAlbum.fromJson(json['album'] as Map), + : SpotifySectionAlbum.fromJson( + Map.from(json['album'] as Map)), ); Map _$$SpotifyHomeFeedSectionItemImplToJson( _$SpotifyHomeFeedSectionItemImpl instance) => { 'typename': instance.typename, - 'playlist': instance.playlist, - 'artist': instance.artist, - 'album': instance.album, + 'playlist': instance.playlist?.toJson(), + 'artist': instance.artist?.toJson(), + 'album': instance.album?.toJson(), }; -_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson( - Map json) => +_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson(Map json) => _$SpotifyHomeFeedSectionImpl( typename: json['typename'] as String, title: json['title'] as String?, uri: json['uri'] as String, items: (json['items'] as List) - .map((e) => - SpotifyHomeFeedSectionItem.fromJson(e as Map)) + .map((e) => SpotifyHomeFeedSectionItem.fromJson( + Map.from(e as Map))) .toList(), ); @@ -148,16 +145,15 @@ Map _$$SpotifyHomeFeedSectionImplToJson( 'typename': instance.typename, 'title': instance.title, 'uri': instance.uri, - 'items': instance.items, + 'items': instance.items.map((e) => e.toJson()).toList(), }; -_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson( - Map json) => +_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson(Map json) => _$SpotifyHomeFeedImpl( greeting: json['greeting'] as String, sections: (json['sections'] as List) - .map( - (e) => SpotifyHomeFeedSection.fromJson(e as Map)) + .map((e) => SpotifyHomeFeedSection.fromJson( + Map.from(e as Map))) .toList(), ); @@ -165,5 +161,5 @@ Map _$$SpotifyHomeFeedImplToJson( _$SpotifyHomeFeedImpl instance) => { 'greeting': instance.greeting, - 'sections': instance.sections, + 'sections': instance.sections.map((e) => e.toJson()).toList(), }; diff --git a/lib/models/spotify/recommendation_seeds.g.dart b/lib/models/spotify/recommendation_seeds.g.dart index bdfa3a074..accb2ed1d 100644 --- a/lib/models/spotify/recommendation_seeds.g.dart +++ b/lib/models/spotify/recommendation_seeds.g.dart @@ -6,8 +6,7 @@ part of 'recommendation_seeds.dart'; // JsonSerializableGenerator // ************************************************************************** -_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson( - Map json) => +_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(Map json) => _$RecommendationSeedsImpl( acousticness: json['acousticness'] as num?, danceability: json['danceability'] as num?, diff --git a/lib/models/spotify_friends.g.dart b/lib/models/spotify_friends.g.dart index 4a32dd094..a1248429e 100644 --- a/lib/models/spotify_friends.g.dart +++ b/lib/models/spotify_friends.g.dart @@ -6,60 +6,55 @@ part of 'spotify_friends.dart'; // JsonSerializableGenerator // ************************************************************************** -SpotifyFriend _$SpotifyFriendFromJson(Map json) => - SpotifyFriend( +SpotifyFriend _$SpotifyFriendFromJson(Map json) => SpotifyFriend( uri: json['uri'] as String, name: json['name'] as String, imageUrl: json['imageUrl'] as String, ); -SpotifyActivityArtist _$SpotifyActivityArtistFromJson( - Map json) => +SpotifyActivityArtist _$SpotifyActivityArtistFromJson(Map json) => SpotifyActivityArtist( uri: json['uri'] as String, name: json['name'] as String, ); -SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson( - Map json) => +SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson(Map json) => SpotifyActivityAlbum( uri: json['uri'] as String, name: json['name'] as String, ); -SpotifyActivityContext _$SpotifyActivityContextFromJson( - Map json) => +SpotifyActivityContext _$SpotifyActivityContextFromJson(Map json) => SpotifyActivityContext( uri: json['uri'] as String, name: json['name'] as String, index: json['index'] as num, ); -SpotifyActivityTrack _$SpotifyActivityTrackFromJson( - Map json) => +SpotifyActivityTrack _$SpotifyActivityTrackFromJson(Map json) => SpotifyActivityTrack( uri: json['uri'] as String, name: json['name'] as String, imageUrl: json['imageUrl'] as String, artist: SpotifyActivityArtist.fromJson( - json['artist'] as Map), - album: - SpotifyActivityAlbum.fromJson(json['album'] as Map), + Map.from(json['artist'] as Map)), + album: SpotifyActivityAlbum.fromJson( + Map.from(json['album'] as Map)), context: SpotifyActivityContext.fromJson( - json['context'] as Map), + Map.from(json['context'] as Map)), ); -SpotifyFriendActivity _$SpotifyFriendActivityFromJson( - Map json) => +SpotifyFriendActivity _$SpotifyFriendActivityFromJson(Map json) => SpotifyFriendActivity( - user: SpotifyFriend.fromJson(json['user'] as Map), - track: - SpotifyActivityTrack.fromJson(json['track'] as Map), + user: SpotifyFriend.fromJson( + Map.from(json['user'] as Map)), + track: SpotifyActivityTrack.fromJson( + Map.from(json['track'] as Map)), ); -SpotifyFriends _$SpotifyFriendsFromJson(Map json) => - SpotifyFriends( +SpotifyFriends _$SpotifyFriendsFromJson(Map json) => SpotifyFriends( friends: (json['friends'] as List) - .map((e) => SpotifyFriendActivity.fromJson(e as Map)) + .map((e) => SpotifyFriendActivity.fromJson( + Map.from(e as Map))) .toList(), ); diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index b24b69f43..aea890a0d 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -8,6 +8,8 @@ import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class AlbumPage extends HookConsumerWidget { + static const name = "album"; + final AlbumSimple album; const AlbumPage({ super.key, @@ -22,7 +24,7 @@ class AlbumPage extends HookConsumerWidget { final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!)); return InheritedTrackView( - collectionId: album.id!, + collection: album, image: album.images.asUrlString( placeholder: ImagePlaceholder.albumArt, ), diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index c3b046910..498909491 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -15,6 +15,8 @@ import 'package:spotube/pages/artist/section/top_tracks.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class ArtistPage extends HookConsumerWidget { + static const name = "artist"; + final String artistId; final logger = getLogger(ArtistPage); ArtistPage(this.artistId, {super.key}); diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 9d4078997..595ac5109 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -52,8 +52,9 @@ class ArtistPageTopTracks extends HookConsumerWidget { if (!isPlaylistPlaying) { await remotePlayback.load( - WebSocketLoadEventData( + WebSocketLoadEventData.playlist( tracks: tracks, + collection: null, initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id), ), ); diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index cbdb446e7..c7cb493a6 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -5,10 +5,13 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/connect/local_devices.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/connect/control/control.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/utils/service_utils.dart'; class ConnectPage extends HookConsumerWidget { + static const name = "connect"; + const ConnectPage({super.key}); @override @@ -65,9 +68,9 @@ class ConnectPage extends HookConsumerWidget { selected: selected, onTap: () { if (selected) { - ServiceUtils.push( + ServiceUtils.pushNamed( context, - "/connect/control", + ConnectControlPage.name, ); } else { connectClientsNotifier.resolveService(device); diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index b78f0ed32..639a9dd97 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -13,6 +13,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/services/audio_player/loop_mode.dart'; @@ -46,6 +47,8 @@ class RemotePlayerQueue extends ConsumerWidget { } class ConnectControlPage extends HookConsumerWidget { + static const name = "connect_control"; + const ConnectControlPage({super.key}); @override @@ -125,9 +128,13 @@ class ConnectControlPage extends HookConsumerWidget { playlist.activeTrack?.name ?? "", style: textTheme.titleLarge!, onTap: () { - ServiceUtils.push( + if (playlist.activeTrack == null) return; + ServiceUtils.pushNamed( context, - "/track/${playlist.activeTrack?.id}", + TrackPage.name, + pathParameters: { + "id": playlist.activeTrack!.id!, + }, ); }, ), diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index 9c0610911..9c9bdddba 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -7,8 +7,10 @@ import 'package:spotube/components/desktop_login/login_form.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/mobile_login/mobile_login.dart'; class DesktopLoginPage extends HookConsumerWidget { + static const name = WebViewLogin.name; const DesktopLoginPage({super.key}); @override diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index 83b04af18..dbec28dc9 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -8,10 +8,12 @@ import 'package:spotube/components/desktop_login/login_form.dart'; import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/home/home.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/service_utils.dart'; class LoginTutorial extends ConsumerWidget { + static const name = "login_tutorial"; const LoginTutorial({super.key}); @override @@ -53,7 +55,7 @@ class LoginTutorial extends ConsumerWidget { overrideDone: FilledButton( onPressed: authenticationNotifier.isLoggedIn ? () { - ServiceUtils.push(context, "/"); + ServiceUtils.pushNamed(context, HomePage.name); } : null, child: Center(child: Text(context.l10n.done)), diff --git a/lib/pages/getting_started/getting_started.dart b/lib/pages/getting_started/getting_started.dart index cbab03b9c..fa2054038 100644 --- a/lib/pages/getting_started/getting_started.dart +++ b/lib/pages/getting_started/getting_started.dart @@ -12,6 +12,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/themes/theme.dart'; class GettingStarting extends HookConsumerWidget { + static const name = "getting_started"; + const GettingStarting({super.key}); @override diff --git a/lib/pages/getting_started/sections/support.dart b/lib/pages/getting_started/sections/support.dart index 468234257..7bccfe06e 100644 --- a/lib/pages/getting_started/sections/support.dart +++ b/lib/pages/getting_started/sections/support.dart @@ -5,6 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/home/home.dart'; +import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -104,7 +106,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { onPressed: () async { await KVStoreService.setDoneGettingStarted(true); if (context.mounted) { - context.go("/"); + context.go(HomePage.name); } }, ), @@ -120,7 +122,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { onPressed: () async { await KVStoreService.setDoneGettingStarted(true); if (context.mounted) { - context.push("/login"); + context.pushNamed(WebViewLogin.name); } }, ), diff --git a/lib/pages/home/feed/feed_section.dart b/lib/pages/home/feed/feed_section.dart index c945251c2..d31b8256d 100644 --- a/lib/pages/home/feed/feed_section.dart +++ b/lib/pages/home/feed/feed_section.dart @@ -10,6 +10,8 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/views/home_section.dart'; class HomeFeedSectionPage extends HookConsumerWidget { + static const name = "home_feed_section"; + final String sectionUri; const HomeFeedSectionPage({super.key, required this.sectionUri}); diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index ca4e7238d..531ea889a 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -15,6 +15,8 @@ import 'package:collection/collection.dart'; import 'package:spotube/utils/platform.dart'; class GenrePlaylistsPage extends HookConsumerWidget { + static const name = "genre_playlists"; + final Category category; const GenrePlaylistsPage({super.key, required this.category}); diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index 291ce737b..bb84fc168 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -9,9 +9,11 @@ import 'package:spotube/collections/gradients.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/home/genres/genre_playlists.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class GenrePage extends HookConsumerWidget { + static const name = "genre"; const GenrePage({super.key}); @override @@ -47,7 +49,13 @@ class GenrePage extends HookConsumerWidget { return InkWell( borderRadius: BorderRadius.circular(8), onTap: () { - context.push("/genre/${category.id}", extra: category); + context.pushNamed( + GenrePlaylistsPage.name, + pathParameters: { + "categoryId": category.id!, + }, + extra: category, + ); }, child: Ink( padding: const EdgeInsets.all(8), diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index a4a71146b..d4e2d94e6 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/connect/connect_device.dart'; import 'package:spotube/components/home/sections/featured.dart'; import 'package:spotube/components/home/sections/feed.dart'; @@ -10,16 +11,15 @@ import 'package:spotube/components/home/sections/friends.dart'; import 'package:spotube/components/home/sections/genres.dart'; import 'package:spotube/components/home/sections/made_for_user.dart'; import 'package:spotube/components/home/sections/new_releases.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/home/sections/recent.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; class HomePage extends HookConsumerWidget { + static const name = "home"; const HomePage({super.key}); @override @@ -34,44 +34,27 @@ class HomePage extends HookConsumerWidget { body: CustomScrollView( controller: controller, slivers: [ - if (mediaQuery.mdAndDown) + if (mediaQuery.smAndDown) SliverAppBar( floating: true, title: Assets.spotubeLogoPng.image(height: 45), actions: [ const ConnectDeviceButton(), const Gap(10), - Consumer(builder: (context, ref, _) { - final auth = ref.watch(authenticationProvider); - final me = ref.watch(meProvider); - final meData = me.asData?.value; - - if (auth == null) { - return const SizedBox(); - } - - return IconButton( - icon: CircleAvatar( - backgroundImage: UniversalImage.imageProvider( - (meData?.images).asUrlString( - placeholder: ImagePlaceholder.artist, - ), - ), - ), - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - ), - onPressed: () { - ServiceUtils.push(context, "/profile"); - }, - ); - }), + IconButton( + icon: const Icon(SpotubeIcons.settings, size: 20), + onPressed: () { + ServiceUtils.pushNamed(context, SettingsPage.name); + }, + ), const Gap(10), ], ) else if (kIsMacOS) const SliverGap(10), const HomeGenresSection(), + const SliverGap(10), + const SliverToBoxAdapter(child: HomeRecentlyPlayedSection()), const SliverToBoxAdapter(child: HomeFeaturedSection()), const HomePageFriendsSection(), const SliverToBoxAdapter(child: HomeNewReleasesSection()), diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index b6aeef2ec..2baeaad94 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -10,6 +10,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; class LastFMLoginPage extends HookConsumerWidget { + static const name = "lastfm_login"; const LastFMLoginPage({super.key}); @override diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index eff30348f..5385f8728 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -12,6 +12,8 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; class LibraryPage extends HookConsumerWidget { + static const name = "library"; + const LibraryPage({super.key}); @override Widget build(BuildContext context, ref) { diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 6552bb5bc..ac38e8602 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -21,6 +21,8 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; class LocalLibraryPage extends HookConsumerWidget { + static const name = "local_library_page"; + final String location; final bool isDownloads; const LocalLibraryPage(this.location, {super.key, this.isDownloads = false}); diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 5044090d2..648e85284 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -24,6 +24,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0); class PlaylistGeneratorPage extends HookConsumerWidget { + static const name = "playlist_generator"; + const PlaylistGeneratorPage({super.key}); @override diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 01b73267f..5ee7ab368 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -10,10 +10,13 @@ import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart'; +import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class PlaylistGenerateResultPage extends HookConsumerWidget { + static const name = "playlist_generate_result"; + final GeneratePlaylistProviderInput state; const PlaylistGenerateResultPage({ @@ -123,8 +126,11 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ); if (playlist != null) { - router.go( - '/playlist/${playlist.id}', + router.goNamed( + PlaylistPage.name, + pathParameters: { + "id": playlist.id!, + }, extra: playlist, ); } diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index ca13864a3..850eccfa0 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -23,6 +23,8 @@ import 'package:spotube/utils/platform.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class LyricsPage extends HookConsumerWidget { + static const name = "lyrics"; + final bool isModal; const LyricsPage({super.key, this.isModal = false}); diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index 6d6f75a92..996e190df 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -20,6 +20,8 @@ import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; class MiniLyricsPage extends HookConsumerWidget { + static const name = "mini_lyrics"; + final Size prevSize; const MiniLyricsPage({super.key, required this.prevSize}); diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index 0a1ff8b35..1f2df95a0 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -7,6 +7,7 @@ import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/platform.dart'; class WebViewLogin extends HookConsumerWidget { + static const name = "login"; const WebViewLogin({super.key}); @override diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 72983518a..44e99aeae 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -3,9 +3,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/tracks_view/track_view.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class LikedPlaylistPage extends HookConsumerWidget { + static const name = PlaylistPage.name; + final PlaylistSimple playlist; const LikedPlaylistPage({ super.key, @@ -18,7 +21,7 @@ class LikedPlaylistPage extends HookConsumerWidget { final tracks = likedTracks.asData?.value ?? []; return InheritedTrackView( - collectionId: playlist.id!, + collection: playlist, image: "assets/liked-tracks.jpg", pagination: PaginationProps( hasNextPage: false, diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index d9d224e04..8fb224581 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -10,6 +10,8 @@ import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class PlaylistPage extends HookConsumerWidget { + static const name = "playlist"; + final PlaylistSimple playlist; const PlaylistPage({ super.key, @@ -29,7 +31,7 @@ class PlaylistPage extends HookConsumerWidget { final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!); return InheritedTrackView( - collectionId: playlist.id!, + collection: playlist, image: playlist.images.asUrlString( placeholder: ImagePlaceholder.collection, ), diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index 52b69835d..d77ae98d8 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -14,6 +14,8 @@ import 'package:spotube/provider/spotify/spotify.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ProfilePage extends HookConsumerWidget { + static const name = "profile"; + const ProfilePage({super.key}); @override diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 42bf3f695..258ecf3cb 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -14,6 +14,7 @@ import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart'; +import 'package:spotube/pages/home/home.dart'; import 'package:spotube/provider/connect/server.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; @@ -22,13 +23,6 @@ import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; -const rootPaths = { - "/": 0, - "/search": 1, - "/library": 2, - "/lyrics": 3, -}; - class RootApp extends HookConsumerWidget { final Widget child; const RootApp({ @@ -42,7 +36,6 @@ class RootApp extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider); final scaffoldMessenger = ScaffoldMessenger.of(context); final theme = Theme.of(context); - final location = GoRouterState.of(context).matchedLocation; useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -179,32 +172,18 @@ class RootApp extends HookConsumerWidget { return null; }, [backgroundColor]); - void onSelectIndexChanged(int d) { - final invertedRouteMap = - rootPaths.map((key, value) => MapEntry(value, key)); - - if (context.mounted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - GoRouter.of(context).go(invertedRouteMap[d]!); - }); - } - } - // ignore: deprecated_member_use return WillPopScope( onWillPop: () async { - if (rootPaths[location] != 0) { - onSelectIndexChanged(0); + final routerState = GoRouterState.of(context); + if (routerState.matchedLocation != "/") { + context.goNamed(HomePage.name); return false; } return true; }, child: Scaffold( - body: Sidebar( - selectedIndex: rootPaths[location], - onSelectedIndexChanged: onSelectIndexChanged, - child: child, - ), + body: Sidebar(child: child), extendBody: true, drawerScrimColor: Colors.transparent, endDrawer: kIsDesktop @@ -238,10 +217,7 @@ class RootApp extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ BottomPlayer(), - SpotubeNavigationBar( - selectedIndex: rootPaths[location], - onSelectedIndexChanged: onSelectIndexChanged, - ), + const SpotubeNavigationBar(), ], ), ), diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index e9ada2365..d5374786d 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -26,6 +27,8 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/utils/platform.dart'; class SearchPage extends HookConsumerWidget { + static const name = "search"; + const SearchPage({super.key}); @override @@ -85,99 +88,117 @@ class SearchPage extends HookConsumerWidget { return SafeArea( bottom: false, child: Scaffold( - appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar() : null, + appBar: kIsDesktop && !kIsMacOS + ? const PageWindowTitleBar(automaticallyImplyLeading: true) + : null, body: !authenticationNotifier.isLoggedIn ? const AnonymousFallback() : Column( children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - color: theme.scaffoldBackgroundColor, - child: SearchAnchor( - searchController: controller, - viewBuilder: (_) => HookBuilder(builder: (context) { - final searchController = useListenable(controller); - final update = useForceUpdate(); - final suggestions = searchController.text.isEmpty - ? KVStoreService.recentSearches - : KVStoreService.recentSearches - .where( - (s) => - weightedRatio( - s.toLowerCase(), - searchController.text.toLowerCase(), - ) > - 50, - ) - .toList(); - - return ListView.builder( - itemCount: suggestions.length, - itemBuilder: (context, index) { - final suggestion = suggestions[index]; - - return ListTile( - leading: const Icon(SpotubeIcons.history), - title: Text(suggestion), - trailing: IconButton( - icon: const Icon(SpotubeIcons.trash), - onPressed: () { + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if ((kIsMobile || kIsMacOS) && context.canPop()) + const BackButton() + else + const Gap(20), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + right: 20, + top: 20, + bottom: 20, + ), + child: SearchAnchor( + searchController: controller, + viewBuilder: (_) => HookBuilder(builder: (context) { + final searchController = + useListenable(controller); + final update = useForceUpdate(); + final suggestions = searchController.text.isEmpty + ? KVStoreService.recentSearches + : KVStoreService.recentSearches + .where( + (s) => + weightedRatio( + s.toLowerCase(), + searchController.text + .toLowerCase(), + ) > + 50, + ) + .toList(); + + return ListView.builder( + itemCount: suggestions.length, + itemBuilder: (context, index) { + final suggestion = suggestions[index]; + + return ListTile( + leading: const Icon(SpotubeIcons.history), + title: Text(suggestion), + trailing: IconButton( + icon: const Icon(SpotubeIcons.trash), + onPressed: () { + KVStoreService.setRecentSearches( + KVStoreService.recentSearches + .where((s) => s != suggestion) + .toList(), + ); + update(); + }, + ), + onTap: () { + controller.closeView(suggestion); + ref + .read( + searchTermStateProvider.notifier) + .state = suggestion; + }, + ); + }, + ); + }), + suggestionsBuilder: (context, controller) { + return []; + }, + viewOnSubmitted: (value) async { + controller.closeView(value); + Timer( + const Duration(milliseconds: 50), + () { + ref + .read(searchTermStateProvider.notifier) + .state = value; + if (value.trim().isEmpty) { + return; + } KVStoreService.setRecentSearches( - KVStoreService.recentSearches - .where((s) => s != suggestion) - .toList(), + { + value, + ...KVStoreService.recentSearches, + }.toList(), ); - update(); }, - ), - onTap: () { - controller.closeView(suggestion); - ref - .read(searchTermStateProvider.notifier) - .state = suggestion; - }, - ); - }, - ); - }), - suggestionsBuilder: (context, controller) { - return []; - }, - viewOnSubmitted: (value) async { - controller.closeView(value); - Timer( - const Duration(milliseconds: 50), - () { - ref.read(searchTermStateProvider.notifier).state = - value; - if (value.trim().isEmpty) { - return; - } - KVStoreService.setRecentSearches( - { - value, - ...KVStoreService.recentSearches, - }.toList(), - ); - }, - ); - }, - builder: (context, controller) { - return SearchBar( - autoFocus: queries.none((s) => - s.asData?.value != null && !s.hasError) && - !kIsMobile, - controller: controller, - leading: const Icon(SpotubeIcons.search), - hintText: "${context.l10n.search}...", - onTap: controller.openView, - onChanged: (_) => controller.openView(), - ); - }, - ), + ); + }, + builder: (context, controller) { + return SearchBar( + autoFocus: queries.none((s) => + s.asData?.value != null && + !s.hasError) && + !kIsMobile, + controller: controller, + leading: const Icon(SpotubeIcons.search), + hintText: "${context.l10n.search}...", + onTap: controller.openView, + onChanged: (_) => controller.openView(), + ); + }, + ), + ), + ), + ], ), Expanded( child: AnimatedSwitcher( diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 7fb58759f..bd7f3c88e 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -76,7 +76,7 @@ class SearchTracksSection extends HookConsumerWidget { if (shouldPlay) { await remotePlayback.load( - WebSocketLoadEventData( + WebSocketLoadEventData.playlist( tracks: [track], ), ); diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 505eecb94..e7d957593 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -17,6 +17,8 @@ final _licenseProvider = FutureProvider((ref) async { }); class AboutSpotube extends HookConsumerWidget { + static const name = "about"; + const AboutSpotube({super.key}); @override diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 9dd85c507..6eccab073 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -11,6 +11,8 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/blacklist_provider.dart'; class BlackListPage extends HookConsumerWidget { + static const name = "blacklist"; + const BlackListPage({super.key}); @override diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index b07ebbb1a..8b6f7312d 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -11,6 +11,8 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; class LogsPage extends HookWidget { + static const name = "logs"; + const LogsPage({super.key}); List<({DateTime? date, String body})> parseLogs(String raw) { diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index ab3a7c92f..6162aa3d3 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -4,10 +4,15 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/utils/service_utils.dart'; class SettingsAccountSection extends HookConsumerWidget { const SettingsAccountSection({super.key}); @@ -15,9 +20,12 @@ class SettingsAccountSection extends HookConsumerWidget { @override Widget build(context, ref) { final theme = Theme.of(context); + final router = GoRouter.of(context); + final auth = ref.watch(authenticationProvider); final scrobbler = ref.watch(scrobblerProvider); - final router = GoRouter.of(context); + final me = ref.watch(meProvider); + final meData = me.asData?.value; final logoutBtnStyle = FilledButton.styleFrom( backgroundColor: Colors.red, @@ -27,6 +35,24 @@ class SettingsAccountSection extends HookConsumerWidget { return SectionCardWithHeading( heading: context.l10n.account, children: [ + if (auth != null) + ListTile( + leading: const Icon(SpotubeIcons.user), + title: const Text("User Profile"), + trailing: Padding( + padding: const EdgeInsets.all(8.0), + child: CircleAvatar( + backgroundImage: UniversalImage.imageProvider( + (meData?.images).asUrlString( + placeholder: ImagePlaceholder.artist, + ), + ), + ), + ), + onTap: () { + ServiceUtils.pushNamed(context, ProfilePage.name); + }, + ), if (auth == null) LayoutBuilder(builder: (context, constrains) { return ListTile( diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index d293518d8..af0fc0959 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -16,6 +16,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/utils/platform.dart'; class SettingsPage extends HookConsumerWidget { + static const name = "settings"; + const SettingsPage({super.key}); @override @@ -29,6 +31,7 @@ class SettingsPage extends HookConsumerWidget { appBar: PageWindowTitleBar( title: Text(context.l10n.settings), centerTitle: true, + automaticallyImplyLeading: true, ), body: Scrollbar( controller: controller, diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart new file mode 100644 index 000000000..83867f936 --- /dev/null +++ b/lib/pages/stats/albums/albums.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/common/album_item.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsAlbumsPage extends HookConsumerWidget { + static const name = "stats_albums"; + const StatsAlbumsPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final albums = ref.watch( + playbackHistoryTopProvider(HistoryDuration.allTime) + .select((s) => s.albums), + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + centerTitle: false, + title: Text("Albums"), + ), + body: ListView.builder( + itemCount: albums.length, + itemBuilder: (context, index) { + final album = albums[index]; + return StatsAlbumItem( + album: album.album, + info: Text("${compactNumberFormatter.format(album.count)} plays"), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart new file mode 100644 index 000000000..755475aef --- /dev/null +++ b/lib/pages/stats/artists/artists.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/common/artist_item.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsArtistsPage extends HookConsumerWidget { + static const name = "stats_artists"; + const StatsArtistsPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final artists = ref.watch( + playbackHistoryTopProvider(HistoryDuration.allTime) + .select((s) => s.artists), + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + centerTitle: false, + title: Text("Artists"), + ), + body: ListView.builder( + itemCount: artists.length, + itemBuilder: (context, index) { + final artist = artists[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text("${compactNumberFormatter.format(artist.count)} plays"), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart new file mode 100644 index 000000000..228d3243e --- /dev/null +++ b/lib/pages/stats/fees/fees.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:sliver_tools/sliver_tools.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/common/artist_item.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsStreamFeesPage extends HookConsumerWidget { + static const name = "stats_stream_fees"; + + const StatsStreamFeesPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme, :hintColor) = Theme.of(context); + + final artists = ref.watch( + playbackHistoryTopProvider(HistoryDuration.days30) + .select((value) => value.artists), + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + centerTitle: false, + title: Text("Streaming fees (hypothetical)"), + ), + body: CustomScrollView( + slivers: [ + SliverCrossAxisConstrained( + maxCrossAxisExtent: 600, + alignment: -1, + child: SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverToBoxAdapter( + child: Text( + "*This is calculated based on Spotify's per stream " + "payout of \$0.003 to \$0.005. This is a hypothetical " + "calculation to give user insight about how much they " + "would have paid to the artists if they were to listen " + "their song in Spotify.", + style: textTheme.bodySmall?.copyWith( + color: hintColor, + ), + ), + ), + ), + ), + SliverList.builder( + itemCount: artists.length, + itemBuilder: (context, index) { + final artist = artists[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(usdFormatter.format(artist.count * 0.005)), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart new file mode 100644 index 000000000..b22f9a4f1 --- /dev/null +++ b/lib/pages/stats/minutes/minutes.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/common/track_item.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsMinutesPage extends HookConsumerWidget { + static const name = "stats_minutes"; + + const StatsMinutesPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final topTracks = ref.watch( + playbackHistoryTopProvider(HistoryDuration.allTime) + .select((s) => s.tracks), + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + title: Text("Minutes listened"), + centerTitle: false, + automaticallyImplyLeading: true, + ), + body: ListView.separated( + separatorBuilder: (context, index) => const Gap(8), + itemCount: topTracks.length, + itemBuilder: (context, index) { + final (:track, :count) = topTracks[index]; + + return StatsTrackItem( + track: track, + info: Text( + "${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins", + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart new file mode 100644 index 000000000..cca7febb2 --- /dev/null +++ b/lib/pages/stats/playlists/playlists.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/common/playlist_item.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsPlaylistsPage extends HookConsumerWidget { + static const name = "stats_playlists"; + const StatsPlaylistsPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final playlists = ref.watch( + playbackHistoryTopProvider(HistoryDuration.allTime) + .select((s) => s.playlists), + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + centerTitle: false, + title: Text("Playlists"), + ), + body: ListView.builder( + itemCount: playlists.length, + itemBuilder: (context, index) { + final playlist = playlists[index]; + return StatsPlaylistItem( + playlist: playlist.playlist.playlist, + info: + Text("${compactNumberFormatter.format(playlist.count)} plays"), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/stats.dart b/lib/pages/stats/stats.dart new file mode 100644 index 000000000..95493591f --- /dev/null +++ b/lib/pages/stats/stats.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/summary/summary.dart'; +import 'package:spotube/components/stats/top/top.dart'; +import 'package:spotube/utils/platform.dart'; + +class StatsPage extends HookConsumerWidget { + static const name = "stats"; + + const StatsPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + return SafeArea( + bottom: false, + child: Scaffold( + appBar: kIsMacOS || kIsMobile ? null : const PageWindowTitleBar(), + body: CustomScrollView( + slivers: [ + if (kIsMacOS) const SliverGap(20), + const StatsPageSummarySection(), + const StatsPageTopSection(), + const SliverToBoxAdapter( + child: SafeArea( + child: SizedBox(), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart new file mode 100644 index 000000000..33480709a --- /dev/null +++ b/lib/pages/stats/streams/streams.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/formatters.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/stats/common/track_item.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +class StatsStreamsPage extends HookConsumerWidget { + static const name = "stats_streams"; + + const StatsStreamsPage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final topTracks = ref.watch( + playbackHistoryTopProvider(HistoryDuration.allTime) + .select((s) => s.tracks), + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + title: Text("Streamed songs"), + centerTitle: false, + automaticallyImplyLeading: true, + ), + body: ListView.separated( + separatorBuilder: (context, index) => const Gap(8), + itemCount: topTracks.length, + itemBuilder: (context, index) { + final (:track, :count) = topTracks[index]; + + return StatsTrackItem( + track: track, + info: Text( + "${compactNumberFormatter.format(count)} streams", + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index fc90d19a9..2109fe6ef 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -21,6 +21,8 @@ import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/extensions/constrains.dart'; class TrackPage extends HookConsumerWidget { + static const name = "track"; + final String trackId; const TrackPage({ super.key, diff --git a/lib/provider/connect/server.dart b/lib/provider/connect/server.dart index ebf53e437..9c4e6466c 100644 --- a/lib/provider/connect/server.dart +++ b/lib/provider/connect/server.dart @@ -9,9 +9,11 @@ import 'package:shelf/shelf_io.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/connect/clients.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -32,6 +34,7 @@ final connectServerProvider = FutureProvider((ref) async { final resolvedService = await ref .watch(connectClientsProvider.selectAsync((s) => s.resolvedService)); final playbackNotifier = ref.read(proxyPlaylistProvider.notifier); + final historyNotifier = ref.read(playbackHistoryProvider.notifier); if (!enabled || resolvedService != null) { return null; @@ -79,7 +82,7 @@ final connectServerProvider = FutureProvider((ref) async { .toJson(), ); channel.sink.add( - WebSocketShuffleEvent(await audioPlayer.isShuffled).toJson(), + WebSocketShuffleEvent(audioPlayer.isShuffled).toJson(), ); channel.sink.add( WebSocketLoopEvent(audioPlayer.loopMode).toJson(), @@ -146,8 +149,14 @@ final connectServerProvider = FutureProvider((ref) async { initialIndex: event.data.initialIndex ?? 0, ); - if (event.data.collectionId != null) { - playbackNotifier.addCollection(event.data.collectionId!); + if (event.data.collectionId == null) return; + playbackNotifier.addCollection(event.data.collectionId!); + if (event.data.collection is AlbumSimple) { + historyNotifier + .addAlbums([event.data.collection as AlbumSimple]); + } else { + historyNotifier.addPlaylists( + [event.data.collection as PlaylistSimple]); } }); diff --git a/lib/provider/history/history.dart b/lib/provider/history/history.dart new file mode 100644 index 000000000..4436626d5 --- /dev/null +++ b/lib/provider/history/history.dart @@ -0,0 +1,129 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/utils/persisted_state_notifier.dart'; + +class PlaybackHistoryState { + final List items; + const PlaybackHistoryState({this.items = const []}); + + factory PlaybackHistoryState.fromJson(Map json) { + return PlaybackHistoryState( + items: json["items"] + ?.map( + (json) => PlaybackHistoryItem.fromJson(json), + ) + .toList() + .cast() ?? + [], + ); + } + + Map toJson() { + return { + "items": items.map((s) => s.toJson()).toList(), + }; + } + + PlaybackHistoryState copyWith({ + List? items, + }) { + return PlaybackHistoryState(items: items ?? this.items); + } +} + +class PlaybackHistoryNotifier + extends PersistedStateNotifier { + final Ref ref; + PlaybackHistoryNotifier(this.ref) + : super(const PlaybackHistoryState(), "playback_history"); + + SpotifyApi get spotify => ref.read(spotifyProvider); + + @override + FutureOr fromJson(Map json) => + PlaybackHistoryState.fromJson(json); + + @override + Map toJson() { + return state.toJson(); + } + + void addPlaylists(List playlists) { + state = state.copyWith( + items: [ + ...state.items, + for (final playlist in playlists) + PlaybackHistoryItem.playlist( + date: DateTime.now(), playlist: playlist), + ], + ); + } + + void addAlbums(List albums) { + state = state.copyWith( + items: [ + ...state.items, + for (final album in albums) + PlaybackHistoryItem.album(date: DateTime.now(), album: album), + ], + ); + } + + void addTrack(Track track) async { + // For some reason Track's artists images are `null` + // so we need to fetch them from the API + final artists = + await spotify.artists.list(track.artists!.map((e) => e.id!).toList()); + + track.artists = artists.toList(); + + state = state.copyWith( + items: [ + ...state.items, + PlaybackHistoryItem.track(date: DateTime.now(), track: track), + ], + ); + } + + void clear() { + state = state.copyWith(items: []); + } +} + +final playbackHistoryProvider = + StateNotifierProvider( + (ref) => PlaybackHistoryNotifier(ref), +); + +typedef PlaybackHistoryGrouped = ({ + List tracks, + List albums, + List playlists, +}); + +final playbackHistoryGroupedProvider = Provider((ref) { + final history = ref.watch(playbackHistoryProvider); + final tracks = history.items + .whereType() + .sorted((a, b) => b.date.compareTo(a.date)) + .toList(); + final albums = history.items + .whereType() + .sorted((a, b) => b.date.compareTo(a.date)) + .toList(); + final playlists = history.items + .whereType() + .sorted((a, b) => b.date.compareTo(a.date)) + .toList(); + + return ( + tracks: tracks, + albums: albums, + playlists: playlists, + ); +}); diff --git a/lib/provider/history/recent.dart b/lib/provider/history/recent.dart new file mode 100644 index 000000000..9953858d8 --- /dev/null +++ b/lib/provider/history/recent.dart @@ -0,0 +1,40 @@ +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/history/history.dart'; +import 'package:spotube/provider/history/state.dart'; + +final recentlyPlayedItems = Provider((ref) { + return ref.watch( + playbackHistoryProvider.select( + (s) => s.items + .toSet() + // unique items + .whereIndexed( + (index, item) => + index == + s.items.lastIndexWhere( + (e) => switch ((e, item)) { + ( + PlaybackHistoryPlaylist(:final playlist), + PlaybackHistoryPlaylist(playlist: final playlist2) + ) => + playlist.id == playlist2.id, + ( + PlaybackHistoryAlbum(:final album), + PlaybackHistoryAlbum(album: final album2) + ) => + album.id == album2.id, + _ => false, + }, + ), + ) + .where( + (s) => s is PlaybackHistoryPlaylist || s is PlaybackHistoryAlbum, + ) + .take(10) + .sortedBy((s) => s.date) + .reversed + .toList(), + ), + ); +}); diff --git a/lib/provider/history/state.dart b/lib/provider/history/state.dart new file mode 100644 index 000000000..67658502f --- /dev/null +++ b/lib/provider/history/state.dart @@ -0,0 +1,35 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:spotify/spotify.dart'; + +part 'state.freezed.dart'; +part 'state.g.dart'; + +enum HistoryDuration { + allTime, + days7, + days30, + months6, + year, + years2, +} + +@freezed +class PlaybackHistoryItem with _$PlaybackHistoryItem { + factory PlaybackHistoryItem.playlist({ + required DateTime date, + required PlaylistSimple playlist, + }) = PlaybackHistoryPlaylist; + + factory PlaybackHistoryItem.album({ + required DateTime date, + required AlbumSimple album, + }) = PlaybackHistoryAlbum; + + factory PlaybackHistoryItem.track({ + required DateTime date, + required Track track, + }) = PlaybackHistoryTrack; + + factory PlaybackHistoryItem.fromJson(Map json) => + _$PlaybackHistoryItemFromJson(json); +} diff --git a/lib/provider/history/state.freezed.dart b/lib/provider/history/state.freezed.dart new file mode 100644 index 000000000..e2ee94210 --- /dev/null +++ b/lib/provider/history/state.freezed.dart @@ -0,0 +1,644 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +PlaybackHistoryItem _$PlaybackHistoryItemFromJson(Map json) { + switch (json['runtimeType']) { + case 'playlist': + return PlaybackHistoryPlaylist.fromJson(json); + case 'album': + return PlaybackHistoryAlbum.fromJson(json); + case 'track': + return PlaybackHistoryTrack.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'PlaybackHistoryItem', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$PlaybackHistoryItem { + DateTime get date => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PlaybackHistoryItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlaybackHistoryItemCopyWith<$Res> { + factory $PlaybackHistoryItemCopyWith( + PlaybackHistoryItem value, $Res Function(PlaybackHistoryItem) then) = + _$PlaybackHistoryItemCopyWithImpl<$Res, PlaybackHistoryItem>; + @useResult + $Res call({DateTime date}); +} + +/// @nodoc +class _$PlaybackHistoryItemCopyWithImpl<$Res, $Val extends PlaybackHistoryItem> + implements $PlaybackHistoryItemCopyWith<$Res> { + _$PlaybackHistoryItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + }) { + return _then(_value.copyWith( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PlaybackHistoryPlaylistImplCopyWith<$Res> + implements $PlaybackHistoryItemCopyWith<$Res> { + factory _$$PlaybackHistoryPlaylistImplCopyWith( + _$PlaybackHistoryPlaylistImpl value, + $Res Function(_$PlaybackHistoryPlaylistImpl) then) = + __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, PlaylistSimple playlist}); +} + +/// @nodoc +class __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res> + extends _$PlaybackHistoryItemCopyWithImpl<$Res, + _$PlaybackHistoryPlaylistImpl> + implements _$$PlaybackHistoryPlaylistImplCopyWith<$Res> { + __$$PlaybackHistoryPlaylistImplCopyWithImpl( + _$PlaybackHistoryPlaylistImpl _value, + $Res Function(_$PlaybackHistoryPlaylistImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? playlist = null, + }) { + return _then(_$PlaybackHistoryPlaylistImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + playlist: null == playlist + ? _value.playlist + : playlist // ignore: cast_nullable_to_non_nullable + as PlaylistSimple, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist { + _$PlaybackHistoryPlaylistImpl( + {required this.date, required this.playlist, final String? $type}) + : $type = $type ?? 'playlist'; + + factory _$PlaybackHistoryPlaylistImpl.fromJson(Map json) => + _$$PlaybackHistoryPlaylistImplFromJson(json); + + @override + final DateTime date; + @override + final PlaylistSimple playlist; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PlaybackHistoryItem.playlist(date: $date, playlist: $playlist)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlaybackHistoryPlaylistImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.playlist, playlist) || + other.playlist == playlist)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, date, playlist); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> + get copyWith => __$$PlaybackHistoryPlaylistImplCopyWithImpl< + _$PlaybackHistoryPlaylistImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) { + return playlist(date, this.playlist); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) { + return playlist?.call(date, this.playlist); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) { + if (playlist != null) { + return playlist(date, this.playlist); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) { + return playlist(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) { + return playlist?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) { + if (playlist != null) { + return playlist(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$PlaybackHistoryPlaylistImplToJson( + this, + ); + } +} + +abstract class PlaybackHistoryPlaylist implements PlaybackHistoryItem { + factory PlaybackHistoryPlaylist( + {required final DateTime date, + required final PlaylistSimple playlist}) = _$PlaybackHistoryPlaylistImpl; + + factory PlaybackHistoryPlaylist.fromJson(Map json) = + _$PlaybackHistoryPlaylistImpl.fromJson; + + @override + DateTime get date; + PlaylistSimple get playlist; + @override + @JsonKey(ignore: true) + _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PlaybackHistoryAlbumImplCopyWith<$Res> + implements $PlaybackHistoryItemCopyWith<$Res> { + factory _$$PlaybackHistoryAlbumImplCopyWith(_$PlaybackHistoryAlbumImpl value, + $Res Function(_$PlaybackHistoryAlbumImpl) then) = + __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, AlbumSimple album}); +} + +/// @nodoc +class __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res> + extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryAlbumImpl> + implements _$$PlaybackHistoryAlbumImplCopyWith<$Res> { + __$$PlaybackHistoryAlbumImplCopyWithImpl(_$PlaybackHistoryAlbumImpl _value, + $Res Function(_$PlaybackHistoryAlbumImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? album = null, + }) { + return _then(_$PlaybackHistoryAlbumImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + album: null == album + ? _value.album + : album // ignore: cast_nullable_to_non_nullable + as AlbumSimple, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum { + _$PlaybackHistoryAlbumImpl( + {required this.date, required this.album, final String? $type}) + : $type = $type ?? 'album'; + + factory _$PlaybackHistoryAlbumImpl.fromJson(Map json) => + _$$PlaybackHistoryAlbumImplFromJson(json); + + @override + final DateTime date; + @override + final AlbumSimple album; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PlaybackHistoryItem.album(date: $date, album: $album)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlaybackHistoryAlbumImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.album, album) || other.album == album)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, date, album); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> + get copyWith => + __$$PlaybackHistoryAlbumImplCopyWithImpl<_$PlaybackHistoryAlbumImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) { + return album(date, this.album); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) { + return album?.call(date, this.album); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) { + if (album != null) { + return album(date, this.album); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) { + return album(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) { + return album?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) { + if (album != null) { + return album(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$PlaybackHistoryAlbumImplToJson( + this, + ); + } +} + +abstract class PlaybackHistoryAlbum implements PlaybackHistoryItem { + factory PlaybackHistoryAlbum( + {required final DateTime date, + required final AlbumSimple album}) = _$PlaybackHistoryAlbumImpl; + + factory PlaybackHistoryAlbum.fromJson(Map json) = + _$PlaybackHistoryAlbumImpl.fromJson; + + @override + DateTime get date; + AlbumSimple get album; + @override + @JsonKey(ignore: true) + _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PlaybackHistoryTrackImplCopyWith<$Res> + implements $PlaybackHistoryItemCopyWith<$Res> { + factory _$$PlaybackHistoryTrackImplCopyWith(_$PlaybackHistoryTrackImpl value, + $Res Function(_$PlaybackHistoryTrackImpl) then) = + __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, Track track}); +} + +/// @nodoc +class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res> + extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryTrackImpl> + implements _$$PlaybackHistoryTrackImplCopyWith<$Res> { + __$$PlaybackHistoryTrackImplCopyWithImpl(_$PlaybackHistoryTrackImpl _value, + $Res Function(_$PlaybackHistoryTrackImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? track = null, + }) { + return _then(_$PlaybackHistoryTrackImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + track: null == track + ? _value.track + : track // ignore: cast_nullable_to_non_nullable + as Track, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack { + _$PlaybackHistoryTrackImpl( + {required this.date, required this.track, final String? $type}) + : $type = $type ?? 'track'; + + factory _$PlaybackHistoryTrackImpl.fromJson(Map json) => + _$$PlaybackHistoryTrackImplFromJson(json); + + @override + final DateTime date; + @override + final Track track; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PlaybackHistoryItem.track(date: $date, track: $track)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlaybackHistoryTrackImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.track, track) || other.track == track)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, date, track); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> + get copyWith => + __$$PlaybackHistoryTrackImplCopyWithImpl<_$PlaybackHistoryTrackImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) { + return track(date, this.track); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) { + return track?.call(date, this.track); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) { + if (track != null) { + return track(date, this.track); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) { + return track(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) { + return track?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) { + if (track != null) { + return track(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$PlaybackHistoryTrackImplToJson( + this, + ); + } +} + +abstract class PlaybackHistoryTrack implements PlaybackHistoryItem { + factory PlaybackHistoryTrack( + {required final DateTime date, + required final Track track}) = _$PlaybackHistoryTrackImpl; + + factory PlaybackHistoryTrack.fromJson(Map json) = + _$PlaybackHistoryTrackImpl.fromJson; + + @override + DateTime get date; + Track get track; + @override + @JsonKey(ignore: true) + _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/provider/history/state.g.dart b/lib/provider/history/state.g.dart new file mode 100644 index 000000000..dfd01c2cd --- /dev/null +++ b/lib/provider/history/state.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$PlaybackHistoryPlaylistImpl _$$PlaybackHistoryPlaylistImplFromJson( + Map json) => + _$PlaybackHistoryPlaylistImpl( + date: DateTime.parse(json['date'] as String), + playlist: PlaylistSimple.fromJson( + Map.from(json['playlist'] as Map)), + $type: json['runtimeType'] as String?, + ); + +Map _$$PlaybackHistoryPlaylistImplToJson( + _$PlaybackHistoryPlaylistImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'playlist': instance.playlist.toJson(), + 'runtimeType': instance.$type, + }; + +_$PlaybackHistoryAlbumImpl _$$PlaybackHistoryAlbumImplFromJson(Map json) => + _$PlaybackHistoryAlbumImpl( + date: DateTime.parse(json['date'] as String), + album: + AlbumSimple.fromJson(Map.from(json['album'] as Map)), + $type: json['runtimeType'] as String?, + ); + +Map _$$PlaybackHistoryAlbumImplToJson( + _$PlaybackHistoryAlbumImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'album': instance.album.toJson(), + 'runtimeType': instance.$type, + }; + +_$PlaybackHistoryTrackImpl _$$PlaybackHistoryTrackImplFromJson(Map json) => + _$PlaybackHistoryTrackImpl( + date: DateTime.parse(json['date'] as String), + track: Track.fromJson(Map.from(json['track'] as Map)), + $type: json['runtimeType'] as String?, + ); + +Map _$$PlaybackHistoryTrackImplToJson( + _$PlaybackHistoryTrackImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'track': instance.track.toJson(), + 'runtimeType': instance.$type, + }; diff --git a/lib/provider/history/summary.dart b/lib/provider/history/summary.dart new file mode 100644 index 000000000..2aa86ac9f --- /dev/null +++ b/lib/provider/history/summary.dart @@ -0,0 +1,62 @@ +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/history/history.dart'; +import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/provider/history/top.dart'; + +final playbackHistorySummaryProvider = Provider((ref) { + final (:tracks, :albums, :playlists) = + ref.watch(playbackHistoryGroupedProvider); + + final totalDurationListened = tracks.fold( + Duration.zero, + (previousValue, element) => previousValue + element.track.duration!, + ); + + final totalTracksListened = tracks + .whereIndexed( + (i, track) => + i == tracks.lastIndexWhere((e) => e.track.id == track.track.id), + ) + .length; + + final artists = + tracks.map((e) => e.track.artists).expand((e) => e ?? []).toList(); + + final totalArtistsListened = artists + .whereIndexed( + (i, artist) => i == artists.lastIndexWhere((e) => e.id == artist.id), + ) + .length; + + final totalAlbumsListened = albums + .whereIndexed( + (i, album) => + i == albums.lastIndexWhere((e) => e.album.id == album.album.id), + ) + .length; + + final totalPlaylistsListened = playlists + .whereIndexed( + (i, playlist) => + i == + playlists + .lastIndexWhere((e) => e.playlist.id == playlist.playlist.id), + ) + .length; + + final tracksThisMonth = ref.watch( + playbackHistoryTopProvider(HistoryDuration.days30).select((s) => s.tracks), + ); + + final streams = tracksThisMonth.fold(0, (acc, el) => acc + el.count); + + return ( + duration: totalDurationListened, + tracks: totalTracksListened, + artists: totalArtistsListened, + fees: streams * 0.005, // Spotify pays $0.003 to $0.005 + albums: totalAlbumsListened, + playlists: totalPlaylistsListened, + ); +}); diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart new file mode 100644 index 000000000..7d4594f08 --- /dev/null +++ b/lib/provider/history/top.dart @@ -0,0 +1,95 @@ +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/history/history.dart'; +import 'package:spotube/provider/history/state.dart'; + +final playbackHistoryTopDurationProvider = + StateProvider((ref) => HistoryDuration.days30); + +final playbackHistoryTopProvider = + Provider.family((ref, HistoryDuration durationState) { + final grouped = ref.watch(playbackHistoryGroupedProvider); + + final duration = switch (durationState) { + HistoryDuration.allTime => const Duration(days: 365 * 2003), + HistoryDuration.days7 => const Duration(days: 7), + HistoryDuration.days30 => const Duration(days: 30), + HistoryDuration.months6 => const Duration(days: 30 * 6), + HistoryDuration.year => const Duration(days: 365), + HistoryDuration.years2 => const Duration(days: 365 * 2), + }; + final tracks = grouped.tracks + .where( + (item) => item.date.isAfter( + DateTime.now().subtract(duration), + ), + ) + .toList(); + final albums = grouped.albums + .where( + (item) => item.date.isAfter( + DateTime.now().subtract(duration), + ), + ) + .toList(); + + final playlists = grouped.playlists + .where( + (item) => item.date.isAfter( + DateTime.now().subtract(duration), + ), + ) + .toList(); + + final tracksWithCount = groupBy( + tracks, + (track) => track.track.id!, + ) + .entries + .map((entry) { + return (count: entry.value.length, track: entry.value.first.track); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + + final albumsWithTrackAlbums = [ + for (final historicAlbum in albums) historicAlbum.album, + for (final track in tracks) track.track.album! + ]; + + final albumsWithCount = groupBy(albumsWithTrackAlbums, (album) => album.id!) + .entries + .map((entry) { + return (count: entry.value.length, album: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + + final artists = + tracks.map((track) => track.track.artists).expand((e) => e ?? []); + + final artistsWithCount = groupBy(artists, (artist) => artist.id!) + .entries + .map((entry) { + return (count: entry.value.length, artist: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + + final playlistsWithCount = + groupBy(playlists, (playlist) => playlist.playlist.id!) + .entries + .map((entry) { + return (count: entry.value.length, playlist: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + + return ( + tracks: tracksWithCount, + albums: albumsWithCount, + artists: artistsWithCount, + playlists: playlistsWithCount, + ); +}); diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/proxy_playlist/player_listeners.dart index bf54fa909..3ee815e67 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/proxy_playlist/player_listeners.dart @@ -3,24 +3,50 @@ import 'dart:async'; import 'package:catcher_2/catcher_2.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/skip_segments.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; extension ProxyPlaylistListeners on ProxyPlaylistNotifier { + Future updatePalette() async { + final palette = ref.read(paletteProvider); + if (!preferences.albumColorSync) { + if (palette != null) ref.read(paletteProvider.notifier).state = null; + return; + } + return Future.microtask(() async { + if (playlist.activeTrack == null) return; + + final palette = await PaletteGenerator.fromImageProvider( + UniversalImage.imageProvider( + (playlist.activeTrack?.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + height: 50, + width: 50, + ), + ); + ref.read(paletteProvider.notifier).state = palette; + }); + } + StreamSubscription subscribeToPlaylist() { - return audioPlayer.playlistStream.listen((playlist) { - state = state.copyWith( - tracks: playlist.medias + return audioPlayer.playlistStream.listen((mpvPlaylist) { + state = playlist.copyWith( + tracks: mpvPlaylist.medias .map((media) => SpotubeMedia.fromMedia(media).track) .toSet(), - active: playlist.index, + active: mpvPlaylist.index, ); - notificationService.addTrack(state.activeTrack!); - discord.updatePresence(state.activeTrack!); + notificationService.addTrack(playlist.activeTrack!); + discord.updatePresence(playlist.activeTrack!); updatePalette(); }); } @@ -46,17 +72,18 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { String? lastScrobbled; return audioPlayer.positionStream.listen((position) { try { - final uid = state.activeTrack is LocalTrack - ? (state.activeTrack as LocalTrack).path - : state.activeTrack?.id; + final uid = playlist.activeTrack is LocalTrack + ? (playlist.activeTrack as LocalTrack).path + : playlist.activeTrack?.id; - if (state.activeTrack == null || + if (playlist.activeTrack == null || lastScrobbled == uid || position.inSeconds < 30) { return; } - scrobbler.scrobble(state.activeTrack!); + scrobbler.scrobble(playlist.activeTrack!); + history.addTrack(playlist.activeTrack!); lastScrobbled = uid; } catch (e, stack) { Catcher2.reportCheckedError(e, stack); @@ -68,9 +95,9 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { String lastTrack = ""; // used to prevent multiple calls to the same track return audioPlayer.positionStream.listen((event) async { if (event < const Duration(seconds: 3) || - state.active == null || - state.active == state.tracks.length - 1) return; - final nextTrack = state.tracks.elementAt(state.active! + 1); + playlist.active == null || + playlist.active == playlist.tracks.length - 1) return; + final nextTrack = playlist.tracks.elementAt(playlist.active! + 1); if (lastTrack == nextTrack.id || nextTrack is LocalTrack) return; diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index 1378c589b..9f371b7a4 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 9811a1f8e..c8eb3657a 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -2,14 +2,12 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/blacklist_provider.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; @@ -32,6 +30,8 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier { ProxyPlaylist get playlist => state; BlackListNotifier get blacklist => ref.read(blacklistProvider.notifier); Discord get discord => ref.read(discordProvider); + PlaybackHistoryNotifier get history => + ref.read(playbackHistoryProvider.notifier); List _subscriptions = []; @@ -167,28 +167,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier { discord.clear(); } - Future updatePalette() async { - final palette = ref.read(paletteProvider); - if (!preferences.albumColorSync) { - if (palette != null) ref.read(paletteProvider.notifier).state = null; - return; - } - return Future.microtask(() async { - if (state.activeTrack == null) return; - - final palette = await PaletteGenerator.fromImageProvider( - UniversalImage.imageProvider( - (state.activeTrack?.album?.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - ), - height: 50, - width: 50, - ), - ); - ref.read(paletteProvider.notifier).state = palette; - }); - } - @override set state(state) { super.state = state; diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index d34586f33..fe7269158 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/provider/palette_provider.dart'; +import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index 95ed4b035..4bcb3a467 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -6,8 +6,7 @@ part of 'user_preferences_state.dart'; // JsonSerializableGenerator // ************************************************************************** -_$UserPreferencesImpl _$$UserPreferencesImplFromJson( - Map json) => +_$UserPreferencesImpl _$$UserPreferencesImplFromJson(Map json) => _$UserPreferencesImpl( audioQuality: $enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ?? diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index d67652b49..8d3e0bfb4 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:catcher_2/catcher_2.dart'; import 'package:flutter/foundation.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 3bb884475..62cc85520 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -11,7 +11,7 @@ class MobileAudioService extends BaseAudioHandler { AudioSession? session; final ProxyPlaylistNotifier playlistNotifier; - // ignore: invalid_use_of_protected_member + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member ProxyPlaylist get playlist => playlistNotifier.state; MobileAudioService(this.playlistNotifier) { @@ -135,7 +135,7 @@ class MobileAudioService extends BaseAudioHandler { playing: audioPlayer.isPlaying, updatePosition: position, bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero, - shuffleMode: await audioPlayer.isShuffled == true + shuffleMode: audioPlayer.isShuffled == true ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, repeatMode: (audioPlayer.loopMode).toAudioServiceRepeatMode(), diff --git a/lib/services/song_link/song_link.g.dart b/lib/services/song_link/song_link.g.dart index 911849e3a..7658a74c8 100644 --- a/lib/services/song_link/song_link.g.dart +++ b/lib/services/song_link/song_link.g.dart @@ -6,8 +6,7 @@ part of 'song_link.dart'; // JsonSerializableGenerator // ************************************************************************** -_$SongLinkImpl _$$SongLinkImplFromJson(Map json) => - _$SongLinkImpl( +_$SongLinkImpl _$$SongLinkImplFromJson(Map json) => _$SongLinkImpl( displayName: json['displayName'] as String, linkId: json['linkId'] as String, platform: json['platform'] as String, diff --git a/lib/services/sourced_track/models/source_info.g.dart b/lib/services/sourced_track/models/source_info.g.dart index 1ec9f75f7..5fe136cee 100644 --- a/lib/services/sourced_track/models/source_info.g.dart +++ b/lib/services/sourced_track/models/source_info.g.dart @@ -6,7 +6,7 @@ part of 'source_info.dart'; // JsonSerializableGenerator // ************************************************************************** -SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo( +SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo( id: json['id'] as String, title: json['title'] as String, artist: json['artist'] as String, diff --git a/lib/services/sourced_track/models/source_map.g.dart b/lib/services/sourced_track/models/source_map.g.dart index e1085aa81..a581cc672 100644 --- a/lib/services/sourced_track/models/source_map.g.dart +++ b/lib/services/sourced_track/models/source_map.g.dart @@ -6,8 +6,7 @@ part of 'source_map.dart'; // JsonSerializableGenerator // ************************************************************************** -SourceQualityMap _$SourceQualityMapFromJson(Map json) => - SourceQualityMap( +SourceQualityMap _$SourceQualityMapFromJson(Map json) => SourceQualityMap( high: json['high'] as String, medium: json['medium'] as String, low: json['low'] as String, @@ -20,16 +19,18 @@ Map _$SourceQualityMapToJson(SourceQualityMap instance) => 'low': instance.low, }; -SourceMap _$SourceMapFromJson(Map json) => SourceMap( +SourceMap _$SourceMapFromJson(Map json) => SourceMap( weba: json['weba'] == null ? null - : SourceQualityMap.fromJson(json['weba'] as Map), + : SourceQualityMap.fromJson( + Map.from(json['weba'] as Map)), m4a: json['m4a'] == null ? null - : SourceQualityMap.fromJson(json['m4a'] as Map), + : SourceQualityMap.fromJson( + Map.from(json['m4a'] as Map)), ); Map _$SourceMapToJson(SourceMap instance) => { - 'weba': instance.weba, - 'm4a': instance.m4a, + 'weba': instance.weba?.toJson(), + 'm4a': instance.m4a?.toJson(), }; diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index ec3bb0cbf..50e923478 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -272,6 +272,22 @@ abstract class ServiceUtils { GoRouter.of(context).go(location, extra: extra); } + static void navigateNamed( + BuildContext context, + String name, { + Object? extra, + Map? pathParameters, + Map? queryParameters, + }) { + if (GoRouterState.of(context).matchedLocation == name) return; + GoRouter.of(context).goNamed( + name, + pathParameters: pathParameters ?? const {}, + queryParameters: queryParameters ?? const {}, + extra: extra, + ); + } + static void push(BuildContext context, String location, {Object? extra}) { final router = GoRouter.of(context); final routerState = GoRouterState.of(context); @@ -283,6 +299,36 @@ abstract class ServiceUtils { router.push(location, extra: extra); } + static void pushNamed( + BuildContext context, + String name, { + Object? extra, + Map pathParameters = const {}, + Map queryParameters = const {}, + }) { + final router = GoRouter.of(context); + final routerState = GoRouterState.of(context); + final routerStack = router.routerDelegate.currentConfiguration.matches + .map((e) => e.matchedLocation); + + final nameLocation = routerState.namedLocation( + name, + pathParameters: pathParameters, + queryParameters: queryParameters, + ); + + if (routerState.matchedLocation == nameLocation || + routerStack.contains(nameLocation)) { + return; + } + router.pushNamed( + name, + pathParameters: pathParameters, + queryParameters: queryParameters, + extra: extra, + ); + } + static DateTime parseSpotifyAlbumDate(AlbumSimple? album) { if (album == null || album.releaseDate == null) { return DateTime.parse("1975-01-01"); diff --git a/pubspec.lock b/pubspec.lock index 61de3f25f..c5688dea4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1455,13 +1455,12 @@ packages: source: hosted version: "1.0.9" media_kit_native_event_loop: - dependency: "direct overridden" + dependency: transitive description: - path: media_kit_native_event_loop - ref: main - resolved-ref: "285f7919bbf4a7d89a62615b14a3766a171ad575" - url: "https://github.com/media-kit/media-kit" - source: git + name: media_kit_native_event_loop + sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e + url: "https://pub.dev" + source: hosted version: "1.0.8" menu_base: dependency: transitive @@ -2048,11 +2047,12 @@ packages: spotify: dependency: "direct main" description: - name: spotify - sha256: "50bd5a07b580ee441d0b4d81227185ada768332c353671aa7555ea47cc68eb9e" - url: "https://pub.dev" - source: hosted - version: "0.13.5" + path: "." + ref: "fix/explicit-to-json" + resolved-ref: c4b37c599413ac7bfd78993e416a56105c62b634 + url: "https://github.com/KRTirtho/spotify-dart.git" + source: git + version: "0.13.6" sprintf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index dc60abf62..6ec4a2fce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,7 +115,10 @@ dependencies: flutter_sharing_intent: ^1.1.0 flutter_broadcasts: ^0.4.0 freezed_annotation: ^2.4.1 - spotify: ^0.13.5 + spotify: + git: + url: https://github.com/KRTirtho/spotify-dart.git + ref: fix/explicit-to-json bonsoir: ^5.1.9 shelf: ^1.4.1 shelf_router: ^1.1.4 @@ -156,11 +159,11 @@ dependency_overrides: git: url: https://github.com/antler119/system_tray ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c - media_kit_native_event_loop: # to fix "macro name must be an identifier" - git: - url: https://github.com/media-kit/media-kit - path: media_kit_native_event_loop - ref: main + # media_kit_native_event_loop: # to fix "macro name must be an identifier" + # git: + # url: https://github.com/media-kit/media-kit + # path: media_kit_native_event_loop + # ref: main flutter: generate: true diff --git a/untranslated_messages.json b/untranslated_messages.json index 3ea0ca23c..aaf069293 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -3,181 +3,207 @@ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "bn": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "ca": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "cs": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "de": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "es": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "eu": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "fa": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "fi": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "fr": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "hi": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "id": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "it": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "ja": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "ka": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "ko": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "ne": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "nl": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "pl": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "pt": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "ru": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "th": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "tr": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "uk": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "vi": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ], "zh": [ "local_library", "add_library_location", "remove_library_location", - "local_tab" + "local_tab", + "stats" ] } From d2683c52d81d807be6ff72f15b8e9eb18181e211 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 11:41:35 +0600 Subject: [PATCH 28/57] fix: some text are garbled in different parts of the app #1463 #1505 --- lib/provider/spotify/lyrics/synced.dart | 4 ++-- .../custom_spotify_endpoints/spotify_endpoints.dart | 13 +++++++------ lib/utils/service_utils.dart | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index 066596a95..afb27a6b2 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -30,7 +30,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier ); } final linesRaw = Map.castFrom( - jsonDecode(res.body), + jsonDecode(utf8.decode(res.bodyBytes)), )["lyrics"]?["lines"] as List?; final lines = linesRaw?.map((line) { @@ -83,7 +83,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier ); } - final json = jsonDecode(res.body) as Map; + final json = jsonDecode(utf8.decode(res.bodyBytes)) as Map; final syncedLyricsRaw = json["syncedLyrics"] as String?; final syncedLyrics = syncedLyricsRaw?.isNotEmpty == true diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart index d8600366a..553f68245 100644 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart @@ -75,7 +75,7 @@ class CustomSpotifyEndpoints { ); if (res.statusCode == 200) { - return jsonDecode(res.body); + return jsonDecode(utf8.decode(res.bodyBytes)); } else { throw Exception( '[CustomSpotifyEndpoints.getView]: Failed to get view' @@ -96,7 +96,7 @@ class CustomSpotifyEndpoints { ); if (res.statusCode == 200) { - final body = jsonDecode(res.body); + final body = jsonDecode(utf8.decode(res.bodyBytes)); return List.from(body["genres"] ?? []); } else { throw Exception( @@ -160,7 +160,7 @@ class CustomSpotifyEndpoints { "accept": "application/json", }, ); - final result = jsonDecode(res.body); + final result = jsonDecode(utf8.decode(res.bodyBytes)); return List.castFrom( result["tracks"].map((track) => Track.fromJson(track)).toList(), ); @@ -175,7 +175,7 @@ class CustomSpotifyEndpoints { "accept": "application/json", }, ); - return SpotifyFriends.fromJson(jsonDecode(res.body)); + return SpotifyFriends.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); } Future getHomeFeed({ @@ -232,7 +232,7 @@ class CustomSpotifyEndpoints { final data = SpotifyHomeFeed.fromJson( transformHomeFeedJsonMap( - jsonDecode(response.body), + jsonDecode(utf8.decode(response.bodyBytes)), ), ); @@ -293,7 +293,8 @@ class CustomSpotifyEndpoints { final data = SpotifyHomeFeedSection.fromJson( transformSectionItemJsonMap( - jsonDecode(response.body)["data"]["homeSections"]["sections"][0], + jsonDecode(utf8.decode(response.bodyBytes))["data"]["homeSections"] + ["sections"][0], ), ); diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 50e923478..1432eb539 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -115,7 +115,7 @@ abstract class ServiceUtils { Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"), headers: authHeader ? headers : null, ); - Map data = jsonDecode(response.body)["response"]; + Map data = jsonDecode(utf8.decode(response.bodyBytes))["response"]; if (data["hits"]?.length == 0) return null; List results = data["hits"]?.map((val) { return { From b2d9e647585ea5e834b949307d4de9cb73d6cacc Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 12:31:20 +0600 Subject: [PATCH 29/57] refactor: use replace http with dio and use it as the default --- .../proxy_playlist/skip_segments.dart | 49 +++--- lib/provider/spotify/lyrics/synced.dart | 46 +++--- lib/provider/spotify/spotify.dart | 4 +- .../spotify_endpoints.dart | 146 +++++++----------- lib/services/dio/dio.dart | 3 + lib/utils/service_utils.dart | 47 ++++-- pubspec.yaml | 2 +- 7 files changed, 148 insertions(+), 149 deletions(-) create mode 100644 lib/services/dio/dio.dart diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 2d90eea63..7f3d1e9a3 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -1,12 +1,11 @@ -import 'dart:convert'; - import 'package:catcher_2/catcher_2.dart'; +import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/dio/dio.dart'; class SourcedSegments { final String source; @@ -30,29 +29,35 @@ Future> getAndCacheSkipSegments(String id) async { ); } - final res = await get(Uri( - scheme: "https", - host: "sponsor.ajay.app", - path: "/api/skipSegments", - queryParameters: { - "videoID": id, - "category": [ - 'sponsor', - 'selfpromo', - 'interaction', - 'intro', - 'outro', - 'music_offtopic' - ], - "actionType": 'skip' - }, - )); + final res = await globalDio.getUri( + Uri( + scheme: "https", + host: "sponsor.ajay.app", + path: "/api/skipSegments", + queryParameters: { + "videoID": id, + "category": [ + 'sponsor', + 'selfpromo', + 'interaction', + 'intro', + 'outro', + 'music_offtopic' + ], + "actionType": 'skip' + }, + ), + options: Options( + responseType: ResponseType.json, + validateStatus: (status) => (status ?? 0) < 500, + ), + ); - if (res.body == "Not Found") { + if (res.data == "Not Found") { return List.castFrom([]); } - final data = jsonDecode(res.body) as List; + final data = res.data as List; final segments = data.map((obj) { final start = obj["segment"].first.toInt(); final end = obj["segment"].last.toInt(); diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index afb27a6b2..04a2ddca2 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -9,29 +9,34 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier Track get _track => arg!; Future getSpotifyLyrics(String? token) async { - final res = await http.get( - Uri.parse( - "https://spclient.wg.spotify.com/color-lyrics/v2/track/${_track.id}?format=json&market=from_token", - ), + final res = await globalDio.getUri( + Uri.parse( + "https://spclient.wg.spotify.com/color-lyrics/v2/track/${_track.id}?format=json&market=from_token", + ), + options: Options( headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36", "App-platform": "WebPlayer", "authorization": "Bearer $token" - }); + }, + responseType: ResponseType.json, + validateStatus: (status) => true, + ), + ); if (res.statusCode != 200) { return SubtitleSimple( lyrics: [], name: _track.name!, - uri: res.request!.url, + uri: res.realUri, rating: 0, provider: "Spotify", ); } - final linesRaw = Map.castFrom( - jsonDecode(utf8.decode(res.bodyBytes)), - )["lyrics"]?["lines"] as List?; + final linesRaw = + Map.castFrom(res.data)["lyrics"] + ?["lines"] as List?; final lines = linesRaw?.map((line) { return LyricSlice( @@ -44,7 +49,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier return SubtitleSimple( lyrics: lines, name: _track.name!, - uri: res.request!.url, + uri: res.realUri, rating: 100, provider: "Spotify", ); @@ -55,7 +60,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier Future getLRCLibLyrics() async { final packageInfo = await PackageInfo.fromPlatform(); - final res = await http.get( + final res = await globalDio.getUri( Uri( scheme: "https", host: "lrclib.net", @@ -67,23 +72,26 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier "duration": _track.duration?.inSeconds.toString(), }, ), - headers: { - "User-Agent": - "Spotube v${packageInfo.version} (https://github.com/KRTirtho/spotube)" - }, + options: Options( + headers: { + "User-Agent": + "Spotube v${packageInfo.version} (https://github.com/KRTirtho/spotube)" + }, + responseType: ResponseType.json, + ), ); if (res.statusCode != 200) { return SubtitleSimple( lyrics: [], name: _track.name!, - uri: res.request!.url, + uri: res.realUri, rating: 0, provider: "LRCLib", ); } - final json = jsonDecode(utf8.decode(res.bodyBytes)) as Map; + final json = res.data as Map; final syncedLyricsRaw = json["syncedLyrics"] as String?; final syncedLyrics = syncedLyricsRaw?.isNotEmpty == true @@ -97,7 +105,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier return SubtitleSimple( lyrics: syncedLyrics!, name: _track.name!, - uri: res.request!.url, + uri: res.realUri, rating: 100, provider: "LRCLib", ); @@ -111,7 +119,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier return SubtitleSimple( lyrics: plainLyrics, name: _track.name!, - uri: res.request!.url, + uri: res.realUri, rating: 0, provider: "LRCLib", ); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index 816420f65..ac83ba727 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -1,10 +1,10 @@ library spotify; import 'dart:async'; -import 'dart:convert'; import 'package:catcher_2/catcher_2.dart'; import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; @@ -23,9 +23,9 @@ import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/wikipedia/wikipedia.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; -import 'package:http/http.dart' as http; import 'package:wikipedia_api/wikipedia_api.dart'; diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart index 553f68245..4bc78f8ac 100644 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:dio/dio.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/spotify/home_feed.dart'; import 'package:spotube/models/spotify_friends.dart'; @@ -9,9 +9,21 @@ import 'package:timezone/timezone.dart' as tz; class CustomSpotifyEndpoints { static const _baseUrl = 'https://api.spotify.com/v1'; final String accessToken; - final http.Client _client; + final Dio _client; - CustomSpotifyEndpoints(this.accessToken) : _client = http.Client(); + CustomSpotifyEndpoints(this.accessToken) + : _client = Dio( + BaseOptions( + baseUrl: _baseUrl, + responseType: ResponseType.json, + headers: { + "content-type": "application/json", + if (accessToken.isNotEmpty) + "authorization": "Bearer $accessToken", + "accept": "application/json", + }, + ), + ); // views API @@ -65,44 +77,34 @@ class CustomSpotifyEndpoints { if (country != null) 'country': country.name, }.entries.map((e) => '${e.key}=${e.value}').join('&'); - final res = await _client.get( + final res = await _client.getUri( Uri.parse('$_baseUrl/views/$view?$queryParams'), - headers: { - "content-type": "application/json", - "authorization": "Bearer $accessToken", - "accept": "application/json", - }, ); if (res.statusCode == 200) { - return jsonDecode(utf8.decode(res.bodyBytes)); + return res.data; } else { throw Exception( '[CustomSpotifyEndpoints.getView]: Failed to get view' '\nStatus code: ${res.statusCode}' - '\nBody: ${res.body}', + '\nBody: ${res.data}', ); } } Future> listGenreSeeds() async { - final res = await _client.get( + final res = await _client.getUri( Uri.parse("$_baseUrl/recommendations/available-genre-seeds"), - headers: { - "content-type": "application/json", - if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken", - "accept": "application/json", - }, ); if (res.statusCode == 200) { - final body = jsonDecode(utf8.decode(res.bodyBytes)); + final body = res.data; return List.from(body["genres"] ?? []); } else { throw Exception( '[CustomSpotifyEndpoints.listGenreSeeds]: Failed to get genre seeds' '\nStatus code: ${res.statusCode}' - '\nBody: ${res.body}', + '\nBody: ${res.data}', ); } } @@ -152,30 +154,18 @@ class CustomSpotifyEndpoints { } final pathQuery = "$_baseUrl/recommendations?${parameters.entries.map((e) => '${e.key}=${e.value}').join('&')}"; - final res = await _client.get( - Uri.parse(pathQuery), - headers: { - "content-type": "application/json", - if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken", - "accept": "application/json", - }, - ); - final result = jsonDecode(utf8.decode(res.bodyBytes)); + final res = await _client.getUri(Uri.parse(pathQuery)); + final result = res.data; return List.castFrom( result["tracks"].map((track) => Track.fromJson(track)).toList(), ); } Future getFriendActivity() async { - final res = await _client.get( + final res = await _client.getUri( Uri.parse("https://guc-spclient.spotify.com/presence-view/v1/buddylist"), - headers: { - "content-type": "application/json", - "authorization": "Bearer $accessToken", - "accept": "application/json", - }, ); - return SpotifyFriends.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); + return SpotifyFriends.fromJson(res.data); } Future getHomeFeed({ @@ -190,50 +180,39 @@ class CustomSpotifyEndpoints { 'origin': 'https://open.spotify.com', 'referer': 'https://open.spotify.com/' }; - final response = await http.get( - Uri( - scheme: "https", - host: "api-partner.spotify.com", - path: "/pathfinder/v1/query", - queryParameters: { - "operationName": "home", - "variables": jsonEncode({ - "timeZone": tz.local.name, - "sp_t": spTCookie, - "country": country.name, - "facet": null, - "sectionItemsLimit": 10 - }), - "extensions": jsonEncode( - { - "persistedQuery": { - "version": 1, + final response = await _client.getUri( + Uri( + scheme: "https", + host: "api-partner.spotify.com", + path: "/pathfinder/v1/query", + queryParameters: { + "operationName": "home", + "variables": jsonEncode({ + "timeZone": tz.local.name, + "sp_t": spTCookie, + "country": country.name, + "facet": null, + "sectionItemsLimit": 10 + }), + "extensions": jsonEncode( + { + "persistedQuery": { + "version": 1, - /// GraphQL persisted Query hash - /// This can change overtime. We've to lookout for it - /// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/ - "sha256Hash": - "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be", - } - }, - ), - }, - ), - headers: headers, - ); - - if (response.statusCode >= 400) { - throw Exception( - "[RequestException] " - "Status: ${response.statusCode}\n" - "Body: ${response.body}", - ); - } + /// GraphQL persisted Query hash + /// This can change overtime. We've to lookout for it + /// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/ + "sha256Hash": + "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be", + } + }, + ), + }, + ), + options: Options(headers: headers)); final data = SpotifyHomeFeed.fromJson( - transformHomeFeedJsonMap( - jsonDecode(utf8.decode(response.bodyBytes)), - ), + transformHomeFeedJsonMap(response.data), ); return data; @@ -252,7 +231,7 @@ class CustomSpotifyEndpoints { 'origin': 'https://open.spotify.com', 'referer': 'https://open.spotify.com/' }; - final response = await http.get( + final response = await _client.getUri( Uri( scheme: "https", host: "api-partner.spotify.com", @@ -280,21 +259,12 @@ class CustomSpotifyEndpoints { ), }, ), - headers: headers, + options: Options(headers: headers), ); - if (response.statusCode >= 400) { - throw Exception( - "[RequestException] " - "Status: ${response.statusCode}\n" - "Body: ${response.body}", - ); - } - final data = SpotifyHomeFeedSection.fromJson( transformSectionItemJsonMap( - jsonDecode(utf8.decode(response.bodyBytes))["data"]["homeSections"] - ["sections"][0], + response.data["data"]["homeSections"]["sections"][0], ), ); diff --git a/lib/services/dio/dio.dart b/lib/services/dio/dio.dart new file mode 100644 index 000000000..cddf19795 --- /dev/null +++ b/lib/services/dio/dio.dart @@ -0,0 +1,3 @@ +import 'package:dio/dio.dart'; + +final globalDio = Dio(); diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 1432eb539..aa2cd985d 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -1,13 +1,12 @@ -import 'dart:convert'; - +import 'package:dio/dio.dart'; import 'package:go_router/go_router.dart'; import 'package:html/dom.dart' hide Text; import 'package:spotify/spotify.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/components/root/update_dialog.dart'; import 'package:spotube/models/logger.dart'; -import 'package:http/http.dart' as http; import 'package:spotube/models/lyrics.dart'; +import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; @@ -70,9 +69,12 @@ abstract class ServiceUtils { } static Future extractLyrics(Uri url) async { - final response = await http.get(url); + final response = await globalDio.getUri( + url, + options: Options(responseType: ResponseType.plain), + ); - Document document = parser.parse(response.body); + Document document = parser.parse(response.data); String? lyrics = document.querySelector('div.lyrics')?.text.trim(); if (lyrics == null) { lyrics = ""; @@ -111,11 +113,14 @@ abstract class ServiceUtils { String reqUrl = "$searchUrl${Uri.encodeComponent(song)}"; Map headers = {"Authorization": 'Bearer $apiKey'}; - final response = await http.get( + final response = await globalDio.getUri( Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"), - headers: authHeader ? headers : null, + options: Options( + headers: authHeader ? headers : null, + responseType: ResponseType.json, + ), ); - Map data = jsonDecode(utf8.decode(response.bodyBytes))["response"]; + Map data = response.data["response"]; if (data["hits"]?.length == 0) return null; List results = data["hits"]?.map((val) { return { @@ -195,8 +200,11 @@ abstract class ServiceUtils { queryParameters: {"q": query}, ); - final res = await http.get(searchUri); - final document = parser.parse(res.body); + final res = await globalDio.getUri( + searchUri, + options: Options(responseType: ResponseType.plain), + ); + final document = parser.parse(res.data); final results = document.querySelectorAll("#tablecontainer table tbody tr td a"); @@ -229,7 +237,11 @@ abstract class ServiceUtils { logger.v("[Selected subtitle] ${topResult.text} | $subtitleUri"); - final lrcDocument = parser.parse((await http.get(subtitleUri)).body); + final lrcDocument = parser.parse((await globalDio.getUri( + subtitleUri, + options: Options(responseType: ResponseType.plain), + )) + .data); final lrcList = lrcDocument .querySelector("#ctl00_ContentPlaceHolder1_lbllyrics") ?.innerHtml @@ -384,14 +396,16 @@ abstract class ServiceUtils { final packageInfo = await PackageInfo.fromPlatform(); if (Env.releaseChannel == ReleaseChannel.nightly) { - final value = await http.get( + final value = await globalDio.getUri( Uri.parse( "https://api.github.com/repos/KRTirtho/spotube/actions/workflows/spotube-release-binary.yml/runs?status=success&per_page=1", ), + options: Options( + responseType: ResponseType.json, + ), ); - final buildNum = - jsonDecode(value.body)["workflow_runs"][0]["run_number"] as int; + final buildNum = value.data["workflow_runs"][0]["run_number"] as int; if (buildNum <= int.parse(packageInfo.buildNumber) || !context.mounted) { return; @@ -406,13 +420,12 @@ abstract class ServiceUtils { }, ); } else { - final value = await http.get( + final value = await globalDio.getUri( Uri.parse( "https://api.github.com/repos/KRTirtho/spotube/releases/latest", ), ); - final tagName = - (jsonDecode(value.body)["tag_name"] as String).replaceAll("v", ""); + final tagName = (value.data["tag_name"] as String).replaceAll("v", ""); final currentVersion = packageInfo.version == "Unknown" ? null : Version.parse(packageInfo.version); diff --git a/pubspec.yaml b/pubspec.yaml index 6ec4a2fce..c3ab2a535 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,6 @@ dependencies: hive_flutter: ^1.1.0 hooks_riverpod: ^2.5.1 html: ^0.15.1 - http: ^1.2.0 image_picker: ^1.1.0 intl: ^0.18.0 introduction_screen: ^3.1.14 @@ -131,6 +130,7 @@ dependencies: crypto: ^3.0.3 local_notifier: ^0.1.6 tray_manager: ^0.2.2 + http: ^1.2.1 dev_dependencies: build_runner: ^2.4.9 From e1786989ffbab9d045f14f25fb62a3b72ec19774 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 12:36:00 +0600 Subject: [PATCH 30/57] cd: use dio in cli as well --- cli/commands/credits.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cli/commands/credits.dart b/cli/commands/credits.dart index 66ec1172b..6bad7a444 100644 --- a/cli/commands/credits.dart +++ b/cli/commands/credits.dart @@ -2,13 +2,19 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:collection/collection.dart'; -import 'package:http/http.dart'; +import 'package:dio/dio.dart'; import 'package:html/parser.dart'; import 'package:path/path.dart'; import 'package:pub_api_client/pub_api_client.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; class CreditsCommand extends Command { + final dio = Dio( + BaseOptions( + responseType: ResponseType.plain, + ), + ); + @override String get description => "Generate credits for used Library's authors"; @@ -66,11 +72,11 @@ class CreditsCommand extends Command { final gitPubspecs = await Future.wait( gitDeps.map( (d) { - Pubspec parser(res) { + Pubspec parser(Response res) { try { - return Pubspec.parse(res.body); + return Pubspec.parse(res.data); } catch (e) { - final document = parse(res.body); + final document = parse(res.data); final pre = document.querySelector('pre'); if (pre == null) { stdout.writeln(d.toString()); @@ -80,8 +86,9 @@ class CreditsCommand extends Command { } } - return get(Uri.parse(d.value)).then(parser).catchError( - (_) => get(Uri.parse(d.value.replaceFirst('/main', '/master'))) + return dio.get(d.value).then(parser).catchError( + (_) => dio + .get(d.value.replaceFirst('/main', '/master')) .then(parser), ); }, From e034455173df8d97c70dfa849ce3eaa99f3c0c66 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 12:47:36 +0600 Subject: [PATCH 31/57] chore: fix home feed not showing up --- lib/provider/authentication_provider.dart | 5 +- .../spotify_endpoints.dart | 57 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication_provider.dart index c94f4f3ee..be61cb4f0 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication_provider.dart @@ -52,8 +52,9 @@ class AuthenticationCredentials { headers: { "Cookie": spDc ?? "", "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" }, + validateStatus: (status) => true, ), ); final body = res.data; @@ -65,7 +66,7 @@ class AuthenticationCredentials { } return AuthenticationCredentials( - cookie: "${res.headers["set-cookie"]}; $spDc", + cookie: "${res.headers["set-cookie"]?.join(";")}; $spDc", accessToken: body['accessToken'], expiration: DateTime.fromMillisecondsSinceEpoch( body['accessTokenExpirationTimestampMs'], diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart index 4bc78f8ac..0c7daeb2f 100644 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart @@ -181,35 +181,36 @@ class CustomSpotifyEndpoints { 'referer': 'https://open.spotify.com/' }; final response = await _client.getUri( - Uri( - scheme: "https", - host: "api-partner.spotify.com", - path: "/pathfinder/v1/query", - queryParameters: { - "operationName": "home", - "variables": jsonEncode({ - "timeZone": tz.local.name, - "sp_t": spTCookie, - "country": country.name, - "facet": null, - "sectionItemsLimit": 10 - }), - "extensions": jsonEncode( - { - "persistedQuery": { - "version": 1, + Uri( + scheme: "https", + host: "api-partner.spotify.com", + path: "/pathfinder/v1/query", + queryParameters: { + "operationName": "home", + "variables": jsonEncode({ + "timeZone": tz.local.name, + "sp_t": spTCookie, + "country": country.name, + "facet": null, + "sectionItemsLimit": 10 + }), + "extensions": jsonEncode( + { + "persistedQuery": { + "version": 1, - /// GraphQL persisted Query hash - /// This can change overtime. We've to lookout for it - /// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/ - "sha256Hash": - "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be", - } - }, - ), - }, - ), - options: Options(headers: headers)); + /// GraphQL persisted Query hash + /// This can change overtime. We've to lookout for it + /// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/ + "sha256Hash": + "eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be", + } + }, + ), + }, + ), + options: Options(headers: headers), + ); final data = SpotifyHomeFeed.fromJson( transformHomeFeedJsonMap(response.data), From c4023aa09de56c19110de6c6883951459aef692b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 13:05:16 +0600 Subject: [PATCH 32/57] chore: downloaded tracks folder not opening --- lib/components/library/local_folder/local_folder_item.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/library/local_folder/local_folder_item.dart b/lib/components/library/local_folder/local_folder_item.dart index 556f09a63..72032198c 100644 --- a/lib/components/library/local_folder/local_folder_item.dart +++ b/lib/components/library/local_folder/local_folder_item.dart @@ -61,7 +61,7 @@ class LocalFolderItem extends HookConsumerWidget { context.goNamed( LocalLibraryPage.name, queryParameters: { - if (isDownloadFolder) "downloads": 1, + if (isDownloadFolder) "downloads": "true", }, extra: folder, ); From 02acbd93271145dde365f6c547e0d9d902be65f1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 15:45:06 +0600 Subject: [PATCH 33/57] feat: play initially available tracks of playlist/album immediately and fetch rest in background #670 --- lib/components/playlist/playlist_card.dart | 40 ++++++++++++++----- .../sections/header/header_buttons.dart | 25 +++++++++--- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index 72e13b26b..9f26f739d 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -36,12 +36,23 @@ class PlaylistCard extends HookConsumerWidget { final updating = useState(false); final me = ref.watch(meProvider); - Future> fetchAllTracks() async { + Future> fetchInitialTracks() async { if (playlist.id == 'user-liked-tracks') { return await ref.read(likedTracksProvider.future); } - await ref.read(playlistTracksProvider(playlist.id!).future); + final result = + await ref.read(playlistTracksProvider(playlist.id!).future); + + return result.items; + } + + Future> fetchAllTracks() async { + final initialTracks = await fetchInitialTracks(); + + if (playlist.id == 'user-liked-tracks') { + return initialTracks; + } return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll(); } @@ -77,23 +88,29 @@ class PlaylistCard extends HookConsumerWidget { return audioPlayer.resume(); } - List fetchedTracks = await fetchAllTracks(); + final fetchedInitialTracks = await fetchInitialTracks(); - if (fetchedTracks.isEmpty || !context.mounted) return; + if (fetchedInitialTracks.isEmpty || !context.mounted) return; final isRemoteDevice = await showSelectDeviceDialog(context, ref); if (isRemoteDevice) { final remotePlayback = ref.read(connectProvider.notifier); + final allTracks = await fetchAllTracks(); await remotePlayback.load( WebSocketLoadEventData.playlist( - tracks: fetchedTracks, + tracks: allTracks, collection: playlist, ), ); } else { - await playlistNotifier.load(fetchedTracks, autoPlay: true); + await playlistNotifier.load(fetchedInitialTracks, autoPlay: true); playlistNotifier.addCollection(playlist.id!); historyNotifier.addPlaylists([playlist]); + + final allTracks = await fetchAllTracks(); + + await playlistNotifier + .addTracks(allTracks.sublist(fetchedInitialTracks.length)); } } finally { if (context.mounted) { @@ -106,21 +123,22 @@ class PlaylistCard extends HookConsumerWidget { try { if (isPlaylistPlaying) return; - final fetchedTracks = await fetchAllTracks(); + final fetchedInitialTracks = await fetchAllTracks(); - if (fetchedTracks.isEmpty) return; + if (fetchedInitialTracks.isEmpty) return; - playlistNotifier.addTracks(fetchedTracks); + playlistNotifier.addTracks(fetchedInitialTracks); playlistNotifier.addCollection(playlist.id!); historyNotifier.addPlaylists([playlist]); if (context.mounted) { final snackbar = SnackBar( - content: Text("Added ${fetchedTracks.length} tracks to queue"), + content: + Text("Added ${fetchedInitialTracks.length} tracks to queue"), action: SnackBarAction( label: "Undo", onPressed: () { playlistNotifier - .removeTracks(fetchedTracks.map((e) => e.id!)); + .removeTracks(fetchedInitialTracks.map((e) => e.id!)); }, ), ); diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/shared/tracks_view/sections/header/header_buttons.dart index 5ffff5122..5cc442cf9 100644 --- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/shared/tracks_view/sections/header/header_buttons.dart @@ -47,12 +47,12 @@ class TrackViewHeaderButtons extends HookConsumerWidget { try { isLoading.value = true; - final allTracks = await props.pagination.onFetchAll(); - + final initialTracks = props.tracks; if (!context.mounted) return; final isRemoteDevice = await showSelectDeviceDialog(context, ref); if (isRemoteDevice) { + final allTracks = await props.pagination.onFetchAll(); final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( props.collection is AlbumSimple @@ -69,9 +69,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget { await remotePlayback.setShuffle(true); } else { await playlistNotifier.load( - allTracks, + initialTracks, autoPlay: true, - initialIndex: Random().nextInt(allTracks.length), + initialIndex: Random().nextInt(initialTracks.length), ); await audioPlayer.setShuffle(true); playlistNotifier.addCollection(props.collectionId); @@ -80,6 +80,12 @@ class TrackViewHeaderButtons extends HookConsumerWidget { } else { historyNotifier.addPlaylists([props.collection as PlaylistSimple]); } + + final allTracks = await props.pagination.onFetchAll(); + + await playlistNotifier.addTracks( + allTracks.sublist(initialTracks.length), + ); } } finally { isLoading.value = false; @@ -90,12 +96,13 @@ class TrackViewHeaderButtons extends HookConsumerWidget { try { isLoading.value = true; - final allTracks = await props.pagination.onFetchAll(); + final initialTracks = props.tracks; if (!context.mounted) return; final isRemoteDevice = await showSelectDeviceDialog(context, ref); if (isRemoteDevice) { + final allTracks = await props.pagination.onFetchAll(); final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( props.collection is AlbumSimple @@ -109,13 +116,19 @@ class TrackViewHeaderButtons extends HookConsumerWidget { ), ); } else { - await playlistNotifier.load(allTracks, autoPlay: true); + await playlistNotifier.load(initialTracks, autoPlay: true); playlistNotifier.addCollection(props.collectionId); if (props.collection is AlbumSimple) { historyNotifier.addAlbums([props.collection as AlbumSimple]); } else { historyNotifier.addPlaylists([props.collection as PlaylistSimple]); } + + final allTracks = await props.pagination.onFetchAll(); + + await playlistNotifier.addTracks( + allTracks.sublist(initialTracks.length), + ); } } finally { isLoading.value = false; From 71341ec0bda6ed985b43836712075b97a2cf8bac Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 1 Jun 2024 21:33:05 +0600 Subject: [PATCH 34/57] feat: upgrade to Flutter 3.22.0 --- .fvm/fvm_config.json | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- android/app/src/main/AndroidManifest.xml | 5 ++ devtools_options.yaml | 1 + lib/main.dart | 21 +------ pubspec.lock | 60 +++++--------------- pubspec.yaml | 3 +- 7 files changed, 26 insertions(+), 68 deletions(-) create mode 100644 devtools_options.yaml diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index d42a42fae..6a56dfc65 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.19.5", + "flutterSdkVersion": "3.22.0", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 694dc1ebf..eb62b58d7 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -20,7 +20,7 @@ on: description: Dry run without uploading to release env: - FLUTTER_VERSION: 3.19.5 + FLUTTER_VERSION: 3.22.0 permissions: contents: write diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5ab7a0b51..52547f045 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,11 @@ android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" > + + + main(List rawArgs) async { ), runAppFunction: () { runApp( - ProviderScope( - child: DevicePreview( - availableLocales: L10n.all, - enabled: false, - data: const DevicePreviewData( - isEnabled: false, - orientation: Orientation.portrait, - ), - builder: (context) { - return const Spotube(); - }, - ), - ), + const ProviderScope(child: Spotube()), ); }, ); @@ -230,10 +217,8 @@ class SpotubeState extends ConsumerState { debugShowCheckedModeBanner: false, title: 'Spotube', builder: (context, child) { - return DevicePreview.appBuilder( - context, - kIsDesktop && !kIsMacOS ? DragToResizeArea(child: child!) : child, - ); + if (kIsDesktop && !kIsMacOS) return DragToResizeArea(child: child!); + return child!; }, themeMode: themeMode, theme: lightTheme, diff --git a/pubspec.lock b/pubspec.lock index c5688dea4..32da1f8dd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -466,14 +466,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - device_frame: - dependency: transitive - description: - name: device_frame - sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d - url: "https://pub.dev" - source: hosted - version: "1.1.0" device_info_plus: dependency: "direct main" description: @@ -490,14 +482,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - device_preview: - dependency: "direct main" - description: - name: device_preview - sha256: "2f097bf31b929e15e6756dbe0ec1bcb63952ab9ed51c25dc5a2c722d2b21fdaf" - url: "https://pub.dev" - source: hosted - version: "1.1.0" dio: dependency: "direct main" description: @@ -1258,10 +1242,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" introduction_screen: dependency: "direct main" description: @@ -1314,26 +1298,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -1474,10 +1458,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" metadata_god: dependency: "direct main" description: @@ -1494,14 +1478,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" oauth2: dependency: transitive description: @@ -1734,14 +1710,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.14.2" - provider: - dependency: transitive - description: - name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c - url: "https://pub.dev" - source: hosted - version: "6.1.2" pub_api_client: dependency: "direct main" description: @@ -2178,10 +2146,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" time: dependency: transitive description: @@ -2386,10 +2354,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c3ab2a535..56c25dd78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,6 @@ dependencies: curved_navigation_bar: ^1.0.3 dbus: ^0.7.8 device_info_plus: ^10.1.0 - device_preview: ^1.1.0 dio: ^5.4.3+1 disable_battery_optimization: ^1.1.1 duration: ^3.0.12 @@ -56,7 +55,7 @@ dependencies: hooks_riverpod: ^2.5.1 html: ^0.15.1 image_picker: ^1.1.0 - intl: ^0.18.0 + intl: any introduction_screen: ^3.1.14 json_annotation: ^4.8.1 logger: ^2.0.2 From 56241f773a53b91ab9652a1e25cba7fb6ec85c9c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 2 Jun 2024 21:15:11 +0600 Subject: [PATCH 35/57] refactor: migrate deprecated warnings --- lib/components/artist/artist_card.dart | 12 ++++----- .../home/sections/friends/friend_item.dart | 2 +- lib/components/home/sections/genres.dart | 2 +- .../local_folder/local_folder_item.dart | 2 +- .../playlist_generate/multi_select_field.dart | 2 +- lib/components/player/player_queue.dart | 3 ++- .../player/sibling_tracks_sheet.dart | 3 ++- lib/components/root/bottom_player.dart | 11 +++----- lib/components/root/sidebar.dart | 10 ++----- .../root/spotube_navigation_bar.dart | 2 +- .../settings/color_scheme_picker_dialog.dart | 4 +-- .../adaptive/adaptive_pop_sheet_list.dart | 2 +- .../shared/links/anchor_button.dart | 2 +- .../shared/page_window_title_bar.dart | 12 ++++----- lib/components/shared/playbutton_card.dart | 14 +++++----- .../shared/themed_button_tab_bar.dart | 2 +- lib/components/stats/common/album_item.dart | 2 +- lib/main.dart | 23 +++------------- lib/pages/desktop_login/desktop_login.dart | 2 +- lib/pages/lyrics/lyrics.dart | 2 +- lib/pages/lyrics/mini_lyrics.dart | 12 ++++----- lib/pages/search/search.dart | 6 ++--- lib/pages/settings/blacklist.dart | 1 - lib/pages/settings/sections/about.dart | 7 +++-- lib/pages/settings/sections/accounts.dart | 2 +- lib/pages/settings/sections/downloads.dart | 1 - lib/themes/theme.dart | 27 ++++++++++++------- 27 files changed, 74 insertions(+), 96 deletions(-) diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index 57971ada9..9c1ee14ac 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -35,6 +35,10 @@ class ArtistCard extends HookConsumerWidget { final radius = BorderRadius.circular(15); + final bgColor = useBrightnessValue( + theme.colorScheme.surface, + theme.colorScheme.surfaceContainerHigh, + ); final double size = useBreakpointValue( xs: 130, sm: 130, @@ -46,12 +50,8 @@ class ArtistCard extends HookConsumerWidget { width: size, margin: const EdgeInsets.symmetric(vertical: 5), child: Material( - shadowColor: theme.colorScheme.background, - color: Color.lerp( - theme.colorScheme.surfaceVariant, - theme.colorScheme.surface, - useBrightnessValue(.9, .7), - ), + shadowColor: theme.colorScheme.surface, + color: bgColor, elevation: 3, shape: RoundedRectangleBorder( borderRadius: radius, diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart index 2b5757565..096964a66 100644 --- a/lib/components/home/sections/friends/friend_item.dart +++ b/lib/components/home/sections/friends/friend_item.dart @@ -30,7 +30,7 @@ class FriendItem extends HookConsumerWidget { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: colorScheme.surfaceVariant.withOpacity(0.3), + color: colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(15), ), constraints: const BoxConstraints( diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index 7dfafd5a0..62f462e26 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -134,7 +134,7 @@ class HomeGenresSection extends HookConsumerWidget { child: Ink( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), - color: colorScheme.surfaceVariant, + color: colorScheme.surfaceContainerHighest, gradient: categoriesQuery.isLoading ? null : gradient, ), padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/components/library/local_folder/local_folder_item.dart b/lib/components/library/local_folder/local_folder_item.dart index 72032198c..6220a9671 100644 --- a/lib/components/library/local_folder/local_folder_item.dart +++ b/lib/components/library/local_folder/local_folder_item.dart @@ -71,7 +71,7 @@ class LocalFolderItem extends HookConsumerWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Color.lerp( - colorScheme.surfaceVariant, + colorScheme.surfaceContainerHighest, colorScheme.surface, lerpValue, ), diff --git a/lib/components/library/playlist_generate/multi_select_field.dart b/lib/components/library/playlist_generate/multi_select_field.dart index e54fc2ba3..d8e0506db 100644 --- a/lib/components/library/playlist_generate/multi_select_field.dart +++ b/lib/components/library/playlist_generate/multi_select_field.dart @@ -71,7 +71,7 @@ class MultiSelectField extends HookWidget { : theme.colorScheme.onSurface.withOpacity(0.1), ), ), - mouseCursor: MaterialStateMouseCursor.textable, + mouseCursor: WidgetStateMouseCursor.textable, onPressed: !enabled ? null : () async { diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 914d7bc97..1665b3dd0 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -122,7 +122,8 @@ class PlayerQueue extends HookConsumerWidget { top: 5.0, ), decoration: BoxDecoration( - color: theme.colorScheme.surfaceVariant.withOpacity(0.5), + color: + theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), borderRadius: borderRadius, ), child: CallbackShortcuts( diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 99b7b430f..0575d8ebf 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -208,7 +208,8 @@ class SiblingTracksSheet extends HookConsumerWidget { : mediaQuery.size.height * .6, decoration: BoxDecoration( borderRadius: borderRadius, - color: theme.colorScheme.surfaceVariant.withOpacity(.5), + color: + theme.colorScheme.surfaceContainerHighest.withOpacity(.5), ), child: Scaffold( backgroundColor: Colors.transparent, diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 5429e172b..b99318dfb 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -14,7 +14,6 @@ import 'package:spotube/components/player/volume_slider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication_provider.dart'; @@ -49,12 +48,6 @@ class BottomPlayer extends HookConsumerWidget { ); final theme = Theme.of(context); - final bg = theme.colorScheme.surfaceVariant; - - final bgColor = useBrightnessValue( - Color.lerp(bg, Colors.white, 0.7), - Color.lerp(bg, Colors.black, 0.45)!, - ); // returning an empty non spacious Container as the overlay will take // place in the global overlay stack aka [_entries] @@ -67,7 +60,9 @@ class BottomPlayer extends HookConsumerWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: DecoratedBox( - decoration: BoxDecoration(color: bgColor?.withOpacity(0.8)), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer.withOpacity(.8), + ), child: Material( type: MaterialType.transparency, textStyle: theme.textTheme.bodyMedium!, diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 0e644a898..4fa14021f 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -14,7 +14,6 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/settings/settings.dart'; @@ -70,12 +69,7 @@ class Sidebar extends HookConsumerWidget { ); final theme = Theme.of(context); - final bg = theme.colorScheme.surfaceVariant; - - final bgColor = useBrightnessValue( - Color.lerp(bg, Colors.white, 0.7), - Color.lerp(bg, Colors.black, 0.45)!, - ); + final bg = theme.colorScheme.surfaceContainer; useEffect(() { if (!context.mounted) return; @@ -159,7 +153,7 @@ class Sidebar extends HookConsumerWidget { ), padding: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( - color: bgColor?.withOpacity(0.8), + color: bg, borderRadius: const BorderRadius.only( topRight: Radius.circular(10), bottomRight: Radius.circular(10), diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index e16ad1a89..3d0c7c75e 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -68,7 +68,7 @@ class SpotubeNavigationBar extends HookConsumerWidget { backgroundColor: theme.colorScheme.secondaryContainer.withOpacity(0.72), buttonBackgroundColor: buttonColor, - color: theme.colorScheme.background, + color: theme.colorScheme.surface, height: panelHeight, animationDuration: const Duration(milliseconds: 350), items: navbarTileList.map( diff --git a/lib/components/settings/color_scheme_picker_dialog.dart b/lib/components/settings/color_scheme_picker_dialog.dart index 8d0983752..579f5a296 100644 --- a/lib/components/settings/color_scheme_picker_dialog.dart +++ b/lib/components/settings/color_scheme_picker_dialog.dart @@ -179,9 +179,9 @@ class ColorTile extends StatelessWidget { colorScheme.primaryContainer, colorScheme.secondary, colorScheme.secondaryContainer, - colorScheme.background, colorScheme.surface, - colorScheme.surfaceVariant, + colorScheme.surface, + colorScheme.surfaceContainerHighest, colorScheme.onPrimary, colorScheme.onSurface, ]; diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 21f56a220..ce7d3b8c9 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -187,7 +187,7 @@ class AdaptivePopSheetList extends StatelessWidget { icon: icon ?? const Icon(SpotubeIcons.moreVertical), tooltip: tooltip, style: theme.iconButtonTheme.style?.copyWith( - shape: MaterialStatePropertyAll( + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: borderRadius, ), diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/shared/links/anchor_button.dart index d78bbf962..c6f0b889b 100644 --- a/lib/components/shared/links/anchor_button.dart +++ b/lib/components/shared/links/anchor_button.dart @@ -29,7 +29,7 @@ class AnchorButton extends HookWidget { onTapUp: (event) => tap.value = false, onTap: onTap, child: MouseRegion( - cursor: MaterialStateMouseCursor.clickable, + cursor: WidgetStateMouseCursor.clickable, child: Text( text, style: style.copyWith( diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index f19757f3b..667098445 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -206,16 +206,16 @@ class WindowTitleBarButtons extends HookConsumerWidget { final theme = Theme.of(context); final colors = WindowButtonColors( normal: Colors.transparent, - iconNormal: foregroundColor ?? theme.colorScheme.onBackground, - mouseOver: theme.colorScheme.onBackground.withOpacity(0.1), - mouseDown: theme.colorScheme.onBackground.withOpacity(0.2), - iconMouseOver: theme.colorScheme.onBackground, - iconMouseDown: theme.colorScheme.onBackground, + iconNormal: foregroundColor ?? theme.colorScheme.onSurface, + mouseOver: theme.colorScheme.onSurface.withOpacity(0.1), + mouseDown: theme.colorScheme.onSurface.withOpacity(0.2), + iconMouseOver: theme.colorScheme.onSurface, + iconMouseDown: theme.colorScheme.onSurface, ); final closeColors = WindowButtonColors( normal: Colors.transparent, - iconNormal: foregroundColor ?? theme.colorScheme.onBackground, + iconNormal: foregroundColor ?? theme.colorScheme.onSurface, mouseOver: Colors.red, mouseDown: Colors.red[800]!, iconMouseOver: Colors.white, diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 80a27eb01..807628b35 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -53,6 +53,10 @@ class PlaybuttonCard extends HookWidget { final mediaQuery = MediaQuery.of(context); final radius = BorderRadius.circular(15); + final bgColor = useBrightnessValue( + theme.colorScheme.surface, + theme.colorScheme.surfaceContainerHigh, + ); final double size = useBreakpointValue( xs: 130, sm: 130, @@ -72,13 +76,9 @@ class PlaybuttonCard extends HookWidget { constraints: BoxConstraints(maxWidth: size), margin: margin, child: Material( - color: Color.lerp( - theme.colorScheme.surfaceVariant, - theme.colorScheme.surface, - useBrightnessValue(.9, .7), - ), + color: bgColor, borderRadius: radius, - shadowColor: theme.colorScheme.background, + shadowColor: theme.colorScheme.surface, elevation: 3, child: InkWell( mouseCursor: SystemMouseCursors.click, @@ -158,7 +158,7 @@ class PlaybuttonCard extends HookWidget { Skeleton.keep( child: IconButton( style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.background, + backgroundColor: theme.colorScheme.surface, foregroundColor: theme.colorScheme.primary, minimumSize: const Size.square(10), ), diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/shared/themed_button_tab_bar.dart index b21ca9924..c245e5f4e 100644 --- a/lib/components/shared/themed_button_tab_bar.dart +++ b/lib/components/shared/themed_button_tab_bar.dart @@ -34,7 +34,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { ), borderWidth: 0, unselectedDecoration: BoxDecoration( - color: theme.colorScheme.background, + color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(15), ), unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith( diff --git a/lib/components/stats/common/album_item.dart b/lib/components/stats/common/album_item.dart index ccc0fa4ec..00b1cbfef 100644 --- a/lib/components/stats/common/album_item.dart +++ b/lib/components/stats/common/album_item.dart @@ -33,7 +33,7 @@ class StatsAlbumItem extends StatelessWidget { Text("${album.albumType?.formatted} • "), Flexible( child: ArtistLink( - artists: album.artists!, + artists: album.artists ?? [], mainAxisAlignment: WrapAlignment.start, ), ), diff --git a/lib/main.dart b/lib/main.dart index 52d0b1415..1693d9d88 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,7 +10,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; @@ -139,28 +138,11 @@ Future main(List rawArgs) async { ); } -class Spotube extends StatefulHookConsumerWidget { +class Spotube extends HookConsumerWidget { const Spotube({super.key}); @override - SpotubeState createState() => SpotubeState(); - - static SpotubeState of(BuildContext context) => - context.findAncestorStateOfType()!; -} - -class SpotubeState extends ConsumerState { - final logger = getLogger(Spotube); - SharedPreferences? localStorage; - - @override - void initState() { - super.initState(); - SharedPreferences.getInstance().then(((value) => localStorage = value)); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { final themeMode = ref.watch(userPreferencesProvider.select((s) => s.themeMode)); final accentMaterialColor = @@ -195,6 +177,7 @@ class SpotubeState extends ConsumerState { () => theme(paletteColor ?? accentMaterialColor, Brightness.light, false), [paletteColor, accentMaterialColor], ); + final darkTheme = useMemoized( () => theme( paletteColor ?? accentMaterialColor, diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index 9c9bdddba..c9367e054 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -17,7 +17,7 @@ class DesktopLoginPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final mediaQuery = MediaQuery.of(context); final theme = Theme.of(context); - final color = theme.colorScheme.surfaceVariant.withOpacity(.3); + final color = theme.colorScheme.surfaceContainerHighest.withOpacity(.3); return SafeArea( child: Scaffold( diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 850eccfa0..1d9b383a7 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -100,7 +100,7 @@ class LyricsPage extends HookConsumerWidget { child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background.withOpacity(.4), + color: Theme.of(context).colorScheme.surface.withOpacity(.4), borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index 996e190df..a026209ce 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -107,8 +107,7 @@ class MiniLyricsPage extends HookConsumerWidget { : const Icon(SpotubeIcons.lyricsOff), style: ButtonStyle( foregroundColor: showLyrics.value - ? MaterialStateProperty.all( - theme.colorScheme.primary) + ? WidgetStateProperty.all(theme.colorScheme.primary) : null, ), onPressed: () async { @@ -132,8 +131,7 @@ class MiniLyricsPage extends HookConsumerWidget { : const Icon(SpotubeIcons.hoverOff), style: ButtonStyle( foregroundColor: hoverMode.value - ? MaterialStateProperty.all( - theme.colorScheme.primary) + ? WidgetStateProperty.all(theme.colorScheme.primary) : null, ), onPressed: () async { @@ -154,7 +152,7 @@ class MiniLyricsPage extends HookConsumerWidget { ), style: ButtonStyle( foregroundColor: snapshot.data == true - ? MaterialStateProperty.all( + ? WidgetStateProperty.all( theme.colorScheme.primary) : null, ), @@ -186,12 +184,12 @@ class MiniLyricsPage extends HookConsumerWidget { child: TabBarView( children: [ SyncedLyrics( - palette: PaletteColor(theme.colorScheme.background, 0), + palette: PaletteColor(theme.colorScheme.surface, 0), isModal: true, defaultTextZoom: 65, ), PlainLyrics( - palette: PaletteColor(theme.colorScheme.background, 0), + palette: PaletteColor(theme.colorScheme.surface, 0), isModal: true, defaultTextZoom: 65, ), diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index d5374786d..50ef152b9 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -212,7 +212,7 @@ class SearchPage extends HookConsumerWidget { Icon( SpotubeIcons.web, size: 120, - color: theme.colorScheme.onBackground + color: theme.colorScheme.onSurface .withOpacity(0.7), ), const SizedBox(height: 20), @@ -220,7 +220,7 @@ class SearchPage extends HookConsumerWidget { context.l10n.search_to_get_results, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, - color: theme.colorScheme.onBackground + color: theme.colorScheme.onSurface .withOpacity(0.5), ), ), @@ -246,7 +246,7 @@ class SearchPage extends HookConsumerWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w900, - color: theme.colorScheme.onBackground + color: theme.colorScheme.onSurface .withOpacity(0.7), ), ), diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 6eccab073..4e9379222 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -20,7 +20,6 @@ class BlackListPage extends HookConsumerWidget { final controller = useScrollController(); final blacklist = ref.watch(blacklistProvider); final searchText = useState(""); - final filteredBlacklist = useMemoized( () { if (searchText.value.isEmpty) { diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart index a8d72cc0d..5e5d23770 100644 --- a/lib/pages/settings/sections/about.dart +++ b/lib/pages/settings/sections/about.dart @@ -43,10 +43,9 @@ class SettingsAboutSection extends HookConsumerWidget { ), trailing: (context, update) => FilledButton( style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll(Colors.red[100]), - foregroundColor: - const MaterialStatePropertyAll(Colors.pinkAccent), - padding: const MaterialStatePropertyAll(EdgeInsets.all(15)), + backgroundColor: WidgetStatePropertyAll(Colors.red[100]), + foregroundColor: const WidgetStatePropertyAll(Colors.pinkAccent), + padding: const WidgetStatePropertyAll(EdgeInsets.all(15)), ), onPressed: () { launchUrlString( diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 6162aa3d3..5acab4807 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -82,7 +82,7 @@ class SettingsAccountSection extends HookConsumerWidget { router.push("/login"); }, style: ButtonStyle( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(25.0), ), diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index 3092ed03f..76ef8e3e9 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -3,7 +3,6 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 51e98269a..cf1da7bed 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -4,13 +4,22 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { final scheme = ColorScheme.fromSeed( seedColor: seed, shadow: Colors.black12, - background: isAmoled ? Colors.black : null, surface: isAmoled ? Colors.black : null, + surfaceContainer: isAmoled ? const Color(0xFF090909) : null, + surfaceContainerHigh: isAmoled ? const Color(0xFF181818) : null, + surfaceContainerHighest: isAmoled ? const Color(0xFF282828) : null, brightness: brightness, ); return ThemeData( useMaterial3: true, colorScheme: scheme, + scaffoldBackgroundColor: isAmoled ? Colors.black : null, + cardTheme: CardTheme( + color: scheme.surfaceContainer, + shadowColor: scheme.shadow, + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ), listTileTheme: ListTileThemeData( horizontalTitleGap: 5, iconColor: scheme.onSurface, @@ -25,7 +34,7 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { navigationBarTheme: const NavigationBarThemeData( labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, height: 50, - iconTheme: MaterialStatePropertyAll( + iconTheme: WidgetStatePropertyAll( IconThemeData(size: 18), ), ), @@ -52,25 +61,25 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { ), sliderTheme: SliderThemeData(overlayShape: SliderComponentShape.noOverlay), searchBarTheme: SearchBarThemeData( - textStyle: const MaterialStatePropertyAll(TextStyle(fontSize: 15)), + textStyle: const WidgetStatePropertyAll(TextStyle(fontSize: 15)), constraints: const BoxConstraints(maxWidth: double.infinity), - padding: const MaterialStatePropertyAll(EdgeInsets.all(8)), - backgroundColor: MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll(EdgeInsets.all(8)), + backgroundColor: WidgetStatePropertyAll( Color.lerp( - scheme.surfaceVariant, + scheme.surfaceContainerHighest, scheme.surface, brightness == Brightness.light ? .9 : .7, ), ), - elevation: const MaterialStatePropertyAll(0), - shape: MaterialStatePropertyAll( + elevation: const WidgetStatePropertyAll(0), + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), ), scrollbarTheme: const ScrollbarThemeData( - thickness: MaterialStatePropertyAll(14), + thickness: WidgetStatePropertyAll(14), ), checkboxTheme: CheckboxThemeData( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), From c607a330ed279dfbebe8d4bd325745ac6301a58f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 2 Jun 2024 22:34:06 +0600 Subject: [PATCH 36/57] fix(playback): skipping tracks with unplayable sources instead of falling back #1492 --- lib/services/sourced_track/sourced_track.dart | 8 +------ .../sourced_track/sources/youtube.dart | 22 ++++++++++++------- lib/themes/theme.dart | 8 ++++++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index a5e094ed5..7eedfad87 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -135,16 +135,10 @@ abstract class SourcedTrack extends Track { return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref); } catch (e) { if (e is DioException || e is ClientException || e is SocketException) { - if (preferences.audioSource == AudioSource.jiosaavn) { - return await JioSaavnSourcedTrack.fetchFromTrack( - track: track, - ref: ref, - weakMatch: true, - ); - } return await JioSaavnSourcedTrack.fetchFromTrack( track: track, ref: ref, + weakMatch: preferences.audioSource == AudioSource.jiosaavn, ); } rethrow; diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 3fc78f0b7..c24edfc0a 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -1,3 +1,4 @@ +import 'package:catcher_2/core/catcher_2.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; @@ -221,14 +222,19 @@ class YoutubeSourcedTrack extends SourcedTrack { final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube"); if (ytLink?.url != null) { - return [ - await toSiblingType( - 0, - YoutubeVideoInfo.fromVideo( - await youtubeClient.videos.get(ytLink!.url!), - ), - ) - ]; + try { + return [ + await toSiblingType( + 0, + YoutubeVideoInfo.fromVideo( + await youtubeClient.videos.get(ytLink!.url!), + ), + ) + ]; + } on VideoUnplayableException catch (e, stack) { + // Ignore this error and continue with the search + Catcher2.reportCheckedError(e, stack); + } } final query = SourcedTrack.getSearchTerm(track); diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index cf1da7bed..390a7509f 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -24,7 +24,13 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { horizontalTitleGap: 5, iconColor: scheme.onSurface, ), - appBarTheme: const AppBarTheme(surfaceTintColor: Colors.transparent), + appBarTheme: const AppBarTheme( + surfaceTintColor: Colors.transparent, + scrolledUnderElevation: 0, + shadowColor: Colors.transparent, + elevation: 0, + backgroundColor: Colors.transparent, + ), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), From e63a4bb63c33bf4291a91925e1ea12c1c1afde19 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Jun 2024 10:09:41 +0600 Subject: [PATCH 37/57] chore: migrate android gradle to declarative config syntax --- .fvm/fvm_config.json | 2 +- .github/workflows/pr-lint.yml | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- android/app/build.gradle | 30 ++++++++------------ android/build.gradle | 13 --------- android/settings.gradle | 30 ++++++++++++++------ lib/themes/theme.dart | 1 - 7 files changed, 37 insertions(+), 43 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 6a56dfc65..df8efa0eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.22.0", + "flutterSdkVersion": "3.22.1", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 156d1a076..2844986de 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -4,7 +4,7 @@ on: pull_request: env: - FLUTTER_VERSION: '3.19.5' + FLUTTER_VERSION: '3.22.1' jobs: lint: diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index eb62b58d7..8e68211c6 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -20,7 +20,7 @@ on: description: Dry run without uploading to release env: - FLUTTER_VERSION: 3.22.0 + FLUTTER_VERSION: 3.22.1 permissions: contents: write diff --git a/android/app/build.gradle b/android/app/build.gradle index 2f85cdebc..7bcd9b6ad 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { @@ -71,6 +68,9 @@ android { release { signingConfig signingConfigs.release } + debug { + signingConfig signingConfigs.release + } } flavorDimensions "default" @@ -81,16 +81,19 @@ android { resValue "string", "app_name_en", "Spotube Nightly" applicationIdSuffix ".nightly" versionNameSuffix "-nightly" + signingConfig signingConfigs.release } dev { dimension "default" resValue "string", "app_name_en", "Spotube Dev" applicationIdSuffix ".dev" versionNameSuffix "-dev" + signingConfig signingConfigs.release } stable { dimension "default" resValue "string", "app_name_en", "Spotube" + signingConfig signingConfigs.release } } @@ -101,15 +104,6 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - constraints { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") { - because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") - } - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") { - because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") - } - } implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' // other deps so just ignore diff --git a/android/build.gradle b/android/build.gradle index 0801de624..bc157bd1a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.22' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf0..896517485 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.2.1" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" \ No newline at end of file diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 390a7509f..28acc2809 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -29,7 +29,6 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { scrolledUnderElevation: 0, shadowColor: Colors.transparent, elevation: 0, - backgroundColor: Colors.transparent, ), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( From bc534aa240c142dc2e4289b96318573579d14c43 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Jun 2024 10:56:51 +0600 Subject: [PATCH 38/57] chore: disable impeller for now --- android/app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 52547f045..589e22ff9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -25,9 +25,9 @@ android:requestLegacyExternalStorage="true" > - + android:value="true" /> --> Date: Mon, 3 Jun 2024 12:46:52 +0600 Subject: [PATCH 39/57] fix(windows): installer tries to install in current directory --- windows/packaging/exe/inno_setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/packaging/exe/inno_setup.iss b/windows/packaging/exe/inno_setup.iss index 64acc2b3c..dbb8082b6 100644 --- a/windows/packaging/exe/inno_setup.iss +++ b/windows/packaging/exe/inno_setup.iss @@ -12,7 +12,7 @@ AppPublisher={{PUBLISHER_NAME}} AppPublisherURL={{PUBLISHER_URL}} AppSupportURL={{PUBLISHER_URL}} AppUpdatesURL={{PUBLISHER_URL}} -DefaultDirName={{INSTALL_DIR_NAME}} +DefaultDirName={autopf}\{{DISPLAY_NAME}} DisableProgramGroupPage=yes OutputDir=. OutputBaseFilename={{OUTPUT_BASE_FILENAME}} From f6ba95fb64986cda613d8cc79aa84841f0ed61f1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Jun 2024 13:13:05 +0600 Subject: [PATCH 40/57] chore: upgrade deps and appbar bg fix --- lib/components/shared/page_window_title_bar.dart | 4 ++++ pubspec.lock | 8 ++++---- pubspec.yaml | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index 667098445..573c7c47c 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -165,6 +165,10 @@ class _PageWindowTitleBarState extends ConsumerState { toolbarTextStyle: widget.toolbarTextStyle, titleTextStyle: widget.titleTextStyle, title: widget.title, + scrolledUnderElevation: 0, + shadowColor: Colors.transparent, + forceMaterialTransparency: true, + elevation: 0, ), ), ); diff --git a/pubspec.lock b/pubspec.lock index 32da1f8dd..cf72db1cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2418,10 +2418,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "0.3.9" window_size: dependency: "direct main" description: @@ -2459,10 +2459,10 @@ packages: dependency: "direct main" description: name: youtube_explode_dart - sha256: "12d32dffd8c85927eb46f7cf7a9dfce690edfe82134c08a90529c51eba58a85c" + sha256: "26c9671d638f3396a1bfb2666f586988ee7b0ba3469e478b22a4c1a168bcf6ee" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index 56c25dd78..80e930fef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,13 +86,13 @@ dependencies: uuid: ^4.4.0 version: ^3.0.2 visibility_detector: ^0.4.0+2 - window_manager: ^0.3.8 + window_manager: ^0.3.9 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git ref: a738913c8ce2c9f47515382d40827e794a334274 path: plugins/window_size - youtube_explode_dart: ^2.2.0 + youtube_explode_dart: ^2.2.1 simple_icons: ^10.1.3 jiosaavn: ^0.1.0 draggable_scrollbar: From 9cd44b6c9ba0f69eb2f7c544e578743ecc778500 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Jun 2024 13:15:10 +0600 Subject: [PATCH 41/57] chore: podspec update --- ios/Podfile.lock | 43 ++++++++--------- ios/Runner.xcodeproj/project.pbxproj | 72 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1d048cc9b..f8533902b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -69,9 +69,6 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - image_picker_ios (0.0.1): - Flutter - integration_test (0.0.1): @@ -87,7 +84,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - SDWebImage (5.18.8): - SDWebImage/Core (= 5.18.8) @@ -97,7 +94,7 @@ PODS: - FlutterMacOS - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - SwiftyGif (5.4.4) - Toast (4.0.0) - url_launcher_ios (0.0.1): @@ -129,14 +126,13 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - - FMDB - OrderedSet - SDWebImage - SwiftyGif @@ -194,45 +190,44 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 + app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 audio_service: f509d65da41b9521a61f1c404dd58651f265a567 - audio_session: 4f3e461722055d21515cf3261b64c973c062f345 + audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de - file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + file_selector_ios: 78baf21d03f1e37a7df97bb2494f9cd86de8fa5d Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 - flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef + flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 - fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 - integration_test: 13825b8a9334a850581300559b8839134b124670 + fluttertoast: 9f2f8e81bb5ce18facb9748d7855bf5a756fe3db + image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3 media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837 metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 13f624a46..34793f682 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -324,6 +324,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */, + 46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -346,6 +347,7 @@ B536BD992B405DB1009B3CE4 /* Embed Frameworks */, B536BD9A2B405DB1009B3CE4 /* Thin Binary */, A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */, + 2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -368,6 +370,7 @@ B536BDB62B405FDE009B3CE4 /* Embed Frameworks */, B536BDB72B405FDE009B3CE4 /* Thin Binary */, 244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */, + 4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -390,6 +393,7 @@ B536BDD82B4060B3009B3CE4 /* Embed Frameworks */, B536BDD92B4060B3009B3CE4 /* Thin Binary */, D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */, + 5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -523,6 +527,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -539,6 +560,57 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; From ab713a4eacf849a907e87d47cb5f479a765cba7c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Jun 2024 13:44:16 +0600 Subject: [PATCH 42/57] chore: bump version and generate changelogs --- .github/workflows/spotube-publish-binary.yml | 2 +- CHANGELOG.md | 34 +++++++++++++++++++- pubspec.yaml | 2 +- windows/runner/Runner.rc | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 960507f9b..0d39ab1d8 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to publish (x.x.x) - default: 3.6.0 + default: 3.7.0 required: true dry_run: description: Dry run diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ca4b693..21fb79d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,39 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [3.6.0-0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0-0) (2024-04-15) +## [3.7.0](https://github.com/krtirtho/spotube/compare/v3.6.0...v3.7.0) (2024-06-03) + + +### Features + +* local library folder cards ([fc5bfa0](https://github.com/krtirtho/spotube/commit/fc5bfa089ce2f46ab786565d6750564d704ee7e0)) +* Local music library ([#1479](https://github.com/krtirtho/spotube/issues/1479)) ([22caa81](https://github.com/krtirtho/spotube/commit/22caa818f4ac31626aaff6952e43512b42237d00)) +* personalized stats based on local music history ([#1522](https://github.com/krtirtho/spotube/issues/1522)) ([82307bc](https://github.com/krtirtho/spotube/commit/82307bc030035b03ab1b8d8ec7b24da19a866b12)) +* play initially available tracks of playlist/album immediately and fetch rest in background [#670](https://github.com/krtirtho/spotube/issues/670) ([02acbd9](https://github.com/krtirtho/spotube/commit/02acbd93271145dde365f6c547e0d9d902be65f1)) +* **player:** add volume slider floating label showing percentage ([#1445](https://github.com/krtirtho/spotube/issues/1445)) ([8fad225](https://github.com/krtirtho/spotube/commit/8fad2251b3536e9468e0fb193939ead98bad3bc6)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082) +* **translations:** add Basque translation ([#1493](https://github.com/krtirtho/spotube/issues/1493)) ([dbc1c45](https://github.com/krtirtho/spotube/commit/dbc1c452dd53153c61589f956ea9836cea7bf2bb)) +* **translations:** add Finnish translations ([#1449](https://github.com/krtirtho/spotube/issues/1449)) ([edc997e](https://github.com/krtirtho/spotube/commit/edc997e7470ce17f60c96b8198dc8851cbf21f18)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082) +* **translations:** add georgian language ([#1450](https://github.com/krtirtho/spotube/issues/1450)) ([1e7f0e1](https://github.com/krtirtho/spotube/commit/1e7f0e1fe71e0a8d86614fc884861f8791469112)) +* **translations:** add Indonesian translation ([#1426](https://github.com/krtirtho/spotube/issues/1426)) ([0280654](https://github.com/krtirtho/spotube/commit/0280654bb6bad373aee521f5a866228d2d38f038)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082) +* **translations:** Improve tr locales ([#1419](https://github.com/krtirtho/spotube/issues/1419)) ([bf45681](https://github.com/krtirtho/spotube/commit/bf45681deb951c772bf6ca05e213c949c04bded1)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082) +* upgrade to Flutter 3.22.0 ([71341ec](https://github.com/krtirtho/spotube/commit/71341ec0bda6ed985b43836712075b97a2cf8bac)) + + +### Bug Fixes + +* fallback to LRCLIB when lyrics line less than 6 lines [#1461](https://github.com/krtirtho/spotube/issues/1461) ([9aea354](https://github.com/krtirtho/spotube/commit/9aea35468fa7cd176ddc8810b37b90c2d8246931)) +* **linux:** tray icon not showing [#541](https://github.com/krtirtho/spotube/issues/541) ([7ac7917](https://github.com/krtirtho/spotube/commit/7ac791757abb30f40374c169c4211916287bb3f3)) +* local track not showing up in queue ([d82261c](https://github.com/krtirtho/spotube/commit/d82261cb25ece63f85af0e40216cf32dccdc9dd5)) +* **macos:** Logs directory not created by default [#1353](https://github.com/krtirtho/spotube/issues/1353) ([4ca8939](https://github.com/krtirtho/spotube/commit/4ca893950b07f678acf7db690112c47d21e54782)) +* **playback:** skipping tracks with unplayable sources instead of falling back [#1492](https://github.com/krtirtho/spotube/issues/1492) ([c607a33](https://github.com/krtirtho/spotube/commit/c607a330ed279dfbebe8d4bd325745ac6301a58f)) +* **search:** load more button not working [#1417](https://github.com/krtirtho/spotube/issues/1417) ([7e07c2e](https://github.com/krtirtho/spotube/commit/7e07c2e1985da7ccb96b1fac2ecd703720068d26)) +* some text are garbled in different parts of the app [#1463](https://github.com/krtirtho/spotube/issues/1463) [#1505](https://github.com/krtirtho/spotube/issues/1505) ([d2683c5](https://github.com/krtirtho/spotube/commit/d2683c52d81d807be6ff72f15b8e9eb18181e211)) +* spotify friends and user profile icon (mobile) showing when not authenticated [#1410](https://github.com/krtirtho/spotube/issues/1410) ([9bccbc9](https://github.com/krtirtho/spotube/commit/9bccbc93c63dd34f6e15ff68c276976ecd1d9a33)) +* **updater:** dead link ([#1408](https://github.com/krtirtho/spotube/issues/1408)) ([6907f9c](https://github.com/krtirtho/spotube/commit/6907f9c756d8f49aadb1b23a2a1dc8bf7d658dc0)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082) +* windows SSL Certificate error breaking login [#905](https://github.com/krtirtho/spotube/issues/905) ([#1474](https://github.com/krtirtho/spotube/issues/1474)) ([937a706](https://github.com/krtirtho/spotube/commit/937a706ac9c0e59943b2609e5cc398dcdbed2344)), closes [#1468](https://github.com/krtirtho/spotube/issues/1468) +* **windows:** installer tries to install in current directory ([c3c9fc5](https://github.com/krtirtho/spotube/commit/c3c9fc544c68b3d897dd7241a61cab7a199b4539)) + +## [3.6.0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0) (2024-04-15) ### Features diff --git a/pubspec.yaml b/pubspec.yaml index 80e930fef..c256f66e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 3.6.0+30 +version: 3.7.0+31 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 0b586d339..276326676 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -69,7 +69,7 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else -#define VERSION_AS_STRING "3.6.0" +#define VERSION_AS_STRING "3.7.0" #endif VS_VERSION_INFO VERSIONINFO From 2b5fd35529f4036278b183ecbabc0d9fa760f297 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Jun 2024 13:52:47 +0600 Subject: [PATCH 43/57] chore: update translations and generate credits --- .gitignore | 2 + README.md | 97 +++++++++++++++++++++++---------------------- lib/l10n/app_ar.arb | 7 +++- lib/l10n/app_bn.arb | 7 +++- lib/l10n/app_ca.arb | 7 +++- lib/l10n/app_cs.arb | 7 +++- lib/l10n/app_de.arb | 7 +++- lib/l10n/app_es.arb | 7 +++- lib/l10n/app_eu.arb | 7 +++- lib/l10n/app_fa.arb | 7 +++- lib/l10n/app_fi.arb | 7 +++- lib/l10n/app_fr.arb | 7 +++- lib/l10n/app_hi.arb | 7 +++- lib/l10n/app_id.arb | 7 +++- lib/l10n/app_it.arb | 7 +++- lib/l10n/app_ja.arb | 7 +++- lib/l10n/app_ka.arb | 7 +++- lib/l10n/app_ko.arb | 7 +++- lib/l10n/app_ne.arb | 7 +++- lib/l10n/app_nl.arb | 7 +++- lib/l10n/app_pl.arb | 7 +++- lib/l10n/app_pt.arb | 7 +++- lib/l10n/app_ru.arb | 7 +++- lib/l10n/app_th.arb | 7 +++- lib/l10n/app_tr.arb | 7 +++- lib/l10n/app_uk.arb | 7 +++- lib/l10n/app_vi.arb | 7 +++- lib/l10n/app_zh.arb | 7 +++- 28 files changed, 207 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 96d810871..4f9ebc281 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,5 @@ android/key.properties .fvm/flutter_sdk **/pb_data + +tm.json diff --git a/README.md b/README.md index f2666fbc2..5db4d5ade 100644 --- a/README.md +++ b/README.md @@ -210,116 +210,117 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms. ### Dependencies +1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included). 1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options. 1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library. +1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification. 1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off. 1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour. 1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds. +1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD. +1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button. 1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets. 1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. -1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons +1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions. 1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration. +1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules. +1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games. 1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop. 1. [device_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on. -1. [device_preview](https://github.com/aloisdeniel/flutter_device_preview) - Approximate how your Flutter app looks and performs on another device. 1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc. 1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc +1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item. 1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration. +1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. 1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times. +1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. 1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. 1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. +1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications. 1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. 1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices. 1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability. +1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs. 1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. 1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. +1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon. +1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices. 1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more. 1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze. 1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android. +1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app. 1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. 1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets +1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too. +1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features. 1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! +1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views. 1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more 1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling. -1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256. 1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps. +1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class. +1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256. 1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze. +1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References. 1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser. 1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests. 1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. 1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues. 1. [introduction_screen](https://pub.dev/packages/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities +1. [io](https://pub.dev/packages/io) - Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values. +1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com 1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package. +1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes. +1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications. 1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs. -1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular. +1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. 1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms. +1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular. 1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. 1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. 1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image. -1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. 1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. +1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. 1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. +1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video 1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area. +1. [process_run](https://github.com/tekartik/process_run.dart/blob/master/packages/process_run) - Process run helpers for Linux/Win/Mac and which like feature for finding executables. +1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information. +1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. +1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks. +1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark. 1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter -1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget 1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. +1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations. +1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection. +1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse. +1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget +1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. 1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community. +1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort. +1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework 1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet. +1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API. 1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget 1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS +1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime. 1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos. +1. [tray_manager](https://github.com/leanflutter/tray_manager) - This plugin allows Flutter desktop apps to defines system tray. 1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. 1. [uuid](https://pub.dev/packages/uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart 1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/ -1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback. -1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. -1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. -1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. -1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification. -1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. -1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com 1. [very_good_infinite_list](https://github.com/VeryGoodOpenSource/very_good_infinite_list) - A library for easily displaying paginated data, created by Very Good Ventures. Great for activity feeds, news feeds, and more. -1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views. -1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework -1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References. +1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback. +1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel. 1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter -1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort. -1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included). 1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry. -1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app. -1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications. -1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too. -1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API. -1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD. -1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse. -1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations. -1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection. -1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel. -1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information. -1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. -1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime. -1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions. -1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. -1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. -1. [flutter_distributor](https://distributor.leanflutter.dev) - A complete tool for packaging and publishing your Flutter apps. -1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs. -1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon. -1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices. -1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class. -1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes. -1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features. -1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules. -1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks. -1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development -1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video -1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark. +1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. 1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter. -1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item. -1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games. +1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents. +1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.

© Copyright Spotube 2024

diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 68308ba1f..b474ec7ea 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -320,5 +320,10 @@ "select": "اختر", "connect_client_alert": "أنت تتم التحكم بواسطة {client}", "this_device": "هذا الجهاز", - "remote": "بعيد" + "remote": "بعيد", + "local_library": "المكتبة المحلية", + "add_library_location": "أضف إلى المكتبة", + "remove_library_location": "إزالة من المكتبة", + "local_tab": "محلي", + "stats": "إحصائيات" } \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 506e78bcb..2cf8dd43e 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -320,5 +320,10 @@ "select": "নির্বাচন করুন", "connect_client_alert": "আপনি {client} দ্বারা নিয়ন্ত্রিত হচ্ছেন", "this_device": "এই ডিভাইস", - "remote": "রিমোট" + "remote": "রিমোট", + "local_library": "স্থানীয় লাইব্রেরি", + "add_library_location": "লাইব্রেরিতে যোগ করুন", + "remove_library_location": "লাইব্রেরি থেকে সরান", + "local_tab": "স্থানীয়", + "stats": "পরিসংখ্যান" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 8faa0d093..ca4b019ae 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -320,5 +320,10 @@ "select": "Selecciona", "connect_client_alert": "Estàs sent controlat per {client}", "this_device": "Aquest dispositiu", - "remote": "Remot" + "remote": "Remot", + "local_library": "Biblioteca local", + "add_library_location": "Afegeix a la biblioteca", + "remove_library_location": "Elimina de la biblioteca", + "local_tab": "Local", + "stats": "Estadístiques" } \ No newline at end of file diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 52f5bcf88..7191c1089 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -320,5 +320,10 @@ "select": "Vybrat", "connect_client_alert": "Zařízení je ovládáno z {client}", "this_device": "Toto zařízení", - "remote": "Ovladač" + "remote": "Ovladač", + "local_library": "Místní knihovna", + "add_library_location": "Přidat do knihovny", + "remove_library_location": "Odebrat z knihovny", + "local_tab": "Místní", + "stats": "Statistiky" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 77435d674..c455e08a6 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -320,5 +320,10 @@ "select": "Auswählen", "connect_client_alert": "Du wirst von {client} gesteuert", "this_device": "Dieses Gerät", - "remote": "Fernbedienung" + "remote": "Fernbedienung", + "local_library": "Lokale Bibliothek", + "add_library_location": "Zur Bibliothek hinzufügen", + "remove_library_location": "Aus der Bibliothek entfernen", + "local_tab": "Lokal", + "stats": "Statistiken" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 11617b423..6558c743c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -320,5 +320,10 @@ "select": "Seleccionar", "connect_client_alert": "Estás siendo controlado por {client}", "this_device": "Este dispositivo", - "remote": "Remoto" + "remote": "Remoto", + "local_library": "Biblioteca local", + "add_library_location": "Añadir a la biblioteca", + "remove_library_location": "Eliminar de la biblioteca", + "local_tab": "Local", + "stats": "Estadísticas" } \ No newline at end of file diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index 9a4ebb463..fb00a925f 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -320,5 +320,10 @@ "select": "Aukeratu", "connect_client_alert": "{client} gailuak kontrolatzen zaitu", "this_device": "Gailu hau", - "remote": "Urrunekoa" + "remote": "Urrunekoa", + "local_library": "Liburutegi lokala", + "add_library_location": "Gehitu liburutegira", + "remove_library_location": "Kendu liburutegitik", + "local_tab": "Tokiko", + "stats": "Estatistikak" } \ No newline at end of file diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 8a0bee3ad..b939de59d 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -320,5 +320,10 @@ "select": "انتخاب", "connect_client_alert": "شما توسط {client} کنترل می‌شوید", "this_device": "این دستگاه", - "remote": "راه‌دور" + "remote": "راه‌دور", + "local_library": "کتابخانه محلی", + "add_library_location": "اضافه کردن به کتابخانه", + "remove_library_location": "حذف از کتابخانه", + "local_tab": "محلی", + "stats": "آمار" } \ No newline at end of file diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 35470791e..d0767e95e 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -320,5 +320,10 @@ "select": "Valitse", "connect_client_alert": "{client} ohjaa sinua", "this_device": "Tämä laite", - "remote": "Etä" + "remote": "Etä", + "local_library": "Paikallinen kirjasto", + "add_library_location": "Lisää kirjastoon", + "remove_library_location": "Poista kirjastosta", + "local_tab": "Paikallinen", + "stats": "Tilastot" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index cabcb8e1e..6bd2d0f84 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -320,5 +320,10 @@ "select": "Sélectionner", "connect_client_alert": "Vous êtes contrôlé par {client}", "this_device": "Cet appareil", - "remote": "À distance" + "remote": "À distance", + "local_library": "Bibliothèque locale", + "add_library_location": "Ajouter à la bibliothèque", + "remove_library_location": "Retirer de la bibliothèque", + "local_tab": "Local", + "stats": "Statistiques" } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index a72e136ec..7dc809c70 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -320,5 +320,10 @@ "select": "चयन करें", "connect_client_alert": "आप {client} द्वारा नियंत्रित हो रहे हैं", "this_device": "यह उपकरण", - "remote": "रिमोट" + "remote": "रिमोट", + "local_library": "स्थानीय पुस्तकालय", + "add_library_location": "पुस्तकालय में जोड़ें", + "remove_library_location": "पुस्तकालय से हटाएं", + "local_tab": "स्थानीय", + "stats": "आंकड़े" } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index b94cdd281..669f5e2a1 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -320,5 +320,10 @@ "select": "Pilih", "connect_client_alert": "Anda dikendalikan oleh {client}", "this_device": "Perangkat Ini", - "remote": "Remot" + "remote": "Remot", + "local_library": "Perpustakaan lokal", + "add_library_location": "Tambahkan ke perpustakaan", + "remove_library_location": "Hapus dari perpustakaan", + "local_tab": "Lokal", + "stats": "Statistik" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index bb1881d6c..9ba30acc8 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -321,5 +321,10 @@ "select": "Seleziona", "connect_client_alert": "Stai venendo controllato da {client}", "this_device": "Questo dispositivo", - "remote": "Remoto" + "remote": "Remoto", + "local_library": "Biblioteca locale", + "add_library_location": "Aggiungi alla biblioteca", + "remove_library_location": "Rimuovi dalla biblioteca", + "local_tab": "Locale", + "stats": "Statistiche" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index ab759404d..35e76b69b 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -320,5 +320,10 @@ "select": "選択する", "connect_client_alert": "{client} によって操作されています", "this_device": "このデバイス", - "remote": "リモート" + "remote": "リモート", + "local_library": "ローカルライブラリ", + "add_library_location": "ライブラリに追加", + "remove_library_location": "ライブラリから削除", + "local_tab": "ローカル", + "stats": "統計" } \ No newline at end of file diff --git a/lib/l10n/app_ka.arb b/lib/l10n/app_ka.arb index 3da064447..28fcc26a3 100644 --- a/lib/l10n/app_ka.arb +++ b/lib/l10n/app_ka.arb @@ -320,5 +320,10 @@ "select": "არჩევა", "connect_client_alert": "თქვენ კონტროლირებული ხართ {client} მოწყობილობით", "this_device": "ეს მოწყობილობა", - "remote": "დისტანციური" + "remote": "დისტანციური", + "local_library": "ადგილობრივი ბიბლიოთეკა", + "add_library_location": "ბიბლიოთეკაში დამატება", + "remove_library_location": "ბიბლიოთეკიდან წაშლა", + "local_tab": "ადგილობრივი", + "stats": "სტატისტიკა" } \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index c94f81425..cb6e09992 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -321,5 +321,10 @@ "select": "선택", "connect_client_alert": "{client}님에 의해 제어되고 있습니다", "this_device": "이 장치", - "remote": "원격" + "remote": "원격", + "local_library": "로컬 도서관", + "add_library_location": "도서관에 추가", + "remove_library_location": "도서관에서 제거", + "local_tab": "로컬", + "stats": "통계" } \ No newline at end of file diff --git a/lib/l10n/app_ne.arb b/lib/l10n/app_ne.arb index 4085b00e7..f8e8d46a7 100644 --- a/lib/l10n/app_ne.arb +++ b/lib/l10n/app_ne.arb @@ -320,5 +320,10 @@ "select": "चयन गर्नुहोस्", "connect_client_alert": "तपाईंलाई {client} द्वारा नियन्त्रित गरिएको छ", "this_device": "यो उपकरण", - "remote": "दूरसंचार" + "remote": "दूरसंचार", + "local_library": "स्थानिय पुस्तकालय", + "add_library_location": "पुस्तकालयमा थप्नुहोस्", + "remove_library_location": "पुस्तकालयबाट हटाउनुहोस्", + "local_tab": "स्थानिय", + "stats": "तथ्याङ्क" } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 0a04c40b0..aa5c846d4 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -321,5 +321,10 @@ "select": "Selecteren", "connect_client_alert": "Je wordt gecontroleerd door {client}", "this_device": "Dit apparaat", - "remote": "Afstandsbediening" + "remote": "Afstandsbediening", + "local_library": "Lokale bibliotheek", + "add_library_location": "Toevoegen aan bibliotheek", + "remove_library_location": "Verwijderen uit bibliotheek", + "local_tab": "Lokaal", + "stats": "Statistieken" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9ce311870..2c4e83698 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -320,5 +320,10 @@ "select": "Wybierz", "connect_client_alert": "Jesteś sterowany przez {client}", "this_device": "To urządzenie", - "remote": "Zdalny" + "remote": "Zdalny", + "local_library": "Biblioteka lokalna", + "add_library_location": "Dodaj do biblioteki", + "remove_library_location": "Usuń z biblioteki", + "local_tab": "Lokalny", + "stats": "Statystyki" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 537325894..88cf5cb38 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -320,5 +320,10 @@ "select": "Selecionar", "connect_client_alert": "Você está sendo controlado por {client}", "this_device": "Este dispositivo", - "remote": "Remoto" + "remote": "Remoto", + "local_library": "Biblioteca local", + "add_library_location": "Adicionar à biblioteca", + "remove_library_location": "Remover da biblioteca", + "local_tab": "Local", + "stats": "Estatísticas" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index a18e02e7d..0a1c1c22d 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -320,5 +320,10 @@ "select": "Выбрать", "connect_client_alert": "Вас контролирует {client}", "this_device": "Это устройство", - "remote": "Дистанционное управление" + "remote": "Дистанционное управление", + "local_library": "Местная библиотека", + "add_library_location": "Добавить в библиотеку", + "remove_library_location": "Удалить из библиотеки", + "local_tab": "Местный", + "stats": "Статистика" } \ No newline at end of file diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 866929fa9..60ced74bf 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -321,5 +321,10 @@ "select": "เลือก", "connect_client_alert": "คุณกำลังถูกควบคุมโดย {client}", "this_device": "อุปกรณ์นี้", - "remote": "ระยะไกล" + "remote": "ระยะไกล", + "local_library": "ห้องสมุดท้องถิ่น", + "add_library_location": "เพิ่มในห้องสมุด", + "remove_library_location": "ลบออกจากห้องสมุด", + "local_tab": "ท้องถิ่น", + "stats": "สถิติ" } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index aab6bc6d8..b329cfa7d 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -320,5 +320,10 @@ "select": "Seç", "connect_client_alert": "{client} tarafından kontrol ediliyorsun.", "this_device": "Bu cihaz", - "remote": "Yönet" + "remote": "Yönet", + "local_library": "Yerel kütüphane", + "add_library_location": "Kütüphaneye ekle", + "remove_library_location": "Kütüphaneden çıkar", + "local_tab": "Yerel", + "stats": "İstatistikler" } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 4208a3d2c..d056524ea 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -320,5 +320,10 @@ "select": "Вибрати", "connect_client_alert": "Вас керує {client}", "this_device": "Цей пристрій", - "remote": "Віддалений" + "remote": "Віддалений", + "local_library": "Місцева бібліотека", + "add_library_location": "Додати до бібліотеки", + "remove_library_location": "Видалити з бібліотеки", + "local_tab": "Місцевий", + "stats": "Статистика" } \ No newline at end of file diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 6115fc0ce..6bbd6cb66 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -320,5 +320,10 @@ "select": "Chọn", "connect_client_alert": "Bạn đang được điều khiển bởi {client}", "this_device": "Thiết bị này", - "remote": "Từ xa" + "remote": "Từ xa", + "local_library": "Thư viện địa phương", + "add_library_location": "Thêm vào thư viện", + "remove_library_location": "Xóa khỏi thư viện", + "local_tab": "Địa phương", + "stats": "Thống kê" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index da5254a32..b145f97bc 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -320,5 +320,10 @@ "select": "选择", "connect_client_alert": "您正在被 {client} 控制", "this_device": "此设备", - "remote": "远程" + "remote": "远程", + "local_library": "本地图书馆", + "add_library_location": "添加到图书馆", + "remove_library_location": "从图书馆中删除", + "local_tab": "本地", + "stats": "统计" } \ No newline at end of file From 8fc44ed6550e8b2b804991ff82df08afb1c94ca8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 5 Jun 2024 17:59:31 +0600 Subject: [PATCH 44/57] fix(linux): application window not visible after launch --- linux/my_application.cc | 30 +++--- untranslated_messages.json | 210 +------------------------------------ 2 files changed, 18 insertions(+), 222 deletions(-) diff --git a/linux/my_application.cc b/linux/my_application.cc index d1ac5d124..767025caa 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -23,7 +23,7 @@ static void my_application_activate(GApplication* application) { gtk_window_present(GTK_WINDOW(windows->data)); return; } - + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -55,10 +55,11 @@ static void my_application_activate(GApplication* application) { } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_realize(GTK_WIDGET(window)); + gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); @@ -70,16 +71,18 @@ static void my_application_activate(GApplication* application) { } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); @@ -97,15 +100,16 @@ static void my_application_dispose(GObject* object) { static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "com.github.KRTirtho.Spotube", APPLICATION_ID, - "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, - nullptr)); -} + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, "flags", + G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, + nullptr)); +} \ No newline at end of file diff --git a/untranslated_messages.json b/untranslated_messages.json index aaf069293..9e26dfeeb 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,209 +1 @@ -{ - "ar": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "bn": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "ca": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "cs": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "de": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "es": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "eu": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "fa": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "fi": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "fr": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "hi": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "id": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "it": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "ja": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "ka": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "ko": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "ne": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "nl": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "pl": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "pt": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "ru": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "th": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "tr": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "uk": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "vi": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ], - - "zh": [ - "local_library", - "add_library_location", - "remove_library_location", - "local_tab", - "stats" - ] -} +{} \ No newline at end of file From 26ee84d990e11789c70bf9bee3ec2f49159a0469 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 5 Jun 2024 22:06:55 +0600 Subject: [PATCH 45/57] chore: remove window_size deps as unused --- linux/flutter/generated_plugin_registrant.cc | 4 ---- linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- pubspec.lock | 9 --------- pubspec.yaml | 5 ----- windows/flutter/generated_plugin_registrant.cc | 3 --- windows/flutter/generated_plugins.cmake | 1 - 7 files changed, 25 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2f61edd64..e22c5732d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -18,7 +18,6 @@ #include #include #include -#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dart_discord_rpc_registrar = @@ -57,7 +56,4 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); - g_autoptr(FlPluginRegistrar) window_size_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); - window_size_plugin_register_with_registrar(window_size_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 48c7e0cad..9ddc2b98d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -15,7 +15,6 @@ list(APPEND FLUTTER_PLUGIN_LIST tray_manager url_launcher_linux window_manager - window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0057db144..047e7f3d1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -25,7 +25,6 @@ import system_tray import tray_manager import url_launcher_macos import window_manager -import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) @@ -48,5 +47,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) - WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index cf72db1cc..e1b0f97cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2422,15 +2422,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.9" - window_size: - dependency: "direct main" - description: - path: "plugins/window_size" - ref: a738913c8ce2c9f47515382d40827e794a334274 - resolved-ref: a738913c8ce2c9f47515382d40827e794a334274 - url: "https://github.com/google/flutter-desktop-embedding.git" - source: git - version: "0.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c256f66e3..60d18bc73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,11 +87,6 @@ dependencies: version: ^3.0.2 visibility_detector: ^0.4.0+2 window_manager: ^0.3.9 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding.git - ref: a738913c8ce2c9f47515382d40827e794a334274 - path: plugins/window_size youtube_explode_dart: ^2.2.1 simple_icons: ^10.1.3 jiosaavn: ^0.1.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f2dd97143..559db3100 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -20,7 +20,6 @@ #include #include #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( @@ -51,6 +50,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); - WindowSizePluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowSizePlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f4e142808..d1464df54 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -17,7 +17,6 @@ list(APPEND FLUTTER_PLUGIN_LIST tray_manager url_launcher_windows window_manager - window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 57cd8607dd8f32b9c1ca8f9df2eca4715998df9d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 08:19:07 +0600 Subject: [PATCH 46/57] Revert "refactor: migrate deprecated warnings" chore: undo flutter 3.22.x related deprecated theme migrations --- lib/components/artist/artist_card.dart | 12 ++++----- .../home/sections/friends/friend_item.dart | 2 +- lib/components/home/sections/genres.dart | 2 +- .../local_folder/local_folder_item.dart | 2 +- .../playlist_generate/multi_select_field.dart | 2 +- lib/components/player/player_queue.dart | 3 +-- .../player/sibling_tracks_sheet.dart | 3 +-- lib/components/root/bottom_player.dart | 11 +++++--- lib/components/root/sidebar.dart | 10 +++++-- .../root/spotube_navigation_bar.dart | 2 +- .../settings/color_scheme_picker_dialog.dart | 4 +-- .../adaptive/adaptive_pop_sheet_list.dart | 2 +- .../shared/links/anchor_button.dart | 2 +- .../shared/page_window_title_bar.dart | 12 ++++----- lib/components/shared/playbutton_card.dart | 14 +++++----- .../shared/themed_button_tab_bar.dart | 2 +- lib/components/stats/common/album_item.dart | 2 +- lib/main.dart | 23 +++++++++++++--- lib/pages/desktop_login/desktop_login.dart | 2 +- lib/pages/lyrics/lyrics.dart | 2 +- lib/pages/lyrics/mini_lyrics.dart | 12 +++++---- lib/pages/search/search.dart | 6 ++--- lib/pages/settings/blacklist.dart | 1 + lib/pages/settings/sections/about.dart | 7 ++--- lib/pages/settings/sections/accounts.dart | 2 +- lib/pages/settings/sections/downloads.dart | 1 + lib/themes/theme.dart | 27 +++++++------------ 27 files changed, 96 insertions(+), 74 deletions(-) diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index 9c1ee14ac..57971ada9 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -35,10 +35,6 @@ class ArtistCard extends HookConsumerWidget { final radius = BorderRadius.circular(15); - final bgColor = useBrightnessValue( - theme.colorScheme.surface, - theme.colorScheme.surfaceContainerHigh, - ); final double size = useBreakpointValue( xs: 130, sm: 130, @@ -50,8 +46,12 @@ class ArtistCard extends HookConsumerWidget { width: size, margin: const EdgeInsets.symmetric(vertical: 5), child: Material( - shadowColor: theme.colorScheme.surface, - color: bgColor, + shadowColor: theme.colorScheme.background, + color: Color.lerp( + theme.colorScheme.surfaceVariant, + theme.colorScheme.surface, + useBrightnessValue(.9, .7), + ), elevation: 3, shape: RoundedRectangleBorder( borderRadius: radius, diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart index 096964a66..2b5757565 100644 --- a/lib/components/home/sections/friends/friend_item.dart +++ b/lib/components/home/sections/friends/friend_item.dart @@ -30,7 +30,7 @@ class FriendItem extends HookConsumerWidget { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: colorScheme.surfaceContainer, + color: colorScheme.surfaceVariant.withOpacity(0.3), borderRadius: BorderRadius.circular(15), ), constraints: const BoxConstraints( diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index 62f462e26..7dfafd5a0 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -134,7 +134,7 @@ class HomeGenresSection extends HookConsumerWidget { child: Ink( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), - color: colorScheme.surfaceContainerHighest, + color: colorScheme.surfaceVariant, gradient: categoriesQuery.isLoading ? null : gradient, ), padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/components/library/local_folder/local_folder_item.dart b/lib/components/library/local_folder/local_folder_item.dart index 6220a9671..72032198c 100644 --- a/lib/components/library/local_folder/local_folder_item.dart +++ b/lib/components/library/local_folder/local_folder_item.dart @@ -71,7 +71,7 @@ class LocalFolderItem extends HookConsumerWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Color.lerp( - colorScheme.surfaceContainerHighest, + colorScheme.surfaceVariant, colorScheme.surface, lerpValue, ), diff --git a/lib/components/library/playlist_generate/multi_select_field.dart b/lib/components/library/playlist_generate/multi_select_field.dart index d8e0506db..e54fc2ba3 100644 --- a/lib/components/library/playlist_generate/multi_select_field.dart +++ b/lib/components/library/playlist_generate/multi_select_field.dart @@ -71,7 +71,7 @@ class MultiSelectField extends HookWidget { : theme.colorScheme.onSurface.withOpacity(0.1), ), ), - mouseCursor: WidgetStateMouseCursor.textable, + mouseCursor: MaterialStateMouseCursor.textable, onPressed: !enabled ? null : () async { diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 1665b3dd0..914d7bc97 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -122,8 +122,7 @@ class PlayerQueue extends HookConsumerWidget { top: 5.0, ), decoration: BoxDecoration( - color: - theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), + color: theme.colorScheme.surfaceVariant.withOpacity(0.5), borderRadius: borderRadius, ), child: CallbackShortcuts( diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 0575d8ebf..99b7b430f 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -208,8 +208,7 @@ class SiblingTracksSheet extends HookConsumerWidget { : mediaQuery.size.height * .6, decoration: BoxDecoration( borderRadius: borderRadius, - color: - theme.colorScheme.surfaceContainerHighest.withOpacity(.5), + color: theme.colorScheme.surfaceVariant.withOpacity(.5), ), child: Scaffold( backgroundColor: Colors.transparent, diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index b99318dfb..5429e172b 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -14,6 +14,7 @@ import 'package:spotube/components/player/volume_slider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication_provider.dart'; @@ -48,6 +49,12 @@ class BottomPlayer extends HookConsumerWidget { ); final theme = Theme.of(context); + final bg = theme.colorScheme.surfaceVariant; + + final bgColor = useBrightnessValue( + Color.lerp(bg, Colors.white, 0.7), + Color.lerp(bg, Colors.black, 0.45)!, + ); // returning an empty non spacious Container as the overlay will take // place in the global overlay stack aka [_entries] @@ -60,9 +67,7 @@ class BottomPlayer extends HookConsumerWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: DecoratedBox( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer.withOpacity(.8), - ), + decoration: BoxDecoration(color: bgColor?.withOpacity(0.8)), child: Material( type: MaterialType.transparency, textStyle: theme.textTheme.bodyMedium!, diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 4fa14021f..0e644a898 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -14,6 +14,7 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/settings/settings.dart'; @@ -69,7 +70,12 @@ class Sidebar extends HookConsumerWidget { ); final theme = Theme.of(context); - final bg = theme.colorScheme.surfaceContainer; + final bg = theme.colorScheme.surfaceVariant; + + final bgColor = useBrightnessValue( + Color.lerp(bg, Colors.white, 0.7), + Color.lerp(bg, Colors.black, 0.45)!, + ); useEffect(() { if (!context.mounted) return; @@ -153,7 +159,7 @@ class Sidebar extends HookConsumerWidget { ), padding: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( - color: bg, + color: bgColor?.withOpacity(0.8), borderRadius: const BorderRadius.only( topRight: Radius.circular(10), bottomRight: Radius.circular(10), diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index 3d0c7c75e..e16ad1a89 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -68,7 +68,7 @@ class SpotubeNavigationBar extends HookConsumerWidget { backgroundColor: theme.colorScheme.secondaryContainer.withOpacity(0.72), buttonBackgroundColor: buttonColor, - color: theme.colorScheme.surface, + color: theme.colorScheme.background, height: panelHeight, animationDuration: const Duration(milliseconds: 350), items: navbarTileList.map( diff --git a/lib/components/settings/color_scheme_picker_dialog.dart b/lib/components/settings/color_scheme_picker_dialog.dart index 579f5a296..8d0983752 100644 --- a/lib/components/settings/color_scheme_picker_dialog.dart +++ b/lib/components/settings/color_scheme_picker_dialog.dart @@ -179,9 +179,9 @@ class ColorTile extends StatelessWidget { colorScheme.primaryContainer, colorScheme.secondary, colorScheme.secondaryContainer, + colorScheme.background, colorScheme.surface, - colorScheme.surface, - colorScheme.surfaceContainerHighest, + colorScheme.surfaceVariant, colorScheme.onPrimary, colorScheme.onSurface, ]; diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index ce7d3b8c9..21f56a220 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -187,7 +187,7 @@ class AdaptivePopSheetList extends StatelessWidget { icon: icon ?? const Icon(SpotubeIcons.moreVertical), tooltip: tooltip, style: theme.iconButtonTheme.style?.copyWith( - shape: WidgetStatePropertyAll( + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: borderRadius, ), diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/shared/links/anchor_button.dart index c6f0b889b..d78bbf962 100644 --- a/lib/components/shared/links/anchor_button.dart +++ b/lib/components/shared/links/anchor_button.dart @@ -29,7 +29,7 @@ class AnchorButton extends HookWidget { onTapUp: (event) => tap.value = false, onTap: onTap, child: MouseRegion( - cursor: WidgetStateMouseCursor.clickable, + cursor: MaterialStateMouseCursor.clickable, child: Text( text, style: style.copyWith( diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index 573c7c47c..d7c8320d1 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -210,16 +210,16 @@ class WindowTitleBarButtons extends HookConsumerWidget { final theme = Theme.of(context); final colors = WindowButtonColors( normal: Colors.transparent, - iconNormal: foregroundColor ?? theme.colorScheme.onSurface, - mouseOver: theme.colorScheme.onSurface.withOpacity(0.1), - mouseDown: theme.colorScheme.onSurface.withOpacity(0.2), - iconMouseOver: theme.colorScheme.onSurface, - iconMouseDown: theme.colorScheme.onSurface, + iconNormal: foregroundColor ?? theme.colorScheme.onBackground, + mouseOver: theme.colorScheme.onBackground.withOpacity(0.1), + mouseDown: theme.colorScheme.onBackground.withOpacity(0.2), + iconMouseOver: theme.colorScheme.onBackground, + iconMouseDown: theme.colorScheme.onBackground, ); final closeColors = WindowButtonColors( normal: Colors.transparent, - iconNormal: foregroundColor ?? theme.colorScheme.onSurface, + iconNormal: foregroundColor ?? theme.colorScheme.onBackground, mouseOver: Colors.red, mouseDown: Colors.red[800]!, iconMouseOver: Colors.white, diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 807628b35..80a27eb01 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -53,10 +53,6 @@ class PlaybuttonCard extends HookWidget { final mediaQuery = MediaQuery.of(context); final radius = BorderRadius.circular(15); - final bgColor = useBrightnessValue( - theme.colorScheme.surface, - theme.colorScheme.surfaceContainerHigh, - ); final double size = useBreakpointValue( xs: 130, sm: 130, @@ -76,9 +72,13 @@ class PlaybuttonCard extends HookWidget { constraints: BoxConstraints(maxWidth: size), margin: margin, child: Material( - color: bgColor, + color: Color.lerp( + theme.colorScheme.surfaceVariant, + theme.colorScheme.surface, + useBrightnessValue(.9, .7), + ), borderRadius: radius, - shadowColor: theme.colorScheme.surface, + shadowColor: theme.colorScheme.background, elevation: 3, child: InkWell( mouseCursor: SystemMouseCursors.click, @@ -158,7 +158,7 @@ class PlaybuttonCard extends HookWidget { Skeleton.keep( child: IconButton( style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.surface, + backgroundColor: theme.colorScheme.background, foregroundColor: theme.colorScheme.primary, minimumSize: const Size.square(10), ), diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/shared/themed_button_tab_bar.dart index c245e5f4e..b21ca9924 100644 --- a/lib/components/shared/themed_button_tab_bar.dart +++ b/lib/components/shared/themed_button_tab_bar.dart @@ -34,7 +34,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { ), borderWidth: 0, unselectedDecoration: BoxDecoration( - color: theme.colorScheme.surface, + color: theme.colorScheme.background, borderRadius: BorderRadius.circular(15), ), unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith( diff --git a/lib/components/stats/common/album_item.dart b/lib/components/stats/common/album_item.dart index 00b1cbfef..ccc0fa4ec 100644 --- a/lib/components/stats/common/album_item.dart +++ b/lib/components/stats/common/album_item.dart @@ -33,7 +33,7 @@ class StatsAlbumItem extends StatelessWidget { Text("${album.albumType?.formatted} • "), Flexible( child: ArtistLink( - artists: album.artists ?? [], + artists: album.artists!, mainAxisAlignment: WrapAlignment.start, ), ), diff --git a/lib/main.dart b/lib/main.dart index 1693d9d88..52d0b1415 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; @@ -138,11 +139,28 @@ Future main(List rawArgs) async { ); } -class Spotube extends HookConsumerWidget { +class Spotube extends StatefulHookConsumerWidget { const Spotube({super.key}); @override - Widget build(BuildContext context, ref) { + SpotubeState createState() => SpotubeState(); + + static SpotubeState of(BuildContext context) => + context.findAncestorStateOfType()!; +} + +class SpotubeState extends ConsumerState { + final logger = getLogger(Spotube); + SharedPreferences? localStorage; + + @override + void initState() { + super.initState(); + SharedPreferences.getInstance().then(((value) => localStorage = value)); + } + + @override + Widget build(BuildContext context) { final themeMode = ref.watch(userPreferencesProvider.select((s) => s.themeMode)); final accentMaterialColor = @@ -177,7 +195,6 @@ class Spotube extends HookConsumerWidget { () => theme(paletteColor ?? accentMaterialColor, Brightness.light, false), [paletteColor, accentMaterialColor], ); - final darkTheme = useMemoized( () => theme( paletteColor ?? accentMaterialColor, diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index c9367e054..9c9bdddba 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -17,7 +17,7 @@ class DesktopLoginPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final mediaQuery = MediaQuery.of(context); final theme = Theme.of(context); - final color = theme.colorScheme.surfaceContainerHighest.withOpacity(.3); + final color = theme.colorScheme.surfaceVariant.withOpacity(.3); return SafeArea( child: Scaffold( diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 1d9b383a7..850eccfa0 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -100,7 +100,7 @@ class LyricsPage extends HookConsumerWidget { child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface.withOpacity(.4), + color: Theme.of(context).colorScheme.background.withOpacity(.4), borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index a026209ce..996e190df 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -107,7 +107,8 @@ class MiniLyricsPage extends HookConsumerWidget { : const Icon(SpotubeIcons.lyricsOff), style: ButtonStyle( foregroundColor: showLyrics.value - ? WidgetStateProperty.all(theme.colorScheme.primary) + ? MaterialStateProperty.all( + theme.colorScheme.primary) : null, ), onPressed: () async { @@ -131,7 +132,8 @@ class MiniLyricsPage extends HookConsumerWidget { : const Icon(SpotubeIcons.hoverOff), style: ButtonStyle( foregroundColor: hoverMode.value - ? WidgetStateProperty.all(theme.colorScheme.primary) + ? MaterialStateProperty.all( + theme.colorScheme.primary) : null, ), onPressed: () async { @@ -152,7 +154,7 @@ class MiniLyricsPage extends HookConsumerWidget { ), style: ButtonStyle( foregroundColor: snapshot.data == true - ? WidgetStateProperty.all( + ? MaterialStateProperty.all( theme.colorScheme.primary) : null, ), @@ -184,12 +186,12 @@ class MiniLyricsPage extends HookConsumerWidget { child: TabBarView( children: [ SyncedLyrics( - palette: PaletteColor(theme.colorScheme.surface, 0), + palette: PaletteColor(theme.colorScheme.background, 0), isModal: true, defaultTextZoom: 65, ), PlainLyrics( - palette: PaletteColor(theme.colorScheme.surface, 0), + palette: PaletteColor(theme.colorScheme.background, 0), isModal: true, defaultTextZoom: 65, ), diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 50ef152b9..d5374786d 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -212,7 +212,7 @@ class SearchPage extends HookConsumerWidget { Icon( SpotubeIcons.web, size: 120, - color: theme.colorScheme.onSurface + color: theme.colorScheme.onBackground .withOpacity(0.7), ), const SizedBox(height: 20), @@ -220,7 +220,7 @@ class SearchPage extends HookConsumerWidget { context.l10n.search_to_get_results, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, - color: theme.colorScheme.onSurface + color: theme.colorScheme.onBackground .withOpacity(0.5), ), ), @@ -246,7 +246,7 @@ class SearchPage extends HookConsumerWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w900, - color: theme.colorScheme.onSurface + color: theme.colorScheme.onBackground .withOpacity(0.7), ), ), diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 4e9379222..6eccab073 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -20,6 +20,7 @@ class BlackListPage extends HookConsumerWidget { final controller = useScrollController(); final blacklist = ref.watch(blacklistProvider); final searchText = useState(""); + final filteredBlacklist = useMemoized( () { if (searchText.value.isEmpty) { diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart index 5e5d23770..a8d72cc0d 100644 --- a/lib/pages/settings/sections/about.dart +++ b/lib/pages/settings/sections/about.dart @@ -43,9 +43,10 @@ class SettingsAboutSection extends HookConsumerWidget { ), trailing: (context, update) => FilledButton( style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Colors.red[100]), - foregroundColor: const WidgetStatePropertyAll(Colors.pinkAccent), - padding: const WidgetStatePropertyAll(EdgeInsets.all(15)), + backgroundColor: MaterialStatePropertyAll(Colors.red[100]), + foregroundColor: + const MaterialStatePropertyAll(Colors.pinkAccent), + padding: const MaterialStatePropertyAll(EdgeInsets.all(15)), ), onPressed: () { launchUrlString( diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 5acab4807..6162aa3d3 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -82,7 +82,7 @@ class SettingsAccountSection extends HookConsumerWidget { router.push("/login"); }, style: ButtonStyle( - shape: WidgetStateProperty.all( + shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(25.0), ), diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index 76ef8e3e9..3092ed03f 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -3,6 +3,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 28acc2809..8659cf0c5 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -4,22 +4,13 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { final scheme = ColorScheme.fromSeed( seedColor: seed, shadow: Colors.black12, + background: isAmoled ? Colors.black : null, surface: isAmoled ? Colors.black : null, - surfaceContainer: isAmoled ? const Color(0xFF090909) : null, - surfaceContainerHigh: isAmoled ? const Color(0xFF181818) : null, - surfaceContainerHighest: isAmoled ? const Color(0xFF282828) : null, brightness: brightness, ); return ThemeData( useMaterial3: true, colorScheme: scheme, - scaffoldBackgroundColor: isAmoled ? Colors.black : null, - cardTheme: CardTheme( - color: scheme.surfaceContainer, - shadowColor: scheme.shadow, - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), - ), listTileTheme: ListTileThemeData( horizontalTitleGap: 5, iconColor: scheme.onSurface, @@ -39,7 +30,7 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { navigationBarTheme: const NavigationBarThemeData( labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, height: 50, - iconTheme: WidgetStatePropertyAll( + iconTheme: MaterialStatePropertyAll( IconThemeData(size: 18), ), ), @@ -66,25 +57,25 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { ), sliderTheme: SliderThemeData(overlayShape: SliderComponentShape.noOverlay), searchBarTheme: SearchBarThemeData( - textStyle: const WidgetStatePropertyAll(TextStyle(fontSize: 15)), + textStyle: const MaterialStatePropertyAll(TextStyle(fontSize: 15)), constraints: const BoxConstraints(maxWidth: double.infinity), - padding: const WidgetStatePropertyAll(EdgeInsets.all(8)), - backgroundColor: WidgetStatePropertyAll( + padding: const MaterialStatePropertyAll(EdgeInsets.all(8)), + backgroundColor: MaterialStatePropertyAll( Color.lerp( - scheme.surfaceContainerHighest, + scheme.surfaceVariant, scheme.surface, brightness == Brightness.light ? .9 : .7, ), ), - elevation: const WidgetStatePropertyAll(0), - shape: WidgetStatePropertyAll( + elevation: const MaterialStatePropertyAll(0), + shape: MaterialStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), ), scrollbarTheme: const ScrollbarThemeData( - thickness: WidgetStatePropertyAll(14), + thickness: MaterialStatePropertyAll(14), ), checkboxTheme: CheckboxThemeData( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), From 982cf0bd435638fa20f17ef527fe21d031b5ffaf Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 08:28:18 +0600 Subject: [PATCH 47/57] fix(windows): revert Flutter version to 3.19.6 to avoid distortion #1553 --- .fvm/fvm_config.json | 2 +- .github/workflows/pr-lint.yml | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- lib/main.dart | 22 ++------------------ macos/Podfile.lock | 6 ------ 5 files changed, 5 insertions(+), 29 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index df8efa0eb..0b54542ff 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.22.1", + "flutterSdkVersion": "3.19.6", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 2844986de..64cc8adc4 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -4,7 +4,7 @@ on: pull_request: env: - FLUTTER_VERSION: '3.22.1' + FLUTTER_VERSION: '3.19.6' jobs: lint: diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 8e68211c6..5e32ee703 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -20,7 +20,7 @@ on: description: Dry run without uploading to release env: - FLUTTER_VERSION: 3.22.1 + FLUTTER_VERSION: 3.19.6 permissions: contents: write diff --git a/lib/main.dart b/lib/main.dart index 52d0b1415..30526bc60 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,7 +10,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; @@ -139,28 +138,11 @@ Future main(List rawArgs) async { ); } -class Spotube extends StatefulHookConsumerWidget { +class Spotube extends HookConsumerWidget { const Spotube({super.key}); @override - SpotubeState createState() => SpotubeState(); - - static SpotubeState of(BuildContext context) => - context.findAncestorStateOfType()!; -} - -class SpotubeState extends ConsumerState { - final logger = getLogger(Spotube); - SharedPreferences? localStorage; - - @override - void initState() { - super.initState(); - SharedPreferences.getInstance().then(((value) => localStorage = value)); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { final themeMode = ref.watch(userPreferencesProvider.select((s) => s.themeMode)); final accentMaterialColor = diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 166bfa714..fcba29349 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -49,8 +49,6 @@ PODS: - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS - - window_size (0.0.2): - - FlutterMacOS DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) @@ -76,7 +74,6 @@ DEPENDENCIES: - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) SPEC REPOS: trunk: @@ -129,8 +126,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos - window_size: - :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a @@ -157,7 +152,6 @@ SPEC CHECKSUMS: tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 - window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 From 73c5b30b63a4c82bb7ad5b52bc10c5594566a800 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 08:43:29 +0600 Subject: [PATCH 48/57] fix: browse anonymously button takes to wrong route --- lib/pages/getting_started/sections/support.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/getting_started/sections/support.dart b/lib/pages/getting_started/sections/support.dart index 7bccfe06e..b6be07e53 100644 --- a/lib/pages/getting_started/sections/support.dart +++ b/lib/pages/getting_started/sections/support.dart @@ -106,7 +106,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { onPressed: () async { await KVStoreService.setDoneGettingStarted(true); if (context.mounted) { - context.go(HomePage.name); + context.goNamed(HomePage.name); } }, ), From 37d002d133cacb3a34884713ac8f6637694af57c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 08:47:07 +0600 Subject: [PATCH 49/57] fix: alternative sources not showing up for SongLink matched results --- lib/services/sourced_track/sources/youtube.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index c24edfc0a..af61a8820 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -221,7 +221,10 @@ class YoutubeSourcedTrack extends SourcedTrack { final links = await SongLinkService.links(track.id!); final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube"); - if (ytLink?.url != null) { + if (ytLink?.url != null + // allows to fetch siblings more results for already sourced track + && + track is! SourcedTrack) { try { return [ await toSiblingType( From 6cb29868d2030b5e9312863c17e8f24889942e24 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 09:07:07 +0600 Subject: [PATCH 50/57] fix: use weak match for Jiosaavn fallback to improve matching --- lib/services/sourced_track/sourced_track.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 7eedfad87..fc3f2e501 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -138,7 +138,7 @@ abstract class SourcedTrack extends Track { return await JioSaavnSourcedTrack.fetchFromTrack( track: track, ref: ref, - weakMatch: preferences.audioSource == AudioSource.jiosaavn, + weakMatch: true, ); } rethrow; From 3394c1b0574e44dc624b2b2f0bf32f343ae9f049 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 10:46:20 +0600 Subject: [PATCH 51/57] fix(android): Media Controls not working above Android 14 #1561 --- android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 589e22ff9..64c32e28d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + From d7d864ff2bc937675a544a7edf645c5148ec836a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 12:29:20 +0600 Subject: [PATCH 52/57] fix(windows): media controls not showing up #1542 --- pubspec.lock | 32 ++++++++++++++++---------------- pubspec.yaml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e1b0f97cf..da410958e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1242,10 +1242,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.18.1" introduction_screen: dependency: "direct main" description: @@ -1298,26 +1298,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.0" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "2.0.1" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.0.1" lints: dependency: transitive description: @@ -1458,10 +1458,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.11.0" metadata_god: dependency: "direct main" description: @@ -1984,10 +1984,10 @@ packages: dependency: "direct main" description: name: smtc_windows - sha256: "799bbe0f8e4436da852c5dcc0be482c97b8ae0f504f65c6b750cd239b4835aa0" + sha256: "0fd64d0c6a0c8ea4ea7908d31195eadc8f6d45d5245159fc67259e9e8704100f" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" source_gen: dependency: transitive description: @@ -2146,10 +2146,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.6.1" time: dependency: transitive description: @@ -2354,10 +2354,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "13.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 60d18bc73..247a97701 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,7 +78,7 @@ dependencies: sidebarx: ^0.17.1 shared_preferences: ^2.2.3 skeleton_text: ^3.0.1 - smtc_windows: ^0.1.2 + smtc_windows: ^0.1.3 stroke_text: ^0.0.2 system_theme: ^2.1.0 titlebar_buttons: ^1.0.0 From 6591ec0e1b441dd8fd535eace19a58c7749389ca Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 13:08:50 +0600 Subject: [PATCH 53/57] fix(ios): download not working #1575 --- .../download_manager/download_manager.dart | 2 ++ pubspec.lock | 28 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/services/download_manager/download_manager.dart b/lib/services/download_manager/download_manager.dart index dbb96791c..54d35b02e 100644 --- a/lib/services/download_manager/download_manager.dart +++ b/lib/services/download_manager/download_manager.dart @@ -80,6 +80,8 @@ class DownloadManager { logger.d("[DownloadManager] $url"); final file = File(savePath.toString()); + await Directory(path.dirname(savePath)).create(recursive: true); + final tmpDirPath = await Directory( path.join( (await getTemporaryDirectory()).path, diff --git a/pubspec.lock b/pubspec.lock index da410958e..c11577f21 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1242,10 +1242,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" introduction_screen: dependency: "direct main" description: @@ -1298,26 +1298,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -1458,10 +1458,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" metadata_god: dependency: "direct main" description: @@ -2146,10 +2146,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" time: dependency: transitive description: @@ -2354,10 +2354,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: From 5f280a19f4d5f8882ae2ff60c6cc595ded7a5a1d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 13:41:02 +0600 Subject: [PATCH 54/57] fix(desktop): titlebar drag to move not working --- lib/components/root/sidebar.dart | 35 ++++++++++--------- .../shared/page_window_title_bar.dart | 10 ++++-- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 0e644a898..8e7374b16 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -26,6 +26,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; +import 'package:window_manager/window_manager.dart'; class Sidebar extends HookConsumerWidget { final Widget child; @@ -207,22 +208,24 @@ class SidebarHeader extends HookWidget { ); } - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - if (kIsMacOS) const SizedBox(height: 25), - Row( - children: [ - Sidebar.brandLogo(), - const SizedBox(width: 10), - Text( - "Spotube", - style: theme.textTheme.titleLarge, - ), - ], - ), - ], + return DragToMoveArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + if (kIsMacOS) const SizedBox(height: 25), + Row( + children: [ + Sidebar.brandLogo(), + const SizedBox(width: 10), + Text( + "Spotube", + style: theme.textTheme.titleLarge, + ), + ], + ), + ], + ), ), ); } diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index d7c8320d1..c5fc11e79 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -125,7 +125,10 @@ class _PageWindowTitleBarState extends ConsumerState { leadingWidth: widget.leadingWidth, toolbarTextStyle: widget.toolbarTextStyle, titleTextStyle: widget.titleTextStyle, - title: widget.title, + title: SizedBox( + width: double.infinity, // workaround to force dragging + child: widget.title ?? const Text(""), + ), pinned: widget.pinned, floating: widget.floating, snap: widget.snap, @@ -164,7 +167,10 @@ class _PageWindowTitleBarState extends ConsumerState { leadingWidth: widget.leadingWidth, toolbarTextStyle: widget.toolbarTextStyle, titleTextStyle: widget.titleTextStyle, - title: widget.title, + title: SizedBox( + width: double.infinity, // workaround to force dragging + child: widget.title ?? const Text(""), + ), scrolledUnderElevation: 0, shadowColor: Colors.transparent, forceMaterialTransparency: true, From 47f98b98aafab9b426733ed44cab2be8a646a98e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 13:48:52 +0600 Subject: [PATCH 55/57] fix(desktop): window is not centered --- lib/services/wm_tools/wm_tools.dart | 1 + pubspec.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/services/wm_tools/wm_tools.dart b/lib/services/wm_tools/wm_tools.dart index 4572a8b43..920e09b57 100644 --- a/lib/services/wm_tools/wm_tools.dart +++ b/lib/services/wm_tools/wm_tools.dart @@ -44,6 +44,7 @@ class WindowManagerTools with WidgetsBindingObserver { backgroundColor: Colors.transparent, minimumSize: Size(300, 700), titleBarStyle: TitleBarStyle.hidden, + center: true, ), () async { final savedSize = KVStoreService.windowSize; diff --git a/pubspec.lock b/pubspec.lock index c11577f21..da410958e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1242,10 +1242,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.18.1" introduction_screen: dependency: "direct main" description: @@ -1298,26 +1298,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.0" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "2.0.1" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.0.1" lints: dependency: transitive description: @@ -1458,10 +1458,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.11.0" metadata_god: dependency: "direct main" description: @@ -2146,10 +2146,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.6.1" time: dependency: transitive description: @@ -2354,10 +2354,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "13.0.0" watcher: dependency: transitive description: From 6c6488ea6da9c1e0bccf5fe501e5574c1a8f937c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 14:36:14 +0600 Subject: [PATCH 56/57] cd: fix version not being extracted in the steps --- .github/workflows/spotube-release-binary.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 5e32ee703..e99aebab9 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -117,6 +117,7 @@ jobs: needs: - build_platform steps: + - uses: actions/checkout@v4 - uses: actions/download-artifact@v3 with: name: Spotube-Release-Binaries From bf769f473b169f5eb9af28d36fc05b1709df03fd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 6 Jun 2024 14:52:28 +0600 Subject: [PATCH 57/57] chore: bump version, generate changelog and credits --- .github/workflows/spotube-publish-binary.yml | 2 +- CHANGELOG.md | 17 +++++++++++++++++ README.md | 1 - pubspec.yaml | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 0d39ab1d8..7f85173fc 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to publish (x.x.x) - default: 3.7.0 + default: 3.7.1 required: true dry_run: description: Dry run diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fb79d42..22919a32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.7.1](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.7.1) (2024-06-06) + + +### Bug Fixes + +* alternative sources not showing up for SongLink matched results ([37d002d](https://github.com/krtirtho/spotube/commit/37d002d133cacb3a34884713ac8f6637694af57c)) +* **android:** Media Controls not working above Android 14 [#1561](https://github.com/krtirtho/spotube/issues/1561) ([3394c1b](https://github.com/krtirtho/spotube/commit/3394c1b0574e44dc624b2b2f0bf32f343ae9f049)) +* browse anonymously button takes to wrong route ([73c5b30](https://github.com/krtirtho/spotube/commit/73c5b30b63a4c82bb7ad5b52bc10c5594566a800)) +* **desktop:** titlebar drag to move not working ([5f280a1](https://github.com/krtirtho/spotube/commit/5f280a19f4d5f8882ae2ff60c6cc595ded7a5a1d)) +* **desktop:** window is not centered ([47f98b9](https://github.com/krtirtho/spotube/commit/47f98b98aafab9b426733ed44cab2be8a646a98e)) +* **ios:** download not working [#1575](https://github.com/krtirtho/spotube/issues/1575) ([6591ec0](https://github.com/krtirtho/spotube/commit/6591ec0e1b441dd8fd535eace19a58c7749389ca)) +* **linux:** application window not visible after launch ([8fc44ed](https://github.com/krtirtho/spotube/commit/8fc44ed6550e8b2b804991ff82df08afb1c94ca8)) +* local track not showing up in queue ([d82261c](https://github.com/krtirtho/spotube/commit/d82261cb25ece63f85af0e40216cf32dccdc9dd5)) +* use weak match for Jiosaavn fallback to improve matching ([6cb2986](https://github.com/krtirtho/spotube/commit/6cb29868d2030b5e9312863c17e8f24889942e24)) +* **windows:** media controls not showing up [#1542](https://github.com/krtirtho/spotube/issues/1542) ([d7d864f](https://github.com/krtirtho/spotube/commit/d7d864ff2bc937675a544a7edf645c5148ec836a)) +* **windows:** revert Flutter version to 3.19.6 to avoid distortion [#1553](https://github.com/krtirtho/spotube/issues/1553) ([982cf0b](https://github.com/krtirtho/spotube/commit/982cf0bd435638fa20f17ef527fe21d031b5ffaf)) + ## [3.7.0](https://github.com/krtirtho/spotube/compare/v3.6.0...v3.7.0) (2024-06-03) diff --git a/README.md b/README.md index 5db4d5ade..71c879bac 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter 1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry. 1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. -1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter. 1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents. 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. diff --git a/pubspec.yaml b/pubspec.yaml index 247a97701..ffa8511fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 3.7.0+31 +version: 3.7.1+32 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube