From f81219e83ef3787f4ab7867f8e7a2a7d94323540 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Jun 2024 22:00:16 +0600 Subject: [PATCH 01/92] chore: introduce breakpoint enum for constrains --- lib/components/library/user_playlists.dart | 6 ++-- lib/extensions/constrains.dart | 32 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 069dfad95..246c39477 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -3,7 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:collection/collection.dart'; import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -17,9 +16,11 @@ import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/pages/library/playlist_generate/playlist_generate.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'; class UserPlaylists extends HookConsumerWidget { const UserPlaylists({super.key}); @@ -110,7 +111,8 @@ class UserPlaylists extends HookConsumerWidget { icon: const Icon(SpotubeIcons.magic), label: Text(context.l10n.generate_playlist), onPressed: () { - GoRouter.of(context).push("/library/generate"); + ServiceUtils.pushNamed( + context, PlaylistGeneratorPage.name); }, ), const Gap(10), diff --git a/lib/extensions/constrains.dart b/lib/extensions/constrains.dart index 1177f5ace..dc1027e2a 100644 --- a/lib/extensions/constrains.dart +++ b/lib/extensions/constrains.dart @@ -1,6 +1,20 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +enum Breakpoint { + xs, + sm, + md, + lg, + xl, + xxl; + + bool operator <=(Breakpoint other) => index <= other.index; + bool operator <(Breakpoint other) => index < other.index; + bool operator >(Breakpoint other) => index > other.index; + bool operator >=(Breakpoint other) => index >= other.index; +} + // ignore: constant_identifier_names const Breakpoints = ( xs: 480.0, @@ -22,6 +36,15 @@ extension SliverBreakpoints on SliverConstraints { crossAxisExtent > Breakpoints.lg && crossAxisExtent <= Breakpoints.xl; bool get is2Xl => crossAxisExtent > Breakpoints.xl; + Breakpoint get breakpoint { + if (isXs) return Breakpoint.xs; + if (isSm) return Breakpoint.sm; + if (isMd) return Breakpoint.md; + if (isLg) return Breakpoint.lg; + if (isXl) return Breakpoint.xl; + return Breakpoint.xxl; + } + bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl; bool get mdAndUp => isMd || isLg || isXl || is2Xl; bool get lgAndUp => isLg || isXl || is2Xl; @@ -45,6 +68,15 @@ extension ContainerBreakpoints on BoxConstraints { biggest.width > Breakpoints.lg && biggest.width <= Breakpoints.xl; bool get is2Xl => biggest.width > Breakpoints.xl; + Breakpoint get breakpoint { + if (isXs) return Breakpoint.xs; + if (isSm) return Breakpoint.sm; + if (isMd) return Breakpoint.md; + if (isLg) return Breakpoint.lg; + if (isXl) return Breakpoint.xl; + return Breakpoint.xxl; + } + bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl; bool get mdAndUp => isMd || isLg || isXl || is2Xl; bool get lgAndUp => isLg || isXl || is2Xl; From 1cfeef54e7c5d8549457cdc6aefab51b4fc7445f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Jun 2024 22:19:45 +0600 Subject: [PATCH 02/92] refactor: move route related components to modules folder --- lib/collections/intents.dart | 2 +- .../dialogs/playlist_add_track_dialog.dart | 2 +- .../horizontal_playbutton_card_view.dart | 6 ++-- .../shared/sort_tracks_dropdown.dart | 2 +- .../sections/header/header_actions.dart | 2 +- .../tracks_view/track_view_provider.dart | 2 +- lib/components/stats/common/album_item.dart | 2 +- .../album/album_card.dart | 0 .../artist/artist_album_list.dart | 0 .../artist/artist_card.dart | 0 .../connect/connect_device.dart | 0 .../connect/local_devices.dart | 0 .../desktop_login/login_form.dart | 0 .../getting_started/blur_card.dart | 0 .../home/sections/featured.dart | 0 .../home/sections/feed.dart | 0 .../home/sections/friends.dart | 2 +- .../home/sections/friends/friend_item.dart | 0 .../home/sections/genres.dart | 0 .../home/sections/made_for_user.dart | 0 .../home/sections/new_releases.dart | 0 .../home/sections/recent.dart | 0 .../local_folder/local_folder_item.dart | 0 .../playlist_generate/multi_select_field.dart | 0 .../recommendation_attribute_dials.dart | 0 .../recommendation_attribute_fields.dart | 2 +- .../seeds_multi_autocomplete.dart | 0 .../playlist_generate/simple_track_tile.dart | 0 .../library/user_albums.dart | 2 +- .../library/user_artists.dart | 2 +- .../library/user_downloads.dart | 2 +- .../library/user_downloads/download_item.dart | 0 .../library/user_local_tracks.dart | 2 +- .../library/user_playlists.dart | 4 +-- .../lyrics/use_synced_lyrics.dart | 0 .../lyrics/zoom_controls.dart | 0 .../player/player.dart | 8 +++--- .../player/player_actions.dart | 2 +- .../player/player_controls.dart | 2 +- .../player/player_overlay.dart | 8 +++--- .../player/player_queue.dart | 0 .../player/player_track_details.dart | 0 .../player/sibling_tracks_sheet.dart | 0 .../player/use_progress.dart | 0 .../player/volume_slider.dart | 0 .../playlist/playlist_card.dart | 0 .../playlist/playlist_create_dialog.dart | 0 .../root/bottom_player.dart | 10 +++---- lib/{components => modules}/root/sidebar.dart | 2 +- .../root/spotube_navigation_bar.dart | 0 .../root/update_dialog.dart | 0 .../settings/color_scheme_picker_dialog.dart | 0 .../settings/section_card_with_heading.dart | 0 lib/pages/artist/artist.dart | 2 +- lib/pages/artist/section/related_artists.dart | 2 +- lib/pages/connect/connect.dart | 2 +- lib/pages/connect/control/control.dart | 4 +-- lib/pages/desktop_login/desktop_login.dart | 2 +- lib/pages/desktop_login/login_tutorial.dart | 2 +- .../getting_started/sections/greeting.dart | 2 +- .../getting_started/sections/playback.dart | 2 +- .../getting_started/sections/region.dart | 2 +- .../getting_started/sections/support.dart | 2 +- lib/pages/home/feed/feed_section.dart | 6 ++-- lib/pages/home/genres/genre_playlists.dart | 2 +- lib/pages/home/home.dart | 16 +++++------ lib/pages/library/library.dart | 10 +++---- lib/pages/library/local_folder.dart | 2 +- .../playlist_generate/playlist_generate.dart | 10 +++---- .../playlist_generate_result.dart | 4 +-- lib/pages/lyrics/mini_lyrics.dart | 6 ++-- lib/pages/lyrics/plain_lyrics.dart | 2 +- lib/pages/lyrics/synced_lyrics.dart | 4 +-- lib/pages/root/root_app.dart | 8 +++--- lib/pages/settings/logs.dart | 2 +- lib/pages/settings/sections/about.dart | 2 +- lib/pages/settings/sections/accounts.dart | 2 +- lib/pages/settings/sections/appearance.dart | 4 +-- lib/pages/settings/sections/desktop.dart | 2 +- lib/pages/settings/sections/developers.dart | 2 +- lib/pages/settings/sections/downloads.dart | 3 +- .../settings/sections/language_region.dart | 2 +- lib/pages/settings/sections/playback.dart | 2 +- .../user_preferences_provider.dart | 2 +- .../user_preferences_state.dart | 2 +- lib/utils/service_utils.dart | 4 +-- pubspec.lock | 28 +++++++++---------- 87 files changed, 107 insertions(+), 108 deletions(-) rename lib/{components => modules}/album/album_card.dart (100%) rename lib/{components => modules}/artist/artist_album_list.dart (100%) rename lib/{components => modules}/artist/artist_card.dart (100%) rename lib/{components => modules}/connect/connect_device.dart (100%) rename lib/{components => modules}/connect/local_devices.dart (100%) rename lib/{components => modules}/desktop_login/login_form.dart (100%) rename lib/{components => modules}/getting_started/blur_card.dart (100%) rename lib/{components => modules}/home/sections/featured.dart (100%) rename lib/{components => modules}/home/sections/feed.dart (100%) rename lib/{components => modules}/home/sections/friends.dart (97%) rename lib/{components => modules}/home/sections/friends/friend_item.dart (100%) rename lib/{components => modules}/home/sections/genres.dart (100%) rename lib/{components => modules}/home/sections/made_for_user.dart (100%) rename lib/{components => modules}/home/sections/new_releases.dart (100%) rename lib/{components => modules}/home/sections/recent.dart (100%) rename lib/{components => modules}/library/local_folder/local_folder_item.dart (100%) rename lib/{components => modules}/library/playlist_generate/multi_select_field.dart (100%) rename lib/{components => modules}/library/playlist_generate/recommendation_attribute_dials.dart (100%) rename lib/{components => modules}/library/playlist_generate/recommendation_attribute_fields.dart (98%) rename lib/{components => modules}/library/playlist_generate/seeds_multi_autocomplete.dart (100%) rename lib/{components => modules}/library/playlist_generate/simple_track_tile.dart (100%) rename lib/{components => modules}/library/user_albums.dart (98%) rename lib/{components => modules}/library/user_artists.dart (98%) rename lib/{components => modules}/library/user_downloads.dart (96%) rename lib/{components => modules}/library/user_downloads/download_item.dart (100%) rename lib/{components => modules}/library/user_local_tracks.dart (97%) rename lib/{components => modules}/library/user_playlists.dart (97%) rename lib/{components => modules}/lyrics/use_synced_lyrics.dart (100%) rename lib/{components => modules}/lyrics/zoom_controls.dart (100%) rename lib/{components => modules}/player/player.dart (98%) rename lib/{components => modules}/player/player_actions.dart (99%) rename lib/{components => modules}/player/player_controls.dart (99%) rename lib/{components => modules}/player/player_overlay.dart (96%) rename lib/{components => modules}/player/player_queue.dart (100%) rename lib/{components => modules}/player/player_track_details.dart (100%) rename lib/{components => modules}/player/sibling_tracks_sheet.dart (100%) rename lib/{components => modules}/player/use_progress.dart (100%) rename lib/{components => modules}/player/volume_slider.dart (100%) rename lib/{components => modules}/playlist/playlist_card.dart (100%) rename lib/{components => modules}/playlist/playlist_create_dialog.dart (100%) rename lib/{components => modules}/root/bottom_player.dart (94%) rename lib/{components => modules}/root/sidebar.dart (99%) rename lib/{components => modules}/root/spotube_navigation_bar.dart (100%) rename lib/{components => modules}/root/update_dialog.dart (100%) rename lib/{components => modules}/settings/color_scheme_picker_dialog.dart (100%) rename lib/{components => modules}/settings/section_card_with_heading.dart (100%) diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 579aff185..6d6e643e0 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:spotube/collections/routes.dart'; -import 'package:spotube/components/player/player_controls.dart'; +import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/library/library.dart'; diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index 5d493a68d..34617b84a 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/playlist/playlist_create_dialog.dart'; +import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; 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 291950bb5..16204952d 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 @@ -5,9 +5,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; -import 'package:spotube/components/album/album_card.dart'; -import 'package:spotube/components/artist/artist_card.dart'; -import 'package:spotube/components/playlist/playlist_card.dart'; +import 'package:spotube/modules/album/album_card.dart'; +import 'package:spotube/modules/artist/artist_card.dart'; +import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart index be72d689b..ab27e7fff 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/shared/sort_tracks_dropdown.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/extensions/context.dart'; 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 8c1c8e153..6ea53b83e 100644 --- a/lib/components/shared/tracks_view/sections/header/header_actions.dart +++ b/lib/components/shared/tracks_view/sections/header/header_actions.dart @@ -4,7 +4,7 @@ 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/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; diff --git a/lib/components/shared/tracks_view/track_view_provider.dart b/lib/components/shared/tracks_view/track_view_provider.dart index 14dc11369..16aa6d9c5 100644 --- a/lib/components/shared/tracks_view/track_view_provider.dart +++ b/lib/components/shared/tracks_view/track_view_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/modules/library/user_local_tracks.dart'; class TrackViewNotifier extends ChangeNotifier { List tracks; diff --git a/lib/components/stats/common/album_item.dart b/lib/components/stats/common/album_item.dart index ccc0fa4ec..af53d273b 100644 --- a/lib/components/stats/common/album_item.dart +++ b/lib/components/stats/common/album_item.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/album/album_card.dart'; +import 'package:spotube/modules/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'; diff --git a/lib/components/album/album_card.dart b/lib/modules/album/album_card.dart similarity index 100% rename from lib/components/album/album_card.dart rename to lib/modules/album/album_card.dart diff --git a/lib/components/artist/artist_album_list.dart b/lib/modules/artist/artist_album_list.dart similarity index 100% rename from lib/components/artist/artist_album_list.dart rename to lib/modules/artist/artist_album_list.dart diff --git a/lib/components/artist/artist_card.dart b/lib/modules/artist/artist_card.dart similarity index 100% rename from lib/components/artist/artist_card.dart rename to lib/modules/artist/artist_card.dart diff --git a/lib/components/connect/connect_device.dart b/lib/modules/connect/connect_device.dart similarity index 100% rename from lib/components/connect/connect_device.dart rename to lib/modules/connect/connect_device.dart diff --git a/lib/components/connect/local_devices.dart b/lib/modules/connect/local_devices.dart similarity index 100% rename from lib/components/connect/local_devices.dart rename to lib/modules/connect/local_devices.dart diff --git a/lib/components/desktop_login/login_form.dart b/lib/modules/desktop_login/login_form.dart similarity index 100% rename from lib/components/desktop_login/login_form.dart rename to lib/modules/desktop_login/login_form.dart diff --git a/lib/components/getting_started/blur_card.dart b/lib/modules/getting_started/blur_card.dart similarity index 100% rename from lib/components/getting_started/blur_card.dart rename to lib/modules/getting_started/blur_card.dart diff --git a/lib/components/home/sections/featured.dart b/lib/modules/home/sections/featured.dart similarity index 100% rename from lib/components/home/sections/featured.dart rename to lib/modules/home/sections/featured.dart diff --git a/lib/components/home/sections/feed.dart b/lib/modules/home/sections/feed.dart similarity index 100% rename from lib/components/home/sections/feed.dart rename to lib/modules/home/sections/feed.dart diff --git a/lib/components/home/sections/friends.dart b/lib/modules/home/sections/friends.dart similarity index 97% rename from lib/components/home/sections/friends.dart rename to lib/modules/home/sections/friends.dart index 4ae802e62..85325f5a2 100644 --- a/lib/components/home/sections/friends.dart +++ b/lib/modules/home/sections/friends.dart @@ -5,7 +5,7 @@ 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/modules/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'; diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/modules/home/sections/friends/friend_item.dart similarity index 100% rename from lib/components/home/sections/friends/friend_item.dart rename to lib/modules/home/sections/friends/friend_item.dart diff --git a/lib/components/home/sections/genres.dart b/lib/modules/home/sections/genres.dart similarity index 100% rename from lib/components/home/sections/genres.dart rename to lib/modules/home/sections/genres.dart diff --git a/lib/components/home/sections/made_for_user.dart b/lib/modules/home/sections/made_for_user.dart similarity index 100% rename from lib/components/home/sections/made_for_user.dart rename to lib/modules/home/sections/made_for_user.dart diff --git a/lib/components/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart similarity index 100% rename from lib/components/home/sections/new_releases.dart rename to lib/modules/home/sections/new_releases.dart diff --git a/lib/components/home/sections/recent.dart b/lib/modules/home/sections/recent.dart similarity index 100% rename from lib/components/home/sections/recent.dart rename to lib/modules/home/sections/recent.dart diff --git a/lib/components/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart similarity index 100% rename from lib/components/library/local_folder/local_folder_item.dart rename to lib/modules/library/local_folder/local_folder_item.dart diff --git a/lib/components/library/playlist_generate/multi_select_field.dart b/lib/modules/library/playlist_generate/multi_select_field.dart similarity index 100% rename from lib/components/library/playlist_generate/multi_select_field.dart rename to lib/modules/library/playlist_generate/multi_select_field.dart diff --git a/lib/components/library/playlist_generate/recommendation_attribute_dials.dart b/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart similarity index 100% rename from lib/components/library/playlist_generate/recommendation_attribute_dials.dart rename to lib/modules/library/playlist_generate/recommendation_attribute_dials.dart diff --git a/lib/components/library/playlist_generate/recommendation_attribute_fields.dart b/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart similarity index 98% rename from lib/components/library/playlist_generate/recommendation_attribute_fields.dart rename to lib/modules/library/playlist_generate/recommendation_attribute_fields.dart index 754373600..7feff03ae 100644 --- a/lib/components/library/playlist_generate/recommendation_attribute_fields.dart +++ b/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart'; +import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; diff --git a/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart b/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart similarity index 100% rename from lib/components/library/playlist_generate/seeds_multi_autocomplete.dart rename to lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart diff --git a/lib/components/library/playlist_generate/simple_track_tile.dart b/lib/modules/library/playlist_generate/simple_track_tile.dart similarity index 100% rename from lib/components/library/playlist_generate/simple_track_tile.dart rename to lib/modules/library/playlist_generate/simple_track_tile.dart diff --git a/lib/components/library/user_albums.dart b/lib/modules/library/user_albums.dart similarity index 98% rename from lib/components/library/user_albums.dart rename to lib/modules/library/user_albums.dart index e1b821131..b7fcc958a 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/modules/library/user_albums.dart @@ -8,7 +8,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/album/album_card.dart'; +import 'package:spotube/modules/album/album_card.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/waypoint.dart'; diff --git a/lib/components/library/user_artists.dart b/lib/modules/library/user_artists.dart similarity index 98% rename from lib/components/library/user_artists.dart rename to lib/modules/library/user_artists.dart index 0ef0ff39d..118447ae6 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/modules/library/user_artists.dart @@ -9,7 +9,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/artist/artist_card.dart'; +import 'package:spotube/modules/artist/artist_card.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; diff --git a/lib/components/library/user_downloads.dart b/lib/modules/library/user_downloads.dart similarity index 96% rename from lib/components/library/user_downloads.dart rename to lib/modules/library/user_downloads.dart index 3a1162e60..a5f3883a9 100644 --- a/lib/components/library/user_downloads.dart +++ b/lib/modules/library/user_downloads.dart @@ -2,7 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/library/user_downloads/download_item.dart'; +import 'package:spotube/modules/library/user_downloads/download_item.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; diff --git a/lib/components/library/user_downloads/download_item.dart b/lib/modules/library/user_downloads/download_item.dart similarity index 100% rename from lib/components/library/user_downloads/download_item.dart rename to lib/modules/library/user_downloads/download_item.dart diff --git a/lib/components/library/user_local_tracks.dart b/lib/modules/library/user_local_tracks.dart similarity index 97% rename from lib/components/library/user_local_tracks.dart rename to lib/modules/library/user_local_tracks.dart index c0d633803..926b4e80c 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/modules/library/user_local_tracks.dart @@ -6,7 +6,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/library/local_folder/local_folder_item.dart'; +import 'package:spotube/modules/library/local_folder/local_folder_item.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; diff --git a/lib/components/library/user_playlists.dart b/lib/modules/library/user_playlists.dart similarity index 97% rename from lib/components/library/user_playlists.dart rename to lib/modules/library/user_playlists.dart index 246c39477..104badf6a 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -9,10 +9,10 @@ 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/playlist/playlist_create_dialog.dart'; +import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/playlist/playlist_card.dart'; +import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/components/lyrics/use_synced_lyrics.dart b/lib/modules/lyrics/use_synced_lyrics.dart similarity index 100% rename from lib/components/lyrics/use_synced_lyrics.dart rename to lib/modules/lyrics/use_synced_lyrics.dart diff --git a/lib/components/lyrics/zoom_controls.dart b/lib/modules/lyrics/zoom_controls.dart similarity index 100% rename from lib/components/lyrics/zoom_controls.dart rename to lib/modules/lyrics/zoom_controls.dart diff --git a/lib/components/player/player.dart b/lib/modules/player/player.dart similarity index 98% rename from lib/components/player/player.dart rename to lib/modules/player/player.dart index 493410585..c87b336b8 100644 --- a/lib/components/player/player.dart +++ b/lib/modules/player/player.dart @@ -6,10 +6,10 @@ 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/player/player_actions.dart'; -import 'package:spotube/components/player/player_controls.dart'; -import 'package:spotube/components/player/player_queue.dart'; -import 'package:spotube/components/player/volume_slider.dart'; +import 'package:spotube/modules/player/player_actions.dart'; +import 'package:spotube/modules/player/player_controls.dart'; +import 'package:spotube/modules/player/player_queue.dart'; +import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/components/shared/animated_gradient.dart'; import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; import 'package:spotube/components/shared/links/artist_link.dart'; diff --git a/lib/components/player/player_actions.dart b/lib/modules/player/player_actions.dart similarity index 99% rename from lib/components/player/player_actions.dart rename to lib/modules/player/player_actions.dart index d28c3900c..41ee9e39b 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -4,7 +4,7 @@ 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/player/sibling_tracks_sheet.dart'; +import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/extensions/artist_simple.dart'; diff --git a/lib/components/player/player_controls.dart b/lib/modules/player/player_controls.dart similarity index 99% rename from lib/components/player/player_controls.dart rename to lib/modules/player/player_controls.dart index 7683de199..ba69560c2 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -8,7 +8,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; -import 'package:spotube/components/player/use_progress.dart'; +import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/components/player/player_overlay.dart b/lib/modules/player/player_overlay.dart similarity index 96% rename from lib/components/player/player_overlay.dart rename to lib/modules/player/player_overlay.dart index 168e022d2..7911a046b 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/player/player_track_details.dart'; -import 'package:spotube/components/root/spotube_navigation_bar.dart'; +import 'package:spotube/modules/player/player_track_details.dart'; +import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/components/shared/panels/sliding_up_panel.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; -import 'package:spotube/components/player/use_progress.dart'; -import 'package:spotube/components/player/player.dart'; +import 'package:spotube/modules/player/use_progress.dart'; +import 'package:spotube/modules/player/player.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/components/player/player_queue.dart b/lib/modules/player/player_queue.dart similarity index 100% rename from lib/components/player/player_queue.dart rename to lib/modules/player/player_queue.dart diff --git a/lib/components/player/player_track_details.dart b/lib/modules/player/player_track_details.dart similarity index 100% rename from lib/components/player/player_track_details.dart rename to lib/modules/player/player_track_details.dart diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart similarity index 100% rename from lib/components/player/sibling_tracks_sheet.dart rename to lib/modules/player/sibling_tracks_sheet.dart diff --git a/lib/components/player/use_progress.dart b/lib/modules/player/use_progress.dart similarity index 100% rename from lib/components/player/use_progress.dart rename to lib/modules/player/use_progress.dart diff --git a/lib/components/player/volume_slider.dart b/lib/modules/player/volume_slider.dart similarity index 100% rename from lib/components/player/volume_slider.dart rename to lib/modules/player/volume_slider.dart diff --git a/lib/components/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart similarity index 100% rename from lib/components/playlist/playlist_card.dart rename to lib/modules/playlist/playlist_card.dart diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart similarity index 100% rename from lib/components/playlist/playlist_create_dialog.dart rename to lib/modules/playlist/playlist_create_dialog.dart diff --git a/lib/components/root/bottom_player.dart b/lib/modules/root/bottom_player.dart similarity index 94% rename from lib/components/root/bottom_player.dart rename to lib/modules/root/bottom_player.dart index 5429e172b..2ab4b14a7 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -6,11 +6,11 @@ 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/player/player_actions.dart'; -import 'package:spotube/components/player/player_overlay.dart'; -import 'package:spotube/components/player/player_track_details.dart'; -import 'package:spotube/components/player/player_controls.dart'; -import 'package:spotube/components/player/volume_slider.dart'; +import 'package:spotube/modules/player/player_actions.dart'; +import 'package:spotube/modules/player/player_overlay.dart'; +import 'package:spotube/modules/player/player_track_details.dart'; +import 'package:spotube/modules/player/player_controls.dart'; +import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/components/root/sidebar.dart b/lib/modules/root/sidebar.dart similarity index 99% rename from lib/components/root/sidebar.dart rename to lib/modules/root/sidebar.dart index 8e7374b16..05f14c398 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -9,7 +9,7 @@ import 'package:sidebarx/sidebarx.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/connect/connect_device.dart'; +import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/modules/root/spotube_navigation_bar.dart similarity index 100% rename from lib/components/root/spotube_navigation_bar.dart rename to lib/modules/root/spotube_navigation_bar.dart diff --git a/lib/components/root/update_dialog.dart b/lib/modules/root/update_dialog.dart similarity index 100% rename from lib/components/root/update_dialog.dart rename to lib/modules/root/update_dialog.dart diff --git a/lib/components/settings/color_scheme_picker_dialog.dart b/lib/modules/settings/color_scheme_picker_dialog.dart similarity index 100% rename from lib/components/settings/color_scheme_picker_dialog.dart rename to lib/modules/settings/color_scheme_picker_dialog.dart diff --git a/lib/components/settings/section_card_with_heading.dart b/lib/modules/settings/section_card_with_heading.dart similarity index 100% rename from lib/components/settings/section_card_with_heading.dart rename to lib/modules/settings/section_card_with_heading.dart diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 498909491..bd416edd4 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/artist/artist_album_list.dart'; +import 'package:spotube/modules/artist/artist_album_list.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/pages/artist/section/footer.dart'; diff --git a/lib/pages/artist/section/related_artists.dart b/lib/pages/artist/section/related_artists.dart index 7fc48dedf..066f73fdc 100644 --- a/lib/pages/artist/section/related_artists.dart +++ b/lib/pages/artist/section/related_artists.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/artist/artist_card.dart'; +import 'package:spotube/modules/artist/artist_card.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class ArtistPageRelatedArtists extends ConsumerWidget { diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index c7cb493a6..a1735d42f 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/connect/local_devices.dart'; +import 'package:spotube/modules/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'; diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index 639a9dd97..20ad3d17e 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -3,8 +3,8 @@ import 'package:gap/gap.dart'; 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/player/player_queue.dart'; -import 'package:spotube/components/player/volume_slider.dart'; +import 'package:spotube/modules/player/player_queue.dart'; +import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/links/anchor_button.dart'; import 'package:spotube/components/shared/links/artist_link.dart'; diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index 9c9bdddba..a5f8c3b1b 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/components/desktop_login/login_form.dart'; +import 'package:spotube/modules/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'; diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index dbec28dc9..2c1535feb 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/components/desktop_login/login_form.dart'; +import 'package:spotube/modules/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'; diff --git a/lib/pages/getting_started/sections/greeting.dart b/lib/pages/getting_started/sections/greeting.dart index 563e43de9..6d6493513 100644 --- a/lib/pages/getting_started/sections/greeting.dart +++ b/lib/pages/getting_started/sections/greeting.dart @@ -3,7 +3,7 @@ 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/getting_started/blur_card.dart'; +import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index 298cf8391..fab51d063 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -4,7 +4,7 @@ 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/getting_started/blur_card.dart'; +import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/string.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/pages/getting_started/sections/region.dart b/lib/pages/getting_started/sections/region.dart index 9303392cc..0a80fba21 100644 --- a/lib/pages/getting_started/sections/region.dart +++ b/lib/pages/getting_started/sections/region.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/getting_started/blur_card.dart'; +import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/pages/getting_started/sections/support.dart b/lib/pages/getting_started/sections/support.dart index b6be07e53..b449def53 100644 --- a/lib/pages/getting_started/sections/support.dart +++ b/lib/pages/getting_started/sections/support.dart @@ -3,7 +3,7 @@ import 'package:gap/gap.dart'; 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/getting_started/blur_card.dart'; +import 'package:spotube/modules/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'; diff --git a/lib/pages/home/feed/feed_section.dart b/lib/pages/home/feed/feed_section.dart index d31b8256d..11780620e 100644 --- a/lib/pages/home/feed/feed_section.dart +++ b/lib/pages/home/feed/feed_section.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; -import 'package:spotube/components/album/album_card.dart'; -import 'package:spotube/components/artist/artist_card.dart'; -import 'package:spotube/components/playlist/playlist_card.dart'; +import 'package:spotube/modules/album/album_card.dart'; +import 'package:spotube/modules/artist/artist_card.dart'; +import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/views/home_section.dart'; diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index 531ea889a..ef701478d 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/fake.dart'; -import 'package:spotube/components/playlist/playlist_card.dart'; +import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/waypoint.dart'; diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index d4e2d94e6..6ed5c0e43 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -4,14 +4,14 @@ 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'; -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/home/sections/recent.dart'; +import 'package:spotube/modules/connect/connect_device.dart'; +import 'package:spotube/modules/home/sections/featured.dart'; +import 'package:spotube/modules/home/sections/feed.dart'; +import 'package:spotube/modules/home/sections/friends.dart'; +import 'package:spotube/modules/home/sections/genres.dart'; +import 'package:spotube/modules/home/sections/made_for_user.dart'; +import 'package:spotube/modules/home/sections/new_releases.dart'; +import 'package:spotube/modules/home/sections/recent.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/pages/settings/settings.dart'; diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index 5385f8728..cc96e4ee6 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart' hide Image; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/library/user_albums.dart'; -import 'package:spotube/components/library/user_artists.dart'; -import 'package:spotube/components/library/user_downloads.dart'; -import 'package:spotube/components/library/user_playlists.dart'; +import 'package:spotube/modules/library/user_albums.dart'; +import 'package:spotube/modules/library/user_artists.dart'; +import 'package:spotube/modules/library/user_downloads.dart'; +import 'package:spotube/modules/library/user_playlists.dart'; import 'package:spotube/components/shared/themed_button_tab_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index ac38e8602..27979b5c7 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -6,7 +6,7 @@ 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/modules/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'; diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 648e85284..88bf8adbc 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -6,11 +6,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/library/playlist_generate/multi_select_field.dart'; -import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart'; -import 'package:spotube/components/library/playlist_generate/recommendation_attribute_fields.dart'; -import 'package:spotube/components/library/playlist_generate/seeds_multi_autocomplete.dart'; -import 'package:spotube/components/library/playlist_generate/simple_track_tile.dart'; +import 'package:spotube/modules/library/playlist_generate/multi_select_field.dart'; +import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart'; +import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_fields.dart'; +import 'package:spotube/modules/library/playlist_generate/seeds_multi_autocomplete.dart'; +import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; 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'; diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 5ee7ab368..266e9f663 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -4,8 +4,8 @@ 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/library/playlist_generate/simple_track_tile.dart'; -import 'package:spotube/components/playlist/playlist_create_dialog.dart'; +import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; +import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; 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'; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index 996e190df..f580f56d1 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -5,9 +5,9 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/player/player_controls.dart'; -import 'package:spotube/components/player/player_queue.dart'; -import 'package:spotube/components/root/sidebar.dart'; +import 'package:spotube/modules/player/player_controls.dart'; +import 'package:spotube/modules/player/player_queue.dart'; +import 'package:spotube/modules/root/sidebar.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index b3a55a275..79456e27f 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -5,7 +5,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/lyrics/zoom_controls.dart'; +import 'package:spotube/modules/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 0e0fff2e4..b07231292 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -4,13 +4,13 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/lyrics/zoom_controls.dart'; +import 'package:spotube/modules/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; -import 'package:spotube/components/lyrics/use_synced_lyrics.dart'; +import 'package:spotube/modules/lyrics/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 258ecf3cb..c2ad64c01 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -7,11 +7,11 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/player/player_queue.dart'; +import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; -import 'package:spotube/components/root/bottom_player.dart'; -import 'package:spotube/components/root/sidebar.dart'; -import 'package:spotube/components/root/spotube_navigation_bar.dart'; +import 'package:spotube/modules/root/bottom_player.dart'; +import 'package:spotube/modules/root/sidebar.dart'; +import 'package:spotube/modules/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'; diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index 8b6f7312d..81d1b4e51 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart index a8d72cc0d..531f4a5eb 100644 --- a/lib/pages/settings/sections/about.dart +++ b/lib/pages/settings/sections/about.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 6162aa3d3..46f0e452d 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; 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/modules/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'; diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart index 25bd40052..a9283e1ad 100644 --- a/lib/pages/settings/sections/appearance.dart +++ b/lib/pages/settings/sections/appearance.dart @@ -3,8 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; -import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; 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'; diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 563068686..fa2601901 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.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/modules/settings/section_card_with_heading.dart'; 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'; diff --git a/lib/pages/settings/sections/developers.dart b/lib/pages/settings/sections/developers.dart index a22cf9f13..f33fe8437 100644 --- a/lib/pages/settings/sections/developers.dart +++ b/lib/pages/settings/sections/developers.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.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/modules/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; class SettingsDevelopersSection extends HookWidget { diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index 3092ed03f..8e679a7d5 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -3,9 +3,8 @@ 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/modules/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'; diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index 76670c771..343b7d860 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -5,7 +5,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index eeae98cbd..f26135fb4 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -6,7 +6,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/piped_instances_provider.dart'; diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index fe7269158..5825104ac 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/modules/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'; diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index 56f66375e..73dd02e82 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/services/sourced_track/enums.dart'; part 'user_preferences_state.g.dart'; diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index aa2cd985d..885f9a2cc 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -2,8 +2,8 @@ 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/modules/library/user_local_tracks.dart'; +import 'package:spotube/modules/root/update_dialog.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/lyrics.dart'; import 'package:spotube/services/dio/dio.dart'; 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 b224af21ea625e993a0f08fa2652dd340f87ac9a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Jun 2024 22:34:03 +0600 Subject: [PATCH 03/92] refactor: left out modules --- lib/{components => modules}/stats/common/album_item.dart | 0 lib/{components => modules}/stats/common/artist_item.dart | 0 lib/{components => modules}/stats/common/playlist_item.dart | 0 lib/{components => modules}/stats/common/track_item.dart | 0 lib/{components => modules}/stats/summary/summary.dart | 2 +- lib/{components => modules}/stats/summary/summary_card.dart | 0 lib/{components => modules}/stats/top/albums.dart | 2 +- lib/{components => modules}/stats/top/artists.dart | 2 +- lib/{components => modules}/stats/top/top.dart | 6 +++--- lib/{components => modules}/stats/top/tracks.dart | 2 +- lib/pages/stats/albums/albums.dart | 2 +- lib/pages/stats/artists/artists.dart | 2 +- lib/pages/stats/fees/fees.dart | 2 +- lib/pages/stats/minutes/minutes.dart | 2 +- lib/pages/stats/playlists/playlists.dart | 2 +- lib/pages/stats/stats.dart | 4 ++-- lib/pages/stats/streams/streams.dart | 2 +- 17 files changed, 15 insertions(+), 15 deletions(-) rename lib/{components => modules}/stats/common/album_item.dart (100%) rename lib/{components => modules}/stats/common/artist_item.dart (100%) rename lib/{components => modules}/stats/common/playlist_item.dart (100%) rename lib/{components => modules}/stats/common/track_item.dart (100%) rename lib/{components => modules}/stats/summary/summary.dart (98%) rename lib/{components => modules}/stats/summary/summary_card.dart (100%) rename lib/{components => modules}/stats/top/albums.dart (92%) rename lib/{components => modules}/stats/top/artists.dart (92%) rename lib/{components => modules}/stats/top/top.dart (95%) rename lib/{components => modules}/stats/top/tracks.dart (92%) diff --git a/lib/components/stats/common/album_item.dart b/lib/modules/stats/common/album_item.dart similarity index 100% rename from lib/components/stats/common/album_item.dart rename to lib/modules/stats/common/album_item.dart diff --git a/lib/components/stats/common/artist_item.dart b/lib/modules/stats/common/artist_item.dart similarity index 100% rename from lib/components/stats/common/artist_item.dart rename to lib/modules/stats/common/artist_item.dart diff --git a/lib/components/stats/common/playlist_item.dart b/lib/modules/stats/common/playlist_item.dart similarity index 100% rename from lib/components/stats/common/playlist_item.dart rename to lib/modules/stats/common/playlist_item.dart diff --git a/lib/components/stats/common/track_item.dart b/lib/modules/stats/common/track_item.dart similarity index 100% rename from lib/components/stats/common/track_item.dart rename to lib/modules/stats/common/track_item.dart diff --git a/lib/components/stats/summary/summary.dart b/lib/modules/stats/summary/summary.dart similarity index 98% rename from lib/components/stats/summary/summary.dart rename to lib/modules/stats/summary/summary.dart index 61f3bd6c8..0b6c60409 100644 --- a/lib/components/stats/summary/summary.dart +++ b/lib/modules/stats/summary/summary.dart @@ -1,7 +1,7 @@ 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/modules/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'; diff --git a/lib/components/stats/summary/summary_card.dart b/lib/modules/stats/summary/summary_card.dart similarity index 100% rename from lib/components/stats/summary/summary_card.dart rename to lib/modules/stats/summary/summary_card.dart diff --git a/lib/components/stats/top/albums.dart b/lib/modules/stats/top/albums.dart similarity index 92% rename from lib/components/stats/top/albums.dart rename to lib/modules/stats/top/albums.dart index 51bcf5b07..808a58a4c 100644 --- a/lib/components/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -1,7 +1,7 @@ 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/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; class TopAlbums extends HookConsumerWidget { diff --git a/lib/components/stats/top/artists.dart b/lib/modules/stats/top/artists.dart similarity index 92% rename from lib/components/stats/top/artists.dart rename to lib/modules/stats/top/artists.dart index d6d0c98d3..24e976010 100644 --- a/lib/components/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -1,7 +1,7 @@ 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/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; class TopArtists extends HookConsumerWidget { diff --git a/lib/components/stats/top/top.dart b/lib/modules/stats/top/top.dart similarity index 95% rename from lib/components/stats/top/top.dart rename to lib/modules/stats/top/top.dart index df1275e83..7c3849216 100644 --- a/lib/components/stats/top/top.dart +++ b/lib/modules/stats/top/top.dart @@ -2,9 +2,9 @@ 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/modules/stats/top/albums.dart'; +import 'package:spotube/modules/stats/top/artists.dart'; +import 'package:spotube/modules/stats/top/tracks.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/components/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart similarity index 92% rename from lib/components/stats/top/tracks.dart rename to lib/modules/stats/top/tracks.dart index bffa4ecd5..ee37af3b6 100644 --- a/lib/components/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -1,7 +1,7 @@ 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/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; class TopTracks extends HookConsumerWidget { diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 83867f936..ecec91b96 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -2,7 +2,7 @@ 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/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index 755475aef..b25399c10 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -2,7 +2,7 @@ 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/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 228d3243e..4993d2704 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -3,7 +3,7 @@ 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/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index b22f9a4f1..0212704a2 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -3,7 +3,7 @@ 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/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index cca7febb2..0b4b6cd7f 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -2,7 +2,7 @@ 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/modules/stats/common/playlist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/stats.dart b/lib/pages/stats/stats.dart index 95493591f..3efd212f0 100644 --- a/lib/pages/stats/stats.dart +++ b/lib/pages/stats/stats.dart @@ -2,8 +2,8 @@ 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/modules/stats/summary/summary.dart'; +import 'package:spotube/modules/stats/top/top.dart'; import 'package:spotube/utils/platform.dart'; class StatsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 33480709a..204829291 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -3,7 +3,7 @@ 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/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; From 4af23241c84ebf3079ff1e8b8188d0704d4632d3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Jun 2024 22:40:44 +0600 Subject: [PATCH 04/92] refactor: move shared components to 1 level up --- lib/collections/routes.dart | 2 +- .../{shared => }/adaptive/adaptive_list_tile.dart | 0 .../adaptive/adaptive_pop_sheet_list.dart | 0 .../adaptive/adaptive_popup_menu_button.dart | 0 .../adaptive/adaptive_select_tile.dart | 0 lib/components/{shared => }/animated_gradient.dart | 0 lib/components/{shared => }/bordered_text.dart | 0 lib/components/{shared => }/compact_search.dart | 0 .../dialogs/confirm_download_dialog.dart | 2 +- .../{shared => }/dialogs/piped_down_dialog.dart | 0 .../dialogs/playlist_add_track_dialog.dart | 2 +- .../{shared => }/dialogs/prompt_dialog.dart | 0 .../dialogs/replace_downloaded_dialog.dart | 0 .../{shared => }/dialogs/select_device_dialog.dart | 0 .../{shared => }/dialogs/track_details_dialog.dart | 6 +++--- .../expandable_search/expandable_search.dart | 0 .../{shared => }/fallbacks/anonymous_fallback.dart | 0 .../{shared => }/fallbacks/not_found.dart | 0 lib/components/{shared => }/heart_button.dart | 0 .../horizontal_playbutton_card_view.dart | 0 lib/components/{shared => }/hover_builder.dart | 0 .../{shared => }/image/universal_image.dart | 0 .../inter_scrollbar/inter_scrollbar.dart | 0 .../{shared => }/links/anchor_button.dart | 0 lib/components/{shared => }/links/artist_link.dart | 2 +- lib/components/{shared => }/links/hyper_link.dart | 2 +- lib/components/{shared => }/links/link_text.dart | 2 +- .../{shared => }/page_window_title_bar.dart | 0 lib/components/{shared => }/panels/controller.dart | 2 +- lib/components/{shared => }/panels/helpers.dart | 2 +- .../{shared => }/panels/sliding_up_panel.dart | 0 lib/components/{shared => }/playbutton_card.dart | 4 ++-- .../{shared => }/shimmers/shimmer_lyrics.dart | 0 .../{shared => }/sort_tracks_dropdown.dart | 2 +- .../{shared => }/spotube_page_route.dart | 0 .../{shared => }/themed_button_tab_bar.dart | 0 .../{shared => }/track_tile/track_options.dart | 14 +++++++------- .../{shared => }/track_tile/track_tile.dart | 10 +++++----- .../tracks_view/sections/body/track_view_body.dart | 14 +++++++------- .../sections/body/track_view_body_headers.dart | 10 +++++----- .../sections/body/track_view_options.dart | 10 +++++----- .../sections/body/use_is_user_playlist.dart | 0 .../sections/header/flexible_header.dart | 10 +++++----- .../sections/header/header_actions.dart | 6 +++--- .../sections/header/header_buttons.dart | 4 ++-- .../{shared => }/tracks_view/track_view.dart | 10 +++++----- .../{shared => }/tracks_view/track_view_props.dart | 0 .../tracks_view/track_view_provider.dart | 0 lib/components/{shared => }/waypoint.dart | 0 lib/hooks/utils/use_palette_color.dart | 2 +- lib/modules/album/album_card.dart | 4 ++-- lib/modules/artist/artist_album_list.dart | 2 +- lib/modules/artist/artist_card.dart | 2 +- lib/modules/home/sections/featured.dart | 2 +- lib/modules/home/sections/feed.dart | 2 +- lib/modules/home/sections/friends/friend_item.dart | 2 +- lib/modules/home/sections/genres.dart | 2 +- lib/modules/home/sections/made_for_user.dart | 2 +- lib/modules/home/sections/new_releases.dart | 2 +- lib/modules/home/sections/recent.dart | 2 +- .../library/local_folder/local_folder_item.dart | 2 +- .../playlist_generate/simple_track_tile.dart | 2 +- lib/modules/library/user_albums.dart | 6 +++--- lib/modules/library/user_artists.dart | 6 +++--- .../library/user_downloads/download_item.dart | 4 ++-- lib/modules/library/user_playlists.dart | 6 +++--- lib/modules/player/player.dart | 12 ++++++------ lib/modules/player/player_actions.dart | 4 ++-- lib/modules/player/player_overlay.dart | 2 +- lib/modules/player/player_queue.dart | 6 +++--- lib/modules/player/player_track_details.dart | 6 +++--- lib/modules/player/sibling_tracks_sheet.dart | 4 ++-- lib/modules/playlist/playlist_card.dart | 4 ++-- lib/modules/playlist/playlist_create_dialog.dart | 2 +- lib/modules/root/sidebar.dart | 2 +- lib/modules/root/update_dialog.dart | 2 +- lib/modules/stats/common/album_item.dart | 4 ++-- lib/modules/stats/common/artist_item.dart | 2 +- lib/modules/stats/common/playlist_item.dart | 4 ++-- lib/modules/stats/common/track_item.dart | 4 ++-- lib/modules/stats/top/top.dart | 2 +- lib/pages/album/album.dart | 4 ++-- lib/pages/artist/artist.dart | 2 +- lib/pages/artist/section/footer.dart | 2 +- lib/pages/artist/section/header.dart | 2 +- lib/pages/artist/section/top_tracks.dart | 4 ++-- lib/pages/connect/connect.dart | 2 +- lib/pages/connect/control/control.dart | 8 ++++---- lib/pages/desktop_login/desktop_login.dart | 2 +- lib/pages/desktop_login/login_tutorial.dart | 4 ++-- lib/pages/getting_started/getting_started.dart | 2 +- lib/pages/home/feed/feed_section.dart | 2 +- lib/pages/home/genres/genre_playlists.dart | 6 +++--- lib/pages/home/genres/genres.dart | 2 +- lib/pages/home/home.dart | 2 +- lib/pages/lastfm_login/lastfm_login.dart | 4 ++-- lib/pages/library/library.dart | 4 ++-- lib/pages/library/local_folder.dart | 12 ++++++------ .../playlist_generate/playlist_generate.dart | 4 ++-- .../playlist_generate_result.dart | 4 ++-- lib/pages/lyrics/lyrics.dart | 8 ++++---- lib/pages/lyrics/mini_lyrics.dart | 4 ++-- lib/pages/lyrics/plain_lyrics.dart | 2 +- lib/pages/lyrics/synced_lyrics.dart | 2 +- lib/pages/playlist/liked_playlist.dart | 4 ++-- lib/pages/playlist/playlist.dart | 8 ++++---- lib/pages/profile/profile.dart | 4 ++-- lib/pages/root/root_app.dart | 2 +- lib/pages/search/search.dart | 6 +++--- lib/pages/search/sections/albums.dart | 2 +- lib/pages/search/sections/artists.dart | 2 +- lib/pages/search/sections/playlists.dart | 2 +- lib/pages/search/sections/tracks.dart | 6 +++--- lib/pages/settings/about.dart | 6 +++--- lib/pages/settings/blacklist.dart | 4 ++-- lib/pages/settings/logs.dart | 4 ++-- lib/pages/settings/sections/about.dart | 2 +- lib/pages/settings/sections/accounts.dart | 2 +- lib/pages/settings/sections/appearance.dart | 2 +- lib/pages/settings/sections/desktop.dart | 2 +- lib/pages/settings/sections/language_region.dart | 2 +- lib/pages/settings/sections/playback.dart | 2 +- lib/pages/settings/settings.dart | 2 +- lib/pages/stats/albums/albums.dart | 2 +- lib/pages/stats/artists/artists.dart | 2 +- lib/pages/stats/fees/fees.dart | 2 +- lib/pages/stats/minutes/minutes.dart | 2 +- lib/pages/stats/playlists/playlists.dart | 2 +- lib/pages/stats/stats.dart | 2 +- lib/pages/stats/streams/streams.dart | 2 +- lib/pages/track/track.dart | 12 ++++++------ lib/provider/authentication_provider.dart | 2 +- lib/provider/proxy_playlist/player_listeners.dart | 2 +- lib/utils/persisted_state_notifier.dart | 2 +- 134 files changed, 205 insertions(+), 205 deletions(-) rename lib/components/{shared => }/adaptive/adaptive_list_tile.dart (100%) rename lib/components/{shared => }/adaptive/adaptive_pop_sheet_list.dart (100%) rename lib/components/{shared => }/adaptive/adaptive_popup_menu_button.dart (100%) rename lib/components/{shared => }/adaptive/adaptive_select_tile.dart (100%) rename lib/components/{shared => }/animated_gradient.dart (100%) rename lib/components/{shared => }/bordered_text.dart (100%) rename lib/components/{shared => }/compact_search.dart (100%) rename lib/components/{shared => }/dialogs/confirm_download_dialog.dart (97%) rename lib/components/{shared => }/dialogs/piped_down_dialog.dart (100%) rename lib/components/{shared => }/dialogs/playlist_add_track_dialog.dart (98%) rename lib/components/{shared => }/dialogs/prompt_dialog.dart (100%) rename lib/components/{shared => }/dialogs/replace_downloaded_dialog.dart (100%) rename lib/components/{shared => }/dialogs/select_device_dialog.dart (100%) rename lib/components/{shared => }/dialogs/track_details_dialog.dart (96%) rename lib/components/{shared => }/expandable_search/expandable_search.dart (100%) rename lib/components/{shared => }/fallbacks/anonymous_fallback.dart (100%) rename lib/components/{shared => }/fallbacks/not_found.dart (100%) rename lib/components/{shared => }/heart_button.dart (100%) rename lib/components/{shared => }/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart (100%) rename lib/components/{shared => }/hover_builder.dart (100%) rename lib/components/{shared => }/image/universal_image.dart (100%) rename lib/components/{shared => }/inter_scrollbar/inter_scrollbar.dart (100%) rename lib/components/{shared => }/links/anchor_button.dart (100%) rename lib/components/{shared => }/links/artist_link.dart (96%) rename lib/components/{shared => }/links/hyper_link.dart (92%) rename lib/components/{shared => }/links/link_text.dart (93%) rename lib/components/{shared => }/page_window_title_bar.dart (100%) rename lib/components/{shared => }/panels/controller.dart (99%) rename lib/components/{shared => }/panels/helpers.dart (98%) rename lib/components/{shared => }/panels/sliding_up_panel.dart (100%) rename lib/components/{shared => }/playbutton_card.dart (98%) rename lib/components/{shared => }/shimmers/shimmer_lyrics.dart (100%) rename lib/components/{shared => }/sort_tracks_dropdown.dart (96%) rename lib/components/{shared => }/spotube_page_route.dart (100%) rename lib/components/{shared => }/themed_button_tab_bar.dart (100%) rename lib/components/{shared => }/track_tile/track_options.dart (96%) rename lib/components/{shared => }/track_tile/track_tile.dart (96%) rename lib/components/{shared => }/tracks_view/sections/body/track_view_body.dart (92%) rename lib/components/{shared => }/tracks_view/sections/body/track_view_body_headers.dart (87%) rename lib/components/{shared => }/tracks_view/sections/body/track_view_options.dart (92%) rename lib/components/{shared => }/tracks_view/sections/body/use_is_user_playlist.dart (100%) rename lib/components/{shared => }/tracks_view/sections/header/flexible_header.dart (94%) rename lib/components/{shared => }/tracks_view/sections/header/header_actions.dart (94%) rename lib/components/{shared => }/tracks_view/sections/header/header_buttons.dart (97%) rename lib/components/{shared => }/tracks_view/track_view.dart (77%) rename lib/components/{shared => }/tracks_view/track_view_props.dart (100%) rename lib/components/{shared => }/tracks_view/track_view_provider.dart (100%) rename lib/components/{shared => }/waypoint.dart (100%) diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index dc2e4b7c1..e1cc5fb6b 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -36,7 +36,7 @@ import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/utils/platform.dart'; -import 'package:spotube/components/shared/spotube_page_route.dart'; +import 'package:spotube/components/spotube_page_route.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/desktop_login/login_tutorial.dart'; diff --git a/lib/components/shared/adaptive/adaptive_list_tile.dart b/lib/components/adaptive/adaptive_list_tile.dart similarity index 100% rename from lib/components/shared/adaptive/adaptive_list_tile.dart rename to lib/components/adaptive/adaptive_list_tile.dart diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/adaptive/adaptive_pop_sheet_list.dart similarity index 100% rename from lib/components/shared/adaptive/adaptive_pop_sheet_list.dart rename to lib/components/adaptive/adaptive_pop_sheet_list.dart diff --git a/lib/components/shared/adaptive/adaptive_popup_menu_button.dart b/lib/components/adaptive/adaptive_popup_menu_button.dart similarity index 100% rename from lib/components/shared/adaptive/adaptive_popup_menu_button.dart rename to lib/components/adaptive/adaptive_popup_menu_button.dart diff --git a/lib/components/shared/adaptive/adaptive_select_tile.dart b/lib/components/adaptive/adaptive_select_tile.dart similarity index 100% rename from lib/components/shared/adaptive/adaptive_select_tile.dart rename to lib/components/adaptive/adaptive_select_tile.dart diff --git a/lib/components/shared/animated_gradient.dart b/lib/components/animated_gradient.dart similarity index 100% rename from lib/components/shared/animated_gradient.dart rename to lib/components/animated_gradient.dart diff --git a/lib/components/shared/bordered_text.dart b/lib/components/bordered_text.dart similarity index 100% rename from lib/components/shared/bordered_text.dart rename to lib/components/bordered_text.dart diff --git a/lib/components/shared/compact_search.dart b/lib/components/compact_search.dart similarity index 100% rename from lib/components/shared/compact_search.dart rename to lib/components/compact_search.dart diff --git a/lib/components/shared/dialogs/confirm_download_dialog.dart b/lib/components/dialogs/confirm_download_dialog.dart similarity index 97% rename from lib/components/shared/dialogs/confirm_download_dialog.dart rename to lib/components/dialogs/confirm_download_dialog.dart index 486310a7a..897c64cb2 100644 --- a/lib/components/shared/dialogs/confirm_download_dialog.dart +++ b/lib/components/dialogs/confirm_download_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/components/shared/dialogs/piped_down_dialog.dart b/lib/components/dialogs/piped_down_dialog.dart similarity index 100% rename from lib/components/shared/dialogs/piped_down_dialog.dart rename to lib/components/dialogs/piped_down_dialog.dart diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/dialogs/playlist_add_track_dialog.dart similarity index 98% rename from lib/components/shared/dialogs/playlist_add_track_dialog.dart rename to lib/components/dialogs/playlist_add_track_dialog.dart index 34617b84a..5af9c9e4d 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/dialogs/playlist_add_track_dialog.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/components/shared/dialogs/prompt_dialog.dart b/lib/components/dialogs/prompt_dialog.dart similarity index 100% rename from lib/components/shared/dialogs/prompt_dialog.dart rename to lib/components/dialogs/prompt_dialog.dart diff --git a/lib/components/shared/dialogs/replace_downloaded_dialog.dart b/lib/components/dialogs/replace_downloaded_dialog.dart similarity index 100% rename from lib/components/shared/dialogs/replace_downloaded_dialog.dart rename to lib/components/dialogs/replace_downloaded_dialog.dart diff --git a/lib/components/shared/dialogs/select_device_dialog.dart b/lib/components/dialogs/select_device_dialog.dart similarity index 100% rename from lib/components/shared/dialogs/select_device_dialog.dart rename to lib/components/dialogs/select_device_dialog.dart diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart similarity index 96% rename from lib/components/shared/dialogs/track_details_dialog.dart rename to lib/components/dialogs/track_details_dialog.dart index da2a140b9..2495863c8 100644 --- a/lib/components/shared/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; -import 'package:spotube/components/shared/links/hyper_link.dart'; -import 'package:spotube/components/shared/links/link_text.dart'; +import 'package:spotube/components/links/artist_link.dart'; +import 'package:spotube/components/links/hyper_link.dart'; +import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; diff --git a/lib/components/shared/expandable_search/expandable_search.dart b/lib/components/expandable_search/expandable_search.dart similarity index 100% rename from lib/components/shared/expandable_search/expandable_search.dart rename to lib/components/expandable_search/expandable_search.dart diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart similarity index 100% rename from lib/components/shared/fallbacks/anonymous_fallback.dart rename to lib/components/fallbacks/anonymous_fallback.dart diff --git a/lib/components/shared/fallbacks/not_found.dart b/lib/components/fallbacks/not_found.dart similarity index 100% rename from lib/components/shared/fallbacks/not_found.dart rename to lib/components/fallbacks/not_found.dart diff --git a/lib/components/shared/heart_button.dart b/lib/components/heart_button.dart similarity index 100% rename from lib/components/shared/heart_button.dart rename to lib/components/heart_button.dart diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart similarity index 100% rename from lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart rename to lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart diff --git a/lib/components/shared/hover_builder.dart b/lib/components/hover_builder.dart similarity index 100% rename from lib/components/shared/hover_builder.dart rename to lib/components/hover_builder.dart diff --git a/lib/components/shared/image/universal_image.dart b/lib/components/image/universal_image.dart similarity index 100% rename from lib/components/shared/image/universal_image.dart rename to lib/components/image/universal_image.dart diff --git a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart b/lib/components/inter_scrollbar/inter_scrollbar.dart similarity index 100% rename from lib/components/shared/inter_scrollbar/inter_scrollbar.dart rename to lib/components/inter_scrollbar/inter_scrollbar.dart diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/links/anchor_button.dart similarity index 100% rename from lib/components/shared/links/anchor_button.dart rename to lib/components/links/anchor_button.dart diff --git a/lib/components/shared/links/artist_link.dart b/lib/components/links/artist_link.dart similarity index 96% rename from lib/components/shared/links/artist_link.dart rename to lib/components/links/artist_link.dart index 5236a0611..47ddecd84 100644 --- a/lib/components/shared/links/artist_link.dart +++ b/lib/components/links/artist_link.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/links/anchor_button.dart'; +import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/components/shared/links/hyper_link.dart b/lib/components/links/hyper_link.dart similarity index 92% rename from lib/components/shared/links/hyper_link.dart rename to lib/components/links/hyper_link.dart index f84517b45..32d715e01 100644 --- a/lib/components/shared/links/hyper_link.dart +++ b/lib/components/links/hyper_link.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spotube/components/shared/links/anchor_button.dart'; +import 'package:spotube/components/links/anchor_button.dart'; import 'package:url_launcher/url_launcher_string.dart'; class Hyperlink extends StatelessWidget { diff --git a/lib/components/shared/links/link_text.dart b/lib/components/links/link_text.dart similarity index 93% rename from lib/components/shared/links/link_text.dart rename to lib/components/links/link_text.dart index db7b6358c..0cab71d0d 100644 --- a/lib/components/shared/links/link_text.dart +++ b/lib/components/links/link_text.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spotube/components/shared/links/anchor_button.dart'; +import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/utils/service_utils.dart'; class LinkText extends StatelessWidget { diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/page_window_title_bar.dart similarity index 100% rename from lib/components/shared/page_window_title_bar.dart rename to lib/components/page_window_title_bar.dart diff --git a/lib/components/shared/panels/controller.dart b/lib/components/panels/controller.dart similarity index 99% rename from lib/components/shared/panels/controller.dart rename to lib/components/panels/controller.dart index 65c2444e7..834e9ce6d 100644 --- a/lib/components/shared/panels/controller.dart +++ b/lib/components/panels/controller.dart @@ -1,4 +1,4 @@ -part of './sliding_up_panel.dart'; +part of 'sliding_up_panel.dart'; class PanelController extends ChangeNotifier { SlidingUpPanelState? _panelState; diff --git a/lib/components/shared/panels/helpers.dart b/lib/components/panels/helpers.dart similarity index 98% rename from lib/components/shared/panels/helpers.dart rename to lib/components/panels/helpers.dart index 6d0dde310..d79fa97c7 100644 --- a/lib/components/shared/panels/helpers.dart +++ b/lib/components/panels/helpers.dart @@ -1,4 +1,4 @@ -part of "./sliding_up_panel.dart"; +part of "sliding_up_panel.dart"; /// if you want to prevent the panel from being dragged using the widget, /// wrap the widget with this diff --git a/lib/components/shared/panels/sliding_up_panel.dart b/lib/components/panels/sliding_up_panel.dart similarity index 100% rename from lib/components/shared/panels/sliding_up_panel.dart rename to lib/components/panels/sliding_up_panel.dart diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/playbutton_card.dart similarity index 98% rename from lib/components/shared/playbutton_card.dart rename to lib/components/playbutton_card.dart index 80a27eb01..ffd91cd2e 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/playbutton_card.dart @@ -5,8 +5,8 @@ import 'package:gap/gap.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/hover_builder.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/hover_builder.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shimmers/shimmer_lyrics.dart similarity index 100% rename from lib/components/shared/shimmers/shimmer_lyrics.dart rename to lib/components/shimmers/shimmer_lyrics.dart diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/sort_tracks_dropdown.dart similarity index 96% rename from lib/components/shared/sort_tracks_dropdown.dart rename to lib/components/sort_tracks_dropdown.dart index ab27e7fff..167270134 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/sort_tracks_dropdown.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/library/user_local_tracks.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; +import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/extensions/context.dart'; class SortTracksDropdown extends StatelessWidget { diff --git a/lib/components/shared/spotube_page_route.dart b/lib/components/spotube_page_route.dart similarity index 100% rename from lib/components/shared/spotube_page_route.dart rename to lib/components/spotube_page_route.dart diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/themed_button_tab_bar.dart similarity index 100% rename from lib/components/shared/themed_button_tab_bar.dart rename to lib/components/themed_button_tab_bar.dart diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart similarity index 96% rename from lib/components/shared/track_tile/track_options.dart rename to lib/components/track_tile/track_options.dart index 4b383c479..de6047445 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -8,13 +8,13 @@ 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/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'; -import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; -import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; +import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; +import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; +import 'package:spotube/components/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/dialogs/track_details_dialog.dart'; +import 'package:spotube/components/heart_button.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart similarity index 96% rename from lib/components/shared/track_tile/track_tile.dart rename to lib/components/track_tile/track_tile.dart index e3aea4dec..9ba87abea 100644 --- a/lib/components/shared/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -7,11 +7,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/hover_builder.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; -import 'package:spotube/components/shared/links/link_text.dart'; -import 'package:spotube/components/shared/track_tile/track_options.dart'; +import 'package:spotube/components/hover_builder.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; +import 'package:spotube/components/links/link_text.dart'; +import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart similarity index 92% rename from lib/components/shared/tracks_view/sections/body/track_view_body.dart rename to lib/components/tracks_view/sections/body/track_view_body.dart index c3605f33a..0c3cca4ee 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -8,13 +8,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; -import 'package:spotube/components/shared/dialogs/select_device_dialog.dart'; -import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/track_tile/track_tile.dart'; -import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart'; -import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; -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/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/expandable_search/expandable_search.dart'; +import 'package:spotube/components/track_tile/track_tile.dart'; +import 'package:spotube/components/tracks_view/sections/body/track_view_body_headers.dart'; +import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; +import 'package:spotube/components/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'; diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/tracks_view/sections/body/track_view_body_headers.dart similarity index 87% rename from lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart rename to lib/components/tracks_view/sections/body/track_view_body_headers.dart index 3a1538a39..564c85d0e 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart +++ b/lib/components/tracks_view/sections/body/track_view_body_headers.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; -import 'package:spotube/components/shared/tracks_view/sections/body/track_view_options.dart'; -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/components/expandable_search/expandable_search.dart'; +import 'package:spotube/components/sort_tracks_dropdown.dart'; +import 'package:spotube/components/tracks_view/sections/body/track_view_options.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; +import 'package:spotube/components/tracks_view/track_view_provider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart similarity index 92% rename from lib/components/shared/tracks_view/sections/body/track_view_options.dart rename to lib/components/tracks_view/sections/body/track_view_options.dart index c2adf38bb..f004b10a7 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -2,11 +2,11 @@ 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'; -import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; -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/components/adaptive/adaptive_pop_sheet_list.dart'; +import 'package:spotube/components/dialogs/confirm_download_dialog.dart'; +import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; +import 'package:spotube/components/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'; diff --git a/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart b/lib/components/tracks_view/sections/body/use_is_user_playlist.dart similarity index 100% rename from lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart rename to lib/components/tracks_view/sections/body/use_is_user_playlist.dart diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/tracks_view/sections/header/flexible_header.dart similarity index 94% rename from lib/components/shared/tracks_view/sections/header/flexible_header.dart rename to lib/components/tracks_view/sections/header/flexible_header.dart index d6e71e8f8..6e8fc2d11 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/tracks_view/sections/header/flexible_header.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/playbutton_card.dart'; -import 'package:spotube/components/shared/tracks_view/sections/header/header_actions.dart'; -import 'package:spotube/components/shared/tracks_view/sections/header/header_buttons.dart'; -import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/playbutton_card.dart'; +import 'package:spotube/components/tracks_view/sections/header/header_actions.dart'; +import 'package:spotube/components/tracks_view/sections/header/header_buttons.dart'; +import 'package:spotube/components/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'; diff --git a/lib/components/shared/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart similarity index 94% rename from lib/components/shared/tracks_view/sections/header/header_actions.dart rename to lib/components/tracks_view/sections/header/header_actions.dart index 6ea53b83e..a1e959d94 100644 --- a/lib/components/shared/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -5,9 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; -import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/components/heart_button.dart'; +import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; +import 'package:spotube/components/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'; diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart similarity index 97% rename from lib/components/shared/tracks_view/sections/header/header_buttons.dart rename to lib/components/tracks_view/sections/header/header_buttons.dart index 5cc442cf9..aa660f016 100644 --- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -7,8 +7,8 @@ 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/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/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'; diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/tracks_view/track_view.dart similarity index 77% rename from lib/components/shared/tracks_view/track_view.dart rename to lib/components/tracks_view/track_view.dart index 03d628a8a..36d334cda 100644 --- a/lib/components/shared/tracks_view/track_view.dart +++ b/lib/components/tracks_view/track_view.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sliver_tools/sliver_tools.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/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/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/tracks_view/sections/header/flexible_header.dart'; +import 'package:spotube/components/tracks_view/sections/body/track_view_body.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/utils/platform.dart'; class TrackView extends HookConsumerWidget { diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/tracks_view/track_view_props.dart similarity index 100% rename from lib/components/shared/tracks_view/track_view_props.dart rename to lib/components/tracks_view/track_view_props.dart diff --git a/lib/components/shared/tracks_view/track_view_provider.dart b/lib/components/tracks_view/track_view_provider.dart similarity index 100% rename from lib/components/shared/tracks_view/track_view_provider.dart rename to lib/components/tracks_view/track_view_provider.dart diff --git a/lib/components/shared/waypoint.dart b/lib/components/waypoint.dart similarity index 100% rename from lib/components/shared/waypoint.dart rename to lib/components/waypoint.dart diff --git a/lib/hooks/utils/use_palette_color.dart b/lib/hooks/utils/use_palette_color.dart index e6d8b3988..64994d2b0 100644 --- a/lib/hooks/utils/use_palette_color.dart +++ b/lib/hooks/utils/use_palette_color.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; final _paletteColorState = StateProvider( (ref) { diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index 7212a5741..a071ac047 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/dialogs/select_device_dialog.dart'; -import 'package:spotube/components/shared/playbutton_card.dart'; +import 'package:spotube/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/playbutton_card.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/modules/artist/artist_album_list.dart b/lib/modules/artist/artist_album_list.dart index a91327cea..9bb65804a 100644 --- a/lib/modules/artist/artist_album_list.dart +++ b/lib/modules/artist/artist_album_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/modules/artist/artist_card.dart b/lib/modules/artist/artist_card.dart index 57971ada9..c1404e420 100644 --- a/lib/modules/artist/artist_card.dart +++ b/lib/modules/artist/artist_card.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; diff --git a/lib/modules/home/sections/featured.dart b/lib/modules/home/sections/featured.dart index 0db5a1e83..4f30c3421 100644 --- a/lib/modules/home/sections/featured.dart +++ b/lib/modules/home/sections/featured.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/modules/home/sections/feed.dart b/lib/modules/home/sections/feed.dart index f3f632cee..f66f01f24 100644 --- a/lib/modules/home/sections/feed.dart +++ b/lib/modules/home/sections/feed.dart @@ -1,7 +1,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/components/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'; diff --git a/lib/modules/home/sections/friends/friend_item.dart b/lib/modules/home/sections/friends/friend_item.dart index 2b5757565..bb97af04a 100644 --- a/lib/modules/home/sections/friends/friend_item.dart +++ b/lib/modules/home/sections/friends/friend_item.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; 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/shared/image/universal_image.dart'; +import 'package:spotube/components/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'; diff --git a/lib/modules/home/sections/genres.dart b/lib/modules/home/sections/genres.dart index 7dfafd5a0..3e12e5e9c 100644 --- a/lib/modules/home/sections/genres.dart +++ b/lib/modules/home/sections/genres.dart @@ -10,7 +10,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/gradients.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/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'; diff --git a/lib/modules/home/sections/made_for_user.dart b/lib/modules/home/sections/made_for_user.dart index d1d269f6f..1b9854d39 100644 --- a/lib/modules/home/sections/made_for_user.dart +++ b/lib/modules/home/sections/made_for_user.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomeMadeForUserSection extends HookConsumerWidget { diff --git a/lib/modules/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart index 82bc0e8cd..08b28138f 100644 --- a/lib/modules/home/sections/new_releases.dart +++ b/lib/modules/home/sections/new_releases.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/modules/home/sections/recent.dart b/lib/modules/home/sections/recent.dart index 0fc5fadf9..5be2fcc22 100644 --- a/lib/modules/home/sections/recent.dart +++ b/lib/modules/home/sections/recent.dart @@ -1,6 +1,6 @@ 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/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/provider/history/recent.dart'; import 'package:spotube/provider/history/state.dart'; diff --git a/lib/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index 72032198c..9408d0080 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/library/local_folder/local_folder_item.dart @@ -6,7 +6,7 @@ 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/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/modules/library/playlist_generate/simple_track_tile.dart b/lib/modules/library/playlist_generate/simple_track_tile.dart index cf4ddb1aa..e6cc281f3 100644 --- a/lib/modules/library/playlist_generate/simple_track_tile.dart +++ b/lib/modules/library/playlist_generate/simple_track_tile.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/image.dart'; class SimpleTrackTile extends HookWidget { diff --git a/lib/modules/library/user_albums.dart b/lib/modules/library/user_albums.dart index b7fcc958a..71e5b65a0 100644 --- a/lib/modules/library/user_albums.dart +++ b/lib/modules/library/user_albums.dart @@ -9,9 +9,9 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/album/album_card.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/modules/library/user_artists.dart b/lib/modules/library/user_artists.dart index 118447ae6..dbdd8682c 100644 --- a/lib/modules/library/user_artists.dart +++ b/lib/modules/library/user_artists.dart @@ -8,10 +8,10 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/modules/artist/artist_card.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/modules/library/user_downloads/download_item.dart b/lib/modules/library/user_downloads/download_item.dart index a145fdad7..bc9abf6ac 100644 --- a/lib/modules/library/user_downloads/download_item.dart +++ b/lib/modules/library/user_downloads/download_item.dart @@ -3,8 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.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/image/universal_image.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/download_manager_provider.dart'; diff --git a/lib/modules/library/user_playlists.dart b/lib/modules/library/user_playlists.dart index 104badf6a..e0c501bb3 100644 --- a/lib/modules/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -10,10 +10,10 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/modules/playlist/playlist_card.dart'; -import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index c87b336b8..7eaf53ae2 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -10,12 +10,12 @@ import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/volume_slider.dart'; -import 'package:spotube/components/shared/animated_gradient.dart'; -import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/panels/sliding_up_panel.dart'; +import 'package:spotube/components/animated_gradient.dart'; +import 'package:spotube/components/dialogs/track_details_dialog.dart'; +import 'package:spotube/components/links/artist_link.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/panels/sliding_up_panel.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 41ee9e39b..df3664858 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -5,8 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; -import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; +import 'package:spotube/components/heart_button.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; diff --git a/lib/modules/player/player_overlay.dart b/lib/modules/player/player_overlay.dart index 7911a046b..084de425f 100644 --- a/lib/modules/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/modules/player/player_track_details.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; -import 'package:spotube/components/shared/panels/sliding_up_panel.dart'; +import 'package:spotube/components/panels/sliding_up_panel.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/modules/player/use_progress.dart'; diff --git a/lib/modules/player/player_queue.dart b/lib/modules/player/player_queue.dart index 914d7bc97..cf16e9a39 100644 --- a/lib/modules/player/player_queue.dart +++ b/lib/modules/player/player_queue.dart @@ -11,9 +11,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.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/track_tile/track_tile.dart'; +import 'package:spotube/components/fallbacks/not_found.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index 4746fe51a..da58e3b19 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -3,9 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; -import 'package:spotube/components/shared/links/link_text.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; +import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 99b7b430f..147319078 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -7,8 +7,8 @@ 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/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index 9f26f739d..7c11eca6d 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/dialogs/select_device_dialog.dart'; -import 'package:spotube/components/shared/playbutton_card.dart'; +import 'package:spotube/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/playbutton_card.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/pages/playlist/playlist.dart'; diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index bac98b64f..6c333986e 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -11,7 +11,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index 05f14c398..79d229efb 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -10,7 +10,7 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/connect/connect_device.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/modules/root/update_dialog.dart b/lib/modules/root/update_dialog.dart index e15903c63..4a3130960 100644 --- a/lib/modules/root/update_dialog.dart +++ b/lib/modules/root/update_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:spotube/components/shared/links/anchor_button.dart'; +import 'package:spotube/components/links/anchor_button.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:version/version.dart'; diff --git a/lib/modules/stats/common/album_item.dart b/lib/modules/stats/common/album_item.dart index af53d273b..0424ca70c 100644 --- a/lib/modules/stats/common/album_item.dart +++ b/lib/modules/stats/common/album_item.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/modules/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/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/album/album.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/modules/stats/common/artist_item.dart b/lib/modules/stats/common/artist_item.dart index 9282d4e1b..7e7281da4 100644 --- a/lib/modules/stats/common/artist_item.dart +++ b/lib/modules/stats/common/artist_item.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/modules/stats/common/playlist_item.dart b/lib/modules/stats/common/playlist_item.dart index b07311ab4..79e40d717 100644 --- a/lib/modules/stats/common/playlist_item.dart +++ b/lib/modules/stats/common/playlist_item.dart @@ -1,7 +1,7 @@ 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/components/image/universal_image.dart'; +import 'package:spotube/components/playbutton_card.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/modules/stats/common/track_item.dart b/lib/modules/stats/common/track_item.dart index 6ba6b8866..33991d432 100644 --- a/lib/modules/stats/common/track_item.dart +++ b/lib/modules/stats/common/track_item.dart @@ -1,7 +1,7 @@ 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/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/track/track.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/modules/stats/top/top.dart b/lib/modules/stats/top/top.dart index 7c3849216..52529f3ff 100644 --- a/lib/modules/stats/top/top.dart +++ b/lib/modules/stats/top/top.dart @@ -1,7 +1,7 @@ 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/themed_button_tab_bar.dart'; import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/tracks.dart'; diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index aea890a0d..dcdc2ce79 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; 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/components/tracks_view/track_view.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index bd416edd4..d38fe778d 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/modules/artist/artist_album_list.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; diff --git a/lib/pages/artist/section/footer.dart b/lib/pages/artist/section/footer.dart index 4707b9394..abe864106 100644 --- a/lib/pages/artist/section/footer.dart +++ b/lib/pages/artist/section/footer.dart @@ -3,7 +3,7 @@ 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/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 5bad674ee..7ca8964dc 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -5,7 +5,7 @@ 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/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 595ac5109..c9397c7bb 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -4,8 +4,8 @@ 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/dialogs/select_device_dialog.dart'; -import 'package:spotube/components/shared/track_tile/track_tile.dart'; +import 'package:spotube/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index a1735d42f..1e4e3938d 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -3,7 +3,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/connect/local_devices.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index 20ad3d17e..afd173876 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -5,10 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/volume_slider.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/links/anchor_button.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/anchor_button.dart'; +import 'package:spotube/components/links/artist_link.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index a5f8c3b1b..1f7cb1b5b 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/modules/desktop_login/login_form.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index 2c1535feb..d96942e41 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -5,8 +5,8 @@ import 'package:introduction_screen/introduction_screen.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/modules/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/components/links/hyper_link.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/getting_started/getting_started.dart b/lib/pages/getting_started/getting_started.dart index fa2054038..97af2ef4e 100644 --- a/lib/pages/getting_started/getting_started.dart +++ b/lib/pages/getting_started/getting_started.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/getting_started/sections/greeting.dart'; import 'package:spotube/pages/getting_started/sections/playback.dart'; diff --git a/lib/pages/home/feed/feed_section.dart b/lib/pages/home/feed/feed_section.dart index 11780620e..42a22f102 100644 --- a/lib/pages/home/feed/feed_section.dart +++ b/lib/pages/home/feed/feed_section.dart @@ -5,7 +5,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/album/album_card.dart'; import 'package:spotube/modules/artist/artist_card.dart'; import 'package:spotube/modules/playlist/playlist_card.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/views/home_section.dart'; diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index ef701478d..4ad376307 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -6,9 +6,9 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/playlist/playlist_card.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:collection/collection.dart'; diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index bb84fc168..cf033bb90 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -6,7 +6,7 @@ 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/gradients.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 6ed5c0e43..27b4cc017 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -12,7 +12,7 @@ import 'package:spotube/modules/home/sections/genres.dart'; import 'package:spotube/modules/home/sections/made_for_user.dart'; import 'package:spotube/modules/home/sections/new_releases.dart'; import 'package:spotube/modules/home/sections/recent.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index 2baeaad94..1f4c64e11 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -4,8 +4,8 @@ import 'package:form_validator/form_validator.dart'; 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/shared/dialogs/prompt_dialog.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index cc96e4ee6..664777619 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart' hide Image; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/modules/library/user_local_tracks.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/modules/library/user_albums.dart'; import 'package:spotube/modules/library/user_artists.dart'; import 'package:spotube/modules/library/user_downloads.dart'; import 'package:spotube/modules/library/user_playlists.dart'; -import 'package:spotube/components/shared/themed_button_tab_bar.dart'; +import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 27979b5c7..648b3c504 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -7,12 +7,12 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/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/components/expandable_search/expandable_search.dart'; +import 'package:spotube/components/fallbacks/not_found.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/sort_tracks_dropdown.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 88bf8adbc..74b7fe260 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -11,8 +11,8 @@ import 'package:spotube/modules/library/playlist_generate/recommendation_attribu import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_fields.dart'; import 'package:spotube/modules/library/playlist_generate/seeds_multi_autocomplete.dart'; import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 266e9f663..a481eac42 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -6,8 +6,8 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 850eccfa0..1a4ea7c16 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -6,10 +6,10 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/themed_button_tab_bar.dart'; +import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index f580f56d1..bd8fc0a1e 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -8,8 +8,8 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/root/sidebar.dart'; -import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 79456e27f..5340e8fd9 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/lyrics/zoom_controls.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; +import 'package:spotube/components/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index b07231292..8a2dd3564 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/lyrics/zoom_controls.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; +import 'package:spotube/components/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 44e99aeae..942f46d50 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; 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/components/tracks_view/track_view.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 8fb224581..f7c5a431e 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; -import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.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/components/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; +import 'package:spotube/components/tracks_view/track_view.dart'; +import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index d77ae98d8..06f6848a2 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -7,8 +7,8 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index c2ad64c01..6f14a10b6 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/player_queue.dart'; -import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; +import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/modules/root/bottom_player.dart'; import 'package:spotube/modules/root/sidebar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index d5374786d..828e4aefc 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -10,9 +10,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; diff --git a/lib/pages/search/sections/albums.dart b/lib/pages/search/sections/albums.dart index d15c34ff3..857eb59ca 100644 --- a/lib/pages/search/sections/albums.dart +++ b/lib/pages/search/sections/albums.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/album_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/search/sections/artists.dart b/lib/pages/search/sections/artists.dart index bb8063dcf..162955804 100644 --- a/lib/pages/search/sections/artists.dart +++ b/lib/pages/search/sections/artists.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/search/sections/playlists.dart b/lib/pages/search/sections/playlists.dart index 13ff483d3..3799f9fa0 100644 --- a/lib/pages/search/sections/playlists.dart +++ b/lib/pages/search/sections/playlists.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index bd7f3c88e..1bde28727 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -2,9 +2,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; -import 'package:spotube/components/shared/dialogs/select_device_dialog.dart'; -import 'package:spotube/components/shared/track_tile/track_tile.dart'; +import 'package:spotube/components/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/dialogs/select_device_dialog.dart'; +import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index e7d957593..2692bfdca 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -2,9 +2,9 @@ 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'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/hyper_link.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_package_info.dart'; diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 6eccab073..c30864fe1 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -5,8 +5,8 @@ import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.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/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/blacklist_provider.dart'; diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index 81d1b4e51..a790c39d0 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -5,8 +5,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.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/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart index 531f4a5eb..c16930799 100644 --- a/lib/pages/settings/sections/about.dart +++ b/lib/pages/settings/sections/about.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart'; +import 'package:spotube/components/adaptive/adaptive_list_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 46f0e452d..a007fbebc 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart index a9283e1ad..67ed282b5 100644 --- a/lib/pages/settings/sections/appearance.dart +++ b/lib/pages/settings/sections/appearance.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index fa2601901..5fbbd8b0e 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -3,7 +3,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/components/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'; diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index 343b7d860..c9776fd67 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -6,7 +6,7 @@ import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/l10n/l10n.dart'; diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index f26135fb4..0d37d9902 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/piped_instances_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index af0fc0959..2d1c9224d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.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'; +import 'package:spotube/components/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/settings/sections/about.dart'; import 'package:spotube/pages/settings/sections/accounts.dart'; diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index ecec91b96..81eba384d 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -1,7 +1,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index b25399c10..f54453265 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -1,7 +1,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 4993d2704..5aa06af90 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -2,7 +2,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 0212704a2..9ce84548e 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -2,7 +2,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index 0b4b6cd7f..bf1fee932 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -1,7 +1,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/stats.dart b/lib/pages/stats/stats.dart index 3efd212f0..7dfab8447 100644 --- a/lib/pages/stats/stats.dart +++ b/lib/pages/stats/stats.dart @@ -1,7 +1,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/summary/summary.dart'; import 'package:spotube/modules/stats/top/top.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 204829291..126318161 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -2,7 +2,7 @@ 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/page_window_title_bar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 2109fe6ef..f797c2f29 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -6,12 +6,12 @@ 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/shared/heart_button.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/links/artist_link.dart'; -import 'package:spotube/components/shared/links/link_text.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/track_tile/track_options.dart'; +import 'package:spotube/components/heart_button.dart'; +import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/links/artist_link.dart'; +import 'package:spotube/components/links/link_text.dart'; +import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication_provider.dart index be61cb4f0..95ce62409 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication_provider.dart @@ -8,7 +8,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart' hide X509Certificate; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/routes.dart'; -import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/proxy_playlist/player_listeners.dart index 3ee815e67..3c36491cf 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/proxy_playlist/player_listeners.dart @@ -4,7 +4,7 @@ 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/components/image/universal_image.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/palette_provider.dart'; diff --git a/lib/utils/persisted_state_notifier.dart b/lib/utils/persisted_state_notifier.dart index 9416a340b..450bb6640 100644 --- a/lib/utils/persisted_state_notifier.dart +++ b/lib/utils/persisted_state_notifier.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/primitive_utils.dart'; From 8cb6c6d12638f23dcfb40d0a153c97d14bb0ab0d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Jun 2024 09:19:41 +0600 Subject: [PATCH 05/92] refactor: breakdown page window titlebar widget into multiple small widgets --- .vscode/settings.json | 1 + lib/components/page_window_title_bar.dart | 653 ------------------ lib/components/titlebar/mouse_state.dart | 71 ++ lib/components/titlebar/titlebar.dart | 179 +++++ lib/components/titlebar/titlebar_buttons.dart | 124 ++++ .../titlebar/titlebar_icon_buttons.dart | 161 +++++ lib/components/titlebar/window_button.dart | 133 ++++ lib/components/tracks_view/track_view.dart | 2 +- lib/modules/player/player.dart | 2 +- lib/pages/artist/artist.dart | 2 +- lib/pages/connect/connect.dart | 2 +- lib/pages/connect/control/control.dart | 2 +- lib/pages/desktop_login/desktop_login.dart | 2 +- lib/pages/desktop_login/login_tutorial.dart | 2 +- .../getting_started/getting_started.dart | 2 +- lib/pages/home/feed/feed_section.dart | 2 +- lib/pages/home/genres/genre_playlists.dart | 2 +- lib/pages/home/genres/genres.dart | 2 +- lib/pages/home/home.dart | 2 +- lib/pages/lastfm_login/lastfm_login.dart | 2 +- lib/pages/library/library.dart | 2 +- lib/pages/library/local_folder.dart | 2 +- .../playlist_generate/playlist_generate.dart | 2 +- .../playlist_generate_result.dart | 2 +- lib/pages/lyrics/lyrics.dart | 2 +- lib/pages/lyrics/mini_lyrics.dart | 2 +- lib/pages/profile/profile.dart | 2 +- lib/pages/search/search.dart | 2 +- lib/pages/settings/about.dart | 2 +- lib/pages/settings/blacklist.dart | 2 +- lib/pages/settings/logs.dart | 2 +- lib/pages/settings/settings.dart | 2 +- lib/pages/stats/albums/albums.dart | 2 +- lib/pages/stats/artists/artists.dart | 2 +- lib/pages/stats/fees/fees.dart | 2 +- lib/pages/stats/minutes/minutes.dart | 2 +- lib/pages/stats/playlists/playlists.dart | 2 +- lib/pages/stats/stats.dart | 2 +- lib/pages/stats/streams/streams.dart | 2 +- lib/pages/track/track.dart | 2 +- 40 files changed, 702 insertions(+), 686 deletions(-) delete mode 100644 lib/components/page_window_title_bar.dart create mode 100644 lib/components/titlebar/mouse_state.dart create mode 100644 lib/components/titlebar/titlebar.dart create mode 100644 lib/components/titlebar/titlebar_buttons.dart create mode 100644 lib/components/titlebar/titlebar_icon_buttons.dart create mode 100644 lib/components/titlebar/window_button.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index de5fbd69a..0ec6ca766 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,6 +17,7 @@ "songlink", "speechiness", "Spotube", + "titlebar", "winget" ], "editor.formatOnSave": true, diff --git a/lib/components/page_window_title_bar.dart b/lib/components/page_window_title_bar.dart deleted file mode 100644 index c5fc11e79..000000000 --- a/lib/components/page_window_title_bar.dart +++ /dev/null @@ -1,653 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/utils/platform.dart'; -import 'package:titlebar_buttons/titlebar_buttons.dart'; -import 'dart:math'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'dart:io' show Platform; - -import 'package:window_manager/window_manager.dart'; - -class PageWindowTitleBar extends StatefulHookConsumerWidget - implements PreferredSizeWidget { - final Widget? leading; - final bool automaticallyImplyLeading; - final List? actions; - final Color? backgroundColor; - final Color? foregroundColor; - final IconThemeData? actionsIconTheme; - final bool? centerTitle; - final double? titleSpacing; - final double toolbarOpacity; - final double? leadingWidth; - final TextStyle? toolbarTextStyle; - final TextStyle? titleTextStyle; - final double? titleWidth; - final Widget? title; - - final bool _sliver; - - const PageWindowTitleBar({ - super.key, - this.actions, - this.title, - this.toolbarOpacity = 1, - this.backgroundColor, - this.actionsIconTheme, - this.automaticallyImplyLeading = false, - this.centerTitle, - this.foregroundColor, - this.leading, - this.leadingWidth, - this.titleSpacing, - this.titleTextStyle, - this.titleWidth, - this.toolbarTextStyle, - }) : _sliver = false, - pinned = false, - floating = false, - snap = false, - stretch = false; - - final bool pinned; - final bool floating; - final bool snap; - final bool stretch; - - const PageWindowTitleBar.sliver({ - super.key, - this.actions, - this.title, - this.backgroundColor, - this.actionsIconTheme, - this.automaticallyImplyLeading = false, - this.centerTitle, - this.foregroundColor, - this.leading, - this.leadingWidth, - this.titleSpacing, - this.titleTextStyle, - this.titleWidth, - this.toolbarTextStyle, - this.pinned = false, - this.floating = false, - this.snap = false, - this.stretch = false, - }) : _sliver = true, - toolbarOpacity = 1; - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - - @override - ConsumerState createState() => _PageWindowTitleBarState(); -} - -class _PageWindowTitleBarState extends ConsumerState { - void onDrag(details) { - final systemTitleBar = - ref.read(userPreferencesProvider.select((s) => s.systemTitleBar)); - if (kIsDesktop && !systemTitleBar) { - windowManager.startDragging(); - } - } - - @override - Widget build(BuildContext context) { - final mediaQuery = MediaQuery.of(context); - - if (widget._sliver) { - return SliverLayoutBuilder( - builder: (context, constraints) { - final hasFullscreen = - mediaQuery.size.width == constraints.crossAxisExtent; - final hasLeadingOrCanPop = - widget.leading != null || Navigator.canPop(context); - - return SliverPadding( - padding: EdgeInsets.only( - left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0, - ), - sliver: SliverAppBar( - leading: widget.leading, - automaticallyImplyLeading: widget.automaticallyImplyLeading, - actions: [ - ...?widget.actions, - WindowTitleBarButtons(foregroundColor: widget.foregroundColor), - ], - backgroundColor: widget.backgroundColor, - foregroundColor: widget.foregroundColor, - actionsIconTheme: widget.actionsIconTheme, - centerTitle: widget.centerTitle, - titleSpacing: widget.titleSpacing, - leadingWidth: widget.leadingWidth, - toolbarTextStyle: widget.toolbarTextStyle, - titleTextStyle: widget.titleTextStyle, - title: SizedBox( - width: double.infinity, // workaround to force dragging - child: widget.title ?? const Text(""), - ), - pinned: widget.pinned, - floating: widget.floating, - snap: widget.snap, - stretch: widget.stretch, - ), - ); - }, - ); - } - - return LayoutBuilder(builder: (context, constrains) { - final hasFullscreen = mediaQuery.size.width == constrains.maxWidth; - final hasLeadingOrCanPop = - widget.leading != null || Navigator.canPop(context); - - return GestureDetector( - onHorizontalDragStart: onDrag, - onVerticalDragStart: onDrag, - child: Padding( - padding: EdgeInsets.only( - left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0, - ), - child: AppBar( - leading: widget.leading, - automaticallyImplyLeading: widget.automaticallyImplyLeading, - actions: [ - ...?widget.actions, - WindowTitleBarButtons(foregroundColor: widget.foregroundColor), - ], - backgroundColor: widget.backgroundColor, - foregroundColor: widget.foregroundColor, - actionsIconTheme: widget.actionsIconTheme, - centerTitle: widget.centerTitle, - titleSpacing: widget.titleSpacing, - toolbarOpacity: widget.toolbarOpacity, - leadingWidth: widget.leadingWidth, - toolbarTextStyle: widget.toolbarTextStyle, - titleTextStyle: widget.titleTextStyle, - title: SizedBox( - width: double.infinity, // workaround to force dragging - child: widget.title ?? const Text(""), - ), - scrolledUnderElevation: 0, - shadowColor: Colors.transparent, - forceMaterialTransparency: true, - elevation: 0, - ), - ), - ); - }); - } -} - -class WindowTitleBarButtons extends HookConsumerWidget { - final Color? foregroundColor; - const WindowTitleBarButtons({ - super.key, - this.foregroundColor, - }); - - @override - Widget build(BuildContext context, ref) { - final preferences = ref.watch(userPreferencesProvider); - final isMaximized = useState(null); - const type = ThemeType.auto; - - Future onClose() async { - await windowManager.close(); - } - - useEffect(() { - if (kIsDesktop) { - windowManager.isMaximized().then((value) { - isMaximized.value = value; - }); - } - return null; - }, []); - - if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) { - return const SizedBox.shrink(); - } - - if (kIsWindows) { - 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, - ); - - final closeColors = WindowButtonColors( - normal: Colors.transparent, - iconNormal: foregroundColor ?? theme.colorScheme.onBackground, - mouseOver: Colors.red, - mouseDown: Colors.red[800]!, - iconMouseOver: Colors.white, - iconMouseDown: Colors.black, - ); - - return Padding( - padding: const EdgeInsets.only(bottom: 25), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MinimizeWindowButton( - onPressed: windowManager.minimize, - colors: colors, - ), - if (isMaximized.value != true) - MaximizeWindowButton( - colors: colors, - onPressed: () { - windowManager.maximize(); - isMaximized.value = true; - }, - ) - else - RestoreWindowButton( - colors: colors, - onPressed: () { - windowManager.unmaximize(); - isMaximized.value = false; - }, - ), - CloseWindowButton( - colors: closeColors, - onPressed: onClose, - ), - ], - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(bottom: 20, left: 10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DecoratedMinimizeButton( - type: type, - onPressed: windowManager.minimize, - ), - DecoratedMaximizeButton( - type: type, - onPressed: () async { - if (await windowManager.isMaximized()) { - await windowManager.unmaximize(); - isMaximized.value = false; - } else { - await windowManager.maximize(); - isMaximized.value = true; - } - }, - ), - DecoratedCloseButton( - type: type, - onPressed: onClose, - ), - ], - ), - ); - } -} - -typedef WindowButtonIconBuilder = Widget Function( - WindowButtonContext buttonContext); -typedef WindowButtonBuilder = Widget Function( - WindowButtonContext buttonContext, Widget icon); - -class WindowButtonContext { - BuildContext context; - MouseState mouseState; - Color? backgroundColor; - Color iconColor; - WindowButtonContext( - {required this.context, - required this.mouseState, - this.backgroundColor, - required this.iconColor}); -} - -class WindowButtonColors { - late Color normal; - late Color mouseOver; - late Color mouseDown; - late Color iconNormal; - late Color iconMouseOver; - late Color iconMouseDown; - WindowButtonColors( - {Color? normal, - Color? mouseOver, - Color? mouseDown, - Color? iconNormal, - Color? iconMouseOver, - Color? iconMouseDown}) { - this.normal = normal ?? _defaultButtonColors.normal; - this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver; - this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown; - this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal; - this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver; - this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown; - } -} - -final _defaultButtonColors = WindowButtonColors( - normal: Colors.transparent, - iconNormal: const Color(0xFF805306), - mouseOver: const Color(0xFF404040), - mouseDown: const Color(0xFF202020), - iconMouseOver: const Color(0xFFFFFFFF), - iconMouseDown: const Color(0xFFF0F0F0), -); - -class WindowButton extends StatelessWidget { - final WindowButtonBuilder? builder; - final WindowButtonIconBuilder? iconBuilder; - late final WindowButtonColors colors; - final bool animate; - final EdgeInsets? padding; - final VoidCallback? onPressed; - - WindowButton( - {super.key, - WindowButtonColors? colors, - this.builder, - @required this.iconBuilder, - this.padding, - this.onPressed, - this.animate = false}) { - this.colors = colors ?? _defaultButtonColors; - } - - Color getBackgroundColor(MouseState mouseState) { - if (mouseState.isMouseDown) return colors.mouseDown; - if (mouseState.isMouseOver) return colors.mouseOver; - return colors.normal; - } - - Color getIconColor(MouseState mouseState) { - if (mouseState.isMouseDown) return colors.iconMouseDown; - if (mouseState.isMouseOver) return colors.iconMouseOver; - return colors.iconNormal; - } - - @override - Widget build(BuildContext context) { - if (kIsWeb) { - return Container(); - } else { - // Don't show button on macOS - if (Platform.isMacOS) { - return Container(); - } - } - - return MouseStateBuilder( - builder: (context, mouseState) { - WindowButtonContext buttonContext = WindowButtonContext( - mouseState: mouseState, - context: context, - backgroundColor: getBackgroundColor(mouseState), - iconColor: getIconColor(mouseState)); - - var icon = - (iconBuilder != null) ? iconBuilder!(buttonContext) : Container(); - - var fadeOutColor = - getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0); - var padding = this.padding ?? const EdgeInsets.all(10); - var animationMs = - mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0); - Widget iconWithPadding = Padding(padding: padding, child: icon); - iconWithPadding = AnimatedContainer( - curve: Curves.easeOut, - duration: Duration(milliseconds: animationMs), - color: buttonContext.backgroundColor ?? fadeOutColor, - child: iconWithPadding); - var button = - (builder != null) ? builder!(buttonContext, icon) : iconWithPadding; - return SizedBox( - width: 45, - height: 32, - child: button, - ); - }, - onPressed: () { - if (onPressed != null) onPressed!(); - }, - ); - } -} - -class MinimizeWindowButton extends WindowButton { - MinimizeWindowButton( - {super.key, super.colors, super.onPressed, bool? animate}) - : super( - animate: animate ?? false, - iconBuilder: (buttonContext) => - MinimizeIcon(color: buttonContext.iconColor), - ); -} - -class MaximizeWindowButton extends WindowButton { - MaximizeWindowButton( - {super.key, super.colors, super.onPressed, bool? animate}) - : super( - animate: animate ?? false, - iconBuilder: (buttonContext) => - MaximizeIcon(color: buttonContext.iconColor), - ); -} - -class RestoreWindowButton extends WindowButton { - RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate}) - : super( - animate: animate ?? false, - iconBuilder: (buttonContext) => - RestoreIcon(color: buttonContext.iconColor), - ); -} - -final _defaultCloseButtonColors = WindowButtonColors( - mouseOver: const Color(0xFFD32F2F), - mouseDown: const Color(0xFFB71C1C), - iconNormal: const Color(0xFF805306), - iconMouseOver: const Color(0xFFFFFFFF)); - -class CloseWindowButton extends WindowButton { - CloseWindowButton( - {super.key, WindowButtonColors? colors, super.onPressed, bool? animate}) - : super( - colors: colors ?? _defaultCloseButtonColors, - animate: animate ?? false, - iconBuilder: (buttonContext) => - CloseIcon(color: buttonContext.iconColor), - ); -} - -// Switched to CustomPaint icons by https://github.com/esDotDev - -/// Close -class CloseIcon extends StatelessWidget { - final Color color; - const CloseIcon({super.key, required this.color}); - @override - Widget build(BuildContext context) => Align( - alignment: Alignment.topLeft, - child: Stack(children: [ - // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason. - Transform.rotate( - angle: pi * .25, - child: - Center(child: Container(width: 14, height: 1, color: color))), - Transform.rotate( - angle: pi * -.25, - child: - Center(child: Container(width: 14, height: 1, color: color))), - ]), - ); -} - -/// Maximize -class MaximizeIcon extends StatelessWidget { - final Color color; - const MaximizeIcon({super.key, required this.color}); - @override - Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color)); -} - -class _MaximizePainter extends _IconPainter { - _MaximizePainter(super.color); - @override - void paint(Canvas canvas, Size size) { - Paint p = getPaint(color); - canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p); - } -} - -/// Restore -class RestoreIcon extends StatelessWidget { - final Color color; - const RestoreIcon({ - super.key, - required this.color, - }); - @override - Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color)); -} - -class _RestorePainter extends _IconPainter { - _RestorePainter(super.color); - @override - void paint(Canvas canvas, Size size) { - Paint p = getPaint(color); - canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p); - canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p); - canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p); - canvas.drawLine( - Offset(size.width, 0), Offset(size.width, size.height - 2), p); - canvas.drawLine(Offset(size.width, size.height - 2), - Offset(size.width - 2, size.height - 2), p); - } -} - -/// Minimize -class MinimizeIcon extends StatelessWidget { - final Color color; - const MinimizeIcon({super.key, required this.color}); - @override - Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color)); -} - -class _MinimizePainter extends _IconPainter { - _MinimizePainter(super.color); - @override - void paint(Canvas canvas, Size size) { - Paint p = getPaint(color); - canvas.drawLine( - Offset(0, size.height / 2), Offset(size.width, size.height / 2), p); - } -} - -/// Helpers -abstract class _IconPainter extends CustomPainter { - _IconPainter(this.color); - final Color color; - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -class _AlignedPaint extends StatelessWidget { - const _AlignedPaint(this.painter); - final CustomPainter painter; - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.center, - child: CustomPaint(size: const Size(10, 10), painter: painter)); - } -} - -Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint() - ..color = color - ..style = PaintingStyle.stroke - ..isAntiAlias = isAntiAlias - ..strokeWidth = 1; - -typedef MouseStateBuilderCB = Widget Function( - BuildContext context, MouseState mouseState); - -class MouseState { - bool isMouseOver = false; - bool isMouseDown = false; - MouseState(); - @override - String toString() { - return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver"; - } -} - -T? _ambiguate(T? value) => value; - -class MouseStateBuilder extends StatefulWidget { - final MouseStateBuilderCB builder; - final VoidCallback? onPressed; - const MouseStateBuilder({super.key, required this.builder, this.onPressed}); - @override - // ignore: library_private_types_in_public_api - _MouseStateBuilderState createState() => _MouseStateBuilderState(); -} - -class _MouseStateBuilderState extends State { - late MouseState _mouseState; - _MouseStateBuilderState() { - _mouseState = MouseState(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (event) { - setState(() { - _mouseState.isMouseOver = true; - }); - }, - onExit: (event) { - setState(() { - _mouseState.isMouseOver = false; - }); - }, - child: GestureDetector( - onTapDown: (_) { - setState(() { - _mouseState.isMouseDown = true; - }); - }, - onTapCancel: () { - setState(() { - _mouseState.isMouseDown = false; - }); - }, - onTap: () { - setState(() { - _mouseState.isMouseDown = false; - _mouseState.isMouseOver = false; - }); - _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { - if (widget.onPressed != null) { - widget.onPressed!(); - } - }); - }, - onTapUp: (_) {}, - child: widget.builder(context, _mouseState))); - } -} diff --git a/lib/components/titlebar/mouse_state.dart b/lib/components/titlebar/mouse_state.dart new file mode 100644 index 000000000..726c6595e --- /dev/null +++ b/lib/components/titlebar/mouse_state.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +typedef MouseStateBuilderCB = Widget Function( + BuildContext context, MouseState mouseState); + +class MouseState { + bool isMouseOver = false; + bool isMouseDown = false; + MouseState(); + @override + String toString() { + return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver"; + } +} + +T? _ambiguate(T? value) => value; + +class MouseStateBuilder extends StatefulWidget { + final MouseStateBuilderCB builder; + final VoidCallback? onPressed; + const MouseStateBuilder({super.key, required this.builder, this.onPressed}); + @override + // ignore: library_private_types_in_public_api + _MouseStateBuilderState createState() => _MouseStateBuilderState(); +} + +class _MouseStateBuilderState extends State { + late MouseState _mouseState; + _MouseStateBuilderState() { + _mouseState = MouseState(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) { + setState(() { + _mouseState.isMouseOver = true; + }); + }, + onExit: (event) { + setState(() { + _mouseState.isMouseOver = false; + }); + }, + child: GestureDetector( + onTapDown: (_) { + setState(() { + _mouseState.isMouseDown = true; + }); + }, + onTapCancel: () { + setState(() { + _mouseState.isMouseDown = false; + }); + }, + onTap: () { + setState(() { + _mouseState.isMouseDown = false; + _mouseState.isMouseOver = false; + }); + _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { + if (widget.onPressed != null) { + widget.onPressed!(); + } + }); + }, + onTapUp: (_) {}, + child: widget.builder(context, _mouseState))); + } +} diff --git a/lib/components/titlebar/titlebar.dart b/lib/components/titlebar/titlebar.dart new file mode 100644 index 000000000..76a5ec8a5 --- /dev/null +++ b/lib/components/titlebar/titlebar.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/titlebar/titlebar_buttons.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; + +import 'package:window_manager/window_manager.dart'; + +class PageWindowTitleBar extends StatefulHookConsumerWidget + implements PreferredSizeWidget { + final Widget? leading; + final bool automaticallyImplyLeading; + final List? actions; + final Color? backgroundColor; + final Color? foregroundColor; + final IconThemeData? actionsIconTheme; + final bool? centerTitle; + final double? titleSpacing; + final double toolbarOpacity; + final double? leadingWidth; + final TextStyle? toolbarTextStyle; + final TextStyle? titleTextStyle; + final double? titleWidth; + final Widget? title; + + final bool _sliver; + + const PageWindowTitleBar({ + super.key, + this.actions, + this.title, + this.toolbarOpacity = 1, + this.backgroundColor, + this.actionsIconTheme, + this.automaticallyImplyLeading = false, + this.centerTitle, + this.foregroundColor, + this.leading, + this.leadingWidth, + this.titleSpacing, + this.titleTextStyle, + this.titleWidth, + this.toolbarTextStyle, + }) : _sliver = false, + pinned = false, + floating = false, + snap = false, + stretch = false; + + final bool pinned; + final bool floating; + final bool snap; + final bool stretch; + + const PageWindowTitleBar.sliver({ + super.key, + this.actions, + this.title, + this.backgroundColor, + this.actionsIconTheme, + this.automaticallyImplyLeading = false, + this.centerTitle, + this.foregroundColor, + this.leading, + this.leadingWidth, + this.titleSpacing, + this.titleTextStyle, + this.titleWidth, + this.toolbarTextStyle, + this.pinned = false, + this.floating = false, + this.snap = false, + this.stretch = false, + }) : _sliver = true, + toolbarOpacity = 1; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + ConsumerState createState() => _PageWindowTitleBarState(); +} + +class _PageWindowTitleBarState extends ConsumerState { + void onDrag(details) { + final systemTitleBar = + ref.read(userPreferencesProvider.select((s) => s.systemTitleBar)); + if (kIsDesktop && !systemTitleBar) { + windowManager.startDragging(); + } + } + + @override + Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + + if (widget._sliver) { + return SliverLayoutBuilder( + builder: (context, constraints) { + final hasFullscreen = + mediaQuery.size.width == constraints.crossAxisExtent; + final hasLeadingOrCanPop = + widget.leading != null || Navigator.canPop(context); + + return SliverPadding( + padding: EdgeInsets.only( + left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0, + ), + sliver: SliverAppBar( + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + actions: [ + ...?widget.actions, + WindowTitleBarButtons(foregroundColor: widget.foregroundColor), + ], + backgroundColor: widget.backgroundColor, + foregroundColor: widget.foregroundColor, + actionsIconTheme: widget.actionsIconTheme, + centerTitle: widget.centerTitle, + titleSpacing: widget.titleSpacing, + leadingWidth: widget.leadingWidth, + toolbarTextStyle: widget.toolbarTextStyle, + titleTextStyle: widget.titleTextStyle, + title: SizedBox( + width: double.infinity, // workaround to force dragging + child: widget.title ?? const Text(""), + ), + pinned: widget.pinned, + floating: widget.floating, + snap: widget.snap, + stretch: widget.stretch, + ), + ); + }, + ); + } + + return LayoutBuilder(builder: (context, constrains) { + final hasFullscreen = mediaQuery.size.width == constrains.maxWidth; + final hasLeadingOrCanPop = + widget.leading != null || Navigator.canPop(context); + + return GestureDetector( + onHorizontalDragStart: onDrag, + onVerticalDragStart: onDrag, + child: Padding( + padding: EdgeInsets.only( + left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0, + ), + child: AppBar( + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + actions: [ + ...?widget.actions, + WindowTitleBarButtons(foregroundColor: widget.foregroundColor), + ], + backgroundColor: widget.backgroundColor, + foregroundColor: widget.foregroundColor, + actionsIconTheme: widget.actionsIconTheme, + centerTitle: widget.centerTitle, + titleSpacing: widget.titleSpacing, + toolbarOpacity: widget.toolbarOpacity, + leadingWidth: widget.leadingWidth, + toolbarTextStyle: widget.toolbarTextStyle, + titleTextStyle: widget.titleTextStyle, + title: SizedBox( + width: double.infinity, // workaround to force dragging + child: widget.title ?? const Text(""), + ), + scrolledUnderElevation: 0, + shadowColor: Colors.transparent, + forceMaterialTransparency: true, + elevation: 0, + ), + ), + ); + }); + } +} diff --git a/lib/components/titlebar/titlebar_buttons.dart b/lib/components/titlebar/titlebar_buttons.dart new file mode 100644 index 000000000..425bf2f16 --- /dev/null +++ b/lib/components/titlebar/titlebar_buttons.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/titlebar/titlebar_icon_buttons.dart'; +import 'package:spotube/components/titlebar/window_button.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:titlebar_buttons/titlebar_buttons.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowTitleBarButtons extends HookConsumerWidget { + final Color? foregroundColor; + const WindowTitleBarButtons({ + super.key, + this.foregroundColor, + }); + + @override + Widget build(BuildContext context, ref) { + final preferences = ref.watch(userPreferencesProvider); + final isMaximized = useState(null); + const type = ThemeType.auto; + + Future onClose() async { + await windowManager.close(); + } + + useEffect(() { + if (kIsDesktop) { + windowManager.isMaximized().then((value) { + isMaximized.value = value; + }); + } + return null; + }, []); + + if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) { + return const SizedBox.shrink(); + } + + if (kIsWindows) { + 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, + ); + + final closeColors = WindowButtonColors( + normal: Colors.transparent, + iconNormal: foregroundColor ?? theme.colorScheme.onBackground, + mouseOver: Colors.red, + mouseDown: Colors.red[800]!, + iconMouseOver: Colors.white, + iconMouseDown: Colors.black, + ); + + return Padding( + padding: const EdgeInsets.only(bottom: 25), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MinimizeWindowButton( + onPressed: windowManager.minimize, + colors: colors, + ), + if (isMaximized.value != true) + MaximizeWindowButton( + colors: colors, + onPressed: () { + windowManager.maximize(); + isMaximized.value = true; + }, + ) + else + RestoreWindowButton( + colors: colors, + onPressed: () { + windowManager.unmaximize(); + isMaximized.value = false; + }, + ), + CloseWindowButton( + colors: closeColors, + onPressed: onClose, + ), + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 20, left: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DecoratedMinimizeButton( + type: type, + onPressed: windowManager.minimize, + ), + DecoratedMaximizeButton( + type: type, + onPressed: () async { + if (await windowManager.isMaximized()) { + await windowManager.unmaximize(); + isMaximized.value = false; + } else { + await windowManager.maximize(); + isMaximized.value = true; + } + }, + ), + DecoratedCloseButton( + type: type, + onPressed: onClose, + ), + ], + ), + ); + } +} diff --git a/lib/components/titlebar/titlebar_icon_buttons.dart b/lib/components/titlebar/titlebar_icon_buttons.dart new file mode 100644 index 000000000..701702623 --- /dev/null +++ b/lib/components/titlebar/titlebar_icon_buttons.dart @@ -0,0 +1,161 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:spotube/components/titlebar/window_button.dart'; + +class MinimizeWindowButton extends WindowButton { + MinimizeWindowButton( + {super.key, super.colors, super.onPressed, bool? animate}) + : super( + animate: animate ?? false, + iconBuilder: (buttonContext) => + MinimizeIcon(color: buttonContext.iconColor), + ); +} + +class MaximizeWindowButton extends WindowButton { + MaximizeWindowButton( + {super.key, super.colors, super.onPressed, bool? animate}) + : super( + animate: animate ?? false, + iconBuilder: (buttonContext) => + MaximizeIcon(color: buttonContext.iconColor), + ); +} + +class RestoreWindowButton extends WindowButton { + RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate}) + : super( + animate: animate ?? false, + iconBuilder: (buttonContext) => + RestoreIcon(color: buttonContext.iconColor), + ); +} + +final _defaultCloseButtonColors = WindowButtonColors( + mouseOver: const Color(0xFFD32F2F), + mouseDown: const Color(0xFFB71C1C), + iconNormal: const Color(0xFF805306), + iconMouseOver: const Color(0xFFFFFFFF)); + +class CloseWindowButton extends WindowButton { + CloseWindowButton( + {super.key, WindowButtonColors? colors, super.onPressed, bool? animate}) + : super( + colors: colors ?? _defaultCloseButtonColors, + animate: animate ?? false, + iconBuilder: (buttonContext) => + CloseIcon(color: buttonContext.iconColor), + ); +} + +// Switched to CustomPaint icons by https://github.com/esDotDev + +/// Close +class CloseIcon extends StatelessWidget { + final Color color; + const CloseIcon({super.key, required this.color}); + @override + Widget build(BuildContext context) => Align( + alignment: Alignment.topLeft, + child: Stack(children: [ + // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason. + Transform.rotate( + angle: pi * .25, + child: + Center(child: Container(width: 14, height: 1, color: color))), + Transform.rotate( + angle: pi * -.25, + child: + Center(child: Container(width: 14, height: 1, color: color))), + ]), + ); +} + +/// Maximize +class MaximizeIcon extends StatelessWidget { + final Color color; + const MaximizeIcon({super.key, required this.color}); + @override + Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color)); +} + +class _MaximizePainter extends _IconPainter { + _MaximizePainter(super.color); + @override + void paint(Canvas canvas, Size size) { + Paint p = getPaint(color); + canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p); + } +} + +/// Restore +class RestoreIcon extends StatelessWidget { + final Color color; + const RestoreIcon({ + super.key, + required this.color, + }); + @override + Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color)); +} + +class _RestorePainter extends _IconPainter { + _RestorePainter(super.color); + @override + void paint(Canvas canvas, Size size) { + Paint p = getPaint(color); + canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p); + canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p); + canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p); + canvas.drawLine( + Offset(size.width, 0), Offset(size.width, size.height - 2), p); + canvas.drawLine(Offset(size.width, size.height - 2), + Offset(size.width - 2, size.height - 2), p); + } +} + +/// Minimize +class MinimizeIcon extends StatelessWidget { + final Color color; + const MinimizeIcon({super.key, required this.color}); + @override + Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color)); +} + +class _MinimizePainter extends _IconPainter { + _MinimizePainter(super.color); + @override + void paint(Canvas canvas, Size size) { + Paint p = getPaint(color); + canvas.drawLine( + Offset(0, size.height / 2), Offset(size.width, size.height / 2), p); + } +} + +/// Helpers +abstract class _IconPainter extends CustomPainter { + _IconPainter(this.color); + final Color color; + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class _AlignedPaint extends StatelessWidget { + const _AlignedPaint(this.painter); + final CustomPainter painter; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: CustomPaint(size: const Size(10, 10), painter: painter)); + } +} + +Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint() + ..color = color + ..style = PaintingStyle.stroke + ..isAntiAlias = isAntiAlias + ..strokeWidth = 1; diff --git a/lib/components/titlebar/window_button.dart b/lib/components/titlebar/window_button.dart new file mode 100644 index 000000000..3201d191a --- /dev/null +++ b/lib/components/titlebar/window_button.dart @@ -0,0 +1,133 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:spotube/components/titlebar/mouse_state.dart'; + +typedef WindowButtonIconBuilder = Widget Function( + WindowButtonContext buttonContext); +typedef WindowButtonBuilder = Widget Function( + WindowButtonContext buttonContext, Widget icon); + +class WindowButtonContext { + BuildContext context; + MouseState mouseState; + Color? backgroundColor; + Color iconColor; + WindowButtonContext( + {required this.context, + required this.mouseState, + this.backgroundColor, + required this.iconColor}); +} + +class WindowButtonColors { + late Color normal; + late Color mouseOver; + late Color mouseDown; + late Color iconNormal; + late Color iconMouseOver; + late Color iconMouseDown; + WindowButtonColors( + {Color? normal, + Color? mouseOver, + Color? mouseDown, + Color? iconNormal, + Color? iconMouseOver, + Color? iconMouseDown}) { + this.normal = normal ?? _defaultButtonColors.normal; + this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver; + this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown; + this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal; + this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver; + this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown; + } +} + +final _defaultButtonColors = WindowButtonColors( + normal: Colors.transparent, + iconNormal: const Color(0xFF805306), + mouseOver: const Color(0xFF404040), + mouseDown: const Color(0xFF202020), + iconMouseOver: const Color(0xFFFFFFFF), + iconMouseDown: const Color(0xFFF0F0F0), +); + +class WindowButton extends StatelessWidget { + final WindowButtonBuilder? builder; + final WindowButtonIconBuilder? iconBuilder; + late final WindowButtonColors colors; + final bool animate; + final EdgeInsets? padding; + final VoidCallback? onPressed; + + WindowButton( + {super.key, + WindowButtonColors? colors, + this.builder, + @required this.iconBuilder, + this.padding, + this.onPressed, + this.animate = false}) { + this.colors = colors ?? _defaultButtonColors; + } + + Color getBackgroundColor(MouseState mouseState) { + if (mouseState.isMouseDown) return colors.mouseDown; + if (mouseState.isMouseOver) return colors.mouseOver; + return colors.normal; + } + + Color getIconColor(MouseState mouseState) { + if (mouseState.isMouseDown) return colors.iconMouseDown; + if (mouseState.isMouseOver) return colors.iconMouseOver; + return colors.iconNormal; + } + + @override + Widget build(BuildContext context) { + if (kIsWeb) { + return Container(); + } else { + // Don't show button on macOS + if (Platform.isMacOS) { + return Container(); + } + } + + return MouseStateBuilder( + builder: (context, mouseState) { + WindowButtonContext buttonContext = WindowButtonContext( + mouseState: mouseState, + context: context, + backgroundColor: getBackgroundColor(mouseState), + iconColor: getIconColor(mouseState)); + + var icon = + (iconBuilder != null) ? iconBuilder!(buttonContext) : Container(); + + var fadeOutColor = + getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0); + var padding = this.padding ?? const EdgeInsets.all(10); + var animationMs = + mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0); + Widget iconWithPadding = Padding(padding: padding, child: icon); + iconWithPadding = AnimatedContainer( + curve: Curves.easeOut, + duration: Duration(milliseconds: animationMs), + color: buttonContext.backgroundColor ?? fadeOutColor, + child: iconWithPadding); + var button = + (builder != null) ? builder!(buttonContext, icon) : iconWithPadding; + return SizedBox( + width: 45, + height: 32, + child: button, + ); + }, + onPressed: () { + if (onPressed != null) onPressed!(); + }, + ); + } +} diff --git a/lib/components/tracks_view/track_view.dart b/lib/components/tracks_view/track_view.dart index 36d334cda..2a3f5237e 100644 --- a/lib/components/tracks_view/track_view.dart +++ b/lib/components/tracks_view/track_view.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/tracks_view/sections/header/flexible_header.dart'; import 'package:spotube/components/tracks_view/sections/body/track_view_body.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 7eaf53ae2..6a8a3e52d 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -13,7 +13,7 @@ import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/components/animated_gradient.dart'; import 'package:spotube/components/dialogs/track_details_dialog.dart'; import 'package:spotube/components/links/artist_link.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/panels/sliding_up_panel.dart'; import 'package:spotube/extensions/artist_simple.dart'; diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index d38fe778d..04389ffc0 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/artist/artist_album_list.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index 1e4e3938d..d3b0d0cbb 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -3,7 +3,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/connect/local_devices.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/connect/control/control.dart'; import 'package:spotube/provider/connect/clients.dart'; diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index afd173876..eb2c48c55 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -8,7 +8,7 @@ import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/components/links/artist_link.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index 1f7cb1b5b..805488987 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/modules/desktop_login/login_form.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart'; diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index d96942e41..d78143e49 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -6,7 +6,7 @@ import 'package:introduction_screen/introduction_screen.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/modules/desktop_login/login_form.dart'; import 'package:spotube/components/links/hyper_link.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/home/home.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/pages/getting_started/getting_started.dart b/lib/pages/getting_started/getting_started.dart index 97af2ef4e..0159a77f4 100644 --- a/lib/pages/getting_started/getting_started.dart +++ b/lib/pages/getting_started/getting_started.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/getting_started/sections/greeting.dart'; import 'package:spotube/pages/getting_started/sections/playback.dart'; diff --git a/lib/pages/home/feed/feed_section.dart b/lib/pages/home/feed/feed_section.dart index 42a22f102..bcfc0b81f 100644 --- a/lib/pages/home/feed/feed_section.dart +++ b/lib/pages/home/feed/feed_section.dart @@ -5,7 +5,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/album/album_card.dart'; import 'package:spotube/modules/artist/artist_card.dart'; import 'package:spotube/modules/playlist/playlist_card.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/views/home_section.dart'; diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index 4ad376307..58436bcf1 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -7,7 +7,7 @@ import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/spotify/spotify.dart'; diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index cf033bb90..4846d633c 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -6,7 +6,7 @@ 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/gradients.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/home/genres/genre_playlists.dart'; diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 27b4cc017..7afd59386 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -12,7 +12,7 @@ import 'package:spotube/modules/home/sections/genres.dart'; import 'package:spotube/modules/home/sections/made_for_user.dart'; import 'package:spotube/modules/home/sections/new_releases.dart'; import 'package:spotube/modules/home/sections/recent.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index 1f4c64e11..da2e4e131 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -5,7 +5,7 @@ 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/dialogs/prompt_dialog.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index 664777619..a0bc1bb7e 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart' hide Image; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/modules/library/user_local_tracks.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/library/user_albums.dart'; import 'package:spotube/modules/library/user_artists.dart'; import 'package:spotube/modules/library/user_downloads.dart'; diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 648b3c504..830e8a5d0 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -10,7 +10,7 @@ import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/sort_tracks_dropdown.dart'; import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/artist_simple.dart'; diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 74b7fe260..c73c0b085 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -12,7 +12,7 @@ import 'package:spotube/modules/library/playlist_generate/recommendation_attribu import 'package:spotube/modules/library/playlist_generate/seeds_multi_autocomplete.dart'; import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index a481eac42..90838300c 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/pages/playlist/playlist.dart'; diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 1a4ea7c16..f75c715c7 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/extensions/constrains.dart'; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index bd8fc0a1e..603f90d30 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -9,7 +9,7 @@ import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/root/sidebar.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index 06f6848a2..e6546960c 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -8,7 +8,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 828e4aefc..4f53f8e6a 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -12,7 +12,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 2692bfdca..4d093cfe5 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -4,7 +4,7 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/hyper_link.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_package_info.dart'; diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index c30864fe1..b5e108216 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/blacklist_provider.dart'; diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index a790c39d0..65e4c82ed 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -6,7 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 2d1c9224d..8bce4bcfd 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/settings/sections/about.dart'; import 'package:spotube/pages/settings/sections/accounts.dart'; diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 81eba384d..868f068a5 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index f54453265..b3f8c2401 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 5aa06af90..ee1414757 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -2,7 +2,7 @@ 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/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 9ce84548e..ea0a0c10d 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -2,7 +2,7 @@ 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/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index bf1fee932..d31f1dfa0 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/stats/stats.dart b/lib/pages/stats/stats.dart index 7dfab8447..b2dc03c25 100644 --- a/lib/pages/stats/stats.dart +++ b/lib/pages/stats/stats.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/summary/summary.dart'; import 'package:spotube/modules/stats/top/top.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 126318161..3df344836 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -2,7 +2,7 @@ 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/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/state.dart'; import 'package:spotube/provider/history/top.dart'; diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index f797c2f29..b5c9e4fa7 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -10,7 +10,7 @@ import 'package:spotube/components/heart_button.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/link_text.dart'; -import 'package:spotube/components/page_window_title_bar.dart'; +import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; From 7816cb8068ce97c2eef1d01a17f0786b2428998e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Jun 2024 09:30:17 +0600 Subject: [PATCH 06/92] refactor: break down heart button hook into a different file --- .../{ => heart_button}/heart_button.dart | 35 +----------- .../heart_button/use_track_toggle_like.dart | 37 ++++++++++++ lib/components/titlebar/mouse_state.dart | 56 ++++++++++--------- lib/components/track_tile/track_options.dart | 2 +- .../sections/header/header_actions.dart | 2 +- lib/modules/player/player_actions.dart | 2 +- lib/pages/track/track.dart | 2 +- 7 files changed, 71 insertions(+), 65 deletions(-) rename lib/components/{ => heart_button}/heart_button.dart (70%) create mode 100644 lib/components/heart_button/use_track_toggle_like.dart diff --git a/lib/components/heart_button.dart b/lib/components/heart_button/heart_button.dart similarity index 70% rename from lib/components/heart_button.dart rename to lib/components/heart_button/heart_button.dart index c296d7a9c..8222b8e6e 100644 --- a/lib/components/heart_button.dart +++ b/lib/components/heart_button/heart_button.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/heart_button/use_track_toggle_like.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HeartButton extends HookConsumerWidget { @@ -55,38 +54,6 @@ class HeartButton extends HookConsumerWidget { } } -typedef UseTrackToggleLike = ({ - bool isLiked, - Future Function(Track track) toggleTrackLike, -}); - -UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { - final savedTracks = ref.watch(likedTracksProvider); - final savedTracksNotifier = ref.watch(likedTracksProvider.notifier); - - final isLiked = useMemoized( - () => - savedTracks.asData?.value.any((element) => element.id == track.id) ?? - false, - [savedTracks.asData?.value, track.id], - ); - - final scrobblerNotifier = ref.read(scrobblerProvider.notifier); - - return ( - isLiked: isLiked, - toggleTrackLike: (track) async { - await savedTracksNotifier.toggleFavorite(track); - - if (!isLiked) { - await scrobblerNotifier.love(track); - } else { - await scrobblerNotifier.unlove(track); - } - }, - ); -} - class TrackHeartButton extends HookConsumerWidget { final Track track; const TrackHeartButton({ diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart new file mode 100644 index 000000000..2a886febc --- /dev/null +++ b/lib/components/heart_button/use_track_toggle_like.dart @@ -0,0 +1,37 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef UseTrackToggleLike = ({ + bool isLiked, + Future Function(Track track) toggleTrackLike, +}); + +UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { + final savedTracks = ref.watch(likedTracksProvider); + final savedTracksNotifier = ref.watch(likedTracksProvider.notifier); + + final isLiked = useMemoized( + () => + savedTracks.asData?.value.any((element) => element.id == track.id) ?? + false, + [savedTracks.asData?.value, track.id], + ); + + final scrobblerNotifier = ref.read(scrobblerProvider.notifier); + + return ( + isLiked: isLiked, + toggleTrackLike: (track) async { + await savedTracksNotifier.toggleFavorite(track); + + if (!isLiked) { + await scrobblerNotifier.love(track); + } else { + await scrobblerNotifier.unlove(track); + } + }, + ); +} diff --git a/lib/components/titlebar/mouse_state.dart b/lib/components/titlebar/mouse_state.dart index 726c6595e..9af2a8b0b 100644 --- a/lib/components/titlebar/mouse_state.dart +++ b/lib/components/titlebar/mouse_state.dart @@ -33,39 +33,41 @@ class _MouseStateBuilderState extends State { @override Widget build(BuildContext context) { return MouseRegion( - onEnter: (event) { + onEnter: (event) { + setState(() { + _mouseState.isMouseOver = true; + }); + }, + onExit: (event) { + setState(() { + _mouseState.isMouseOver = false; + }); + }, + child: GestureDetector( + onTapDown: (_) { setState(() { - _mouseState.isMouseOver = true; + _mouseState.isMouseDown = true; }); }, - onExit: (event) { + onTapCancel: () { setState(() { + _mouseState.isMouseDown = false; + }); + }, + onTap: () { + setState(() { + _mouseState.isMouseDown = false; _mouseState.isMouseOver = false; }); + _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { + if (widget.onPressed != null) { + widget.onPressed!(); + } + }); }, - child: GestureDetector( - onTapDown: (_) { - setState(() { - _mouseState.isMouseDown = true; - }); - }, - onTapCancel: () { - setState(() { - _mouseState.isMouseDown = false; - }); - }, - onTap: () { - setState(() { - _mouseState.isMouseDown = false; - _mouseState.isMouseOver = false; - }); - _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { - if (widget.onPressed != null) { - widget.onPressed!(); - } - }); - }, - onTapUp: (_) {}, - child: widget.builder(context, _mouseState))); + onTapUp: (_) {}, + child: widget.builder(context, _mouseState), + ), + ); } } diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index de6047445..89f6679da 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -12,7 +12,7 @@ import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/dialogs/track_details_dialog.dart'; -import 'package:spotube/components/heart_button.dart'; +import 'package:spotube/components/heart_button/use_track_toggle_like.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/constrains.dart'; diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index a1e959d94..3e0c4cc13 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/heart_button.dart'; +import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index df3664858..41de7388e 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; -import 'package:spotube/components/heart_button.dart'; +import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index b5c9e4fa7..1e9b2067a 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -6,7 +6,7 @@ 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/heart_button.dart'; +import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/link_text.dart'; From d115e570580fb06e630c296167bd5131f7e5863b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Jun 2024 09:56:29 +0600 Subject: [PATCH 07/92] fix: popup menu item opacity --- lib/components/adaptive/adaptive_pop_sheet_list.dart | 5 ++++- lib/themes/theme.dart | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/components/adaptive/adaptive_pop_sheet_list.dart b/lib/components/adaptive/adaptive_pop_sheet_list.dart index 21f56a220..1686801c9 100644 --- a/lib/components/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/adaptive/adaptive_pop_sheet_list.dart @@ -226,7 +226,10 @@ class _AdaptivePopSheetListItem extends StatelessWidget { }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: IgnorePointer(child: item), + child: IconTheme.merge( + data: const IconThemeData(opacity: 1), + child: IgnorePointer(child: item), + ), ), ); } diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 8659cf0c5..1129b7915 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -48,6 +48,9 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), color: scheme.surface, elevation: 4, + labelTextStyle: MaterialStatePropertyAll( + TextStyle(color: scheme.onSurface), + ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, From f9087b63d5db6837495239a4c188aca06411f997 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Jun 2024 22:52:34 +0600 Subject: [PATCH 08/92] refactor: remove catcher_2 and use custom zoned based error handling --- lib/collections/routes.dart | 3 +- .../configurators/use_endless_playback.dart | 4 +- lib/main.dart | 127 +++++++----------- lib/provider/authentication_provider.dart | 6 +- lib/provider/connect/connect.dart | 9 +- lib/provider/connect/server.dart | 4 +- lib/provider/download_manager_provider.dart | 6 +- .../local_tracks/local_tracks_provider.dart | 8 +- lib/provider/piped_instances_provider.dart | 4 +- .../proxy_playlist/player_listeners.dart | 4 +- .../proxy_playlist/skip_segments.dart | 4 +- lib/provider/scrobbler_provider.dart | 4 +- lib/provider/server/server.dart | 4 +- lib/provider/spotify/lyrics/synced.dart | 2 +- lib/provider/spotify/playlist/generate.dart | 2 +- lib/provider/spotify/spotify.dart | 2 +- lib/services/audio_player/audio_player.dart | 4 +- lib/services/audio_player/custom_player.dart | 4 +- .../download_manager/download_manager.dart | 4 +- lib/services/logger/logger.dart | 53 ++++++++ lib/services/song_link/song_link.dart | 4 +- .../sourced_track/sources/youtube.dart | 4 +- lib/utils/duration.dart | 4 +- 23 files changed, 148 insertions(+), 122 deletions(-) create mode 100644 lib/services/logger/logger.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index e1cc5fb6b..b9e06c611 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -1,4 +1,3 @@ -import 'package:catcher_2/catcher_2.dart'; import 'package:flutter/foundation.dart' hide Category; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; @@ -46,7 +45,7 @@ import 'package:spotube/pages/root/root_app.dart'; import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart'; -final rootNavigatorKey = Catcher2.navigatorKey; +final rootNavigatorKey = GlobalKey(); final shellRouteNavigatorKey = GlobalKey(); final routerProvider = Provider((ref) { return GoRouter( diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index 98f38165a..97eb3f484 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -1,4 +1,4 @@ -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -62,7 +62,7 @@ void useEndlessPlayback(WidgetRef ref) { }), ); } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); } } diff --git a/lib/main.dart b/lib/main.dart index 30526bc60..75d2ada51 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ -import 'package:catcher_2/catcher_2.dart'; +import 'dart:async'; + import 'package:dart_discord_rpc/dart_discord_rpc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -19,7 +20,6 @@ import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.da 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'; import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/connect/clients.dart'; @@ -30,6 +30,7 @@ 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/logger/logger.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/themes/theme.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; @@ -44,98 +45,73 @@ import 'package:window_manager/window_manager.dart'; Future main(List rawArgs) async { final arguments = await startCLI(rawArgs); + AppLogger.initialize(arguments["verbose"]); - final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + AppLogger.runZoned(() async { + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - await registerWindowsScheme("spotify"); + await registerWindowsScheme("spotify"); - tz.initializeTimeZones(); + tz.initializeTimeZones(); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - MediaKit.ensureInitialized(); + MediaKit.ensureInitialized(); - // force High Refresh Rate on some Android devices (like One Plus) - if (kIsAndroid) { - await FlutterDisplayMode.setHighRefreshRate(); - } + // force High Refresh Rate on some Android devices (like One Plus) + if (kIsAndroid) { + await FlutterDisplayMode.setHighRefreshRate(); + } - if (kIsDesktop) { - await windowManager.setPreventClose(true); - } + if (kIsDesktop) { + await windowManager.setPreventClose(true); + } - await SystemTheme.accentColor.load(); + await SystemTheme.accentColor.load(); - if (!kIsWeb) { - MetadataGod.initialize(); - } + if (!kIsWeb) { + MetadataGod.initialize(); + } - if (kIsWindows || kIsLinux) { - DiscordRPC.initialize(); - } + if (kIsWindows || kIsLinux) { + DiscordRPC.initialize(); + } - await KVStoreService.initialize(); + await KVStoreService.initialize(); - final hiveCacheDir = - kIsWeb ? null : (await getApplicationSupportDirectory()).path; + final hiveCacheDir = + kIsWeb ? null : (await getApplicationSupportDirectory()).path; - Hive.init(hiveCacheDir); + Hive.init(hiveCacheDir); - Hive.registerAdapter(SkipSegmentAdapter()); + Hive.registerAdapter(SkipSegmentAdapter()); - Hive.registerAdapter(SourceMatchAdapter()); - Hive.registerAdapter(SourceTypeAdapter()); + Hive.registerAdapter(SourceMatchAdapter()); + Hive.registerAdapter(SourceTypeAdapter()); - // Cache versioning entities with Adapter - SourceMatch.version = 'v1'; - SkipSegment.version = 'v1'; + // Cache versioning entities with Adapter + SourceMatch.version = 'v1'; + SkipSegment.version = 'v1'; - await Hive.openLazyBox( - SourceMatch.boxName, - path: hiveCacheDir, - ); - await Hive.openLazyBox( - SkipSegment.boxName, - path: hiveCacheDir, - ); - await PersistedStateNotifier.initializeBoxes( - path: hiveCacheDir, - ); + await Hive.openLazyBox( + SourceMatch.boxName, + path: hiveCacheDir, + ); + await Hive.openLazyBox( + SkipSegment.boxName, + path: hiveCacheDir, + ); + await PersistedStateNotifier.initializeBoxes( + path: hiveCacheDir, + ); - if (kIsDesktop) { - await localNotifier.setup(appName: "Spotube"); - await WindowManagerTools.initialize(); - } + if (kIsDesktop) { + await localNotifier.setup(appName: "Spotube"); + await WindowManagerTools.initialize(); + } - Catcher2( - enableLogger: arguments["verbose"], - debugConfig: Catcher2Options( - SilentReportMode(), - [ - ConsoleHandler( - enableDeviceParameters: false, - enableApplicationParameters: false, - ), - if (!kIsWeb) FileHandler(await getLogsPath(), printLogs: false), - ], - ), - releaseConfig: Catcher2Options( - SilentReportMode(), - [ - if (arguments["verbose"] ?? false) ConsoleHandler(), - if (!kIsWeb) - FileHandler( - await getLogsPath(), - printLogs: false, - ), - ], - ), - runAppFunction: () { - runApp( - const ProviderScope(child: Spotube()), - ); - }, - ); + runApp(const ProviderScope(child: Spotube())); + }); } class Spotube extends HookConsumerWidget { @@ -166,6 +142,7 @@ class Spotube extends HookConsumerWidget { useEffect(() { FlutterNativeSplash.remove(); + return () { /// For enabling hot reload for audio player if (!kDebugMode) return; diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication_provider.dart index 95ce62409..52c7f281a 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication_provider.dart @@ -73,10 +73,10 @@ class AuthenticationCredentials { ), ); } catch (e) { - if (rootNavigatorKey?.currentContext != null) { + if (rootNavigatorKey.currentContext != null) { showPromptDialog( - context: rootNavigatorKey!.currentContext!, - title: rootNavigatorKey!.currentContext!.l10n + context: rootNavigatorKey.currentContext!, + title: rootNavigatorKey.currentContext!.l10n .error("Authentication Failure"), message: e.toString(), cancelText: null, diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index 6360c750b..feb9fbd2b 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/connect/connect.dart'; @@ -99,10 +99,7 @@ class ConnectNotifier extends AsyncNotifier { }); }, onError: (error) { - Catcher2.reportCheckedError( - error, - StackTrace.current, - ); + AppLogger.reportError(error, StackTrace.current); }, ); @@ -113,7 +110,7 @@ class ConnectNotifier extends AsyncNotifier { return channel; } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); rethrow; } } diff --git a/lib/provider/connect/server.dart b/lib/provider/connect/server.dart index 9c4e6466c..aeaaf1495 100644 --- a/lib/provider/connect/server.dart +++ b/lib/provider/connect/server.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -215,7 +215,7 @@ final connectServerProvider = FutureProvider((ref) async { ref.read(volumeProvider.notifier).setVolume(event.data); }); } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); channel.sink.add(WebSocketErrorEvent(e.toString()).toJson()); } }, diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index c964f982e..0e80d729b 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; @@ -130,7 +130,7 @@ class DownloadManagerProvider extends ChangeNotifier { return Uint8List.fromList(bytes); } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); return null; } } @@ -216,7 +216,7 @@ class DownloadManagerProvider extends ChangeNotifier { ); } } catch (e) { - Catcher2.reportCheckedError(e, StackTrace.current); + AppLogger.reportError(e, StackTrace.current); continue; } } diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart index 867774bd8..6d2da59c7 100644 --- a/lib/provider/local_tracks/local_tracks_provider.dart +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:metadata_god/metadata_god.dart'; @@ -56,7 +56,7 @@ final localTracksProvider = try { entities.addAll(Directory(location).listSync(recursive: true)); } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); } } @@ -92,7 +92,7 @@ final localTracksProvider = if (e is FfiException) { return {"file": file}; } - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); return {}; } }, @@ -119,7 +119,7 @@ final localTracksProvider = } return tracks; } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); return {}; } }); diff --git a/lib/provider/piped_instances_provider.dart b/lib/provider/piped_instances_provider.dart index d571f7308..3c5d5f043 100644 --- a/lib/provider/piped_instances_provider.dart +++ b/lib/provider/piped_instances_provider.dart @@ -1,4 +1,4 @@ -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart'; @@ -10,7 +10,7 @@ final pipedInstancesFutureProvider = FutureProvider>( return await pipedClient.instanceList(); } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); return []; } }, diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/proxy_playlist/player_listeners.dart index 3c36491cf..2c1423a5c 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/proxy_playlist/player_listeners.dart @@ -2,7 +2,7 @@ import 'dart:async'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/image.dart'; @@ -86,7 +86,7 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { history.addTrack(playlist.activeTrack!); lastScrobbled = uid; } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); } }); } diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 7f3d1e9a3..12d066acb 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -1,4 +1,4 @@ -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/skip_segment.dart'; @@ -71,7 +71,7 @@ Future> getAndCacheSkipSegments(String id) async { return List.castFrom(segments); } catch (e, stack) { await SkipSegment.box.put(id, []); - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); return List.castFrom([]); } } diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart index 9ad2a58ba..ab111ea45 100644 --- a/lib/provider/scrobbler_provider.dart +++ b/lib/provider/scrobbler_provider.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:spotify/spotify.dart'; @@ -52,7 +52,7 @@ class ScrobblerNotifier extends PersistedStateNotifier { trackNumber: track.trackNumber, ); } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); } }); } diff --git a/lib/provider/server/server.dart b/lib/provider/server/server.dart index 009cc534f..b6a7dfe95 100644 --- a/lib/provider/server/server.dart +++ b/lib/provider/server/server.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:math'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart' hide Response; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -108,7 +108,7 @@ class PlaybackServer { headers: res.headers.map, ); } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); return Response.internalServerError(); } } diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index 04a2ddca2..ef83a1a18 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -145,7 +145,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier return lyrics; } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); rethrow; } } diff --git a/lib/provider/spotify/playlist/generate.dart b/lib/provider/spotify/playlist/generate.dart index 15447b54f..2e1196dda 100644 --- a/lib/provider/spotify/playlist/generate.dart +++ b/lib/provider/spotify/playlist/generate.dart @@ -24,7 +24,7 @@ final generatePlaylistProvider = FutureProvider.autoDispose ?.cast(), ) .catchError((e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); return Recommendations(); }); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index ac83ba727..e592e93b3 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -2,7 +2,7 @@ library spotify; import 'dart:async'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 8d3e0bfb4..8b391a075 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; @@ -57,7 +57,7 @@ abstract class AudioPlayerInterface { ), ) { _mkPlayer.stream.error.listen((event) { - Catcher2.reportCheckedError(event, StackTrace.current); + AppLogger.reportError(event, StackTrace.current); }); } diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index e32a0d144..f0dc8f13c 100644 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:media_kit/media_kit.dart'; import 'package:flutter_broadcasts/flutter_broadcasts.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -48,7 +48,7 @@ class CustomPlayer extends Player { } }), stream.error.listen((event) { - Catcher2.reportCheckedError('[MediaKitError] \n$event', null); + AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current); }), ]; PackageInfo.fromPlatform().then((packageInfo) { diff --git a/lib/services/download_manager/download_manager.dart b/lib/services/download_manager/download_manager.dart index 54d35b02e..afbee88cb 100644 --- a/lib/services/download_manager/download_manager.dart +++ b/lib/services/download_manager/download_manager.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; @@ -148,7 +148,7 @@ class DownloadManager { } } } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); var task = getDownload(url)!; if (task.status.value != DownloadStatus.canceled && diff --git a/lib/services/logger/logger.dart b/lib/services/logger/logger.dart new file mode 100644 index 000000000..1a9f37717 --- /dev/null +++ b/lib/services/logger/logger.dart @@ -0,0 +1,53 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class AppLogger { + static late final Logger log; + + static initialize(bool verbose) { + log = Logger( + level: kDebugMode || (verbose && kReleaseMode) ? Level.all : Level.info, + ); + } + + static R? runZoned(R Function() body) { + FlutterError.onError = (details) { + reportError(details.exception, details.stack ?? StackTrace.current); + }; + + PlatformDispatcher.instance.onError = (error, stackTrace) { + reportError(error, stackTrace); + return true; + }; + + if (!kIsWeb) { + Isolate.current.addErrorListener( + RawReceivePort((pair) async { + final isolateError = pair as List; + reportError( + isolateError.first.toString(), + isolateError.last, + ); + }).sendPort, + ); + } + + return runZonedGuarded( + body, + (error, stackTrace) { + reportError(error, stackTrace); + }, + ); + } + + static void reportError( + dynamic error, [ + StackTrace? stackTrace, + message = "", + ]) { + log.e(message, error: error, stackTrace: stackTrace); + } +} diff --git a/lib/services/song_link/song_link.dart b/lib/services/song_link/song_link.dart index b02f60cbf..e3cffa521 100644 --- a/lib/services/song_link/song_link.dart +++ b/lib/services/song_link/song_link.dart @@ -2,7 +2,7 @@ library song_link; import 'dart:convert'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:html/parser.dart'; @@ -47,7 +47,7 @@ abstract class SongLinkService { return songLinks?.map((link) => SongLink.fromJson(link)).toList() ?? []; } catch (e, stackTrace) { - Catcher2.reportCheckedError(e, stackTrace); + AppLogger.reportError(e, stackTrace); return []; } } diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index af61a8820..b144d7015 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -1,9 +1,9 @@ -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'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/source_match.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/song_link/song_link.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; @@ -236,7 +236,7 @@ class YoutubeSourcedTrack extends SourcedTrack { ]; } on VideoUnplayableException catch (e, stack) { // Ignore this error and continue with the search - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); } } diff --git a/lib/utils/duration.dart b/lib/utils/duration.dart index a2bb4d165..1869cea15 100644 --- a/lib/utils/duration.dart +++ b/lib/utils/duration.dart @@ -1,4 +1,4 @@ -import 'package:catcher_2/catcher_2.dart'; +import 'package:spotube/services/logger/logger.dart'; /// Parses duration string formatted by Duration.toString() to [Duration]. /// The string should be of form hours:minutes:seconds.microseconds @@ -51,7 +51,7 @@ Duration? tryParseDuration(String input) { try { return parseDuration(input); } catch (e, stack) { - Catcher2.reportCheckedError(e, stack); + AppLogger.reportError(e, stack); return null; } } From de61d90938db580a177ffbd6066dfb023a1a3164 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Jun 2024 22:58:14 +0600 Subject: [PATCH 09/92] refactor: add back exceptions to file support --- lib/services/logger/logger.dart | 36 +++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/services/logger/logger.dart b/lib/services/logger/logger.dart index 1a9f37717..82708478d 100644 --- a/lib/services/logger/logger.dart +++ b/lib/services/logger/logger.dart @@ -1,16 +1,23 @@ import 'dart:async'; +import 'dart:io'; import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotube/utils/platform.dart'; class AppLogger { static late final Logger log; + static late final File logFile; static initialize(bool verbose) { log = Logger( level: kDebugMode || (verbose && kReleaseMode) ? Level.all : Level.info, ); + + getLogsPath().then((value) => logFile = value); } static R? runZoned(R Function() body) { @@ -43,11 +50,36 @@ class AppLogger { ); } - static void reportError( + static Future getLogsPath() async { + String dir = (await getApplicationDocumentsDirectory()).path; + if (kIsAndroid) { + dir = (await getExternalStorageDirectory())?.path ?? ""; + } + + if (kIsMacOS) { + dir = join((await getLibraryDirectory()).path, "Logs"); + } + final file = File(join(dir, ".spotube_logs")); + if (!await file.exists()) { + await file.create(recursive: true); + } + return file; + } + + static Future reportError( dynamic error, [ StackTrace? stackTrace, message = "", - ]) { + ]) async { log.e(message, error: error, stackTrace: stackTrace); + + if (kReleaseMode) { + await logFile.writeAsString( + "[${DateTime.now()}]---------------------\n" + "$error\n$stackTrace\n" + "----------------------------------------\n", + mode: FileMode.writeOnlyAppend, + ); + } } } From 2822d5dbfddd7845d0eb4d863d4c0256876649dc Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Jun 2024 23:05:19 +0600 Subject: [PATCH 10/92] chore: fix widget binding errors --- lib/services/logger/logger.dart | 47 ++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/services/logger/logger.dart b/lib/services/logger/logger.dart index 82708478d..6ba76ea19 100644 --- a/lib/services/logger/logger.dart +++ b/lib/services/logger/logger.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -16,34 +17,38 @@ class AppLogger { log = Logger( level: kDebugMode || (verbose && kReleaseMode) ? Level.all : Level.info, ); - - getLogsPath().then((value) => logFile = value); } static R? runZoned(R Function() body) { - FlutterError.onError = (details) { - reportError(details.exception, details.stack ?? StackTrace.current); - }; + return runZonedGuarded( + () { + WidgetsFlutterBinding.ensureInitialized(); - PlatformDispatcher.instance.onError = (error, stackTrace) { - reportError(error, stackTrace); - return true; - }; + FlutterError.onError = (details) { + reportError(details.exception, details.stack ?? StackTrace.current); + }; - if (!kIsWeb) { - Isolate.current.addErrorListener( - RawReceivePort((pair) async { - final isolateError = pair as List; - reportError( - isolateError.first.toString(), - isolateError.last, + PlatformDispatcher.instance.onError = (error, stackTrace) { + reportError(error, stackTrace); + return true; + }; + + if (!kIsWeb) { + Isolate.current.addErrorListener( + RawReceivePort((pair) async { + final isolateError = pair as List; + reportError( + isolateError.first.toString(), + isolateError.last, + ); + }).sendPort, ); - }).sendPort, - ); - } + } - return runZonedGuarded( - body, + getLogsPath().then((value) => logFile = value); + + return body(); + }, (error, stackTrace) { reportError(error, stackTrace); }, From 9ce911a8abe0e442ab226194f0b138662a85d7af Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 10 Jun 2024 21:47:53 +0600 Subject: [PATCH 11/92] cd: upgrade to flutter 3.22.2 --- .fvm/fvm_config.json | 2 +- .github/workflows/pr-lint.yml | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 0b54542ff..df8efa0eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.19.6", + "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 64cc8adc4..db158029a 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.6' + FLUTTER_VERSION: 3.22.2 jobs: lint: diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index e99aebab9..02b47f18c 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.6 + FLUTTER_VERSION: 3.22.2 permissions: contents: write From 6067314c5a4f296301e44315f3cc371008129a6e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 10 Jun 2024 22:27:33 +0600 Subject: [PATCH 12/92] cd: revert to flutter 3.22.1 --- .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 02b47f18c..5b74c9b59 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.2 + FLUTTER_VERSION: 3.22.1 permissions: contents: write From 4f2175987d9619b67a4456299155abcd0301da13 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 11 Jun 2024 23:02:23 +0600 Subject: [PATCH 13/92] refactor: remove uncessary methods --- .../spotify_endpoints.dart | 52 ------------------- pubspec.lock | 28 +++++----- 2 files changed, 14 insertions(+), 66 deletions(-) diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart index 0c7daeb2f..3b3583660 100644 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart @@ -109,58 +109,6 @@ class CustomSpotifyEndpoints { } } - void _addList( - Map parameters, String key, Iterable paramList) { - if (paramList.isNotEmpty) { - parameters[key] = paramList.join(','); - } - } - - void _addTunableTrackMap( - Map parameters, Map? tunableTrackMap) { - if (tunableTrackMap != null) { - parameters.addAll(tunableTrackMap.map((k, v) => - MapEntry(k, v is int ? v.toString() : v.toStringAsFixed(2)))); - } - } - - Future> getRecommendations({ - Iterable? seedArtists, - Iterable? seedGenres, - Iterable? seedTracks, - int limit = 20, - Market? market, - Map? max, - Map? min, - Map? target, - }) async { - assert(limit >= 1 && limit <= 100, 'limit should be 1 <= limit <= 100'); - final seedsNum = (seedArtists?.length ?? 0) + - (seedGenres?.length ?? 0) + - (seedTracks?.length ?? 0); - assert( - seedsNum >= 1 && seedsNum <= 5, - 'Up to 5 seed values may be provided in any combination of seed_artists,' - ' seed_tracks and seed_genres.'); - final parameters = {'limit': limit.toString()}; - final _ = { - 'seed_artists': seedArtists, - 'seed_genres': seedGenres, - 'seed_tracks': seedTracks - }.forEach((key, list) => _addList(parameters, key, list!)); - if (market != null) parameters['market'] = market.name; - for (var map in [min, max, target]) { - _addTunableTrackMap(parameters, map); - } - final pathQuery = - "$_baseUrl/recommendations?${parameters.entries.map((e) => '${e.key}=${e.value}').join('&')}"; - 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.getUri( Uri.parse("https://guc-spclient.spotify.com/presence-view/v1/buddylist"), 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 cb8d24ff311740b110ed677005910d71730d5027 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 11 Jun 2024 23:58:50 +0600 Subject: [PATCH 14/92] chore: remove uncessary dependencies --- pubspec.lock | 62 ++++++---------------------------------------------- pubspec.yaml | 13 +++++------ 2 files changed, 13 insertions(+), 62 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index c11577f21..c1866e7d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.11" build_runner_core: dependency: transitive description: @@ -273,14 +273,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - catcher_2: - dependency: "direct main" - description: - name: catcher_2 - sha256: "3c8f6cedc8c5eab61192830096d4f303900a5d0bddbf96a07ff9f7a8d5ff8fcd" - url: "https://pub.dev" - source: hosted - version: "1.2.4" change_case: dependency: transitive description: @@ -370,7 +362,7 @@ packages: source: hosted version: "0.3.4+1" crypto: - dependency: "direct main" + dependency: "direct dev" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -850,14 +842,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_mailer: - dependency: transitive - description: - name: flutter_mailer - sha256: "4fffaa35e911ff5ec2e5a4ebbca62c372e99a154eb3bb2c0bf79f09adf6ecf4c" - url: "https://pub.dev" - source: hosted - version: "2.1.2" flutter_native_splash: dependency: "direct main" description: @@ -964,14 +948,6 @@ packages: description: flutter source: sdk version: "0.0.0" - fluttertoast: - dependency: transitive - description: - name: fluttertoast - sha256: "81b68579e23fcbcada2db3d50302813d2371664afe6165bc78148050ab94bf66" - url: "https://pub.dev" - source: hosted - version: "8.2.5" form_validator: dependency: "direct main" description: @@ -1358,14 +1334,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - mailer: - dependency: transitive - description: - name: mailer - sha256: d25d89555c1031abacb448f07b801d7c01b4c21d4558e944b12b64394c84a3cb - url: "https://pub.dev" - source: hosted - version: "6.1.0" matcher: dependency: transitive description: @@ -1711,7 +1679,7 @@ packages: source: hosted version: "0.14.2" pub_api_client: - dependency: "direct main" + dependency: "direct dev" description: name: pub_api_client sha256: cc3d2c93df3823553de6a3e7d3ac09a3f43f8c271af4f43c2795266090ac9625 @@ -1735,7 +1703,7 @@ packages: source: hosted version: "2.3.0" pubspec_parse: - dependency: "direct main" + dependency: "direct dev" description: name: pubspec_parse sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 @@ -1767,7 +1735,7 @@ packages: source: hosted version: "4.1.0" riverpod: - dependency: transitive + dependency: "direct main" description: name: riverpod sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d @@ -1831,14 +1799,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - sentry: - dependency: transitive - description: - name: sentry - sha256: "19a267774906ca3a3c4677fc7e9582ea9da79ae9a28f84bbe4885dac2c269b70" - url: "https://pub.dev" - source: hosted - version: "7.20.0" shared_preferences: dependency: "direct main" description: @@ -1951,14 +1911,6 @@ packages: url: "https://pub.dev" source: hosted version: "10.1.3" - skeleton_text: - dependency: "direct main" - description: - name: skeleton_text - sha256: bacd536bf0664efe1cae53bcbd78c3d4040a120f300f69dc85d83f358471cc6c - url: "https://pub.dev" - source: hosted - version: "3.0.1" skeletonizer: dependency: "direct main" description: @@ -2455,5 +2407,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index ffa8511fb..bd1717c89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,6 @@ dependencies: auto_size_text: ^3.0.0 buttons_tabbar: ^1.3.8 cached_network_image: ^3.3.1 - catcher_2: ^1.2.4 collection: ^1.15.0 curved_navigation_bar: ^1.0.3 dbus: ^0.7.8 @@ -65,7 +64,7 @@ dependencies: mime: ^1.0.2 package_info_plus: ^6.0.0 palette_generator: ^0.3.3 - path: ^1.8.0 + path: ^1.9.0 path_provider: ^2.1.3 permission_handler: ^11.3.1 piped_client: ^0.1.1 @@ -77,7 +76,6 @@ dependencies: scroll_to_index: ^3.0.1 sidebarx: ^0.17.1 shared_preferences: ^2.2.3 - skeleton_text: ^3.0.1 smtc_windows: ^0.1.3 stroke_text: ^0.0.2 system_theme: ^2.1.0 @@ -118,16 +116,15 @@ dependencies: shelf_web_socket: ^1.0.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 http: ^1.2.1 + riverpod: ^2.5.1 dev_dependencies: - build_runner: ^2.4.9 + build_runner: ^2.4.11 + crypto: ^3.0.3 envied_generator: ^0.5.4+1 flutter_gen_runner: ^5.4.0 flutter_launcher_icons: ^0.13.1 @@ -142,6 +139,8 @@ dev_dependencies: custom_lint: ^0.6.4 riverpod_lint: ^2.3.10 process_run: ^0.14.2 + pubspec_parse: ^1.2.2 + pub_api_client: ^2.4.0 xml: ^6.5.0 io: ^1.0.4 From 064d92d35d5d2752ac0625d5204be2d098acdc51 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 12 Jun 2024 20:46:49 +0600 Subject: [PATCH 15/92] refactor: merge connect and playback server into one server --- lib/main.dart | 8 +- lib/pages/root/root_app.dart | 5 +- lib/provider/connect/server.dart | 270 -------------------- lib/provider/server/bonsoir.dart | 41 +++ lib/provider/server/pipeline.dart | 11 + lib/provider/server/router.dart | 20 ++ lib/provider/server/routes/connect.dart | 205 +++++++++++++++ lib/provider/server/routes/playback.dart | 73 ++++++ lib/provider/server/server.dart | 130 ++-------- lib/services/audio_player/audio_player.dart | 4 +- 10 files changed, 382 insertions(+), 385 deletions(-) delete mode 100644 lib/provider/connect/server.dart create mode 100644 lib/provider/server/bonsoir.dart create mode 100644 lib/provider/server/pipeline.dart create mode 100644 lib/provider/server/router.dart create mode 100644 lib/provider/server/routes/connect.dart create mode 100644 lib/provider/server/routes/playback.dart diff --git a/lib/main.dart b/lib/main.dart index 75d2ada51..1f5e59096 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,14 +18,14 @@ 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/server/bonsoir.dart'; +import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/connect/clients.dart'; -import 'package:spotube/provider/connect/server.dart'; import 'package:spotube/provider/palette_provider.dart'; -import 'package:spotube/provider/server/server.dart'; 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'; @@ -130,8 +130,8 @@ class Spotube extends HookConsumerWidget { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); - ref.listen(playbackServerProvider, (_, __) {}); - ref.listen(connectServerProvider, (_, __) {}); + ref.listen(serverProvider, (_, __) {}); + ref.listen(bonsoirProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 6f14a10b6..93a84f0a6 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -15,9 +15,9 @@ import 'package:spotube/modules/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'; +import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; @@ -36,6 +36,7 @@ class RootApp extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider); final scaffoldMessenger = ScaffoldMessenger.of(context); final theme = Theme.of(context); + final connectRoutes = ref.watch(serverConnectRoutesProvider); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -90,7 +91,7 @@ class RootApp extends HookConsumerWidget { ); } }), - connectClientStream.listen((clientOrigin) { + connectRoutes.connectClientStream.listen((clientOrigin) { scaffoldMessenger.showSnackBar( SnackBar( backgroundColor: Colors.yellow[600], diff --git a/lib/provider/connect/server.dart b/lib/provider/connect/server.dart deleted file mode 100644 index aeaaf1495..000000000 --- a/lib/provider/connect/server.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:spotube/services/logger/logger.dart'; -import 'package:shelf/shelf.dart'; -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'; -import 'package:bonsoir/bonsoir.dart'; -import 'package:spotube/services/device_info/device_info.dart'; -import 'package:spotube/utils/primitive_utils.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -import 'package:spotube/provider/volume_provider.dart'; - -final logger = getLogger('ConnectServer'); -final _connectClientStreamController = StreamController.broadcast(); - -Stream get connectClientStream => _connectClientStreamController.stream; - -final connectServerProvider = FutureProvider((ref) async { - final enabled = - ref.watch(userPreferencesProvider.select((s) => s.enableConnect)); - 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; - } - - final app = Router(); - - app.get( - "/ping", - (Request req) { - return Response.ok("pong"); - }, - ); - - final subscriptions = []; - - FutureOr websocket(Request req) => webSocketHandler( - (WebSocketChannel channel, String? protocol) async { - final context = - (req.context["shelf.io.connection_info"] as HttpConnectionInfo?); - final origin = - "${context?.remoteAddress.host}:${context?.remotePort}"; - _connectClientStreamController.add(origin); - - ref.listen( - proxyPlaylistProvider, - (previous, next) { - channel.sink.add( - WebSocketQueueEvent(next).toJson(), - ); - }, - fireImmediately: true, - ); - - // because audioPlayer events doesn't fireImmediately - channel.sink.add( - WebSocketPlayingEvent(audioPlayer.isPlaying).toJson(), - ); - channel.sink.add( - WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero) - .toJson(), - ); - channel.sink.add( - WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero) - .toJson(), - ); - channel.sink.add( - WebSocketShuffleEvent(audioPlayer.isShuffled).toJson(), - ); - channel.sink.add( - WebSocketLoopEvent(audioPlayer.loopMode).toJson(), - ); - channel.sink.add( - WebSocketVolumeEvent(audioPlayer.volume).toJson(), - ); - - subscriptions.addAll([ - audioPlayer.positionStream.listen( - (position) { - channel.sink.add( - WebSocketPositionEvent(position).toJson(), - ); - }, - ), - audioPlayer.playingStream.listen( - (playing) { - channel.sink.add( - WebSocketPlayingEvent(playing).toJson(), - ); - }, - ), - audioPlayer.durationStream.listen( - (duration) { - channel.sink.add( - WebSocketDurationEvent(duration).toJson(), - ); - }, - ), - audioPlayer.shuffledStream.listen( - (shuffled) { - channel.sink.add( - WebSocketShuffleEvent(shuffled).toJson(), - ); - }, - ), - audioPlayer.loopModeStream.listen( - (loopMode) { - channel.sink.add( - WebSocketLoopEvent(loopMode).toJson(), - ); - }, - ), - audioPlayer.volumeStream.listen( - (volume) { - channel.sink.add( - WebSocketVolumeEvent(volume).toJson(), - ); - }, - ), - channel.stream.listen( - (message) { - try { - final event = WebSocketEvent.fromJson( - jsonDecode(message), - (data) => data, - ); - - event.onLoad((event) async { - await playbackNotifier.load( - event.data.tracks, - autoPlay: true, - initialIndex: event.data.initialIndex ?? 0, - ); - - 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]); - } - }); - - event.onPause((event) async { - await audioPlayer.pause(); - }); - - event.onResume((event) async { - await audioPlayer.resume(); - }); - - event.onStop((event) async { - await audioPlayer.stop(); - }); - - event.onNext((event) async { - await playbackNotifier.next(); - }); - - event.onPrevious((event) async { - await playbackNotifier.previous(); - }); - - event.onJump((event) async { - await playbackNotifier.jumpTo(event.data); - }); - - event.onSeek((event) async { - await audioPlayer.seek(event.data); - }); - - event.onShuffle((event) async { - await audioPlayer.setShuffle(event.data); - }); - - event.onLoop((event) async { - await audioPlayer.setLoopMode(event.data); - }); - - event.onAddTrack((event) async { - await playbackNotifier.addTrack(event.data); - }); - - event.onRemoveTrack((event) async { - await playbackNotifier.removeTrack(event.data); - }); - - event.onReorder((event) async { - await playbackNotifier.moveTrack( - event.data.oldIndex, - event.data.newIndex, - ); - }); - - event.onVolume((event) async { - ref.read(volumeProvider.notifier).setVolume(event.data); - }); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - channel.sink.add(WebSocketErrorEvent(e.toString()).toJson()); - } - }, - onDone: () { - logger.i('Connection closed'); - }, - ), - ]); - }, - )(req); - - final port = Random().nextInt(17000) + 3000; - - final server = await serve( - (request) { - if (request.url.path.startsWith('ws')) { - return websocket(request); - } - return app(request); - }, - InternetAddress.anyIPv4, - port, - ); - - logger.i('Server running on http://${server.address.host}:${server.port}'); - - final service = BonsoirService( - name: await DeviceInfoService.instance.computerName(), - type: '_spotube._tcp', - port: port, - attributes: { - "id": PrimitiveUtils.uuid.v4(), - "deviceId": await DeviceInfoService.instance.deviceId(), - }, - ); - - final broadcast = BonsoirBroadcast(service: service); - - await broadcast.ready; - await broadcast.start(); - - ref.onDispose(() async { - logger.i('Stopping server'); - for (final subscription in subscriptions) { - await subscription.cancel(); - } - await broadcast.stop(); - await server.close(); - }); - - return app; -}); diff --git a/lib/provider/server/bonsoir.dart b/lib/provider/server/bonsoir.dart new file mode 100644 index 000000000..fcc40e54f --- /dev/null +++ b/lib/provider/server/bonsoir.dart @@ -0,0 +1,41 @@ +import 'package:bonsoir/bonsoir.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/connect/clients.dart'; +import 'package:spotube/provider/server/server.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/device_info/device_info.dart'; +import 'package:spotube/utils/primitive_utils.dart'; + +final bonsoirProvider = FutureProvider((ref) async { + final enabled = ref.watch( + userPreferencesProvider.select((s) => s.enableConnect), + ); + final resolvedService = await ref.watch( + connectClientsProvider.selectAsync((s) => s.resolvedService), + ); + + if (!enabled || resolvedService != null) { + return null; + } + + final (server: _, :port) = await ref.watch(serverProvider.future); + + final service = BonsoirService( + name: await DeviceInfoService.instance.computerName(), + type: '_spotube._tcp', + port: port, + attributes: { + "id": PrimitiveUtils.uuid.v4(), + "deviceId": await DeviceInfoService.instance.deviceId(), + }, + ); + + final broadcast = BonsoirBroadcast(service: service); + + await broadcast.ready; + await broadcast.start(); + + ref.onDispose(() async { + await broadcast.stop(); + }); +}); diff --git a/lib/provider/server/pipeline.dart b/lib/provider/server/pipeline.dart new file mode 100644 index 000000000..8f97ce89b --- /dev/null +++ b/lib/provider/server/pipeline.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelf/shelf.dart'; + +final pipelineProvider = Provider((ref) { + const pipeline = Pipeline(); + if (kDebugMode) { + pipeline.addMiddleware(logRequests()); + } + return pipeline; +}); diff --git a/lib/provider/server/router.dart b/lib/provider/server/router.dart new file mode 100644 index 000000000..e2a579cc6 --- /dev/null +++ b/lib/provider/server/router.dart @@ -0,0 +1,20 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; +import 'package:spotube/provider/server/routes/connect.dart'; +import 'package:spotube/provider/server/routes/playback.dart'; + +final serverRouterProvider = Provider((ref) { + final playbackRoutes = ref.watch(serverPlaybackRoutesProvider); + final connectRoutes = ref.watch(serverConnectRoutesProvider); + + final router = Router(); + + router.get("/ping", (Request request) => Response.ok("pong")); + + router.get("/stream/", playbackRoutes.getStreamTrackId); + + router.all("/ws", connectRoutes.websocket); + + return router; +}); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart new file mode 100644 index 000000000..eee3365e9 --- /dev/null +++ b/lib/provider/server/routes/connect.dart @@ -0,0 +1,205 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelf/shelf.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/history/history.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/volume_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +extension _WebsocketSinkExts on WebSocketSink { + void addEvent(WebSocketEvent event) { + add(event.toJson()); + } +} + +class ServerConnectRoutes { + final Ref ref; + final StreamController _connectClientStreamController; + final List subscriptions; + final SpotubeLogger logger; + ServerConnectRoutes(this.ref) + : _connectClientStreamController = StreamController.broadcast(), + subscriptions = [], + logger = getLogger('ConnectServer') { + ref.onDispose(() { + _connectClientStreamController.close(); + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + } + + ProxyPlaylistNotifier get playbackNotifier => + ref.read(proxyPlaylistProvider.notifier); + PlaybackHistoryNotifier get historyNotifier => + ref.read(playbackHistoryProvider.notifier); + Stream get connectClientStream => + _connectClientStreamController.stream; + + FutureOr websocket(Request req) { + return webSocketHandler( + ( + WebSocketChannel channel, + String? protocol, + ) async { + final context = + (req.context["shelf.io.connection_info"] as HttpConnectionInfo?); + final origin = "${context?.remoteAddress.host}:${context?.remotePort}"; + _connectClientStreamController.add(origin); + + ref.listen( + proxyPlaylistProvider, + (previous, next) { + channel.sink.addEvent(WebSocketQueueEvent(next)); + }, + fireImmediately: true, + ); + + // because audioPlayer events doesn't fireImmediately + channel.sink.addEvent(WebSocketPlayingEvent(audioPlayer.isPlaying)); + channel.sink.addEvent( + WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero), + ); + channel.sink.addEvent( + WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero), + ); + channel.sink.addEvent(WebSocketShuffleEvent(audioPlayer.isShuffled)); + channel.sink.addEvent(WebSocketLoopEvent(audioPlayer.loopMode)); + channel.sink.addEvent(WebSocketVolumeEvent(audioPlayer.volume)); + + subscriptions.addAll([ + audioPlayer.positionStream.listen( + (position) { + channel.sink.addEvent(WebSocketPositionEvent(position)); + }, + ), + audioPlayer.playingStream.listen( + (playing) { + channel.sink.addEvent(WebSocketPlayingEvent(playing)); + }, + ), + audioPlayer.durationStream.listen( + (duration) { + channel.sink.addEvent(WebSocketDurationEvent(duration)); + }, + ), + audioPlayer.shuffledStream.listen( + (shuffled) { + channel.sink.addEvent(WebSocketShuffleEvent(shuffled)); + }, + ), + audioPlayer.loopModeStream.listen( + (loopMode) { + channel.sink.addEvent(WebSocketLoopEvent(loopMode)); + }, + ), + audioPlayer.volumeStream.listen( + (volume) { + channel.sink.addEvent(WebSocketVolumeEvent(volume)); + }, + ), + channel.stream.listen( + (message) { + try { + final event = WebSocketEvent.fromJson( + jsonDecode(message), + (data) => data, + ); + + event.onLoad((event) async { + await playbackNotifier.load( + event.data.tracks, + autoPlay: true, + initialIndex: event.data.initialIndex ?? 0, + ); + + 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]); + } + }); + + event.onPause((event) async { + await audioPlayer.pause(); + }); + + event.onResume((event) async { + await audioPlayer.resume(); + }); + + event.onStop((event) async { + await audioPlayer.stop(); + }); + + event.onNext((event) async { + await playbackNotifier.next(); + }); + + event.onPrevious((event) async { + await playbackNotifier.previous(); + }); + + event.onJump((event) async { + await playbackNotifier.jumpTo(event.data); + }); + + event.onSeek((event) async { + await audioPlayer.seek(event.data); + }); + + event.onShuffle((event) async { + await audioPlayer.setShuffle(event.data); + }); + + event.onLoop((event) async { + await audioPlayer.setLoopMode(event.data); + }); + + event.onAddTrack((event) async { + await playbackNotifier.addTrack(event.data); + }); + + event.onRemoveTrack((event) async { + await playbackNotifier.removeTrack(event.data); + }); + + event.onReorder((event) async { + await playbackNotifier.moveTrack( + event.data.oldIndex, + event.data.newIndex, + ); + }); + + event.onVolume((event) async { + ref.read(volumeProvider.notifier).setVolume(event.data); + }); + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + channel.sink.addEvent(WebSocketErrorEvent(e.toString())); + } + }, + onDone: () { + logger.i('Connection closed'); + }, + ), + ]); + }, + )(req); + } +} + +final serverConnectRoutesProvider = Provider((ref) => ServerConnectRoutes(ref)); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart new file mode 100644 index 000000000..dd9d6c3bb --- /dev/null +++ b/lib/provider/server/routes/playback.dart @@ -0,0 +1,73 @@ +import 'package:dio/dio.dart' hide Response; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelf/shelf.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/server/active_sourced_track.dart'; +import 'package:spotube/provider/server/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/logger/logger.dart'; + +class ServerPlaybackRoutes { + final Ref ref; + UserPreferences get userPreferences => ref.read(userPreferencesProvider); + ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider); + final Dio dio; + + ServerPlaybackRoutes(this.ref) : dio = Dio(); + + /// @get('/stream/') + Future getStreamTrackId(Request request, String trackId) async { + try { + final track = + playlist.tracks.firstWhere((element) => element.id == trackId); + final activeSourcedTrack = ref.read(activeSourcedTrackProvider); + final sourcedTrack = activeSourcedTrack?.id == track.id + ? activeSourcedTrack + : await ref.read(sourcedTrackProvider(track).future); + + ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); + + final res = await dio.get( + sourcedTrack!.url, + options: Options( + headers: { + ...request.headers, + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "host": Uri.parse(sourcedTrack.url).host, + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + }, + responseType: ResponseType.stream, + validateStatus: (status) => status! < 500, + ), + ); + + final audioStream = + (res.data?.stream as Stream?)?.asBroadcastStream(); + + audioStream!.listen( + (event) {}, + cancelOnError: true, + ); + + return Response( + res.statusCode!, + body: audioStream, + context: { + "shelf.io.buffer_output": false, + }, + headers: res.headers.map, + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + return Response.internalServerError(); + } + } +} + +final serverPlaybackRoutesProvider = + Provider((ref) => ServerPlaybackRoutes(ref)); diff --git a/lib/provider/server/server.dart b/lib/provider/server/server.dart index b6a7dfe95..5232bb17b 100644 --- a/lib/provider/server/server.dart +++ b/lib/provider/server/server.dart @@ -1,119 +1,35 @@ import 'dart:io'; import 'dart:math'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:dio/dio.dart' hide Response; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; -import 'package:shelf/shelf.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelf/shelf_io.dart'; -import 'package:shelf_router/shelf_router.dart'; -import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/server/active_sourced_track.dart'; -import 'package:spotube/provider/server/sourced_track.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; - -class PlaybackServer { - final Ref ref; - UserPreferences get userPreferences => ref.read(userPreferencesProvider); - ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider); - final Logger logger; - final Dio dio; - - final Router router; - - static final port = Random().nextInt(17000) + 1500; +import 'package:spotube/provider/server/pipeline.dart'; +import 'package:spotube/provider/server/router.dart'; +import 'package:spotube/services/logger/logger.dart'; - PlaybackServer(this.ref) - : logger = getLogger('PlaybackServer'), - dio = Dio(), - router = Router() { - router.get('/stream/', getStreamTrackId); +int serverPort = 0; +final serverProvider = FutureProvider( + (ref) async { + final pipeline = ref.watch(pipelineProvider); + final router = ref.watch(serverRouterProvider); - const pipeline = Pipeline(); + final port = Random().nextInt(17000) + 1500; - if (kDebugMode) { - pipeline.addMiddleware(logRequests()); - } + final server = await serve( + pipeline.addHandler(router.call), + InternetAddress.anyIPv4, + port, + ); - serve(pipeline.addHandler(router.call), InternetAddress.loopbackIPv4, port) - .then((server) { - logger - .t('Playback server at http://${server.address.host}:${server.port}'); + AppLogger.log + .t('Playback server at http://${server.address.host}:${server.port}'); - ref.onDispose(() { - dio.close(force: true); - server.close(); - }); + ref.onDispose(() { + server.close(); }); - } - - /// @get('/stream/') - Future getStreamTrackId(Request request, String trackId) async { - try { - final track = - playlist.tracks.firstWhere((element) => element.id == trackId); - final activeSourcedTrack = ref.read(activeSourcedTrackProvider); - final sourcedTrack = activeSourcedTrack?.id == track.id - ? activeSourcedTrack - : await ref.read(sourcedTrackProvider(track).future); - - ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); - - final res = await dio.get( - sourcedTrack!.url, - options: Options( - headers: { - ...request.headers, - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", - "host": Uri.parse(sourcedTrack.url).host, - "Cache-Control": "max-age=0", - "Connection": "keep-alive", - }, - responseType: ResponseType.stream, - validateStatus: (status) => status! < 500, - ), - ); - - final audioStream = - (res.data?.stream as Stream?)?.asBroadcastStream(); - - // if (res.statusCode! > 300) { - // debugPrint( - // "[[Request]]\n" - // "URI: ${res.requestOptions.uri}\n" - // "Status: ${res.statusCode}\n" - // "Request Headers: ${res.requestOptions.headers}\n" - // "Response Body: ${res.data}\n" - // "Response Headers: ${res.headers.map}", - // ); - // } - - audioStream!.listen( - (event) {}, - cancelOnError: true, - ); - return Response( - res.statusCode!, - body: audioStream, - context: { - "shelf.io.buffer_output": false, - }, - headers: res.headers.map, - ); - } catch (e, stack) { - AppLogger.reportError(e, stack); - return Response.internalServerError(); - } - } -} + serverPort = port; -final playbackServerProvider = Provider((ref) { - return PlaybackServer(ref); -}); + return (server: server, port: port); + }, +); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 8b391a075..df23039c9 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,10 +1,10 @@ import 'dart:io'; +import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; import 'dart:async'; @@ -27,7 +27,7 @@ class SpotubeMedia extends mk.Media { }) : super( track is LocalTrack ? track.path - : "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}", + : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", extras: { ...?extras, "track": switch (track) { From 9034ee29dbb1b2dfede3cca6ff76a100e7815c64 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 13 Jun 2024 21:23:12 +0600 Subject: [PATCH 16/92] chore: use flutter version in runner rc --- windows/runner/Runner.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 276326676..62e150f8a 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.7.0" +#define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO From 2540d16cede192f5b173f9cee0aa383e44f3dec1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 13 Jun 2024 21:43:29 +0600 Subject: [PATCH 17/92] chore: remove circleci config --- .circleci/config.yml | 177 ------------------------------------------- CONTRIBUTION.md | 2 +- 2 files changed, 1 insertion(+), 178 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index a55310ce4..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,177 +0,0 @@ -version: 2.1 - -orbs: - gh: circleci/github-cli@2.2.0 - -jobs: - flutter_linux_arm: - machine: - image: ubuntu-2204:current - resource_class: arm.medium - parameters: - version: - type: string - default: 3.1.1 - channel: - type: enum - enum: - - release - - nightly - default: release - github_run_number: - type: string - default: "0" - dry_run: - type: boolean - default: true - steps: - - checkout - - gh/setup - - - run: - name: Get current date - command: | - echo "export CURRENT_DATE=$(date +%Y-%m-%d)" >> $BASH_ENV - - - run: - name: Install dependencies - command: | - 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 zip rpm - - - run: - name: Install Flutter - command: | - git clone https://github.com/flutter/flutter.git - cd flutter && git checkout stable && cd .. - export PATH="$PATH:`pwd`/flutter/bin" - flutter precache - flutter doctor -v - - - run: - name: Install AppImageTool - command: | - wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage" - chmod +x appimagetool - mv appimagetool flutter/bin - - - persist_to_workspace: - root: flutter - paths: - - . - - - when: - condition: - equal: [<< parameters.channel >>, nightly] - steps: - - run: - name: Replace pubspec version and BUILD_VERSION Env (nightly) - command: | - curl -sS https://webi.sh/yq | sh - yq -i '.version |= sub("\+\d+", "+<< parameters.channel >>.")' pubspec.yaml - yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml - echo 'export BUILD_VERSION="<< parameters.version >>+<< parameters.channel >>.<< parameters.github_run_number >>"' >> $BASH_ENV - - - when: - condition: - equal: [<< parameters.channel >>, release] - steps: - - run: echo 'export BUILD_VERSION="<< parameters.version >>"' >> $BASH_ENV - - - run: - name: Generate .env file - command: | - echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env - - - run: - name: Replace Version in files - command: | - sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml - echo "build_arch: aarch64" >> linux/packaging/rpm/make_config.yaml - - - run: - name: Build secrets - command: | - export PATH="$PATH:`pwd`/flutter/bin" - flutter config --enable-linux-desktop - flutter pub get - dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns - - - run: - name: Build Flutter app - command: | - export PATH="$PATH:`pwd`/flutter/bin" - export PATH="$PATH":"$HOME/.pub-cache/bin" - 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=appimage - flutter_distributor package --platform=linux --targets=rpm - - - when: - condition: - equal: [<< parameters.channel >>, nightly] - steps: - - run: make tar VERSION=nightly ARCH=arm64 PKG_ARCH=aarch64 - - - when: - condition: - equal: [<< parameters.channel >>, release] - steps: - - run: make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64 - - - run: - name: Move artifacts - command: | - mkdir bundle - mv build/spotube-linux-*-aarch64.tar.xz bundle/ - mv dist/**/spotube-*-linux.deb bundle/Spotube-linux-aarch64.deb - mv dist/**/spotube-*-linux.rpm bundle/Spotube-linux-aarch64.rpm - mv dist/**/spotube-*-linux.AppImage bundle/Spotube-linux-aarch64.AppImage - zip -r Spotube-linux-aarch64.zip bundle - - - store_artifacts: - path: Spotube-linux-aarch64.zip - - - when: - condition: - and: - - equal: [<< parameters.dry_run >>, false] - - equal: [<< parameters.channel >>, release] - steps: - - run: - name: Upload to release (release) - command: gh release upload v<< parameters.version >> bundle/* --clobber - - - when: - condition: - and: - - equal: [<< parameters.dry_run >>, false] - - equal: [<< parameters.channel >>, nightly] - steps: - - run: - name: Upload to release (nightly) - command: gh release upload nightly bundle/* --clobber - -parameters: - GHA_Actor: - type: string - default: "" - GHA_Action: - type: string - default: "" - GHA_Event: - type: string - default: "" - GHA_Meta: - type: string - default: "" - -workflows: - build_flutter_for_arm_workflow: - when: << pipeline.parameters.GHA_Action >> - jobs: - - flutter_linux_arm: - context: - - org-global - - GITHUB_CREDS diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index e859f9e6f..0cfff0cab 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -32,7 +32,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents This project and everyone participating in it is governed by the [Spotube Code of Conduct](https://github.com/KRTirtho/spotube/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior -to <>. +to krtirtho@gmail.com. ## I Have a Question From 3fb003ea60d90a8f8da7eb177bc747d4eb219199 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Jun 2024 00:29:09 +0600 Subject: [PATCH 18/92] refactor(preferences): use Drift sql db for preferences --- .../sections/body/track_view_options.dart | 2 +- .../configurators/use_close_behavior.dart | 3 +- lib/models/database/database.dart | 54 + lib/models/database/database.g.dart | 1707 +++++++++++++++++ lib/models/database/tables/preferences.dart | 125 ++ lib/models/database/typeconverters/color.dart | 29 + .../database/typeconverters/locale.dart | 19 + .../database/typeconverters/string_list.dart | 15 + lib/modules/player/sibling_tracks_sheet.dart | 3 +- lib/modules/root/bottom_player.dart | 3 +- lib/modules/root/sidebar.dart | 3 +- lib/modules/root/spotube_navigation_bar.dart | 3 +- .../getting_started/sections/playback.dart | 2 +- .../getting_started/sections/region.dart | 4 +- .../playlist_generate/playlist_generate.dart | 2 +- lib/pages/settings/sections/appearance.dart | 2 +- lib/pages/settings/sections/desktop.dart | 3 +- .../settings/sections/language_region.dart | 2 +- lib/pages/settings/sections/playback.dart | 3 +- lib/provider/database/database.dart | 4 + .../proxy_playlist_provider.dart | 2 +- .../proxy_playlist/skip_segments.dart | 3 +- lib/provider/server/routes/playback.dart | 1 - lib/provider/spotify/album/releases.dart | 4 +- lib/provider/spotify/artist/albums.dart | 4 +- lib/provider/spotify/artist/top_tracks.dart | 3 +- lib/provider/spotify/category/categories.dart | 3 +- lib/provider/spotify/category/playlists.dart | 4 +- lib/provider/spotify/playlist/generate.dart | 2 +- lib/provider/spotify/search/search.dart | 4 +- lib/provider/spotify/views/home.dart | 2 +- lib/provider/spotify/views/home_section.dart | 2 +- lib/provider/spotify/views/view.dart | 2 +- .../user_preferences_provider.dart | 189 +- .../user_preferences_state.dart | 142 -- .../user_preferences_state.freezed.dart | 751 -------- .../user_preferences_state.g.dart | 388 ---- .../sourced_track/models/video_info.dart | 3 +- lib/services/sourced_track/sourced_track.dart | 3 +- lib/services/sourced_track/sources/piped.dart | 3 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 21 + pubspec.lock | 48 + pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 48 files changed, 2187 insertions(+), 1400 deletions(-) create mode 100644 lib/models/database/database.dart create mode 100644 lib/models/database/database.g.dart create mode 100644 lib/models/database/tables/preferences.dart create mode 100644 lib/models/database/typeconverters/color.dart create mode 100644 lib/models/database/typeconverters/locale.dart create mode 100644 lib/models/database/typeconverters/string_list.dart create mode 100644 lib/provider/database/database.dart delete mode 100644 lib/provider/user_preferences/user_preferences_state.dart delete mode 100644 lib/provider/user_preferences/user_preferences_state.freezed.dart delete mode 100644 lib/provider/user_preferences/user_preferences_state.g.dart diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index f004b10a7..1accba347 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -8,11 +8,11 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/components/tracks_view/track_view_provider.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/database/database.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'; class TrackViewBodyOptions extends HookConsumerWidget { const TrackViewBodyOptions({super.key}); diff --git a/lib/hooks/configurators/use_close_behavior.dart b/lib/hooks/configurators/use_close_behavior.dart index 3df6a5288..2bdc65ef6 100644 --- a/lib/hooks/configurators/use_close_behavior.dart +++ b/lib/hooks/configurators/use_close_behavior.dart @@ -2,8 +2,9 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/configurators/use_window_listener.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:local_notifier/local_notifier.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart new file mode 100644 index 000000000..7d8fe088a --- /dev/null +++ b/lib/models/database/database.dart @@ -0,0 +1,54 @@ +library database; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:flutter/material.dart' hide Table; +import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:drift/native.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; + +part 'database.g.dart'; + +part 'tables/preferences.dart'; +part 'typeconverters/color.dart'; +part 'typeconverters/locale.dart'; +part 'typeconverters/string_list.dart'; + +@DriftDatabase(tables: [PreferencesTable]) +class AppDatabase extends _$AppDatabase { + AppDatabase() : super(_openConnection()); + + @override + int get schemaVersion => 1; +} + +LazyDatabase _openConnection() { + // the LazyDatabase util lets us find the right location for the file async. + return LazyDatabase(() async { + // put the database file, called db.sqlite here, into the documents folder + // for your app. + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(join(dbFolder.path, 'db.sqlite')); + + // Also work around limitations on old Android versions + if (Platform.isAndroid) { + await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); + } + + // Make sqlite3 pick a more suitable location for temporary files - the + // one from the system may be inaccessible due to sandboxing. + final cacheBase = (await getTemporaryDirectory()).path; + // We can't access /tmp on Android, which sqlite3 would try by default. + // Explicitly tell it about the correct temporary directory. + sqlite3.tempDirectory = cacheBase; + + return NativeDatabase.createInBackground(file); + }); +} diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart new file mode 100644 index 000000000..1516b2665 --- /dev/null +++ b/lib/models/database/database.g.dart @@ -0,0 +1,1707 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $PreferencesTableTable extends PreferencesTable + with TableInfo<$PreferencesTableTable, PreferencesTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PreferencesTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _audioQualityMeta = + const VerificationMeta('audioQuality'); + @override + late final GeneratedColumnWithTypeConverter + audioQuality = GeneratedColumn( + 'audio_quality', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceQualities.high.name)) + .withConverter( + $PreferencesTableTable.$converteraudioQuality); + static const VerificationMeta _albumColorSyncMeta = + const VerificationMeta('albumColorSync'); + @override + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _amoledDarkThemeMeta = + const VerificationMeta('amoledDarkTheme'); + @override + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _checkUpdateMeta = + const VerificationMeta('checkUpdate'); + @override + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _normalizeAudioMeta = + const VerificationMeta('normalizeAudio'); + @override + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _showSystemTrayIconMeta = + const VerificationMeta('showSystemTrayIcon'); + @override + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _systemTitleBarMeta = + const VerificationMeta('systemTitleBar'); + @override + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _skipNonMusicMeta = + const VerificationMeta('skipNonMusic'); + @override + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _closeBehaviorMeta = + const VerificationMeta('closeBehavior'); + @override + late final GeneratedColumnWithTypeConverter + closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)) + .withConverter( + $PreferencesTableTable.$convertercloseBehavior); + static const VerificationMeta _accentColorSchemeMeta = + const VerificationMeta('accentColorScheme'); + @override + late final GeneratedColumnWithTypeConverter + accentColorScheme = GeneratedColumn( + 'accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Blue:0xFF2196F3")) + .withConverter( + $PreferencesTableTable.$converteraccentColorScheme); + static const VerificationMeta _layoutModeMeta = + const VerificationMeta('layoutMode'); + @override + late final GeneratedColumnWithTypeConverter layoutMode = + GeneratedColumn('layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)) + .withConverter( + $PreferencesTableTable.$converterlayoutMode); + static const VerificationMeta _localeMeta = const VerificationMeta('locale'); + @override + late final GeneratedColumnWithTypeConverter locale = + GeneratedColumn('locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant( + '{"languageCode":"system","countryCode":"system"}')) + .withConverter($PreferencesTableTable.$converterlocale); + static const VerificationMeta _marketMeta = const VerificationMeta('market'); + @override + late final GeneratedColumnWithTypeConverter market = + GeneratedColumn('market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)) + .withConverter($PreferencesTableTable.$convertermarket); + static const VerificationMeta _searchModeMeta = + const VerificationMeta('searchMode'); + @override + late final GeneratedColumnWithTypeConverter searchMode = + GeneratedColumn('search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)) + .withConverter( + $PreferencesTableTable.$convertersearchMode); + static const VerificationMeta _downloadLocationMeta = + const VerificationMeta('downloadLocation'); + @override + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + static const VerificationMeta _localLibraryLocationMeta = + const VerificationMeta('localLibraryLocation'); + @override + late final GeneratedColumnWithTypeConverter, String> + localLibraryLocation = GeneratedColumn( + 'local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")) + .withConverter>( + $PreferencesTableTable.$converterlocalLibraryLocation); + static const VerificationMeta _pipedInstanceMeta = + const VerificationMeta('pipedInstance'); + @override + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + static const VerificationMeta _themeModeMeta = + const VerificationMeta('themeMode'); + @override + late final GeneratedColumnWithTypeConverter themeMode = + GeneratedColumn('theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)) + .withConverter($PreferencesTableTable.$converterthemeMode); + static const VerificationMeta _audioSourceMeta = + const VerificationMeta('audioSource'); + @override + late final GeneratedColumnWithTypeConverter audioSource = + GeneratedColumn('audio_source', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(AudioSource.youtube.name)) + .withConverter( + $PreferencesTableTable.$converteraudioSource); + static const VerificationMeta _streamMusicCodecMeta = + const VerificationMeta('streamMusicCodec'); + @override + late final GeneratedColumnWithTypeConverter + streamMusicCodec = GeneratedColumn( + 'stream_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.weba.name)) + .withConverter( + $PreferencesTableTable.$converterstreamMusicCodec); + static const VerificationMeta _downloadMusicCodecMeta = + const VerificationMeta('downloadMusicCodec'); + @override + late final GeneratedColumnWithTypeConverter + downloadMusicCodec = GeneratedColumn( + 'download_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.m4a.name)) + .withConverter( + $PreferencesTableTable.$converterdownloadMusicCodec); + static const VerificationMeta _discordPresenceMeta = + const VerificationMeta('discordPresence'); + @override + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _endlessPlaybackMeta = + const VerificationMeta('endlessPlayback'); + @override + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _enableConnectMeta = + const VerificationMeta('enableConnect'); + @override + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + @override + List get $columns => [ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + context.handle(_audioQualityMeta, const VerificationResult.success()); + if (data.containsKey('album_color_sync')) { + context.handle( + _albumColorSyncMeta, + albumColorSync.isAcceptableOrUnknown( + data['album_color_sync']!, _albumColorSyncMeta)); + } + if (data.containsKey('amoled_dark_theme')) { + context.handle( + _amoledDarkThemeMeta, + amoledDarkTheme.isAcceptableOrUnknown( + data['amoled_dark_theme']!, _amoledDarkThemeMeta)); + } + if (data.containsKey('check_update')) { + context.handle( + _checkUpdateMeta, + checkUpdate.isAcceptableOrUnknown( + data['check_update']!, _checkUpdateMeta)); + } + if (data.containsKey('normalize_audio')) { + context.handle( + _normalizeAudioMeta, + normalizeAudio.isAcceptableOrUnknown( + data['normalize_audio']!, _normalizeAudioMeta)); + } + if (data.containsKey('show_system_tray_icon')) { + context.handle( + _showSystemTrayIconMeta, + showSystemTrayIcon.isAcceptableOrUnknown( + data['show_system_tray_icon']!, _showSystemTrayIconMeta)); + } + if (data.containsKey('system_title_bar')) { + context.handle( + _systemTitleBarMeta, + systemTitleBar.isAcceptableOrUnknown( + data['system_title_bar']!, _systemTitleBarMeta)); + } + if (data.containsKey('skip_non_music')) { + context.handle( + _skipNonMusicMeta, + skipNonMusic.isAcceptableOrUnknown( + data['skip_non_music']!, _skipNonMusicMeta)); + } + context.handle(_closeBehaviorMeta, const VerificationResult.success()); + context.handle(_accentColorSchemeMeta, const VerificationResult.success()); + context.handle(_layoutModeMeta, const VerificationResult.success()); + context.handle(_localeMeta, const VerificationResult.success()); + context.handle(_marketMeta, const VerificationResult.success()); + context.handle(_searchModeMeta, const VerificationResult.success()); + if (data.containsKey('download_location')) { + context.handle( + _downloadLocationMeta, + downloadLocation.isAcceptableOrUnknown( + data['download_location']!, _downloadLocationMeta)); + } + context.handle( + _localLibraryLocationMeta, const VerificationResult.success()); + if (data.containsKey('piped_instance')) { + context.handle( + _pipedInstanceMeta, + pipedInstance.isAcceptableOrUnknown( + data['piped_instance']!, _pipedInstanceMeta)); + } + context.handle(_themeModeMeta, const VerificationResult.success()); + context.handle(_audioSourceMeta, const VerificationResult.success()); + context.handle(_streamMusicCodecMeta, const VerificationResult.success()); + context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); + if (data.containsKey('discord_presence')) { + context.handle( + _discordPresenceMeta, + discordPresence.isAcceptableOrUnknown( + data['discord_presence']!, _discordPresenceMeta)); + } + if (data.containsKey('endless_playback')) { + context.handle( + _endlessPlaybackMeta, + endlessPlayback.isAcceptableOrUnknown( + data['endless_playback']!, _endlessPlaybackMeta)); + } + if (data.containsKey('enable_connect')) { + context.handle( + _enableConnectMeta, + enableConnect.isAcceptableOrUnknown( + data['enable_connect']!, _enableConnectMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioQuality: $PreferencesTableTable.$converteraudioQuality.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}audio_quality'])!), + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: $PreferencesTableTable.$convertercloseBehavior.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}close_behavior'])!), + accentColorScheme: $PreferencesTableTable.$converteraccentColorScheme + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}accent_color_scheme'])!), + layoutMode: $PreferencesTableTable.$converterlayoutMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}layout_mode'])!), + locale: $PreferencesTableTable.$converterlocale.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!), + market: $PreferencesTableTable.$convertermarket.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!), + searchMode: $PreferencesTableTable.$convertersearchMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}search_mode'])!), + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: $PreferencesTableTable + .$converterlocalLibraryLocation + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!), + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), + audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}audio_source'])!), + streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}stream_music_codec'])!), + downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}download_music_codec'])!), + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + ); + } + + @override + $PreferencesTableTable createAlias(String alias) { + return $PreferencesTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converteraudioQuality = + const EnumNameConverter(SourceQualities.values); + static JsonTypeConverter2 + $convertercloseBehavior = + const EnumNameConverter(CloseBehavior.values); + static TypeConverter $converteraccentColorScheme = + const SpotubeColorConverter(); + static JsonTypeConverter2 $converterlayoutMode = + const EnumNameConverter(LayoutMode.values); + static TypeConverter $converterlocale = + const LocaleConverter(); + static JsonTypeConverter2 $convertermarket = + const EnumNameConverter(Market.values); + static JsonTypeConverter2 $convertersearchMode = + const EnumNameConverter(SearchMode.values); + static TypeConverter, String> $converterlocalLibraryLocation = + const StringListConverter(); + static JsonTypeConverter2 $converterthemeMode = + const EnumNameConverter(ThemeMode.values); + static JsonTypeConverter2 $converteraudioSource = + const EnumNameConverter(AudioSource.values); + static JsonTypeConverter2 + $converterstreamMusicCodec = + const EnumNameConverter(SourceCodecs.values); + static JsonTypeConverter2 + $converterdownloadMusicCodec = + const EnumNameConverter(SourceCodecs.values); +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final SourceQualities audioQuality; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final CloseBehavior closeBehavior; + final SpotubeColor accentColorScheme; + final LayoutMode layoutMode; + final Locale locale; + final Market market; + final SearchMode searchMode; + final String downloadLocation; + final List localLibraryLocation; + final String pipedInstance; + final ThemeMode themeMode; + final AudioSource audioSource; + final SourceCodecs streamMusicCodec; + final SourceCodecs downloadMusicCodec; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + const PreferencesTableData( + {required this.id, + required this.audioQuality, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.themeMode, + required this.audioSource, + required this.streamMusicCodec, + required this.downloadMusicCodec, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + map['audio_quality'] = Variable( + $PreferencesTableTable.$converteraudioQuality.toSql(audioQuality)); + } + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + { + map['close_behavior'] = Variable( + $PreferencesTableTable.$convertercloseBehavior.toSql(closeBehavior)); + } + { + map['accent_color_scheme'] = Variable($PreferencesTableTable + .$converteraccentColorScheme + .toSql(accentColorScheme)); + } + { + map['layout_mode'] = Variable( + $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode)); + } + { + map['locale'] = Variable( + $PreferencesTableTable.$converterlocale.toSql(locale)); + } + { + map['market'] = Variable( + $PreferencesTableTable.$convertermarket.toSql(market)); + } + { + map['search_mode'] = Variable( + $PreferencesTableTable.$convertersearchMode.toSql(searchMode)); + } + map['download_location'] = Variable(downloadLocation); + { + map['local_library_location'] = Variable($PreferencesTableTable + .$converterlocalLibraryLocation + .toSql(localLibraryLocation)); + } + map['piped_instance'] = Variable(pipedInstance); + { + map['theme_mode'] = Variable( + $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); + } + { + map['audio_source'] = Variable( + $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); + } + { + map['stream_music_codec'] = Variable($PreferencesTableTable + .$converterstreamMusicCodec + .toSql(streamMusicCodec)); + } + { + map['download_music_codec'] = Variable($PreferencesTableTable + .$converterdownloadMusicCodec + .toSql(downloadMusicCodec)); + } + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + audioQuality: Value(audioQuality), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + themeMode: Value(themeMode), + audioSource: Value(audioSource), + streamMusicCodec: Value(streamMusicCodec), + downloadMusicCodec: Value(downloadMusicCodec), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + audioQuality: $PreferencesTableTable.$converteraudioQuality + .fromJson(serializer.fromJson(json['audioQuality'])), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: $PreferencesTableTable.$convertercloseBehavior + .fromJson(serializer.fromJson(json['closeBehavior'])), + accentColorScheme: + serializer.fromJson(json['accentColorScheme']), + layoutMode: $PreferencesTableTable.$converterlayoutMode + .fromJson(serializer.fromJson(json['layoutMode'])), + locale: serializer.fromJson(json['locale']), + market: $PreferencesTableTable.$convertermarket + .fromJson(serializer.fromJson(json['market'])), + searchMode: $PreferencesTableTable.$convertersearchMode + .fromJson(serializer.fromJson(json['searchMode'])), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson>(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + themeMode: $PreferencesTableTable.$converterthemeMode + .fromJson(serializer.fromJson(json['themeMode'])), + audioSource: $PreferencesTableTable.$converteraudioSource + .fromJson(serializer.fromJson(json['audioSource'])), + streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec + .fromJson(serializer.fromJson(json['streamMusicCodec'])), + downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec + .fromJson(serializer.fromJson(json['downloadMusicCodec'])), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioQuality': serializer.toJson( + $PreferencesTableTable.$converteraudioQuality.toJson(audioQuality)), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson( + $PreferencesTableTable.$convertercloseBehavior.toJson(closeBehavior)), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson( + $PreferencesTableTable.$converterlayoutMode.toJson(layoutMode)), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson( + $PreferencesTableTable.$convertermarket.toJson(market)), + 'searchMode': serializer.toJson( + $PreferencesTableTable.$convertersearchMode.toJson(searchMode)), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': + serializer.toJson>(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'themeMode': serializer.toJson( + $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), + 'audioSource': serializer.toJson( + $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), + 'streamMusicCodec': serializer.toJson($PreferencesTableTable + .$converterstreamMusicCodec + .toJson(streamMusicCodec)), + 'downloadMusicCodec': serializer.toJson($PreferencesTableTable + .$converterdownloadMusicCodec + .toJson(downloadMusicCodec)), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), + }; + } + + PreferencesTableData copyWith( + {int? id, + SourceQualities? audioQuality, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + CloseBehavior? closeBehavior, + SpotubeColor? accentColorScheme, + LayoutMode? layoutMode, + Locale? locale, + Market? market, + SearchMode? searchMode, + String? downloadLocation, + List? localLibraryLocation, + String? pipedInstance, + ThemeMode? themeMode, + AudioSource? audioSource, + SourceCodecs? streamMusicCodec, + SourceCodecs? downloadMusicCodec, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect}) => + PreferencesTableData( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + ); + @override + String toString() { + return (StringBuffer('PreferencesTableData(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PreferencesTableData && + other.id == this.id && + other.audioQuality == this.audioQuality && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.themeMode == this.themeMode && + other.audioSource == this.audioSource && + other.streamMusicCodec == this.streamMusicCodec && + other.downloadMusicCodec == this.downloadMusicCodec && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect); +} + +class PreferencesTableCompanion extends UpdateCompanion { + final Value id; + final Value audioQuality; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value> localLibraryLocation; + final Value pipedInstance; + final Value themeMode; + final Value audioSource; + final Value streamMusicCodec; + final Value downloadMusicCodec; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + const PreferencesTableCompanion({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? audioQuality, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? themeMode, + Expression? audioSource, + Expression? streamMusicCodec, + Expression? downloadMusicCodec, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (audioQuality != null) 'audio_quality': audioQuality, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSource != null) 'audio_source': audioSource, + if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, + if (downloadMusicCodec != null) + 'download_music_codec': downloadMusicCodec, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, + }); + } + + PreferencesTableCompanion copyWith( + {Value? id, + Value? audioQuality, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value>? localLibraryLocation, + Value? pipedInstance, + Value? themeMode, + Value? audioSource, + Value? streamMusicCodec, + Value? downloadMusicCodec, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect}) { + return PreferencesTableCompanion( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (audioQuality.present) { + map['audio_quality'] = Variable($PreferencesTableTable + .$converteraudioQuality + .toSql(audioQuality.value)); + } + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); + } + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); + } + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable($PreferencesTableTable + .$convertercloseBehavior + .toSql(closeBehavior.value)); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable($PreferencesTableTable + .$converteraccentColorScheme + .toSql(accentColorScheme.value)); + } + if (layoutMode.present) { + map['layout_mode'] = Variable( + $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode.value)); + } + if (locale.present) { + map['locale'] = Variable( + $PreferencesTableTable.$converterlocale.toSql(locale.value)); + } + if (market.present) { + map['market'] = Variable( + $PreferencesTableTable.$convertermarket.toSql(market.value)); + } + if (searchMode.present) { + map['search_mode'] = Variable( + $PreferencesTableTable.$convertersearchMode.toSql(searchMode.value)); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = Variable($PreferencesTableTable + .$converterlocalLibraryLocation + .toSql(localLibraryLocation.value)); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable( + $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); + } + if (audioSource.present) { + map['audio_source'] = Variable($PreferencesTableTable + .$converteraudioSource + .toSql(audioSource.value)); + } + if (streamMusicCodec.present) { + map['stream_music_codec'] = Variable($PreferencesTableTable + .$converterstreamMusicCodec + .toSql(streamMusicCodec.value)); + } + if (downloadMusicCodec.present) { + map['download_music_codec'] = Variable($PreferencesTableTable + .$converterdownloadMusicCodec + .toSql(downloadMusicCodec.value)); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PreferencesTableCompanion(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + _$AppDatabaseManager get managers => _$AppDatabaseManager(this); + late final $PreferencesTableTable preferencesTable = + $PreferencesTableTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [preferencesTable]; +} + +typedef $$PreferencesTableTableInsertCompanionBuilder + = PreferencesTableCompanion Function({ + Value id, + Value audioQuality, + Value albumColorSync, + Value amoledDarkTheme, + Value checkUpdate, + Value normalizeAudio, + Value showSystemTrayIcon, + Value systemTitleBar, + Value skipNonMusic, + Value closeBehavior, + Value accentColorScheme, + Value layoutMode, + Value locale, + Value market, + Value searchMode, + Value downloadLocation, + Value> localLibraryLocation, + Value pipedInstance, + Value themeMode, + Value audioSource, + Value streamMusicCodec, + Value downloadMusicCodec, + Value discordPresence, + Value endlessPlayback, + Value enableConnect, +}); +typedef $$PreferencesTableTableUpdateCompanionBuilder + = PreferencesTableCompanion Function({ + Value id, + Value audioQuality, + Value albumColorSync, + Value amoledDarkTheme, + Value checkUpdate, + Value normalizeAudio, + Value showSystemTrayIcon, + Value systemTitleBar, + Value skipNonMusic, + Value closeBehavior, + Value accentColorScheme, + Value layoutMode, + Value locale, + Value market, + Value searchMode, + Value downloadLocation, + Value> localLibraryLocation, + Value pipedInstance, + Value themeMode, + Value audioSource, + Value streamMusicCodec, + Value downloadMusicCodec, + Value discordPresence, + Value endlessPlayback, + Value enableConnect, +}); + +class $$PreferencesTableTableTableManager extends RootTableManager< + _$AppDatabase, + $PreferencesTableTable, + PreferencesTableData, + $$PreferencesTableTableFilterComposer, + $$PreferencesTableTableOrderingComposer, + $$PreferencesTableTableProcessedTableManager, + $$PreferencesTableTableInsertCompanionBuilder, + $$PreferencesTableTableUpdateCompanionBuilder> { + $$PreferencesTableTableTableManager( + _$AppDatabase db, $PreferencesTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$PreferencesTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$PreferencesTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$PreferencesTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value audioQuality = const Value.absent(), + Value albumColorSync = const Value.absent(), + Value amoledDarkTheme = const Value.absent(), + Value checkUpdate = const Value.absent(), + Value normalizeAudio = const Value.absent(), + Value showSystemTrayIcon = const Value.absent(), + Value systemTitleBar = const Value.absent(), + Value skipNonMusic = const Value.absent(), + Value closeBehavior = const Value.absent(), + Value accentColorScheme = const Value.absent(), + Value layoutMode = const Value.absent(), + Value locale = const Value.absent(), + Value market = const Value.absent(), + Value searchMode = const Value.absent(), + Value downloadLocation = const Value.absent(), + Value> localLibraryLocation = const Value.absent(), + Value pipedInstance = const Value.absent(), + Value themeMode = const Value.absent(), + Value audioSource = const Value.absent(), + Value streamMusicCodec = const Value.absent(), + Value downloadMusicCodec = const Value.absent(), + Value discordPresence = const Value.absent(), + Value endlessPlayback = const Value.absent(), + Value enableConnect = const Value.absent(), + }) => + PreferencesTableCompanion( + id: id, + audioQuality: audioQuality, + albumColorSync: albumColorSync, + amoledDarkTheme: amoledDarkTheme, + checkUpdate: checkUpdate, + normalizeAudio: normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon, + systemTitleBar: systemTitleBar, + skipNonMusic: skipNonMusic, + closeBehavior: closeBehavior, + accentColorScheme: accentColorScheme, + layoutMode: layoutMode, + locale: locale, + market: market, + searchMode: searchMode, + downloadLocation: downloadLocation, + localLibraryLocation: localLibraryLocation, + pipedInstance: pipedInstance, + themeMode: themeMode, + audioSource: audioSource, + streamMusicCodec: streamMusicCodec, + downloadMusicCodec: downloadMusicCodec, + discordPresence: discordPresence, + endlessPlayback: endlessPlayback, + enableConnect: enableConnect, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value audioQuality = const Value.absent(), + Value albumColorSync = const Value.absent(), + Value amoledDarkTheme = const Value.absent(), + Value checkUpdate = const Value.absent(), + Value normalizeAudio = const Value.absent(), + Value showSystemTrayIcon = const Value.absent(), + Value systemTitleBar = const Value.absent(), + Value skipNonMusic = const Value.absent(), + Value closeBehavior = const Value.absent(), + Value accentColorScheme = const Value.absent(), + Value layoutMode = const Value.absent(), + Value locale = const Value.absent(), + Value market = const Value.absent(), + Value searchMode = const Value.absent(), + Value downloadLocation = const Value.absent(), + Value> localLibraryLocation = const Value.absent(), + Value pipedInstance = const Value.absent(), + Value themeMode = const Value.absent(), + Value audioSource = const Value.absent(), + Value streamMusicCodec = const Value.absent(), + Value downloadMusicCodec = const Value.absent(), + Value discordPresence = const Value.absent(), + Value endlessPlayback = const Value.absent(), + Value enableConnect = const Value.absent(), + }) => + PreferencesTableCompanion.insert( + id: id, + audioQuality: audioQuality, + albumColorSync: albumColorSync, + amoledDarkTheme: amoledDarkTheme, + checkUpdate: checkUpdate, + normalizeAudio: normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon, + systemTitleBar: systemTitleBar, + skipNonMusic: skipNonMusic, + closeBehavior: closeBehavior, + accentColorScheme: accentColorScheme, + layoutMode: layoutMode, + locale: locale, + market: market, + searchMode: searchMode, + downloadLocation: downloadLocation, + localLibraryLocation: localLibraryLocation, + pipedInstance: pipedInstance, + themeMode: themeMode, + audioSource: audioSource, + streamMusicCodec: streamMusicCodec, + downloadMusicCodec: downloadMusicCodec, + discordPresence: discordPresence, + endlessPlayback: endlessPlayback, + enableConnect: enableConnect, + ), + )); +} + +class $$PreferencesTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $PreferencesTableTable, + PreferencesTableData, + $$PreferencesTableTableFilterComposer, + $$PreferencesTableTableOrderingComposer, + $$PreferencesTableTableProcessedTableManager, + $$PreferencesTableTableInsertCompanionBuilder, + $$PreferencesTableTableUpdateCompanionBuilder> { + $$PreferencesTableTableProcessedTableManager(super.$state); +} + +class $$PreferencesTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $PreferencesTableTable> { + $$PreferencesTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get audioQuality => $state.composableBuilder( + column: $state.table.audioQuality, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get albumColorSync => $state.composableBuilder( + column: $state.table.albumColorSync, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get amoledDarkTheme => $state.composableBuilder( + column: $state.table.amoledDarkTheme, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get checkUpdate => $state.composableBuilder( + column: $state.table.checkUpdate, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get normalizeAudio => $state.composableBuilder( + column: $state.table.normalizeAudio, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get showSystemTrayIcon => $state.composableBuilder( + column: $state.table.showSystemTrayIcon, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get systemTitleBar => $state.composableBuilder( + column: $state.table.systemTitleBar, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get skipNonMusic => $state.composableBuilder( + column: $state.table.skipNonMusic, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get closeBehavior => $state.composableBuilder( + column: $state.table.closeBehavior, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get accentColorScheme => $state.composableBuilder( + column: $state.table.accentColorScheme, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get layoutMode => $state.composableBuilder( + column: $state.table.layoutMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters get locale => + $state.composableBuilder( + column: $state.table.locale, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters get market => + $state.composableBuilder( + column: $state.table.market, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get searchMode => $state.composableBuilder( + column: $state.table.searchMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get downloadLocation => $state.composableBuilder( + column: $state.table.downloadLocation, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters, List, String> + get localLibraryLocation => $state.composableBuilder( + column: $state.table.localLibraryLocation, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get pipedInstance => $state.composableBuilder( + column: $state.table.pipedInstance, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters get themeMode => + $state.composableBuilder( + column: $state.table.themeMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get audioSource => $state.composableBuilder( + column: $state.table.audioSource, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get streamMusicCodec => $state.composableBuilder( + column: $state.table.streamMusicCodec, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get downloadMusicCodec => $state.composableBuilder( + column: $state.table.downloadMusicCodec, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get discordPresence => $state.composableBuilder( + column: $state.table.discordPresence, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get endlessPlayback => $state.composableBuilder( + column: $state.table.endlessPlayback, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get enableConnect => $state.composableBuilder( + column: $state.table.enableConnect, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$PreferencesTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $PreferencesTableTable> { + $$PreferencesTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get audioQuality => $state.composableBuilder( + column: $state.table.audioQuality, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get albumColorSync => $state.composableBuilder( + column: $state.table.albumColorSync, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get amoledDarkTheme => $state.composableBuilder( + column: $state.table.amoledDarkTheme, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get checkUpdate => $state.composableBuilder( + column: $state.table.checkUpdate, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get normalizeAudio => $state.composableBuilder( + column: $state.table.normalizeAudio, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get showSystemTrayIcon => $state.composableBuilder( + column: $state.table.showSystemTrayIcon, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get systemTitleBar => $state.composableBuilder( + column: $state.table.systemTitleBar, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get skipNonMusic => $state.composableBuilder( + column: $state.table.skipNonMusic, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get closeBehavior => $state.composableBuilder( + column: $state.table.closeBehavior, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get accentColorScheme => $state.composableBuilder( + column: $state.table.accentColorScheme, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get layoutMode => $state.composableBuilder( + column: $state.table.layoutMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get locale => $state.composableBuilder( + column: $state.table.locale, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get market => $state.composableBuilder( + column: $state.table.market, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get searchMode => $state.composableBuilder( + column: $state.table.searchMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get downloadLocation => $state.composableBuilder( + column: $state.table.downloadLocation, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get localLibraryLocation => $state.composableBuilder( + column: $state.table.localLibraryLocation, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get pipedInstance => $state.composableBuilder( + column: $state.table.pipedInstance, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get themeMode => $state.composableBuilder( + column: $state.table.themeMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get audioSource => $state.composableBuilder( + column: $state.table.audioSource, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get streamMusicCodec => $state.composableBuilder( + column: $state.table.streamMusicCodec, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get downloadMusicCodec => $state.composableBuilder( + column: $state.table.downloadMusicCodec, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get discordPresence => $state.composableBuilder( + column: $state.table.discordPresence, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get endlessPlayback => $state.composableBuilder( + column: $state.table.endlessPlayback, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get enableConnect => $state.composableBuilder( + column: $state.table.enableConnect, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +class _$AppDatabaseManager { + final _$AppDatabase _db; + _$AppDatabaseManager(this._db); + $$PreferencesTableTableTableManager get preferencesTable => + $$PreferencesTableTableTableManager(_db, _db.preferencesTable); +} diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart new file mode 100644 index 000000000..ae4ec1e80 --- /dev/null +++ b/lib/models/database/tables/preferences.dart @@ -0,0 +1,125 @@ +part of '../database.dart'; + +enum LayoutMode { + compact, + extended, + adaptive, +} + +enum CloseBehavior { + minimizeToTray, + close, +} + +enum AudioSource { + youtube, + piped, + jiosaavn; + + String get label => name[0].toUpperCase() + name.substring(1); +} + +enum MusicCodec { + m4a._("M4a (Best for downloaded music)"), + weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); + + final String label; + const MusicCodec._(this.label); +} + +enum SearchMode { + youtube._("YouTube"), + youtubeMusic._("YouTube Music"); + + final String label; + + const SearchMode._(this.label); + + factory SearchMode.fromString(String key) { + return SearchMode.values.firstWhere((e) => e.name == key); + } +} + +class PreferencesTable extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get audioQuality => textEnum() + .withDefault(Constant(SourceQualities.high.name))(); + BoolColumn get albumColorSync => + boolean().withDefault(const Constant(true))(); + BoolColumn get amoledDarkTheme => + boolean().withDefault(const Constant(false))(); + BoolColumn get checkUpdate => boolean().withDefault(const Constant(true))(); + BoolColumn get normalizeAudio => + boolean().withDefault(const Constant(false))(); + BoolColumn get showSystemTrayIcon => + boolean().withDefault(const Constant(false))(); + BoolColumn get systemTitleBar => + boolean().withDefault(const Constant(false))(); + BoolColumn get skipNonMusic => boolean().withDefault(const Constant(false))(); + TextColumn get closeBehavior => textEnum() + .withDefault(Constant(CloseBehavior.close.name))(); + TextColumn get accentColorScheme => text() + .withDefault(const Constant("Blue:0xFF2196F3")) + .map(const SpotubeColorConverter())(); + TextColumn get layoutMode => + textEnum().withDefault(Constant(LayoutMode.adaptive.name))(); + TextColumn get locale => text() + .withDefault( + const Constant('{"languageCode":"system","countryCode":"system"}'), + ) + .map(const LocaleConverter())(); + TextColumn get market => + textEnum().withDefault(Constant(Market.US.name))(); + TextColumn get searchMode => + textEnum().withDefault(Constant(SearchMode.youtube.name))(); + TextColumn get downloadLocation => text().withDefault(const Constant(""))(); + TextColumn get localLibraryLocation => + text().withDefault(const Constant("")).map(const StringListConverter())(); + TextColumn get pipedInstance => + text().withDefault(const Constant("https://pipedapi.kavin.rocks"))(); + TextColumn get themeMode => + textEnum().withDefault(Constant(ThemeMode.system.name))(); + TextColumn get audioSource => + textEnum().withDefault(Constant(AudioSource.youtube.name))(); + TextColumn get streamMusicCodec => + textEnum().withDefault(Constant(SourceCodecs.weba.name))(); + TextColumn get downloadMusicCodec => + textEnum().withDefault(Constant(SourceCodecs.m4a.name))(); + BoolColumn get discordPresence => + boolean().withDefault(const Constant(true))(); + BoolColumn get endlessPlayback => + boolean().withDefault(const Constant(true))(); + BoolColumn get enableConnect => + boolean().withDefault(const Constant(false))(); + + // Default values as PreferencesTableData + static PreferencesTableData defaults() { + return PreferencesTableData( + id: 0, + audioQuality: SourceQualities.high, + albumColorSync: true, + amoledDarkTheme: false, + checkUpdate: true, + normalizeAudio: false, + showSystemTrayIcon: false, + systemTitleBar: false, + skipNonMusic: false, + closeBehavior: CloseBehavior.close, + accentColorScheme: SpotubeColor(Colors.blue.value, name: "Blue"), + layoutMode: LayoutMode.adaptive, + locale: const Locale("system", "system"), + market: Market.US, + searchMode: SearchMode.youtube, + downloadLocation: "", + localLibraryLocation: [], + pipedInstance: "https://pipedapi.kavin.rocks", + themeMode: ThemeMode.system, + audioSource: AudioSource.youtube, + streamMusicCodec: SourceCodecs.weba, + downloadMusicCodec: SourceCodecs.m4a, + discordPresence: true, + endlessPlayback: true, + enableConnect: false, + ); + } +} diff --git a/lib/models/database/typeconverters/color.dart b/lib/models/database/typeconverters/color.dart new file mode 100644 index 000000000..70c273747 --- /dev/null +++ b/lib/models/database/typeconverters/color.dart @@ -0,0 +1,29 @@ +part of '../database.dart'; + +class ColorConverter extends TypeConverter { + const ColorConverter(); + + @override + Color fromSql(int fromDb) { + return Color(fromDb); + } + + @override + int toSql(Color value) { + return value.value; + } +} + +class SpotubeColorConverter extends TypeConverter { + const SpotubeColorConverter(); + + @override + SpotubeColor fromSql(String fromDb) { + return SpotubeColor.fromString(fromDb); + } + + @override + String toSql(SpotubeColor value) { + return value.toString(); + } +} diff --git a/lib/models/database/typeconverters/locale.dart b/lib/models/database/typeconverters/locale.dart new file mode 100644 index 000000000..c460088e3 --- /dev/null +++ b/lib/models/database/typeconverters/locale.dart @@ -0,0 +1,19 @@ +part of '../database.dart'; + +class LocaleConverter extends TypeConverter { + const LocaleConverter(); + + @override + Locale fromSql(String fromDb) { + final rawMap = jsonDecode(fromDb) as Map; + return Locale(rawMap["languageCode"], rawMap["countryCode"]); + } + + @override + String toSql(Locale value) { + return jsonEncode({ + "languageCode": value.languageCode, + "countryCode": value.countryCode, + }); + } +} diff --git a/lib/models/database/typeconverters/string_list.dart b/lib/models/database/typeconverters/string_list.dart new file mode 100644 index 000000000..5c30a9971 --- /dev/null +++ b/lib/models/database/typeconverters/string_list.dart @@ -0,0 +1,15 @@ +part of '../database.dart'; + +class StringListConverter extends TypeConverter, String> { + const StringListConverter(); + + @override + List fromSql(String fromDb) { + return fromDb.split(","); + } + + @override + String toSql(List value) { + return value.join(","); + } +} diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 147319078..a6136e621 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -14,10 +14,11 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.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/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index 2ab4b14a7..147841761 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_overlay.dart'; import 'package:spotube/modules/player/player_track_details.dart'; @@ -20,7 +21,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication_provider.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'; + import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index 79d229efb..592a3d905 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -9,6 +9,7 @@ import 'package:sidebarx/sidebarx.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -23,7 +24,7 @@ import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.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'; import 'package:spotube/utils/service_utils.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/modules/root/spotube_navigation_bar.dart b/lib/modules/root/spotube_navigation_bar.dart index e16ad1a89..c624a40c2 100644 --- a/lib/modules/root/spotube_navigation_bar.dart +++ b/lib/modules/root/spotube_navigation_bar.dart @@ -10,9 +10,10 @@ import 'package:spotube/collections/side_bar_tiles.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/models/database/database.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); diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index fab51d063..e7087afd7 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -4,11 +4,11 @@ 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/models/database/database.dart'; import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/string.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; final audioSourceToIconMap = { AudioSource.youtube: const Icon( diff --git a/lib/pages/getting_started/sections/region.dart b/lib/pages/getting_started/sections/region.dart index 0a80fba21..9e31a273c 100644 --- a/lib/pages/getting_started/sections/region.dart +++ b/lib/pages/getting_started/sections/region.dart @@ -55,14 +55,14 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget { ), const Gap(16), DropdownMenu( - initialSelection: preferences.recommendationMarket, + initialSelection: preferences.market, onSelected: (value) { if (value == null) return; ref .read(userPreferencesProvider.notifier) .setRecommendationMarket(value); }, - hintText: preferences.recommendationMarket.name, + hintText: preferences.market.name, label: Text(context.l10n.market_place_region), inputDecorationTheme: const InputDecorationTheme(isDense: true), diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index c73c0b085..b62013c5c 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -39,7 +39,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget { final genresCollection = ref.watch(categoryGenresProvider); final limit = useValueNotifier(10); - final market = useValueNotifier(preferences.recommendationMarket); + final market = useValueNotifier(preferences.market); final genres = useState>([]); final artists = useState>([]); diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart index 67ed282b5..f97add426 100644 --- a/lib/pages/settings/sections/appearance.dart +++ b/lib/pages/settings/sections/appearance.dart @@ -3,12 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/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'; class SettingsAppearanceSection extends HookConsumerWidget { final bool isGettingStarted; diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 5fbbd8b0e..88f0ae6d4 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/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 { diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index c9776fd67..18c2d088e 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -57,7 +57,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget { secondary: const Icon(SpotubeIcons.shoppingBag), title: Text(context.l10n.market_place_region), subtitle: Text(context.l10n.recommendation_country), - value: preferences.recommendationMarket, + value: preferences.market, onChanged: (value) { if (value == null) return; preferencesNotifier.setRecommendationMarket(value); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 0d37d9902..6273c557f 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -6,12 +6,13 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/piped_instances_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/services/sourced_track/enums.dart'; class SettingsPlaybackSection extends HookConsumerWidget { diff --git a/lib/provider/database/database.dart b/lib/provider/database/database.dart new file mode 100644 index 000000000..95976e56b --- /dev/null +++ b/lib/provider/database/database.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/database/database.dart'; + +final databaseProvider = Provider((ref) => AppDatabase()); diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index c8eb3657a..d52073da4 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -14,7 +14,7 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/server/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/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/provider/discord_provider.dart'; diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 12d066acb..461ac24e6 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -1,10 +1,11 @@ +import 'package:spotube/models/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.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 { diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index dd9d6c3bb..679f58b10 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -7,7 +7,6 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/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/logger/logger.dart'; class ServerPlaybackRoutes { diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart index cacddbdf5..43d2e4748 100644 --- a/lib/provider/spotify/album/releases.dart +++ b/lib/provider/spotify/album/releases.dart @@ -30,7 +30,7 @@ class AlbumReleasesNotifier @override fetch(int offset, int limit) async { - final market = ref.read(userPreferencesProvider).recommendationMarket; + final market = ref.read(userPreferencesProvider).market; final albums = await spotify.browse .newReleases(country: market) @@ -43,7 +43,7 @@ class AlbumReleasesNotifier build() async { ref.watch(spotifyProvider); ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); ref.watch(allFollowedArtistsProvider); diff --git a/lib/provider/spotify/artist/albums.dart b/lib/provider/spotify/artist/albums.dart index 16bd87681..32aa38a6f 100644 --- a/lib/provider/spotify/artist/albums.dart +++ b/lib/provider/spotify/artist/albums.dart @@ -30,7 +30,7 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< @override fetch(arg, offset, limit) async { - final market = ref.read(userPreferencesProvider).recommendationMarket; + final market = ref.read(userPreferencesProvider).market; final albums = await spotify.artists .albums(arg, country: market) .getPage(limit, offset); @@ -44,7 +44,7 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.watch(spotifyProvider); ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final albums = await fetch(arg, 0, 20); return ArtistAlbumsState( diff --git a/lib/provider/spotify/artist/top_tracks.dart b/lib/provider/spotify/artist/top_tracks.dart index fa40d6469..a2862c3d9 100644 --- a/lib/provider/spotify/artist/top_tracks.dart +++ b/lib/provider/spotify/artist/top_tracks.dart @@ -6,8 +6,7 @@ final artistTopTracksProvider = ref.cacheFor(); final spotify = ref.watch(spotifyProvider); - final market = ref - .watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final tracks = await spotify.artists.topTracks(artistId, market); return tracks.toList(); diff --git a/lib/provider/spotify/category/categories.dart b/lib/provider/spotify/category/categories.dart index 7652215c8..6237b64c4 100644 --- a/lib/provider/spotify/category/categories.dart +++ b/lib/provider/spotify/category/categories.dart @@ -3,8 +3,7 @@ part of '../spotify.dart'; final categoriesProvider = FutureProvider( (ref) async { final spotify = ref.watch(spotifyProvider); - final market = ref - .watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); final categories = await spotify.categories .list( diff --git a/lib/provider/spotify/category/playlists.dart b/lib/provider/spotify/category/playlists.dart index 979b7f319..18d4845f7 100644 --- a/lib/provider/spotify/category/playlists.dart +++ b/lib/provider/spotify/category/playlists.dart @@ -33,7 +33,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< final preferences = ref.read(userPreferencesProvider); final playlists = await Pages( spotify, - "v1/browse/categories/$arg/playlists?country=${preferences.recommendationMarket.name}&locale=${preferences.locale}", + "v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}", (json) => json == null ? null : PlaylistSimple.fromJson(json), 'playlists', (json) => PlaylistsFeatured.fromJson(json), @@ -48,7 +48,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.watch(spotifyProvider); ref.watch(userPreferencesProvider.select((s) => s.locale)); - ref.watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + ref.watch(userPreferencesProvider.select((s) => s.market)); final playlists = await fetch(arg, 0, 8); diff --git a/lib/provider/spotify/playlist/generate.dart b/lib/provider/spotify/playlist/generate.dart index 2e1196dda..0832003e9 100644 --- a/lib/provider/spotify/playlist/generate.dart +++ b/lib/provider/spotify/playlist/generate.dart @@ -5,7 +5,7 @@ final generatePlaylistProvider = FutureProvider.autoDispose (ref, input) async { final spotify = ref.watch(spotifyProvider); final market = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final recommendation = await spotify.recommendations diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart index bd97f08b3..dc00d913e 100644 --- a/lib/provider/spotify/search/search.dart +++ b/lib/provider/spotify/search/search.dart @@ -42,7 +42,7 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier value.recommendationMarket), + userPreferencesProvider.select((value) => value.market), ); final results = await fetch(arg, 0, 10); diff --git a/lib/provider/spotify/views/home.dart b/lib/provider/spotify/views/home.dart index 810d110dd..515869533 100644 --- a/lib/provider/spotify/views/home.dart +++ b/lib/provider/spotify/views/home.dart @@ -5,7 +5,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart final homeViewProvider = FutureProvider((ref) async { final country = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( authenticationProvider.select((s) => s?.getCookie("sp_t")), diff --git a/lib/provider/spotify/views/home_section.dart b/lib/provider/spotify/views/home_section.dart index 1078fa72d..04c4cbd6e 100644 --- a/lib/provider/spotify/views/home_section.dart +++ b/lib/provider/spotify/views/home_section.dart @@ -8,7 +8,7 @@ final homeSectionViewProvider = FutureProvider.family( (ref, sectionUri) async { final country = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( authenticationProvider.select((s) => s?.getCookie("sp_t")), diff --git a/lib/provider/spotify/views/view.dart b/lib/provider/spotify/views/view.dart index f1af998bb..ff565febf 100644 --- a/lib/provider/spotify/views/view.dart +++ b/lib/provider/spotify/views/view.dart @@ -4,7 +4,7 @@ final viewProvider = FutureProvider.family, String>( (ref, viewName) async { final customSpotify = ref.watch(customSpotifyEndpointProvider); final market = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final locale = ref.watch( userPreferencesProvider.select((s) => s.locale), diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 5825104ac..8b96305fc 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -1,54 +1,116 @@ -import 'dart:async'; - +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/provider/database/database.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'; 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; +typedef UserPreferences = PreferencesTableData; - UserPreferencesNotifier(this.ref) - : super(UserPreferences.withDefaults(), "preferences"); +class UserPreferencesNotifier extends Notifier { + @override + build() { + final db = ref.watch(databaseProvider); + + (db.select(db.preferencesTable)..where((tbl) => tbl.id.equals(0))) + .getSingleOrNull() + .then((result) async { + if (result == null) { + await db.into(db.preferencesTable).insert( + PreferencesTableCompanion.insert( + id: const Value(0), + downloadLocation: Value(await _getDefaultDownloadDirectory()), + ), + ); + } + + state = await (db.select(db.preferencesTable) + ..where((tbl) => tbl.id.equals(0))) + .getSingle(); + + final subscription = (db.select(db.preferencesTable) + ..where((tbl) => tbl.id.equals(0))) + .watchSingle() + .listen((event) async { + state = event; + + if (kIsDesktop) { + await windowManager.setTitleBarStyle( + state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + ); + } + + await audioPlayer.setAudioNormalization(state.normalizeAudio); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + }); - void reset() { - state = UserPreferences.withDefaults(); + return PreferencesTable.defaults(); + } + + Future _getDefaultDownloadDirectory() async { + if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; + + if (kIsMacOS) { + return join((await getLibraryDirectory()).path, "Caches"); + } + + return getDownloadsDirectory().then((dir) { + return join(dir!.path, "Spotube"); + }); + } + + Future setData(PreferencesTableCompanion data) async { + final db = ref.read(databaseProvider); + + final query = db.update(db.preferencesTable)..where((t) => t.id.equals(0)); + + await query.write(data); + } + + Future reset() async { + final db = ref.read(databaseProvider); + + final query = db.update(db.preferencesTable)..where((t) => t.id.equals(0)); + + await query.replace(PreferencesTableCompanion.insert()); } void setStreamMusicCodec(SourceCodecs codec) { - state = state.copyWith(streamMusicCodec: codec); + setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); } void setDownloadMusicCodec(SourceCodecs codec) { - state = state.copyWith(downloadMusicCodec: codec); + setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec))); } void setThemeMode(ThemeMode mode) { - state = state.copyWith(themeMode: mode); + setData(PreferencesTableCompanion(themeMode: Value(mode))); } void setRecommendationMarket(Market country) { - state = state.copyWith(recommendationMarket: country); + setData(PreferencesTableCompanion(market: Value(country))); } void setAccentColorScheme(SpotubeColor color) { - state = state.copyWith(accentColorScheme: color); + setData(PreferencesTableCompanion(accentColorScheme: Value(color))); } void setAlbumColorSync(bool sync) { - state = state.copyWith(albumColorSync: sync); + setData(PreferencesTableCompanion(albumColorSync: Value(sync))); if (!sync) { ref.read(paletteProvider.notifier).state = null; @@ -58,126 +120,87 @@ class UserPreferencesNotifier extends PersistedStateNotifier { } void setCheckUpdate(bool check) { - state = state.copyWith(checkUpdate: check); + setData(PreferencesTableCompanion(checkUpdate: Value(check))); } void setAudioQuality(SourceQualities quality) { - state = state.copyWith(audioQuality: quality); + setData(PreferencesTableCompanion(audioQuality: Value(quality))); } void setDownloadLocation(String downloadDir) { if (downloadDir.isEmpty) return; - state = state.copyWith(downloadLocation: downloadDir); + setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir))); } void setLocalLibraryLocation(List localLibraryDirs) { //if (localLibraryDir.isEmpty) return; - state = state.copyWith(localLibraryLocation: localLibraryDirs); + setData(PreferencesTableCompanion( + localLibraryLocation: Value(localLibraryDirs))); } void setLayoutMode(LayoutMode mode) { - state = state.copyWith(layoutMode: mode); + setData(PreferencesTableCompanion(layoutMode: Value(mode))); } void setCloseBehavior(CloseBehavior behavior) { - state = state.copyWith(closeBehavior: behavior); + setData(PreferencesTableCompanion(closeBehavior: Value(behavior))); } void setShowSystemTrayIcon(bool show) { - state = state.copyWith(showSystemTrayIcon: show); + setData(PreferencesTableCompanion(showSystemTrayIcon: Value(show))); } void setLocale(Locale locale) { - state = state.copyWith(locale: locale); + setData(PreferencesTableCompanion(locale: Value(locale))); } void setPipedInstance(String instance) { - state = state.copyWith(pipedInstance: instance); + setData(PreferencesTableCompanion(pipedInstance: Value(instance))); } void setSearchMode(SearchMode mode) { - state = state.copyWith(searchMode: mode); + setData(PreferencesTableCompanion(searchMode: Value(mode))); } void setSkipNonMusic(bool skip) { - state = state.copyWith(skipNonMusic: skip); + setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); } void setAudioSource(AudioSource type) { - state = state.copyWith(audioSource: type); + setData(PreferencesTableCompanion(audioSource: Value(type))); } void setSystemTitleBar(bool isSystemTitleBar) { - state = state.copyWith(systemTitleBar: isSystemTitleBar); - if (kIsDesktop) { - windowManager.setTitleBarStyle( - isSystemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); - } + setData( + PreferencesTableCompanion( + systemTitleBar: Value(isSystemTitleBar), + ), + ); } void setDiscordPresence(bool discordPresence) { - state = state.copyWith(discordPresence: discordPresence); + setData(PreferencesTableCompanion(discordPresence: Value(discordPresence))); } void setAmoledDarkTheme(bool isAmoled) { - state = state.copyWith(amoledDarkTheme: isAmoled); + setData(PreferencesTableCompanion(amoledDarkTheme: Value(isAmoled))); } void setNormalizeAudio(bool normalize) { - state = state.copyWith(normalizeAudio: normalize); + setData(PreferencesTableCompanion(normalizeAudio: Value(normalize))); audioPlayer.setAudioNormalization(normalize); } void setEndlessPlayback(bool endless) { - state = state.copyWith(endlessPlayback: endless); + setData(PreferencesTableCompanion(endlessPlayback: Value(endless))); } void setEnableConnect(bool enable) { - state = state.copyWith(enableConnect: enable); - } - - Future _getDefaultDownloadDirectory() async { - if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; - - if (kIsMacOS) { - return path.join((await getLibraryDirectory()).path, "Caches"); - } - - return getDownloadsDirectory().then((dir) { - return path.join(dir!.path, "Spotube"); - }); - } - - @override - FutureOr onInit() async { - if (state.downloadLocation.isEmpty) { - state = state.copyWith( - downloadLocation: await _getDefaultDownloadDirectory(), - ); - } - - if (kIsDesktop) { - await windowManager.setTitleBarStyle( - state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); - } - - await audioPlayer.setAudioNormalization(state.normalizeAudio); - } - - @override - FutureOr fromJson(Map json) { - return UserPreferences.fromJson(json); - } - - @override - Map toJson() { - return state.toJson(); + setData(PreferencesTableCompanion(enableConnect: Value(enable))); } } final userPreferencesProvider = - StateNotifierProvider( - (ref) => UserPreferencesNotifier(ref), + NotifierProvider( + () => UserPreferencesNotifier(), ); diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart deleted file mode 100644 index 73dd02e82..000000000 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; - -part 'user_preferences_state.g.dart'; -part 'user_preferences_state.freezed.dart'; - -@JsonEnum() -enum LayoutMode { - compact, - extended, - adaptive, -} - -@JsonEnum() -enum CloseBehavior { - minimizeToTray, - close, -} - -@JsonEnum() -enum AudioSource { - youtube, - piped, - jiosaavn; - - String get label => name[0].toUpperCase() + name.substring(1); -} - -@JsonEnum() -enum MusicCodec { - m4a._("M4a (Best for downloaded music)"), - weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); - - final String label; - const MusicCodec._(this.label); -} - -@JsonEnum() -enum SearchMode { - youtube._("YouTube"), - youtubeMusic._("YouTube Music"); - - final String label; - - const SearchMode._(this.label); - - factory SearchMode.fromString(String key) { - return SearchMode.values.firstWhere((e) => e.name == key); - } -} - -@freezed -class UserPreferences with _$UserPreferences { - const factory UserPreferences({ - @Default(SourceQualities.high) SourceQualities audioQuality, - @Default(true) bool albumColorSync, - @Default(false) bool amoledDarkTheme, - @Default(true) bool checkUpdate, - @Default(false) bool normalizeAudio, - @Default(false) bool showSystemTrayIcon, - @Default(false) bool skipNonMusic, - @Default(false) bool systemTitleBar, - @Default(CloseBehavior.close) CloseBehavior closeBehavior, - @Default(SpotubeColor(0xFF2196F3, name: "Blue")) - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue, - ) - SpotubeColor accentColorScheme, - @Default(LayoutMode.adaptive) LayoutMode layoutMode, - @Default(Locale("system", "system")) - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue, - ) - Locale locale, - @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, - @Default(SourceCodecs.weba) SourceCodecs streamMusicCodec, - @Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec, - @Default(true) bool discordPresence, - @Default(true) bool endlessPlayback, - @Default(false) bool enableConnect, - }) = _UserPreferences; - factory UserPreferences.fromJson(Map json) => - _$UserPreferencesFromJson(json); - - factory UserPreferences.withDefaults() => UserPreferences.fromJson({}); - - static SpotubeColor _accentColorSchemeFromJson(Map json) { - return SpotubeColor.fromString(json["color"]); - } - - static Map? _accentColorSchemeReadValue( - Map json, String key) { - if (json[key] is String) { - return {"color": json[key]}; - } - - return json[key] as Map?; - } - - static Map _accentColorSchemeToJson(SpotubeColor color) { - return {"color": color.toString()}; - } - - static Locale _localeFromJson(Map json) { - return Locale(json["languageCode"], json["countryCode"]); - } - - static Map _localeToJson(Locale locale) { - return { - "languageCode": locale.languageCode, - "countryCode": locale.countryCode, - }; - } - - static Map? _localeReadValue( - Map json, String key) { - if (json[key] is String) { - final map = jsonDecode(json[key]); - return { - "languageCode": map["lc"], - "countryCode": map["cc"], - }; - } - - return json[key] as Map?; - } -} diff --git a/lib/provider/user_preferences/user_preferences_state.freezed.dart b/lib/provider/user_preferences/user_preferences_state.freezed.dart deleted file mode 100644 index 89c7210a3..000000000 --- a/lib/provider/user_preferences/user_preferences_state.freezed.dart +++ /dev/null @@ -1,751 +0,0 @@ -// 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 'user_preferences_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'); - -UserPreferences _$UserPreferencesFromJson(Map json) { - return _UserPreferences.fromJson(json); -} - -/// @nodoc -mixin _$UserPreferences { - SourceQualities get audioQuality => throw _privateConstructorUsedError; - bool get albumColorSync => throw _privateConstructorUsedError; - bool get amoledDarkTheme => throw _privateConstructorUsedError; - bool get checkUpdate => throw _privateConstructorUsedError; - bool get normalizeAudio => throw _privateConstructorUsedError; - bool get showSystemTrayIcon => throw _privateConstructorUsedError; - bool get skipNonMusic => throw _privateConstructorUsedError; - bool get systemTitleBar => throw _privateConstructorUsedError; - CloseBehavior get closeBehavior => throw _privateConstructorUsedError; - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor get accentColorScheme => throw _privateConstructorUsedError; - LayoutMode get layoutMode => throw _privateConstructorUsedError; - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale get locale => throw _privateConstructorUsedError; - 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; - SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError; - SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError; - bool get discordPresence => throw _privateConstructorUsedError; - bool get endlessPlayback => throw _privateConstructorUsedError; - bool get enableConnect => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $UserPreferencesCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $UserPreferencesCopyWith<$Res> { - factory $UserPreferencesCopyWith( - UserPreferences value, $Res Function(UserPreferences) then) = - _$UserPreferencesCopyWithImpl<$Res, UserPreferences>; - @useResult - $Res call( - {SourceQualities audioQuality, - bool albumColorSync, - bool amoledDarkTheme, - bool checkUpdate, - bool normalizeAudio, - bool showSystemTrayIcon, - bool skipNonMusic, - bool systemTitleBar, - CloseBehavior closeBehavior, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor accentColorScheme, - LayoutMode layoutMode, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale locale, - Market recommendationMarket, - SearchMode searchMode, - String downloadLocation, - List localLibraryLocation, - String pipedInstance, - ThemeMode themeMode, - AudioSource audioSource, - SourceCodecs streamMusicCodec, - SourceCodecs downloadMusicCodec, - bool discordPresence, - bool endlessPlayback, - bool enableConnect}); -} - -/// @nodoc -class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> - implements $UserPreferencesCopyWith<$Res> { - _$UserPreferencesCopyWithImpl(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? audioQuality = null, - Object? albumColorSync = null, - Object? amoledDarkTheme = null, - Object? checkUpdate = null, - Object? normalizeAudio = null, - Object? showSystemTrayIcon = null, - Object? skipNonMusic = null, - Object? systemTitleBar = null, - Object? closeBehavior = null, - Object? accentColorScheme = null, - Object? layoutMode = null, - Object? locale = null, - Object? recommendationMarket = null, - Object? searchMode = null, - Object? downloadLocation = null, - Object? localLibraryLocation = null, - Object? pipedInstance = null, - Object? themeMode = null, - Object? audioSource = null, - Object? streamMusicCodec = null, - Object? downloadMusicCodec = null, - Object? discordPresence = null, - Object? endlessPlayback = null, - Object? enableConnect = null, - }) { - return _then(_value.copyWith( - audioQuality: null == audioQuality - ? _value.audioQuality - : audioQuality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - albumColorSync: null == albumColorSync - ? _value.albumColorSync - : albumColorSync // ignore: cast_nullable_to_non_nullable - as bool, - amoledDarkTheme: null == amoledDarkTheme - ? _value.amoledDarkTheme - : amoledDarkTheme // ignore: cast_nullable_to_non_nullable - as bool, - checkUpdate: null == checkUpdate - ? _value.checkUpdate - : checkUpdate // ignore: cast_nullable_to_non_nullable - as bool, - normalizeAudio: null == normalizeAudio - ? _value.normalizeAudio - : normalizeAudio // ignore: cast_nullable_to_non_nullable - as bool, - showSystemTrayIcon: null == showSystemTrayIcon - ? _value.showSystemTrayIcon - : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable - as bool, - skipNonMusic: null == skipNonMusic - ? _value.skipNonMusic - : skipNonMusic // ignore: cast_nullable_to_non_nullable - as bool, - systemTitleBar: null == systemTitleBar - ? _value.systemTitleBar - : systemTitleBar // ignore: cast_nullable_to_non_nullable - as bool, - closeBehavior: null == closeBehavior - ? _value.closeBehavior - : closeBehavior // ignore: cast_nullable_to_non_nullable - as CloseBehavior, - accentColorScheme: null == accentColorScheme - ? _value.accentColorScheme - : accentColorScheme // ignore: cast_nullable_to_non_nullable - as SpotubeColor, - layoutMode: null == layoutMode - ? _value.layoutMode - : layoutMode // ignore: cast_nullable_to_non_nullable - as LayoutMode, - locale: null == locale - ? _value.locale - : locale // ignore: cast_nullable_to_non_nullable - as Locale, - recommendationMarket: null == recommendationMarket - ? _value.recommendationMarket - : recommendationMarket // ignore: cast_nullable_to_non_nullable - as Market, - searchMode: null == searchMode - ? _value.searchMode - : searchMode // ignore: cast_nullable_to_non_nullable - as SearchMode, - downloadLocation: null == downloadLocation - ? _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 - as String, - themeMode: null == themeMode - ? _value.themeMode - : themeMode // ignore: cast_nullable_to_non_nullable - as ThemeMode, - audioSource: null == audioSource - ? _value.audioSource - : audioSource // ignore: cast_nullable_to_non_nullable - as AudioSource, - streamMusicCodec: null == streamMusicCodec - ? _value.streamMusicCodec - : streamMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - downloadMusicCodec: null == downloadMusicCodec - ? _value.downloadMusicCodec - : downloadMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - discordPresence: null == discordPresence - ? _value.discordPresence - : discordPresence // ignore: cast_nullable_to_non_nullable - as bool, - endlessPlayback: null == endlessPlayback - ? _value.endlessPlayback - : endlessPlayback // ignore: cast_nullable_to_non_nullable - as bool, - enableConnect: null == enableConnect - ? _value.enableConnect - : enableConnect // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$UserPreferencesImplCopyWith<$Res> - implements $UserPreferencesCopyWith<$Res> { - factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value, - $Res Function(_$UserPreferencesImpl) then) = - __$$UserPreferencesImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {SourceQualities audioQuality, - bool albumColorSync, - bool amoledDarkTheme, - bool checkUpdate, - bool normalizeAudio, - bool showSystemTrayIcon, - bool skipNonMusic, - bool systemTitleBar, - CloseBehavior closeBehavior, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor accentColorScheme, - LayoutMode layoutMode, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale locale, - Market recommendationMarket, - SearchMode searchMode, - String downloadLocation, - List localLibraryLocation, - String pipedInstance, - ThemeMode themeMode, - AudioSource audioSource, - SourceCodecs streamMusicCodec, - SourceCodecs downloadMusicCodec, - bool discordPresence, - bool endlessPlayback, - bool enableConnect}); -} - -/// @nodoc -class __$$UserPreferencesImplCopyWithImpl<$Res> - extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl> - implements _$$UserPreferencesImplCopyWith<$Res> { - __$$UserPreferencesImplCopyWithImpl( - _$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? audioQuality = null, - Object? albumColorSync = null, - Object? amoledDarkTheme = null, - Object? checkUpdate = null, - Object? normalizeAudio = null, - Object? showSystemTrayIcon = null, - Object? skipNonMusic = null, - Object? systemTitleBar = null, - Object? closeBehavior = null, - Object? accentColorScheme = null, - Object? layoutMode = null, - Object? locale = null, - Object? recommendationMarket = null, - Object? searchMode = null, - Object? downloadLocation = null, - Object? localLibraryLocation = null, - Object? pipedInstance = null, - Object? themeMode = null, - Object? audioSource = null, - Object? streamMusicCodec = null, - Object? downloadMusicCodec = null, - Object? discordPresence = null, - Object? endlessPlayback = null, - Object? enableConnect = null, - }) { - return _then(_$UserPreferencesImpl( - audioQuality: null == audioQuality - ? _value.audioQuality - : audioQuality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - albumColorSync: null == albumColorSync - ? _value.albumColorSync - : albumColorSync // ignore: cast_nullable_to_non_nullable - as bool, - amoledDarkTheme: null == amoledDarkTheme - ? _value.amoledDarkTheme - : amoledDarkTheme // ignore: cast_nullable_to_non_nullable - as bool, - checkUpdate: null == checkUpdate - ? _value.checkUpdate - : checkUpdate // ignore: cast_nullable_to_non_nullable - as bool, - normalizeAudio: null == normalizeAudio - ? _value.normalizeAudio - : normalizeAudio // ignore: cast_nullable_to_non_nullable - as bool, - showSystemTrayIcon: null == showSystemTrayIcon - ? _value.showSystemTrayIcon - : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable - as bool, - skipNonMusic: null == skipNonMusic - ? _value.skipNonMusic - : skipNonMusic // ignore: cast_nullable_to_non_nullable - as bool, - systemTitleBar: null == systemTitleBar - ? _value.systemTitleBar - : systemTitleBar // ignore: cast_nullable_to_non_nullable - as bool, - closeBehavior: null == closeBehavior - ? _value.closeBehavior - : closeBehavior // ignore: cast_nullable_to_non_nullable - as CloseBehavior, - accentColorScheme: null == accentColorScheme - ? _value.accentColorScheme - : accentColorScheme // ignore: cast_nullable_to_non_nullable - as SpotubeColor, - layoutMode: null == layoutMode - ? _value.layoutMode - : layoutMode // ignore: cast_nullable_to_non_nullable - as LayoutMode, - locale: null == locale - ? _value.locale - : locale // ignore: cast_nullable_to_non_nullable - as Locale, - recommendationMarket: null == recommendationMarket - ? _value.recommendationMarket - : recommendationMarket // ignore: cast_nullable_to_non_nullable - as Market, - searchMode: null == searchMode - ? _value.searchMode - : searchMode // ignore: cast_nullable_to_non_nullable - as SearchMode, - downloadLocation: null == downloadLocation - ? _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 - as String, - themeMode: null == themeMode - ? _value.themeMode - : themeMode // ignore: cast_nullable_to_non_nullable - as ThemeMode, - audioSource: null == audioSource - ? _value.audioSource - : audioSource // ignore: cast_nullable_to_non_nullable - as AudioSource, - streamMusicCodec: null == streamMusicCodec - ? _value.streamMusicCodec - : streamMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - downloadMusicCodec: null == downloadMusicCodec - ? _value.downloadMusicCodec - : downloadMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - discordPresence: null == discordPresence - ? _value.discordPresence - : discordPresence // ignore: cast_nullable_to_non_nullable - as bool, - endlessPlayback: null == endlessPlayback - ? _value.endlessPlayback - : endlessPlayback // ignore: cast_nullable_to_non_nullable - as bool, - enableConnect: null == enableConnect - ? _value.enableConnect - : enableConnect // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$UserPreferencesImpl implements _UserPreferences { - const _$UserPreferencesImpl( - {this.audioQuality = SourceQualities.high, - this.albumColorSync = true, - this.amoledDarkTheme = false, - this.checkUpdate = true, - this.normalizeAudio = false, - this.showSystemTrayIcon = false, - this.skipNonMusic = false, - this.systemTitleBar = false, - this.closeBehavior = CloseBehavior.close, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"), - this.layoutMode = LayoutMode.adaptive, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - this.locale = const Locale("system", "system"), - 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, - this.streamMusicCodec = SourceCodecs.weba, - this.downloadMusicCodec = SourceCodecs.m4a, - this.discordPresence = true, - this.endlessPlayback = true, - this.enableConnect = false}) - : _localLibraryLocation = localLibraryLocation; - - factory _$UserPreferencesImpl.fromJson(Map json) => - _$$UserPreferencesImplFromJson(json); - - @override - @JsonKey() - final SourceQualities audioQuality; - @override - @JsonKey() - final bool albumColorSync; - @override - @JsonKey() - final bool amoledDarkTheme; - @override - @JsonKey() - final bool checkUpdate; - @override - @JsonKey() - final bool normalizeAudio; - @override - @JsonKey() - final bool showSystemTrayIcon; - @override - @JsonKey() - final bool skipNonMusic; - @override - @JsonKey() - final bool systemTitleBar; - @override - @JsonKey() - final CloseBehavior closeBehavior; - @override - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - final SpotubeColor accentColorScheme; - @override - @JsonKey() - final LayoutMode layoutMode; - @override - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - final Locale locale; - @override - @JsonKey() - final Market recommendationMarket; - @override - @JsonKey() - final SearchMode searchMode; - @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; - @override - @JsonKey() - final ThemeMode themeMode; - @override - @JsonKey() - final AudioSource audioSource; - @override - @JsonKey() - final SourceCodecs streamMusicCodec; - @override - @JsonKey() - final SourceCodecs downloadMusicCodec; - @override - @JsonKey() - final bool discordPresence; - @override - @JsonKey() - final bool endlessPlayback; - @override - @JsonKey() - final bool enableConnect; - - @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, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$UserPreferencesImpl && - (identical(other.audioQuality, audioQuality) || - other.audioQuality == audioQuality) && - (identical(other.albumColorSync, albumColorSync) || - other.albumColorSync == albumColorSync) && - (identical(other.amoledDarkTheme, amoledDarkTheme) || - other.amoledDarkTheme == amoledDarkTheme) && - (identical(other.checkUpdate, checkUpdate) || - other.checkUpdate == checkUpdate) && - (identical(other.normalizeAudio, normalizeAudio) || - other.normalizeAudio == normalizeAudio) && - (identical(other.showSystemTrayIcon, showSystemTrayIcon) || - other.showSystemTrayIcon == showSystemTrayIcon) && - (identical(other.skipNonMusic, skipNonMusic) || - other.skipNonMusic == skipNonMusic) && - (identical(other.systemTitleBar, systemTitleBar) || - other.systemTitleBar == systemTitleBar) && - (identical(other.closeBehavior, closeBehavior) || - other.closeBehavior == closeBehavior) && - (identical(other.accentColorScheme, accentColorScheme) || - other.accentColorScheme == accentColorScheme) && - (identical(other.layoutMode, layoutMode) || - other.layoutMode == layoutMode) && - (identical(other.locale, locale) || other.locale == locale) && - (identical(other.recommendationMarket, recommendationMarket) || - other.recommendationMarket == recommendationMarket) && - (identical(other.searchMode, searchMode) || - 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) || - other.themeMode == themeMode) && - (identical(other.audioSource, audioSource) || - other.audioSource == audioSource) && - (identical(other.streamMusicCodec, streamMusicCodec) || - other.streamMusicCodec == streamMusicCodec) && - (identical(other.downloadMusicCodec, downloadMusicCodec) || - other.downloadMusicCodec == downloadMusicCodec) && - (identical(other.discordPresence, discordPresence) || - other.discordPresence == discordPresence) && - (identical(other.endlessPlayback, endlessPlayback) || - other.endlessPlayback == endlessPlayback) && - (identical(other.enableConnect, enableConnect) || - other.enableConnect == enableConnect)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hashAll([ - runtimeType, - audioQuality, - albumColorSync, - amoledDarkTheme, - checkUpdate, - normalizeAudio, - showSystemTrayIcon, - skipNonMusic, - systemTitleBar, - closeBehavior, - accentColorScheme, - layoutMode, - locale, - recommendationMarket, - searchMode, - downloadLocation, - const DeepCollectionEquality().hash(_localLibraryLocation), - pipedInstance, - themeMode, - audioSource, - streamMusicCodec, - downloadMusicCodec, - discordPresence, - endlessPlayback, - enableConnect - ]); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => - __$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$UserPreferencesImplToJson( - this, - ); - } -} - -abstract class _UserPreferences implements UserPreferences { - const factory _UserPreferences( - {final SourceQualities audioQuality, - final bool albumColorSync, - final bool amoledDarkTheme, - final bool checkUpdate, - final bool normalizeAudio, - final bool showSystemTrayIcon, - final bool skipNonMusic, - final bool systemTitleBar, - final CloseBehavior closeBehavior, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - final SpotubeColor accentColorScheme, - final LayoutMode layoutMode, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - final Locale locale, - final Market recommendationMarket, - final SearchMode searchMode, - final String downloadLocation, - final List localLibraryLocation, - final String pipedInstance, - final ThemeMode themeMode, - final AudioSource audioSource, - final SourceCodecs streamMusicCodec, - final SourceCodecs downloadMusicCodec, - final bool discordPresence, - final bool endlessPlayback, - final bool enableConnect}) = _$UserPreferencesImpl; - - factory _UserPreferences.fromJson(Map json) = - _$UserPreferencesImpl.fromJson; - - @override - SourceQualities get audioQuality; - @override - bool get albumColorSync; - @override - bool get amoledDarkTheme; - @override - bool get checkUpdate; - @override - bool get normalizeAudio; - @override - bool get showSystemTrayIcon; - @override - bool get skipNonMusic; - @override - bool get systemTitleBar; - @override - CloseBehavior get closeBehavior; - @override - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor get accentColorScheme; - @override - LayoutMode get layoutMode; - @override - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale get locale; - @override - Market get recommendationMarket; - @override - SearchMode get searchMode; - @override - String get downloadLocation; - @override - List get localLibraryLocation; - @override - String get pipedInstance; - @override - ThemeMode get themeMode; - @override - AudioSource get audioSource; - @override - SourceCodecs get streamMusicCodec; - @override - SourceCodecs get downloadMusicCodec; - @override - bool get discordPresence; - @override - bool get endlessPlayback; - @override - bool get enableConnect; - @override - @JsonKey(ignore: true) - _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart deleted file mode 100644 index 4bcb3a467..000000000 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ /dev/null @@ -1,388 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user_preferences_state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$UserPreferencesImpl _$$UserPreferencesImplFromJson(Map json) => - _$UserPreferencesImpl( - audioQuality: - $enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ?? - SourceQualities.high, - albumColorSync: json['albumColorSync'] as bool? ?? true, - amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, - checkUpdate: json['checkUpdate'] as bool? ?? true, - normalizeAudio: json['normalizeAudio'] as bool? ?? false, - showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? false, - skipNonMusic: json['skipNonMusic'] as bool? ?? false, - systemTitleBar: json['systemTitleBar'] as bool? ?? false, - closeBehavior: - $enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ?? - CloseBehavior.close, - accentColorScheme: UserPreferences._accentColorSchemeReadValue( - json, 'accentColorScheme') == - null - ? const SpotubeColor(0xFF2196F3, name: "Blue") - : UserPreferences._accentColorSchemeFromJson( - UserPreferences._accentColorSchemeReadValue( - json, 'accentColorScheme') as Map), - layoutMode: - $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ?? - LayoutMode.adaptive, - locale: UserPreferences._localeReadValue(json, 'locale') == null - ? const Locale("system", "system") - : UserPreferences._localeFromJson( - UserPreferences._localeReadValue(json, 'locale') - as Map), - recommendationMarket: - $enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ?? - Market.US, - searchMode: - $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']) ?? - ThemeMode.system, - audioSource: - $enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ?? - AudioSource.youtube, - streamMusicCodec: $enumDecodeNullable( - _$SourceCodecsEnumMap, json['streamMusicCodec']) ?? - SourceCodecs.weba, - downloadMusicCodec: $enumDecodeNullable( - _$SourceCodecsEnumMap, json['downloadMusicCodec']) ?? - SourceCodecs.m4a, - discordPresence: json['discordPresence'] as bool? ?? true, - endlessPlayback: json['endlessPlayback'] as bool? ?? true, - enableConnect: json['enableConnect'] as bool? ?? false, - ); - -Map _$$UserPreferencesImplToJson( - _$UserPreferencesImpl instance) => - { - 'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!, - 'albumColorSync': instance.albumColorSync, - 'amoledDarkTheme': instance.amoledDarkTheme, - 'checkUpdate': instance.checkUpdate, - 'normalizeAudio': instance.normalizeAudio, - 'showSystemTrayIcon': instance.showSystemTrayIcon, - 'skipNonMusic': instance.skipNonMusic, - 'systemTitleBar': instance.systemTitleBar, - 'closeBehavior': _$CloseBehaviorEnumMap[instance.closeBehavior]!, - 'accentColorScheme': - UserPreferences._accentColorSchemeToJson(instance.accentColorScheme), - 'layoutMode': _$LayoutModeEnumMap[instance.layoutMode]!, - 'locale': UserPreferences._localeToJson(instance.locale), - '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]!, - 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, - 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, - 'discordPresence': instance.discordPresence, - 'endlessPlayback': instance.endlessPlayback, - 'enableConnect': instance.enableConnect, - }; - -const _$SourceQualitiesEnumMap = { - SourceQualities.high: 'high', - SourceQualities.medium: 'medium', - SourceQualities.low: 'low', -}; - -const _$CloseBehaviorEnumMap = { - CloseBehavior.minimizeToTray: 'minimizeToTray', - CloseBehavior.close: 'close', -}; - -const _$LayoutModeEnumMap = { - LayoutMode.compact: 'compact', - LayoutMode.extended: 'extended', - LayoutMode.adaptive: 'adaptive', -}; - -const _$MarketEnumMap = { - Market.AD: 'AD', - Market.AE: 'AE', - Market.AF: 'AF', - Market.AG: 'AG', - Market.AI: 'AI', - Market.AL: 'AL', - Market.AM: 'AM', - Market.AO: 'AO', - Market.AQ: 'AQ', - Market.AR: 'AR', - Market.AS: 'AS', - Market.AT: 'AT', - Market.AU: 'AU', - Market.AW: 'AW', - Market.AX: 'AX', - Market.AZ: 'AZ', - Market.BA: 'BA', - Market.BB: 'BB', - Market.BD: 'BD', - Market.BE: 'BE', - Market.BF: 'BF', - Market.BG: 'BG', - Market.BH: 'BH', - Market.BI: 'BI', - Market.BJ: 'BJ', - Market.BL: 'BL', - Market.BM: 'BM', - Market.BN: 'BN', - Market.BO: 'BO', - Market.BQ: 'BQ', - Market.BR: 'BR', - Market.BS: 'BS', - Market.BT: 'BT', - Market.BV: 'BV', - Market.BW: 'BW', - Market.BY: 'BY', - Market.BZ: 'BZ', - Market.CA: 'CA', - Market.CC: 'CC', - Market.CD: 'CD', - Market.CF: 'CF', - Market.CG: 'CG', - Market.CH: 'CH', - Market.CI: 'CI', - Market.CK: 'CK', - Market.CL: 'CL', - Market.CM: 'CM', - Market.CN: 'CN', - Market.CO: 'CO', - Market.CR: 'CR', - Market.CU: 'CU', - Market.CV: 'CV', - Market.CW: 'CW', - Market.CX: 'CX', - Market.CY: 'CY', - Market.CZ: 'CZ', - Market.DE: 'DE', - Market.DJ: 'DJ', - Market.DK: 'DK', - Market.DM: 'DM', - Market.DO: 'DO', - Market.DZ: 'DZ', - Market.EC: 'EC', - Market.EE: 'EE', - Market.EG: 'EG', - Market.EH: 'EH', - Market.ER: 'ER', - Market.ES: 'ES', - Market.ET: 'ET', - Market.FI: 'FI', - Market.FJ: 'FJ', - Market.FK: 'FK', - Market.FM: 'FM', - Market.FO: 'FO', - Market.FR: 'FR', - Market.GA: 'GA', - Market.GB: 'GB', - Market.GD: 'GD', - Market.GE: 'GE', - Market.GF: 'GF', - Market.GG: 'GG', - Market.GH: 'GH', - Market.GI: 'GI', - Market.GL: 'GL', - Market.GM: 'GM', - Market.GN: 'GN', - Market.GP: 'GP', - Market.GQ: 'GQ', - Market.GR: 'GR', - Market.GS: 'GS', - Market.GT: 'GT', - Market.GU: 'GU', - Market.GW: 'GW', - Market.GY: 'GY', - Market.HK: 'HK', - Market.HM: 'HM', - Market.HN: 'HN', - Market.HR: 'HR', - Market.HT: 'HT', - Market.HU: 'HU', - Market.ID: 'ID', - Market.IE: 'IE', - Market.IL: 'IL', - Market.IM: 'IM', - Market.IN: 'IN', - Market.IO: 'IO', - Market.IQ: 'IQ', - Market.IR: 'IR', - Market.IS: 'IS', - Market.IT: 'IT', - Market.JE: 'JE', - Market.JM: 'JM', - Market.JO: 'JO', - Market.JP: 'JP', - Market.KE: 'KE', - Market.KG: 'KG', - Market.KH: 'KH', - Market.KI: 'KI', - Market.KM: 'KM', - Market.KN: 'KN', - Market.KP: 'KP', - Market.KR: 'KR', - Market.KW: 'KW', - Market.KY: 'KY', - Market.KZ: 'KZ', - Market.LA: 'LA', - Market.LB: 'LB', - Market.LC: 'LC', - Market.LI: 'LI', - Market.LK: 'LK', - Market.LR: 'LR', - Market.LS: 'LS', - Market.LT: 'LT', - Market.LU: 'LU', - Market.LV: 'LV', - Market.LY: 'LY', - Market.MA: 'MA', - Market.MC: 'MC', - Market.MD: 'MD', - Market.ME: 'ME', - Market.MF: 'MF', - Market.MG: 'MG', - Market.MH: 'MH', - Market.MK: 'MK', - Market.ML: 'ML', - Market.MM: 'MM', - Market.MN: 'MN', - Market.MO: 'MO', - Market.MP: 'MP', - Market.MQ: 'MQ', - Market.MR: 'MR', - Market.MS: 'MS', - Market.MT: 'MT', - Market.MU: 'MU', - Market.MV: 'MV', - Market.MW: 'MW', - Market.MX: 'MX', - Market.MY: 'MY', - Market.MZ: 'MZ', - Market.NA: 'NA', - Market.NC: 'NC', - Market.NE: 'NE', - Market.NF: 'NF', - Market.NG: 'NG', - Market.NI: 'NI', - Market.NL: 'NL', - Market.NO: 'NO', - Market.NP: 'NP', - Market.NR: 'NR', - Market.NU: 'NU', - Market.NZ: 'NZ', - Market.OM: 'OM', - Market.PA: 'PA', - Market.PE: 'PE', - Market.PF: 'PF', - Market.PG: 'PG', - Market.PH: 'PH', - Market.PK: 'PK', - Market.PL: 'PL', - Market.PM: 'PM', - Market.PN: 'PN', - Market.PR: 'PR', - Market.PS: 'PS', - Market.PT: 'PT', - Market.PW: 'PW', - Market.PY: 'PY', - Market.QA: 'QA', - Market.RE: 'RE', - Market.RO: 'RO', - Market.RS: 'RS', - Market.RU: 'RU', - Market.RW: 'RW', - Market.SA: 'SA', - Market.SB: 'SB', - Market.SC: 'SC', - Market.SD: 'SD', - Market.SE: 'SE', - Market.SG: 'SG', - Market.SH: 'SH', - Market.SI: 'SI', - Market.SJ: 'SJ', - Market.SK: 'SK', - Market.SL: 'SL', - Market.SM: 'SM', - Market.SN: 'SN', - Market.SO: 'SO', - Market.SR: 'SR', - Market.SS: 'SS', - Market.ST: 'ST', - Market.SV: 'SV', - Market.SX: 'SX', - Market.SY: 'SY', - Market.SZ: 'SZ', - Market.TC: 'TC', - Market.TD: 'TD', - Market.TF: 'TF', - Market.TG: 'TG', - Market.TH: 'TH', - Market.TJ: 'TJ', - Market.TK: 'TK', - Market.TL: 'TL', - Market.TM: 'TM', - Market.TN: 'TN', - Market.TO: 'TO', - Market.TR: 'TR', - Market.TT: 'TT', - Market.TV: 'TV', - Market.TW: 'TW', - Market.TZ: 'TZ', - Market.UA: 'UA', - Market.UG: 'UG', - Market.UM: 'UM', - Market.US: 'US', - Market.UY: 'UY', - Market.UZ: 'UZ', - Market.VA: 'VA', - Market.VC: 'VC', - Market.VE: 'VE', - Market.VG: 'VG', - Market.VI: 'VI', - Market.VN: 'VN', - Market.VU: 'VU', - Market.WF: 'WF', - Market.WS: 'WS', - Market.XK: 'XK', - Market.YE: 'YE', - Market.YT: 'YT', - Market.ZA: 'ZA', - Market.ZM: 'ZM', - Market.ZW: 'ZW', -}; - -const _$SearchModeEnumMap = { - SearchMode.youtube: 'youtube', - SearchMode.youtubeMusic: 'youtubeMusic', -}; - -const _$ThemeModeEnumMap = { - ThemeMode.system: 'system', - ThemeMode.light: 'light', - ThemeMode.dark: 'dark', -}; - -const _$AudioSourceEnumMap = { - AudioSource.youtube: 'youtube', - AudioSource.piped: 'piped', - AudioSource.jiosaavn: 'jiosaavn', -}; - -const _$SourceCodecsEnumMap = { - SourceCodecs.m4a: 'm4a', - SourceCodecs.weba: 'weba', -}; diff --git a/lib/services/sourced_track/models/video_info.dart b/lib/services/sourced_track/models/video_info.dart index 031a8943b..58dd02808 100644 --- a/lib/services/sourced_track/models/video_info.dart +++ b/lib/services/sourced_track/models/video_info.dart @@ -1,5 +1,6 @@ import 'package:piped_client/piped_client.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/models/database/database.dart'; + import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class YoutubeVideoInfo { diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 7eedfad87..977b980be 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -5,8 +5,9 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.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/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 8444db537..b6689f6a9 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -2,9 +2,10 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/source_match.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/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e22c5732d..b8e26367b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) system_theme_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin"); system_theme_plugin_register_with_registrar(system_theme_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 9ddc2b98d..20d4a4dda 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST local_notifier media_kit_libs_linux screen_retriever + sqlite3_flutter_libs system_theme system_tray tray_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 047e7f3d1..545467052 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -20,6 +20,7 @@ import path_provider_foundation import screen_retriever import shared_preferences_foundation import sqflite +import sqlite3_flutter_libs import system_theme import system_tray import tray_manager @@ -42,6 +43,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index fcba29349..58d09cd9f 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -39,6 +39,21 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS + - sqlite3 (3.46.0): + - sqlite3/common (= 3.46.0) + - sqlite3/common (3.46.0) + - sqlite3/fts5 (3.46.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.46.0): + - sqlite3/common + - sqlite3/rtree (3.46.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.46.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - system_theme (0.0.1): - FlutterMacOS - system_tray (0.0.1): @@ -69,6 +84,7 @@ DEPENDENCIES: - 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/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/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`) @@ -78,6 +94,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - OrderedSet + - sqlite3 EXTERNAL SOURCES: app_links: @@ -116,6 +133,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos system_theme: :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos system_tray: @@ -147,6 +166,8 @@ SPEC CHECKSUMS: screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d + sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 diff --git a/pubspec.lock b/pubspec.lock index c1866e7d7..c5871a2ac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -507,6 +515,22 @@ packages: url: "https://github.com/thielepaul/flutter-draggable-scrollbar.git" source: git version: "0.1.0" + drift: + dependency: "direct main" + description: + name: drift + sha256: "6acedc562ffeed308049f78fb1906abad3d65714580b6745441ee6d50ec564cd" + url: "https://pub.dev" + source: hosted + version: "2.18.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: d9b020736ea85fff1568699ce18b89fabb3f0f042e8a7a05e84a3ec20d39acde + url: "https://pub.dev" + source: hosted + version: "2.18.0" duration: dependency: "direct main" description: @@ -1997,6 +2021,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.4" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "9f89a7e7dc36eac2035808427eba1c3fbd79e59c3a22093d8dace6d36b1fe89e" + url: "https://pub.dev" + source: hosted + version: "0.5.23" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: ade9a67fd70d0369329ed3373208de7ebd8662470e8c396fc8d0d60f9acdfc9f + url: "https://pub.dev" + source: hosted + version: "0.36.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bd1717c89..ddace46ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -121,6 +121,9 @@ dependencies: tray_manager: ^0.2.2 http: ^1.2.1 riverpod: ^2.5.1 + drift: ^2.18.0 + sqlite3_flutter_libs: ^0.5.23 + sqlite3: ^2.4.3 dev_dependencies: build_runner: ^2.4.11 @@ -143,6 +146,7 @@ dev_dependencies: pub_api_client: ^2.4.0 xml: ^6.5.0 io: ^1.0.4 + drift_dev: ^2.18.0 dependency_overrides: uuid: ^4.4.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 559db3100..b978edb94 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); SystemTrayPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d1464df54..4fcc467a0 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_windows_audio permission_handler_windows screen_retriever + sqlite3_flutter_libs system_theme system_tray tray_manager From 52d4f60ccc59dcbe7b1229dd4a7c8a2226c3b6b0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Jun 2024 21:24:42 +0600 Subject: [PATCH 19/92] refactor: use drift for skip segments and source matches --- lib/main.dart | 19 - lib/models/database/database.dart | 5 +- lib/models/database/database.g.dart | 878 +++++++++++++++++- lib/models/database/tables/skip_segment.dart | 9 + lib/models/database/tables/source_match.dart | 25 + lib/models/skip_segment.dart | 25 - lib/models/skip_segment.g.dart | 44 - lib/models/source_match.dart | 54 -- lib/models/source_match.g.dart | 119 --- .../proxy_playlist/skip_segments.dart | 50 +- .../sourced_track/sources/jiosaavn.dart | 42 +- lib/services/sourced_track/sources/piped.dart | 47 +- .../sourced_track/sources/youtube.dart | 42 +- lib/utils/service_utils.dart | 11 +- 14 files changed, 1019 insertions(+), 351 deletions(-) create mode 100644 lib/models/database/tables/skip_segment.dart create mode 100644 lib/models/database/tables/source_match.dart delete mode 100644 lib/models/skip_segment.dart delete mode 100644 lib/models/skip_segment.g.dart delete mode 100644 lib/models/source_match.dart delete mode 100644 lib/models/source_match.g.dart diff --git a/lib/main.dart b/lib/main.dart index 1f5e59096..bdccadd4f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,8 +22,6 @@ import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; import 'package:spotube/l10n/l10n.dart'; -import 'package:spotube/models/skip_segment.dart'; -import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -84,23 +82,6 @@ Future main(List rawArgs) async { Hive.init(hiveCacheDir); - Hive.registerAdapter(SkipSegmentAdapter()); - - Hive.registerAdapter(SourceMatchAdapter()); - Hive.registerAdapter(SourceTypeAdapter()); - - // Cache versioning entities with Adapter - SourceMatch.version = 'v1'; - SkipSegment.version = 'v1'; - - await Hive.openLazyBox( - SourceMatch.boxName, - path: hiveCacheDir, - ); - await Hive.openLazyBox( - SkipSegment.boxName, - path: hiveCacheDir, - ); await PersistedStateNotifier.initializeBoxes( path: hiveCacheDir, ); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 7d8fe088a..e7ac25580 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -17,11 +17,14 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; part 'database.g.dart'; part 'tables/preferences.dart'; +part 'tables/source_match.dart'; +part 'tables/skip_segment.dart'; + part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; -@DriftDatabase(tables: [PreferencesTable]) +@DriftDatabase(tables: [PreferencesTable, SourceMatchTable, SkipSegmentTable]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 1516b2665..9cc8a1c1a 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1204,16 +1204,608 @@ class PreferencesTableCompanion extends UpdateCompanion { } } +class $SourceMatchTableTable extends SourceMatchTable + with TableInfo<$SourceMatchTableTable, SourceMatchTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SourceMatchTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _trackIdMeta = + const VerificationMeta('trackId'); + @override + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _sourceIdMeta = + const VerificationMeta('sourceId'); + @override + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _sourceTypeMeta = + const VerificationMeta('sourceType'); + @override + late final GeneratedColumnWithTypeConverter sourceType = + GeneratedColumn('source_type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceType.youtube.name)) + .withConverter( + $SourceMatchTableTable.$convertersourceType); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceId, sourceType, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'source_match_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('track_id')) { + context.handle(_trackIdMeta, + trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); + } else if (isInserting) { + context.missing(_trackIdMeta); + } + if (data.containsKey('source_id')) { + context.handle(_sourceIdMeta, + sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta)); + } else if (isInserting) { + context.missing(_sourceIdMeta); + } + context.handle(_sourceTypeMeta, const VerificationResult.success()); + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SourceMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SourceMatchTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, + sourceType: $SourceMatchTableTable.$convertersourceType.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}source_type'])!), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SourceMatchTableTable createAlias(String alias) { + return $SourceMatchTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertersourceType = + const EnumNameConverter(SourceType.values); +} + +class SourceMatchTableData extends DataClass + implements Insertable { + final int id; + final String trackId; + final String sourceId; + final SourceType sourceType; + final DateTime createdAt; + const SourceMatchTableData( + {required this.id, + required this.trackId, + required this.sourceId, + required this.sourceType, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['source_id'] = Variable(sourceId); + { + map['source_type'] = Variable( + $SourceMatchTableTable.$convertersourceType.toSql(sourceType)); + } + map['created_at'] = Variable(createdAt); + return map; + } + + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( + id: Value(id), + trackId: Value(trackId), + sourceId: Value(sourceId), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceId: serializer.fromJson(json['sourceId']), + sourceType: $SourceMatchTableTable.$convertersourceType + .fromJson(serializer.fromJson(json['sourceType'])), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceId': serializer.toJson(sourceId), + 'sourceType': serializer.toJson( + $SourceMatchTableTable.$convertersourceType.toJson(sourceType)), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceId, + SourceType? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceId == this.sourceId && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceId; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceId = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceId, + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceId = Value(sourceId); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceId, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceId != null) 'source_id': sourceId, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceId, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (sourceType.present) { + map['source_type'] = Variable( + $SourceMatchTableTable.$convertersourceType.toSql(sourceType.value)); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $SkipSegmentTableTable extends SkipSegmentTable + with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SkipSegmentTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _startMeta = const VerificationMeta('start'); + @override + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _endMeta = const VerificationMeta('end'); + @override + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _trackIdMeta = + const VerificationMeta('trackId'); + @override + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('start')) { + context.handle( + _startMeta, start.isAcceptableOrUnknown(data['start']!, _startMeta)); + } else if (isInserting) { + context.missing(_startMeta); + } + if (data.containsKey('end')) { + context.handle( + _endMeta, end.isAcceptableOrUnknown(data['end']!, _endMeta)); + } else if (isInserting) { + context.missing(_endMeta); + } + if (data.containsKey('track_id')) { + context.handle(_trackIdMeta, + trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); + } else if (isInserting) { + context.missing(_trackIdMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SkipSegmentTableTable createAlias(String alias) { + return $SkipSegmentTableTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); late final $PreferencesTableTable preferencesTable = $PreferencesTableTable(this); + late final $SourceMatchTableTable sourceMatchTable = + $SourceMatchTableTable(this); + late final $SkipSegmentTableTable skipSegmentTable = + $SkipSegmentTableTable(this); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [preferencesTable]; + List get allSchemaEntities => + [preferencesTable, sourceMatchTable, skipSegmentTable, uniqTrackMatch]; } typedef $$PreferencesTableTableInsertCompanionBuilder @@ -1699,9 +2291,293 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$SourceMatchTableTableInsertCompanionBuilder + = SourceMatchTableCompanion Function({ + Value id, + required String trackId, + required String sourceId, + Value sourceType, + Value createdAt, +}); +typedef $$SourceMatchTableTableUpdateCompanionBuilder + = SourceMatchTableCompanion Function({ + Value id, + Value trackId, + Value sourceId, + Value sourceType, + Value createdAt, +}); + +class $$SourceMatchTableTableTableManager extends RootTableManager< + _$AppDatabase, + $SourceMatchTableTable, + SourceMatchTableData, + $$SourceMatchTableTableFilterComposer, + $$SourceMatchTableTableOrderingComposer, + $$SourceMatchTableTableProcessedTableManager, + $$SourceMatchTableTableInsertCompanionBuilder, + $$SourceMatchTableTableUpdateCompanionBuilder> { + $$SourceMatchTableTableTableManager( + _$AppDatabase db, $SourceMatchTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$SourceMatchTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$SourceMatchTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$SourceMatchTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value trackId = const Value.absent(), + Value sourceId = const Value.absent(), + Value sourceType = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SourceMatchTableCompanion( + id: id, + trackId: trackId, + sourceId: sourceId, + sourceType: sourceType, + createdAt: createdAt, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String trackId, + required String sourceId, + Value sourceType = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SourceMatchTableCompanion.insert( + id: id, + trackId: trackId, + sourceId: sourceId, + sourceType: sourceType, + createdAt: createdAt, + ), + )); +} + +class $$SourceMatchTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $SourceMatchTableTable, + SourceMatchTableData, + $$SourceMatchTableTableFilterComposer, + $$SourceMatchTableTableOrderingComposer, + $$SourceMatchTableTableProcessedTableManager, + $$SourceMatchTableTableInsertCompanionBuilder, + $$SourceMatchTableTableUpdateCompanionBuilder> { + $$SourceMatchTableTableProcessedTableManager(super.$state); +} + +class $$SourceMatchTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $SourceMatchTableTable> { + $$SourceMatchTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get sourceId => $state.composableBuilder( + column: $state.table.sourceId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get sourceType => $state.composableBuilder( + column: $state.table.sourceType, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$SourceMatchTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $SourceMatchTableTable> { + $$SourceMatchTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get sourceId => $state.composableBuilder( + column: $state.table.sourceId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get sourceType => $state.composableBuilder( + column: $state.table.sourceType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +typedef $$SkipSegmentTableTableInsertCompanionBuilder + = SkipSegmentTableCompanion Function({ + Value id, + required int start, + required int end, + required String trackId, + Value createdAt, +}); +typedef $$SkipSegmentTableTableUpdateCompanionBuilder + = SkipSegmentTableCompanion Function({ + Value id, + Value start, + Value end, + Value trackId, + Value createdAt, +}); + +class $$SkipSegmentTableTableTableManager extends RootTableManager< + _$AppDatabase, + $SkipSegmentTableTable, + SkipSegmentTableData, + $$SkipSegmentTableTableFilterComposer, + $$SkipSegmentTableTableOrderingComposer, + $$SkipSegmentTableTableProcessedTableManager, + $$SkipSegmentTableTableInsertCompanionBuilder, + $$SkipSegmentTableTableUpdateCompanionBuilder> { + $$SkipSegmentTableTableTableManager( + _$AppDatabase db, $SkipSegmentTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$SkipSegmentTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$SkipSegmentTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$SkipSegmentTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value start = const Value.absent(), + Value end = const Value.absent(), + Value trackId = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SkipSegmentTableCompanion( + id: id, + start: start, + end: end, + trackId: trackId, + createdAt: createdAt, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int start, + required int end, + required String trackId, + Value createdAt = const Value.absent(), + }) => + SkipSegmentTableCompanion.insert( + id: id, + start: start, + end: end, + trackId: trackId, + createdAt: createdAt, + ), + )); +} + +class $$SkipSegmentTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $SkipSegmentTableTable, + SkipSegmentTableData, + $$SkipSegmentTableTableFilterComposer, + $$SkipSegmentTableTableOrderingComposer, + $$SkipSegmentTableTableProcessedTableManager, + $$SkipSegmentTableTableInsertCompanionBuilder, + $$SkipSegmentTableTableUpdateCompanionBuilder> { + $$SkipSegmentTableTableProcessedTableManager(super.$state); +} + +class $$SkipSegmentTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $SkipSegmentTableTable> { + $$SkipSegmentTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get start => $state.composableBuilder( + column: $state.table.start, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get end => $state.composableBuilder( + column: $state.table.end, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$SkipSegmentTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $SkipSegmentTableTable> { + $$SkipSegmentTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get start => $state.composableBuilder( + column: $state.table.start, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get end => $state.composableBuilder( + column: $state.table.end, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); $$PreferencesTableTableTableManager get preferencesTable => $$PreferencesTableTableTableManager(_db, _db.preferencesTable); + $$SourceMatchTableTableTableManager get sourceMatchTable => + $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); + $$SkipSegmentTableTableTableManager get skipSegmentTable => + $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); } diff --git a/lib/models/database/tables/skip_segment.dart b/lib/models/database/tables/skip_segment.dart new file mode 100644 index 000000000..719f26171 --- /dev/null +++ b/lib/models/database/tables/skip_segment.dart @@ -0,0 +1,9 @@ +part of '../database.dart'; + +class SkipSegmentTable extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get start => integer()(); + IntColumn get end => integer()(); + TextColumn get trackId => text()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); +} diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart new file mode 100644 index 000000000..78d0eb059 --- /dev/null +++ b/lib/models/database/tables/source_match.dart @@ -0,0 +1,25 @@ +part of '../database.dart'; + +enum SourceType { + youtube._("YouTube"), + youtubeMusic._("YouTube Music"), + jiosaavn._("JioSaavn"); + + final String label; + + const SourceType._(this.label); +} + +@TableIndex( + name: "uniq_track_match", + columns: {#trackId, #sourceId, #sourceType}, + unique: true, +) +class SourceMatchTable extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get trackId => text()(); + TextColumn get sourceId => text()(); + TextColumn get sourceType => + textEnum().withDefault(Constant(SourceType.youtube.name))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); +} diff --git a/lib/models/skip_segment.dart b/lib/models/skip_segment.dart deleted file mode 100644 index 90f20f5a9..000000000 --- a/lib/models/skip_segment.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:hive/hive.dart'; - -part 'skip_segment.g.dart'; - -@HiveType(typeId: 2) -class SkipSegment { - @HiveField(0) - final int start; - @HiveField(1) - final int end; - SkipSegment(this.start, this.end); - - static String version = 'v1'; - static final boxName = "oss.krtirtho.spotube.skip_segments.$version"; - static LazyBox get box => Hive.lazyBox(boxName); - - SkipSegment.fromJson(Map json) - : start = json['start'], - end = json['end']; - - Map toJson() => { - 'start': start, - 'end': end, - }; -} diff --git a/lib/models/skip_segment.g.dart b/lib/models/skip_segment.g.dart deleted file mode 100644 index f2ad4459a..000000000 --- a/lib/models/skip_segment.g.dart +++ /dev/null @@ -1,44 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'skip_segment.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SkipSegmentAdapter extends TypeAdapter { - @override - final int typeId = 2; - - @override - SkipSegment read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SkipSegment( - fields[0] as int, - fields[1] as int, - ); - } - - @override - void write(BinaryWriter writer, SkipSegment obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.start) - ..writeByte(1) - ..write(obj.end); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SkipSegmentAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/models/source_match.dart b/lib/models/source_match.dart deleted file mode 100644 index 57a9f9634..000000000 --- a/lib/models/source_match.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'source_match.g.dart'; - -@JsonEnum() -@HiveType(typeId: 5) -enum SourceType { - @HiveField(0) - youtube._("YouTube"), - - @HiveField(1) - youtubeMusic._("YouTube Music"), - - @HiveField(2) - jiosaavn._("JioSaavn"); - - final String label; - - const SourceType._(this.label); -} - -@JsonSerializable() -@HiveType(typeId: 6) -class SourceMatch { - @HiveField(0) - String id; - - @HiveField(1) - String sourceId; - - @HiveField(2) - SourceType sourceType; - - @HiveField(3) - DateTime createdAt; - - SourceMatch({ - required this.id, - required this.sourceId, - required this.sourceType, - required this.createdAt, - }); - - factory SourceMatch.fromJson(Map json) => - _$SourceMatchFromJson(json); - - Map toJson() => _$SourceMatchToJson(this); - - static String version = 'v1'; - static final boxName = "oss.krtirtho.spotube.source_matches.$version"; - - static LazyBox get box => Hive.lazyBox(boxName); -} diff --git a/lib/models/source_match.g.dart b/lib/models/source_match.g.dart deleted file mode 100644 index 3b469694a..000000000 --- a/lib/models/source_match.g.dart +++ /dev/null @@ -1,119 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'source_match.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SourceMatchAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - SourceMatch read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SourceMatch( - id: fields[0] as String, - sourceId: fields[1] as String, - sourceType: fields[2] as SourceType, - createdAt: fields[3] as DateTime, - ); - } - - @override - void write(BinaryWriter writer, SourceMatch obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.sourceId) - ..writeByte(2) - ..write(obj.sourceType) - ..writeByte(3) - ..write(obj.createdAt); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SourceMatchAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SourceTypeAdapter extends TypeAdapter { - @override - final int typeId = 5; - - @override - SourceType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return SourceType.youtube; - case 1: - return SourceType.youtubeMusic; - case 2: - return SourceType.jiosaavn; - default: - return SourceType.youtube; - } - } - - @override - void write(BinaryWriter writer, SourceType obj) { - switch (obj) { - case SourceType.youtube: - writer.writeByte(0); - break; - case SourceType.youtubeMusic: - writer.writeByte(1); - break; - case SourceType.jiosaavn: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SourceTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( - id: json['id'] as String, - sourceId: json['sourceId'] as String, - sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']), - createdAt: DateTime.parse(json['createdAt'] as String), - ); - -Map _$SourceMatchToJson(SourceMatch instance) => - { - 'id': instance.id, - 'sourceId': instance.sourceId, - 'sourceType': _$SourceTypeEnumMap[instance.sourceType]!, - 'createdAt': instance.createdAt.toIso8601String(), - }; - -const _$SourceTypeEnumMap = { - SourceType.youtube: 'youtube', - SourceType.youtubeMusic: 'youtubeMusic', - SourceType.jiosaavn: 'jiosaavn', -}; diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 461ac24e6..005797f45 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -1,8 +1,8 @@ import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.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'; @@ -10,24 +10,21 @@ import 'package:spotube/services/dio/dio.dart'; class SourcedSegments { final String source; - final List segments; + final List segments; SourcedSegments({required this.source, required this.segments}); } -Future> getAndCacheSkipSegments(String id) async { +Future> getAndCacheSkipSegments( + String id, Ref ref) async { + final database = ref.read(databaseProvider); try { - final cached = await SkipSegment.box.get(id) as List?; - if (cached != null && cached.isNotEmpty) { - return List.castFrom( - cached - .map( - (json) => SkipSegment.fromJson( - Map.castFrom(json), - ), - ) - .toList(), - ); + final cached = await (database.select(database.skipSegmentTable) + ..where((s) => s.trackId.equals(id))) + .get(); + + if (cached.isNotEmpty) { + return cached; } final res = await globalDio.getUri( @@ -55,25 +52,30 @@ Future> getAndCacheSkipSegments(String id) async { ); if (res.data == "Not Found") { - return List.castFrom([]); + return List.castFrom([]); } final data = res.data as List; final segments = data.map((obj) { final start = obj["segment"].first.toInt(); final end = obj["segment"].last.toInt(); - return SkipSegment(start, end); + return SkipSegmentTableCompanion.insert( + trackId: id, + start: start, + end: end, + ); }).toList(); - await SkipSegment.box.put( - id, - segments.map((e) => e.toJson()).toList(), - ); - return List.castFrom(segments); + await database.batch((b) { + b.insertAll(database.skipSegmentTable, segments); + }); + + return await (database.select(database.skipSegmentTable) + ..where((s) => s.trackId.equals(id))) + .get(); } catch (e, stack) { - await SkipSegment.box.put(id, []); AppLogger.reportError(e, stack); - return List.castFrom([]); + return List.castFrom([]); } } @@ -100,7 +102,7 @@ final segmentProvider = FutureProvider( ); } - final segments = await getAndCacheSkipSegments(track.sourceInfo.id); + final segments = await getAndCacheSkipSegments(track.sourceInfo.id, ref); return SourcedSegments( source: track.sourceInfo.id, diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index f731de6c1..865e3d634 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/source_match.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; @@ -39,7 +41,10 @@ class JioSaavnSourcedTrack extends SourcedTrack { required Ref ref, bool weakMatch = false, }) async { - final cachedSource = await SourceMatch.box.get(track.id); + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(track.id!))) + .getSingleOrNull(); if (cachedSource == null || cachedSource.sourceType != SourceType.jiosaavn) { @@ -50,15 +55,13 @@ class JioSaavnSourcedTrack extends SourcedTrack { throw TrackNotFoundError(track); } - await SourceMatch.box.put( - track.id!, - SourceMatch( - id: track.id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: siblings.first.info.id, - ), - ); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: track.id!, + sourceId: siblings.first.info.id, + sourceType: const Value(SourceType.jiosaavn), + ), + ); return JioSaavnSourcedTrack( ref: ref, @@ -206,15 +209,14 @@ class JioSaavnSourcedTrack extends SourcedTrack { final (:info, :source) = toSiblingType(item); - await SourceMatch.box.put( - id!, - SourceMatch( - id: id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: info.id, - ), - ); + final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: id!, + sourceId: info.id, + sourceType: const Value(SourceType.jiosaavn), + ), + ); return JioSaavnSourcedTrack( ref: ref, diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index b6689f6a9..d156b26e5 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -1,9 +1,10 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/source_match.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -49,7 +50,10 @@ class PipedSourcedTrack extends SourcedTrack { required Track track, required Ref ref, }) async { - final cachedSource = await SourceMatch.box.get(track.id); + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(track.id!))) + .getSingleOrNull(); final preferences = ref.read(userPreferencesProvider); final pipedClient = ref.read(pipedProvider); @@ -59,17 +63,17 @@ class PipedSourcedTrack extends SourcedTrack { throw TrackNotFoundError(track); } - await SourceMatch.box.put( - track.id!, - SourceMatch( - id: track.id!, - sourceType: preferences.searchMode == SearchMode.youtube - ? SourceType.youtube - : SourceType.youtubeMusic, - createdAt: DateTime.now(), - sourceId: siblings.first.info.id, - ), - ); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: track.id!, + sourceId: siblings.first.info.id, + sourceType: Value( + preferences.searchMode == SearchMode.youtube + ? SourceType.youtube + : SourceType.youtubeMusic, + ), + ), + ); return PipedSourcedTrack( ref: ref, @@ -268,15 +272,14 @@ class PipedSourcedTrack extends SourcedTrack { final manifest = await pipedClient.streams(newSourceInfo.id); - await SourceMatch.box.put( - id!, - SourceMatch( - id: id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: newSourceInfo.id, - ), - ); + final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: id!, + sourceId: newSourceInfo.id, + sourceType: const Value(SourceType.youtube), + ), + ); return PipedSourcedTrack( ref: ref, diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index b144d7015..0501a499d 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -1,8 +1,10 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/source_match.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/song_link/song_link.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -46,7 +48,10 @@ class YoutubeSourcedTrack extends SourcedTrack { required Track track, required Ref ref, }) async { - final cachedSource = await SourceMatch.box.get(track.id); + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(track.id!))) + .getSingleOrNull(); if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { final siblings = await fetchSiblings(ref: ref, track: track); @@ -54,15 +59,13 @@ class YoutubeSourcedTrack extends SourcedTrack { throw TrackNotFoundError(track); } - await SourceMatch.box.put( - track.id!, - SourceMatch( - id: track.id!, - sourceType: SourceType.youtube, - createdAt: DateTime.now(), - sourceId: siblings.first.info.id, - ), - ); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: track.id!, + sourceId: siblings.first.info.id, + sourceType: const Value(SourceType.youtube), + ), + ); return YoutubeSourcedTrack( ref: ref, @@ -283,15 +286,14 @@ class YoutubeSourcedTrack extends SourcedTrack { onTimeout: () => throw ClientException("Timeout"), ); - await SourceMatch.box.put( - id!, - SourceMatch( - id: id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: newSourceInfo.id, - ), - ); + final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: id!, + sourceId: newSourceInfo.id, + sourceType: const Value(SourceType.youtube), + ), + ); return YoutubeSourcedTrack( ref: ref, diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 885f9a2cc..5950bc8ce 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -6,6 +6,7 @@ import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/modules/root/update_dialog.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/lyrics.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -20,7 +21,6 @@ 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 { @@ -392,7 +392,14 @@ abstract class ServiceUtils { WidgetRef ref, ) async { if (!Env.enableUpdateChecker) return; - if (!ref.read(userPreferencesProvider.select((s) => s.checkUpdate))) return; + final database = ref.read(databaseProvider); + final checkUpdate = await (database.selectOnly(database.preferencesTable) + ..addColumns([database.preferencesTable.checkUpdate]) + ..where(database.preferencesTable.id.equals(0))) + .map((row) => row.read(database.preferencesTable.checkUpdate)) + .getSingleOrNull(); + + if (checkUpdate == false) return; final packageInfo = await PackageInfo.fromPlatform(); if (Env.releaseChannel == ReleaseChannel.nightly) { From bf6cec8d6939bd373f6015af08d8ab34a6e631cd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Jun 2024 22:23:12 +0600 Subject: [PATCH 20/92] refactor(blacklist): use drift sql db instead of hive --- lib/components/track_tile/track_options.dart | 29 +- lib/components/track_tile/track_tile.dart | 8 +- lib/models/database/database.dart | 10 +- lib/models/database/database.g.dart | 399 ++++++++++++++++++- lib/models/database/tables/blacklist.dart | 18 + lib/modules/artist/artist_card.dart | 6 +- lib/pages/artist/section/header.dart | 24 +- lib/pages/settings/blacklist.dart | 32 +- lib/provider/blacklist_provider.dart | 106 ++--- 9 files changed, 511 insertions(+), 121 deletions(-) create mode 100644 lib/models/database/tables/blacklist.dart diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 89f6679da..fd3018bac 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -18,6 +18,7 @@ import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; @@ -170,11 +171,8 @@ class TrackOptions extends HookConsumerWidget { final favorites = useTrackToggleLike(track, ref); final isBlackListed = useMemoized( - () => blacklist.contains( - BlacklistedElement.track( - track.id!, - track.name!, - ), + () => blacklist.asData?.value.any( + (element) => element.elementId == track.id, ), [blacklist, track], ); @@ -258,13 +256,16 @@ class TrackOptions extends HookConsumerWidget { .removeTracks(playlistId ?? "", [track.id!]); break; case TrackOptionValue.blacklist: - if (isBlackListed) { - ref.read(blacklistProvider.notifier).remove( - BlacklistedElement.track(track.id!, track.name!), - ); + if (isBlackListed == null) break; + if (isBlackListed == true) { + await ref.read(blacklistProvider.notifier).remove(track.id!); } else { - ref.read(blacklistProvider.notifier).add( - BlacklistedElement.track(track.id!, track.name!), + await ref.read(blacklistProvider.notifier).add( + BlacklistTableCompanion.insert( + name: track.name!, + elementId: track.id!, + elementType: BlacklistedType.track, + ), ); } break; @@ -399,10 +400,10 @@ class TrackOptions extends HookConsumerWidget { PopSheetEntry( value: TrackOptionValue.blacklist, leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: !isBlackListed ? Colors.red[400] : null, - textColor: !isBlackListed ? Colors.red[400] : null, + iconColor: isBlackListed != true ? Colors.red[400] : null, + textColor: isBlackListed != true ? Colors.red[400] : null, title: Text( - isBlackListed + isBlackListed == true ? context.l10n.remove_from_blacklist : context.l10n.add_to_blacklist, ), diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 9ba87abea..e2e7e2932 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -53,14 +53,10 @@ class TrackTile extends HookConsumerWidget { final theme = Theme.of(context); final blacklist = ref.watch(blacklistProvider); + final blacklistNotifier = ref.watch(blacklistProvider.notifier); final isBlackListed = useMemoized( - () => blacklist.contains( - BlacklistedElement.track( - track.id!, - track.name!, - ), - ), + () => blacklistNotifier.contains(track), [blacklist, track], ); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index e7ac25580..ad09933d1 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,12 +19,20 @@ part 'database.g.dart'; part 'tables/preferences.dart'; part 'tables/source_match.dart'; part 'tables/skip_segment.dart'; +part 'tables/blacklist.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; -@DriftDatabase(tables: [PreferencesTable, SourceMatchTable, SkipSegmentTable]) +@DriftDatabase( + tables: [ + PreferencesTable, + SourceMatchTable, + SkipSegmentTable, + BlacklistTable, + ], +) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 9cc8a1c1a..8c996d215 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1789,6 +1789,266 @@ class SkipSegmentTableCompanion extends UpdateCompanion { } } +class $BlacklistTableTable extends BlacklistTable + with TableInfo<$BlacklistTableTable, BlacklistTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $BlacklistTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _elementTypeMeta = + const VerificationMeta('elementType'); + @override + late final GeneratedColumnWithTypeConverter + elementType = GeneratedColumn('element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $BlacklistTableTable.$converterelementType); + static const VerificationMeta _elementIdMeta = + const VerificationMeta('elementId'); + @override + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + context.handle(_elementTypeMeta, const VerificationResult.success()); + if (data.containsKey('element_id')) { + context.handle(_elementIdMeta, + elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); + } else if (isInserting) { + context.missing(_elementIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: $BlacklistTableTable.$converterelementType.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}element_type'])!), + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + $BlacklistTableTable createAlias(String alias) { + return $BlacklistTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converterelementType = + const EnumNameConverter(BlacklistedType.values); +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final BlacklistedType elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType)); + } + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: $BlacklistTableTable.$converterelementType + .fromJson(serializer.fromJson(json['elementType'])), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson( + $BlacklistTableTable.$converterelementType.toJson(elementType)), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, + String? name, + BlacklistedType? elementType, + String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType.value)); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -1798,14 +2058,23 @@ abstract class _$AppDatabase extends GeneratedDatabase { $SourceMatchTableTable(this); late final $SkipSegmentTableTable skipSegmentTable = $SkipSegmentTableTable(this); + late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); late final Index uniqTrackMatch = Index('uniq_track_match', 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [preferencesTable, sourceMatchTable, skipSegmentTable, uniqTrackMatch]; + List get allSchemaEntities => [ + preferencesTable, + sourceMatchTable, + skipSegmentTable, + blacklistTable, + uniqTrackMatch, + uniqueBlacklist + ]; } typedef $$PreferencesTableTableInsertCompanionBuilder @@ -2571,6 +2840,130 @@ class $$SkipSegmentTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + required String name, + required BlacklistedType elementType, + required String elementId, +}); +typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + Value name, + Value elementType, + Value elementId, +}); + +class $$BlacklistTableTableTableManager extends RootTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableTableManager( + _$AppDatabase db, $BlacklistTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$BlacklistTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$BlacklistTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$BlacklistTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value elementType = const Value.absent(), + Value elementId = const Value.absent(), + }) => + BlacklistTableCompanion( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) => + BlacklistTableCompanion.insert( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + )); +} + +class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableProcessedTableManager(super.$state); +} + +class $$BlacklistTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$BlacklistTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); @@ -2580,4 +2973,6 @@ class _$AppDatabaseManager { $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); $$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); + $$BlacklistTableTableTableManager get blacklistTable => + $$BlacklistTableTableTableManager(_db, _db.blacklistTable); } diff --git a/lib/models/database/tables/blacklist.dart b/lib/models/database/tables/blacklist.dart new file mode 100644 index 000000000..8a8d9dee8 --- /dev/null +++ b/lib/models/database/tables/blacklist.dart @@ -0,0 +1,18 @@ +part of '../database.dart'; + +enum BlacklistedType { + artist, + track; +} + +@TableIndex( + name: "unique_blacklist", + unique: true, + columns: {#elementType, #elementId}, +) +class BlacklistTable extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + TextColumn get elementType => textEnum()(); + TextColumn get elementId => text()(); +} diff --git a/lib/modules/artist/artist_card.dart b/lib/modules/artist/artist_card.dart index c1404e420..896271f24 100644 --- a/lib/modules/artist/artist_card.dart +++ b/lib/modules/artist/artist_card.dart @@ -27,8 +27,8 @@ class ArtistCard extends HookConsumerWidget { ); final isBlackListed = ref.watch( blacklistProvider.select( - (blacklist) => blacklist.contains( - BlacklistedElement.artist(artist.id!, artist.name!), + (blacklist) => blacklist.asData?.value.any( + (element) => element.elementId == artist.id, ), ), ); @@ -55,7 +55,7 @@ class ArtistCard extends HookConsumerWidget { elevation: 3, shape: RoundedRectangleBorder( borderRadius: radius, - side: isBlackListed + side: isBlackListed == true ? const BorderSide( color: Colors.red, width: 2, diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 7ca8964dc..a30535dd7 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -10,6 +10,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_breakpoint_value.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -39,10 +40,9 @@ class ArtistPageHeader extends HookConsumerWidget { ); final auth = ref.watch(authenticationProvider); - final blacklist = ref.watch(blacklistProvider); - final isBlackListed = blacklist.contains( - BlacklistedElement.artist(artistId, artist.name!), - ); + ref.watch(blacklistProvider); + final blacklistNotifier = ref.watch(blacklistProvider.notifier); + final isBlackListed = blacklistNotifier.containsArtist(artist); final image = artist.images.asUrlString( placeholder: ImagePlaceholder.artist, @@ -187,14 +187,16 @@ class ArtistPageHeader extends HookConsumerWidget { ), onPressed: () async { if (isBlackListed) { - ref.read(blacklistProvider.notifier).remove( - BlacklistedElement.artist( - artist.id!, artist.name!), - ); + await ref + .read(blacklistProvider.notifier) + .remove(artist.id!); } else { - ref.read(blacklistProvider.notifier).add( - BlacklistedElement.artist( - artist.id!, artist.name!), + await ref.read(blacklistProvider.notifier).add( + BlacklistTableCompanion.insert( + name: artist.name!, + elementId: artist.id!, + elementType: BlacklistedType.artist, + ), ); } }, diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index b5e108216..1f018dab6 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget { final filteredBlacklist = useMemoized( () { if (searchText.value.isEmpty) { - return blacklist; + return blacklist.asData?.value ?? []; } - return blacklist - .map( - (e) => ( - weightedRatio("${e.name} ${e.type.name}", searchText.value), - e, - ), - ) - .sorted((a, b) => b.$1.compareTo(a.$1)) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList(); + return blacklist.asData?.value + .map( + (e) => ( + weightedRatio( + "${e.name} ${e.elementType.name}", searchText.value), + e, + ), + ) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList() ?? + []; }, [blacklist, searchText.value], ); @@ -70,14 +72,14 @@ class BlackListPage extends HookConsumerWidget { final item = filteredBlacklist.elementAt(index); return ListTile( leading: Text("${index + 1}."), - title: Text("${item.name} (${item.type.name})"), - subtitle: Text(item.id), + title: Text("${item.name} (${item.elementType.name})"), + subtitle: Text(item.elementId), trailing: IconButton( icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), onPressed: () { ref .read(blacklistProvider.notifier) - .remove(filteredBlacklist.elementAt(index)); + .remove(filteredBlacklist.elementAt(index).elementId); }, ), ); diff --git a/lib/provider/blacklist_provider.dart b/lib/provider/blacklist_provider.dart index 4f4881128..a51d399fd 100644 --- a/lib/provider/blacklist_provider.dart +++ b/lib/provider/blacklist_provider.dart @@ -2,69 +2,59 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/current_playlist.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; -enum BlacklistedType { - artist, - track; - - static BlacklistedType fromName(String name) => - BlacklistedType.values.firstWhere((e) => e.name == name); -} - -class BlacklistedElement { - final String id; - final String name; - final BlacklistedType type; - - BlacklistedElement.artist(this.id, this.name) : type = BlacklistedType.artist; - - BlacklistedElement.track(this.id, this.name) : type = BlacklistedType.track; - - BlacklistedElement.fromJson(Map json) - : id = json['id'], - name = json['name'], - type = BlacklistedType.fromName(json['type']); +class BlackListNotifier extends AsyncNotifier> { + @override + build() async { + final database = ref.watch(databaseProvider); - Map toJson() => {'id': id, 'type': type.name, 'name': name}; + final subscription = database + .select(database.blacklistTable) + .watch() + .listen((event) => state = AsyncData(event)); - @override - operator ==(other) => - other is BlacklistedElement && - other.id == id && - other.type == type && - other.name == name; + ref.onDispose(() { + subscription.cancel(); + }); - @override - int get hashCode => id.hashCode ^ type.hashCode ^ name.hashCode; -} + return await database.select(database.blacklistTable).get(); + } -class BlackListNotifier - extends PersistedStateNotifier> { - BlackListNotifier() : super({}, "blacklist"); + AppDatabase get _database => ref.read(databaseProvider); - void add(BlacklistedElement element) { - state = state.union({element}); + Future add(BlacklistTableCompanion element) async { + _database.into(_database.blacklistTable).insert(element); } - void remove(BlacklistedElement element) { - state = state.difference({element}); + Future remove(String elementId) async { + await (_database.delete(_database.blacklistTable) + ..where((tbl) => tbl.elementId.equals(elementId))) + .go(); } bool contains(TrackSimple track) { final containsTrack = - state.contains(BlacklistedElement.track(track.id!, track.name!)); + state.asData?.value.any((element) => element.elementId == track.id) ?? + false; final containsTrackArtists = track.artists?.any( - (artist) => state.contains( - BlacklistedElement.artist(artist.id!, artist.name ?? "Spotify"), - ), + (artist) => + state.asData?.value.any((el) => el.elementId == artist.id) ?? + false, ) ?? false; return containsTrack || containsTrackArtists; } + bool containsArtist(ArtistSimple artist) { + return state.asData?.value + .any((element) => element.elementId == artist.id) ?? + false; + } + /// Filters the non blacklisted tracks from the given [tracks] Iterable filter(Iterable tracks) { return tracks.whereNot(contains).toList(); @@ -75,34 +65,12 @@ class BlackListNotifier id: playlist.id, name: playlist.name, thumbnail: playlist.thumbnail, - tracks: playlist.tracks.where( - (track) { - return !state - .contains(BlacklistedElement.track(track.id!, track.name!)) && - !(track.artists ?? []).any( - (artist) => state.contains( - BlacklistedElement.artist(artist.id!, artist.name!), - ), - ); - }, - ).toList(), + tracks: playlist.tracks.where((track) => !contains(track)).toList(), ); } - - @override - Set fromJson(Map json) { - return json['blacklist'] - .map((e) => BlacklistedElement.fromJson(e)) - .toSet(); - } - - @override - Map toJson() { - return {'blacklist': state.map((e) => e.toJson()).toList()}; - } } final blacklistProvider = - StateNotifierProvider>((ref) { - return BlackListNotifier(); -}); + AsyncNotifierProvider>( + () => BlackListNotifier(), +); From a799ca55bcb8833c1a2e89078df0460229ef5053 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 16 Jun 2024 20:58:54 +0600 Subject: [PATCH 21/92] chore: add encrypted text column support --- lib/main.dart | 2 + lib/models/database/database.dart | 6 ++- .../typeconverters/encrypted_text.dart | 39 +++++++++++++++++ lib/services/kv_store/encrypted_kv_store.dart | 43 +++++++++++++++++++ lib/services/kv_store/kv_store.dart | 35 +++++++++++++++ pubspec.lock | 24 +++++++++++ pubspec.yaml | 1 + 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 lib/models/database/typeconverters/encrypted_text.dart create mode 100644 lib/services/kv_store/encrypted_kv_store.dart diff --git a/lib/main.dart b/lib/main.dart index bdccadd4f..09db495c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,6 +27,7 @@ import 'package:spotube/provider/palette_provider.dart'; 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/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; @@ -76,6 +77,7 @@ Future main(List rawArgs) async { } await KVStoreService.initialize(); + await EncryptedKvStoreService.initialize(); final hiveCacheDir = kIsWeb ? null : (await getApplicationSupportDirectory()).path; diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index ad09933d1..ac0223fd7 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -4,11 +4,14 @@ import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; +import 'package:encrypt/encrypt.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:flutter/material.dart' hide Table; +import 'package:flutter/material.dart' hide Table, Key; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; import 'package:sqlite3/sqlite3.dart'; @@ -24,6 +27,7 @@ part 'tables/blacklist.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; +part 'typeconverters/encrypted_text.dart'; @DriftDatabase( tables: [ diff --git a/lib/models/database/typeconverters/encrypted_text.dart b/lib/models/database/typeconverters/encrypted_text.dart new file mode 100644 index 000000000..27921788c --- /dev/null +++ b/lib/models/database/typeconverters/encrypted_text.dart @@ -0,0 +1,39 @@ +part of '../database.dart'; + +class DecryptedText { + final String value; + const DecryptedText(this.value); + + static Encrypter? _encrypter; + + factory DecryptedText.decrypted(String value) { + _encrypter ??= Encrypter( + Salsa20( + Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync), + ), + ); + + return DecryptedText( + _encrypter!.decrypt( + Encrypted.fromBase64(value), + iv: KVStoreService.ivKey, + ), + ); + } + + String encrypt() { + return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64; + } +} + +class EncryptedTextConverter extends TypeConverter { + @override + DecryptedText fromSql(String fromDb) { + return DecryptedText.decrypted(fromDb); + } + + @override + String toSql(DecryptedText value) { + return value.encrypt(); + } +} diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart new file mode 100644 index 000000000..d8f69690b --- /dev/null +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -0,0 +1,43 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:uuid/uuid.dart'; + +abstract class EncryptedKvStoreService { + static const _storage = FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + + static late final String _encryptionKeySync; + + static Future initialize() async { + _encryptionKeySync = await encryptionKey; + } + + static String get encryptionKeySync => _encryptionKeySync; + + static Future get encryptionKey async { + try { + final value = await _storage.read(key: 'encryption'); + final key = const Uuid().v4(); + + if (value == null) { + await setEncryptionKey(key); + return key; + } + + return value; + } catch (e) { + return KVStoreService.encryptionKey; + } + } + + static Future setEncryptionKey(String key) async { + try { + await _storage.write(key: 'encryption', value: key); + } catch (e) { + await KVStoreService.setEncryptionKey(key); + } + } +} diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index ae62a0554..6b19c0322 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'package:encrypt/encrypt.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; +import 'package:uuid/uuid.dart'; abstract class KVStoreService { static SharedPreferences? _sharedPreferences; @@ -43,4 +45,37 @@ abstract class KVStoreService { value.toJson(), ), ); + + static String get encryptionKey { + final value = sharedPreferences.getString('encryption'); + + final key = const Uuid().v4(); + if (value == null) { + setEncryptionKey(key); + return key; + } + + return value; + } + + static Future setEncryptionKey(String key) async { + await sharedPreferences.setString('encryption', key); + } + + static IV get ivKey { + final iv = sharedPreferences.getString('iv'); + final value = IV.fromSecureRandom(8); + + if (iv == null) { + setIVKey(value); + + return value; + } + + return IV.fromBase64(iv); + } + + static Future setIVKey(IV iv) async { + await sharedPreferences.setString('iv', iv.base64); + } } diff --git a/pubspec.lock b/pubspec.lock index c5871a2ac..70b0655ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + url: "https://pub.dev" + source: hosted + version: "1.5.3" async: dependency: "direct main" description: @@ -539,6 +547,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.13" + encrypt: + dependency: "direct main" + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" envied: dependency: "direct main" description: @@ -1670,6 +1686,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ddace46ee..a923f5a3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -124,6 +124,7 @@ dependencies: drift: ^2.18.0 sqlite3_flutter_libs: ^0.5.23 sqlite3: ^2.4.3 + encrypt: ^5.0.3 dev_dependencies: build_runner: ^2.4.11 From d18f74fd65486f25319d5804a191616a9220d7da Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 16 Jun 2024 22:33:23 +0600 Subject: [PATCH 22/92] refactor: use drift db based authentication --- lib/collections/routes.dart | 8 +- .../fallbacks/anonymous_fallback.dart | 2 +- lib/components/heart_button/heart_button.dart | 4 +- lib/components/track_tile/track_options.dart | 2 +- .../sections/header/header_actions.dart | 2 +- .../configurators/use_endless_playback.dart | 4 +- lib/models/database/database.dart | 10 +- lib/models/database/database.g.dart | 3869 +++++++++-------- .../database/tables/authentication.dart | 8 + .../typeconverters/encrypted_text.dart | 5 + .../database/typeconverters/string_list.dart | 2 +- lib/modules/desktop_login/login_form.dart | 7 +- lib/modules/home/sections/friends.dart | 4 +- lib/modules/home/sections/new_releases.dart | 4 +- .../local_folder/local_folder_item.dart | 2 +- lib/modules/library/user_albums.dart | 4 +- lib/modules/library/user_artists.dart | 4 +- lib/modules/library/user_playlists.dart | 4 +- lib/modules/player/player.dart | 2 +- lib/modules/player/player_actions.dart | 2 +- lib/modules/root/bottom_player.dart | 2 +- lib/modules/root/sidebar.dart | 4 +- lib/pages/artist/section/header.dart | 2 +- lib/pages/desktop_login/login_tutorial.dart | 9 +- lib/pages/lyrics/lyrics.dart | 4 +- lib/pages/lyrics/mini_lyrics.dart | 4 +- lib/pages/mobile_login/mobile_login.dart | 6 +- lib/pages/search/search.dart | 7 +- lib/pages/settings/sections/accounts.dart | 6 +- .../authentication.dart} | 190 +- .../custom_spotify_endpoint_provider.dart | 4 +- lib/provider/spotify/views/home.dart | 4 +- lib/provider/spotify/views/home_section.dart | 4 +- lib/provider/spotify_provider.dart | 6 +- .../user_preferences_provider.dart | 7 +- lib/services/kv_store/encrypted_kv_store.dart | 18 +- 36 files changed, 2315 insertions(+), 1911 deletions(-) create mode 100644 lib/models/database/tables/authentication.dart rename lib/provider/{authentication_provider.dart => authentication/authentication.dart} (51%) diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index b9e06c611..b3cba5810 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -32,7 +32,7 @@ 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/provider/authentication/authentication.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/spotube_page_route.dart'; @@ -59,11 +59,9 @@ final routerProvider = Provider((ref) { path: "/", name: HomePage.name, redirect: (context, state) async { - final authNotifier = ref.read(authenticationProvider.notifier); - final json = await authNotifier.box.get(authNotifier.cacheKey); + final auth = await ref.read(authenticationProvider.future); - if (json?["cookie"] == null && - !KVStoreService.doneGettingStarted) { + if (auth == null && !KVStoreService.doneGettingStarted) { return "/getting-started"; } diff --git a/lib/components/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart index 5ced6bb6d..799297e3e 100644 --- a/lib/components/fallbacks/anonymous_fallback.dart +++ b/lib/components/fallbacks/anonymous_fallback.dart @@ -3,7 +3,7 @@ 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/provider/authentication/authentication.dart'; import 'package:spotube/utils/service_utils.dart'; class AnonymousFallback extends ConsumerWidget { diff --git a/lib/components/heart_button/heart_button.dart b/lib/components/heart_button/heart_button.dart index 8222b8e6e..fa4318cc0 100644 --- a/lib/components/heart_button/heart_button.dart +++ b/lib/components/heart_button/heart_button.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/heart_button/use_track_toggle_like.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HeartButton extends HookConsumerWidget { @@ -26,7 +26,7 @@ class HeartButton extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(authenticationProvider); - if (auth == null) return const SizedBox.shrink(); + if (auth.asData?.value == null) return const SizedBox.shrink(); return IconButton( tooltip: tooltip, diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index fd3018bac..d54a0c153 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -20,7 +20,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.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'; diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index 3e0c4cc13..f20cd5533 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index 97eb3f484..9b90b23dc 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -2,7 +2,7 @@ import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -18,7 +18,7 @@ void useEndlessPlayback(WidgetRef ref) { useEffect( () { - if (!endlessPlayback || auth == null) return null; + if (!endlessPlayback || auth.asData?.value == null) return null; void listener(int index) async { try { diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index ac0223fd7..56f72ee73 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,10 +19,11 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; part 'database.g.dart'; +part 'tables/authentication.dart'; +part 'tables/blacklist.dart'; part 'tables/preferences.dart'; -part 'tables/source_match.dart'; part 'tables/skip_segment.dart'; -part 'tables/blacklist.dart'; +part 'tables/source_match.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; @@ -31,10 +32,11 @@ part 'typeconverters/encrypted_text.dart'; @DriftDatabase( tables: [ + AuthenticationTable, + BlacklistTable, PreferencesTable, - SourceMatchTable, SkipSegmentTable, - BlacklistTable, + SourceMatchTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 8c996d215..0ac7005e5 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3,12 +3,12 @@ part of 'database.dart'; // ignore_for_file: type=lint -class $PreferencesTableTable extends PreferencesTable - with TableInfo<$PreferencesTableTable, PreferencesTableData> { +class $AuthenticationTableTable extends AuthenticationTable + with TableInfo<$AuthenticationTableTable, AuthenticationTableData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $PreferencesTableTable(this.attachedDatabase, [this._alias]); + $AuthenticationTableTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -18,370 +18,319 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _audioQualityMeta = - const VerificationMeta('audioQuality'); - @override - late final GeneratedColumnWithTypeConverter - audioQuality = GeneratedColumn( - 'audio_quality', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceQualities.high.name)) - .withConverter( - $PreferencesTableTable.$converteraudioQuality); - static const VerificationMeta _albumColorSyncMeta = - const VerificationMeta('albumColorSync'); + static const VerificationMeta _cookieMeta = const VerificationMeta('cookie'); @override - late final GeneratedColumn albumColorSync = GeneratedColumn( - 'album_color_sync', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("album_color_sync" IN (0, 1))'), - defaultValue: const Constant(true)); - static const VerificationMeta _amoledDarkThemeMeta = - const VerificationMeta('amoledDarkTheme'); + late final GeneratedColumnWithTypeConverter cookie = + GeneratedColumn('cookie', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $AuthenticationTableTable.$convertercookie); + static const VerificationMeta _accessTokenMeta = + const VerificationMeta('accessToken'); @override - late final GeneratedColumn amoledDarkTheme = GeneratedColumn( - 'amoled_dark_theme', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("amoled_dark_theme" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _checkUpdateMeta = - const VerificationMeta('checkUpdate'); + late final GeneratedColumnWithTypeConverter + accessToken = GeneratedColumn('access_token', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $AuthenticationTableTable.$converteraccessToken); + static const VerificationMeta _expirationMeta = + const VerificationMeta('expiration'); @override - late final GeneratedColumn checkUpdate = GeneratedColumn( - 'check_update', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("check_update" IN (0, 1))'), - defaultValue: const Constant(true)); - static const VerificationMeta _normalizeAudioMeta = - const VerificationMeta('normalizeAudio'); + late final GeneratedColumn expiration = GeneratedColumn( + 'expiration', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); @override - late final GeneratedColumn normalizeAudio = GeneratedColumn( - 'normalize_audio', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("normalize_audio" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _showSystemTrayIconMeta = - const VerificationMeta('showSystemTrayIcon'); + List get $columns => [id, cookie, accessToken, expiration]; @override - late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( - 'show_system_tray_icon', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("show_system_tray_icon" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _systemTitleBarMeta = - const VerificationMeta('systemTitleBar'); + String get aliasedName => _alias ?? actualTableName; @override - late final GeneratedColumn systemTitleBar = GeneratedColumn( - 'system_title_bar', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("system_title_bar" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _skipNonMusicMeta = - const VerificationMeta('skipNonMusic'); + String get actualTableName => $name; + static const String $name = 'authentication_table'; @override - late final GeneratedColumn skipNonMusic = GeneratedColumn( - 'skip_non_music', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("skip_non_music" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _closeBehaviorMeta = - const VerificationMeta('closeBehavior'); + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + context.handle(_cookieMeta, const VerificationResult.success()); + context.handle(_accessTokenMeta, const VerificationResult.success()); + if (data.containsKey('expiration')) { + context.handle( + _expirationMeta, + expiration.isAcceptableOrUnknown( + data['expiration']!, _expirationMeta)); + } else if (isInserting) { + context.missing(_expirationMeta); + } + return context; + } + @override - late final GeneratedColumnWithTypeConverter - closeBehavior = GeneratedColumn( - 'close_behavior', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(CloseBehavior.close.name)) - .withConverter( - $PreferencesTableTable.$convertercloseBehavior); - static const VerificationMeta _accentColorSchemeMeta = - const VerificationMeta('accentColorScheme'); + Set get $primaryKey => {id}; @override - late final GeneratedColumnWithTypeConverter - accentColorScheme = GeneratedColumn( - 'accent_color_scheme', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("Blue:0xFF2196F3")) - .withConverter( - $PreferencesTableTable.$converteraccentColorScheme); - static const VerificationMeta _layoutModeMeta = - const VerificationMeta('layoutMode'); + AuthenticationTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthenticationTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + cookie: $AuthenticationTableTable.$convertercookie.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}cookie'])!), + accessToken: $AuthenticationTableTable.$converteraccessToken.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}access_token'])!), + expiration: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, + ); + } + @override - late final GeneratedColumnWithTypeConverter layoutMode = - GeneratedColumn('layout_mode', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(LayoutMode.adaptive.name)) - .withConverter( - $PreferencesTableTable.$converterlayoutMode); - static const VerificationMeta _localeMeta = const VerificationMeta('locale'); + $AuthenticationTableTable createAlias(String alias) { + return $AuthenticationTableTable(attachedDatabase, alias); + } + + static TypeConverter $convertercookie = + EncryptedTextConverter(); + static TypeConverter $converteraccessToken = + EncryptedTextConverter(); +} + +class AuthenticationTableData extends DataClass + implements Insertable { + final int id; + final DecryptedText cookie; + final DecryptedText accessToken; + final DateTime expiration; + const AuthenticationTableData( + {required this.id, + required this.cookie, + required this.accessToken, + required this.expiration}); @override - late final GeneratedColumnWithTypeConverter locale = - GeneratedColumn('locale', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant( - '{"languageCode":"system","countryCode":"system"}')) - .withConverter($PreferencesTableTable.$converterlocale); - static const VerificationMeta _marketMeta = const VerificationMeta('market'); + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + map['cookie'] = Variable( + $AuthenticationTableTable.$convertercookie.toSql(cookie)); + } + { + map['access_token'] = Variable( + $AuthenticationTableTable.$converteraccessToken.toSql(accessToken)); + } + map['expiration'] = Variable(expiration); + return map; + } + + AuthenticationTableCompanion toCompanion(bool nullToAbsent) { + return AuthenticationTableCompanion( + id: Value(id), + cookie: Value(cookie), + accessToken: Value(accessToken), + expiration: Value(expiration), + ); + } + + factory AuthenticationTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthenticationTableData( + id: serializer.fromJson(json['id']), + cookie: serializer.fromJson(json['cookie']), + accessToken: serializer.fromJson(json['accessToken']), + expiration: serializer.fromJson(json['expiration']), + ); + } @override - late final GeneratedColumnWithTypeConverter market = - GeneratedColumn('market', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(Market.US.name)) - .withConverter($PreferencesTableTable.$convertermarket); - static const VerificationMeta _searchModeMeta = - const VerificationMeta('searchMode'); + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'cookie': serializer.toJson(cookie), + 'accessToken': serializer.toJson(accessToken), + 'expiration': serializer.toJson(expiration), + }; + } + + AuthenticationTableData copyWith( + {int? id, + DecryptedText? cookie, + DecryptedText? accessToken, + DateTime? expiration}) => + AuthenticationTableData( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); @override - late final GeneratedColumnWithTypeConverter searchMode = - GeneratedColumn('search_mode', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SearchMode.youtube.name)) - .withConverter( - $PreferencesTableTable.$convertersearchMode); - static const VerificationMeta _downloadLocationMeta = - const VerificationMeta('downloadLocation'); + String toString() { + return (StringBuffer('AuthenticationTableData(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } + @override - late final GeneratedColumn downloadLocation = GeneratedColumn( - 'download_location', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("")); - static const VerificationMeta _localLibraryLocationMeta = - const VerificationMeta('localLibraryLocation'); + int get hashCode => Object.hash(id, cookie, accessToken, expiration); @override - late final GeneratedColumnWithTypeConverter, String> - localLibraryLocation = GeneratedColumn( - 'local_library_location', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("")) - .withConverter>( - $PreferencesTableTable.$converterlocalLibraryLocation); - static const VerificationMeta _pipedInstanceMeta = - const VerificationMeta('pipedInstance'); + bool operator ==(Object other) => + identical(this, other) || + (other is AuthenticationTableData && + other.id == this.id && + other.cookie == this.cookie && + other.accessToken == this.accessToken && + other.expiration == this.expiration); +} + +class AuthenticationTableCompanion + extends UpdateCompanion { + final Value id; + final Value cookie; + final Value accessToken; + final Value expiration; + const AuthenticationTableCompanion({ + this.id = const Value.absent(), + this.cookie = const Value.absent(), + this.accessToken = const Value.absent(), + this.expiration = const Value.absent(), + }); + AuthenticationTableCompanion.insert({ + this.id = const Value.absent(), + required DecryptedText cookie, + required DecryptedText accessToken, + required DateTime expiration, + }) : cookie = Value(cookie), + accessToken = Value(accessToken), + expiration = Value(expiration); + static Insertable custom({ + Expression? id, + Expression? cookie, + Expression? accessToken, + Expression? expiration, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (cookie != null) 'cookie': cookie, + if (accessToken != null) 'access_token': accessToken, + if (expiration != null) 'expiration': expiration, + }); + } + + AuthenticationTableCompanion copyWith( + {Value? id, + Value? cookie, + Value? accessToken, + Value? expiration}) { + return AuthenticationTableCompanion( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } + @override - late final GeneratedColumn pipedInstance = GeneratedColumn( - 'piped_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://pipedapi.kavin.rocks")); - static const VerificationMeta _themeModeMeta = - const VerificationMeta('themeMode'); + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (cookie.present) { + map['cookie'] = Variable( + $AuthenticationTableTable.$convertercookie.toSql(cookie.value)); + } + if (accessToken.present) { + map['access_token'] = Variable($AuthenticationTableTable + .$converteraccessToken + .toSql(accessToken.value)); + } + if (expiration.present) { + map['expiration'] = Variable(expiration.value); + } + return map; + } + @override - late final GeneratedColumnWithTypeConverter themeMode = - GeneratedColumn('theme_mode', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(ThemeMode.system.name)) - .withConverter($PreferencesTableTable.$converterthemeMode); - static const VerificationMeta _audioSourceMeta = - const VerificationMeta('audioSource'); - @override - late final GeneratedColumnWithTypeConverter audioSource = - GeneratedColumn('audio_source', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(AudioSource.youtube.name)) - .withConverter( - $PreferencesTableTable.$converteraudioSource); - static const VerificationMeta _streamMusicCodecMeta = - const VerificationMeta('streamMusicCodec'); - @override - late final GeneratedColumnWithTypeConverter - streamMusicCodec = GeneratedColumn( - 'stream_music_codec', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceCodecs.weba.name)) - .withConverter( - $PreferencesTableTable.$converterstreamMusicCodec); - static const VerificationMeta _downloadMusicCodecMeta = - const VerificationMeta('downloadMusicCodec'); + String toString() { + return (StringBuffer('AuthenticationTableCompanion(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } +} + +class $BlacklistTableTable extends BlacklistTable + with TableInfo<$BlacklistTableTable, BlacklistTableData> { @override - late final GeneratedColumnWithTypeConverter - downloadMusicCodec = GeneratedColumn( - 'download_music_codec', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceCodecs.m4a.name)) - .withConverter( - $PreferencesTableTable.$converterdownloadMusicCodec); - static const VerificationMeta _discordPresenceMeta = - const VerificationMeta('discordPresence'); + final GeneratedDatabase attachedDatabase; + final String? _alias; + $BlacklistTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn discordPresence = GeneratedColumn( - 'discord_presence', aliasedName, false, - type: DriftSqlType.bool, + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("discord_presence" IN (0, 1))'), - defaultValue: const Constant(true)); - static const VerificationMeta _endlessPlaybackMeta = - const VerificationMeta('endlessPlayback'); + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override - late final GeneratedColumn endlessPlayback = GeneratedColumn( - 'endless_playback', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("endless_playback" IN (0, 1))'), - defaultValue: const Constant(true)); - static const VerificationMeta _enableConnectMeta = - const VerificationMeta('enableConnect'); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _elementTypeMeta = + const VerificationMeta('elementType'); @override - late final GeneratedColumn enableConnect = GeneratedColumn( - 'enable_connect', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("enable_connect" IN (0, 1))'), - defaultValue: const Constant(false)); + late final GeneratedColumnWithTypeConverter + elementType = GeneratedColumn('element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $BlacklistTableTable.$converterelementType); + static const VerificationMeta _elementIdMeta = + const VerificationMeta('elementId'); @override - List get $columns => [ - id, - audioQuality, - albumColorSync, - amoledDarkTheme, - checkUpdate, - normalizeAudio, - showSystemTrayIcon, - systemTitleBar, - skipNonMusic, - closeBehavior, - accentColorScheme, - layoutMode, - locale, - market, - searchMode, - downloadLocation, - localLibraryLocation, - pipedInstance, - themeMode, - audioSource, - streamMusicCodec, - downloadMusicCodec, - discordPresence, - endlessPlayback, - enableConnect - ]; + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'preferences_table'; + static const String $name = 'blacklist_table'; @override - VerificationContext validateIntegrity( - Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } - context.handle(_audioQualityMeta, const VerificationResult.success()); - if (data.containsKey('album_color_sync')) { - context.handle( - _albumColorSyncMeta, - albumColorSync.isAcceptableOrUnknown( - data['album_color_sync']!, _albumColorSyncMeta)); - } - if (data.containsKey('amoled_dark_theme')) { - context.handle( - _amoledDarkThemeMeta, - amoledDarkTheme.isAcceptableOrUnknown( - data['amoled_dark_theme']!, _amoledDarkThemeMeta)); - } - if (data.containsKey('check_update')) { - context.handle( - _checkUpdateMeta, - checkUpdate.isAcceptableOrUnknown( - data['check_update']!, _checkUpdateMeta)); - } - if (data.containsKey('normalize_audio')) { - context.handle( - _normalizeAudioMeta, - normalizeAudio.isAcceptableOrUnknown( - data['normalize_audio']!, _normalizeAudioMeta)); - } - if (data.containsKey('show_system_tray_icon')) { - context.handle( - _showSystemTrayIconMeta, - showSystemTrayIcon.isAcceptableOrUnknown( - data['show_system_tray_icon']!, _showSystemTrayIconMeta)); - } - if (data.containsKey('system_title_bar')) { - context.handle( - _systemTitleBarMeta, - systemTitleBar.isAcceptableOrUnknown( - data['system_title_bar']!, _systemTitleBarMeta)); - } - if (data.containsKey('skip_non_music')) { - context.handle( - _skipNonMusicMeta, - skipNonMusic.isAcceptableOrUnknown( - data['skip_non_music']!, _skipNonMusicMeta)); - } - context.handle(_closeBehaviorMeta, const VerificationResult.success()); - context.handle(_accentColorSchemeMeta, const VerificationResult.success()); - context.handle(_layoutModeMeta, const VerificationResult.success()); - context.handle(_localeMeta, const VerificationResult.success()); - context.handle(_marketMeta, const VerificationResult.success()); - context.handle(_searchModeMeta, const VerificationResult.success()); - if (data.containsKey('download_location')) { - context.handle( - _downloadLocationMeta, - downloadLocation.isAcceptableOrUnknown( - data['download_location']!, _downloadLocationMeta)); - } - context.handle( - _localLibraryLocationMeta, const VerificationResult.success()); - if (data.containsKey('piped_instance')) { - context.handle( - _pipedInstanceMeta, - pipedInstance.isAcceptableOrUnknown( - data['piped_instance']!, _pipedInstanceMeta)); - } - context.handle(_themeModeMeta, const VerificationResult.success()); - context.handle(_audioSourceMeta, const VerificationResult.success()); - context.handle(_streamMusicCodecMeta, const VerificationResult.success()); - context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); - if (data.containsKey('discord_presence')) { - context.handle( - _discordPresenceMeta, - discordPresence.isAcceptableOrUnknown( - data['discord_presence']!, _discordPresenceMeta)); - } - if (data.containsKey('endless_playback')) { + if (data.containsKey('name')) { context.handle( - _endlessPlaybackMeta, - endlessPlayback.isAcceptableOrUnknown( - data['endless_playback']!, _endlessPlaybackMeta)); + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); } - if (data.containsKey('enable_connect')) { - context.handle( - _enableConnectMeta, - enableConnect.isAcceptableOrUnknown( - data['enable_connect']!, _enableConnectMeta)); + context.handle(_elementTypeMeta, const VerificationResult.success()); + if (data.containsKey('element_id')) { + context.handle(_elementIdMeta, + elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); + } else if (isInserting) { + context.missing(_elementIdMeta); } return context; } @@ -389,304 +338,73 @@ class $PreferencesTableTable extends PreferencesTable @override Set get $primaryKey => {id}; @override - PreferencesTableData map(Map data, {String? tablePrefix}) { + BlacklistTableData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PreferencesTableData( + return BlacklistTableData( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - audioQuality: $PreferencesTableTable.$converteraudioQuality.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}audio_quality'])!), - albumColorSync: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, - amoledDarkTheme: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, - checkUpdate: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, - normalizeAudio: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, - showSystemTrayIcon: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, - systemTitleBar: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, - skipNonMusic: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, - closeBehavior: $PreferencesTableTable.$convertercloseBehavior.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}close_behavior'])!), - accentColorScheme: $PreferencesTableTable.$converteraccentColorScheme - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}accent_color_scheme'])!), - layoutMode: $PreferencesTableTable.$converterlayoutMode.fromSql( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: $BlacklistTableTable.$converterelementType.fromSql( attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}layout_mode'])!), - locale: $PreferencesTableTable.$converterlocale.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}locale'])!), - market: $PreferencesTableTable.$convertermarket.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}market'])!), - searchMode: $PreferencesTableTable.$convertersearchMode.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}search_mode'])!), - downloadLocation: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}download_location'])!, - localLibraryLocation: $PreferencesTableTable - .$converterlocalLibraryLocation - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}local_library_location'])!), - pipedInstance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, - themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), - audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}audio_source'])!), - streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}stream_music_codec'])!), - downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}download_music_codec'])!), - discordPresence: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, - endlessPlayback: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, - enableConnect: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + DriftSqlType.string, data['${effectivePrefix}element_type'])!), + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, ); } @override - $PreferencesTableTable createAlias(String alias) { - return $PreferencesTableTable(attachedDatabase, alias); + $BlacklistTableTable createAlias(String alias) { + return $BlacklistTableTable(attachedDatabase, alias); } - static JsonTypeConverter2 - $converteraudioQuality = - const EnumNameConverter(SourceQualities.values); - static JsonTypeConverter2 - $convertercloseBehavior = - const EnumNameConverter(CloseBehavior.values); - static TypeConverter $converteraccentColorScheme = - const SpotubeColorConverter(); - static JsonTypeConverter2 $converterlayoutMode = - const EnumNameConverter(LayoutMode.values); - static TypeConverter $converterlocale = - const LocaleConverter(); - static JsonTypeConverter2 $convertermarket = - const EnumNameConverter(Market.values); - static JsonTypeConverter2 $convertersearchMode = - const EnumNameConverter(SearchMode.values); - static TypeConverter, String> $converterlocalLibraryLocation = - const StringListConverter(); - static JsonTypeConverter2 $converterthemeMode = - const EnumNameConverter(ThemeMode.values); - static JsonTypeConverter2 $converteraudioSource = - const EnumNameConverter(AudioSource.values); - static JsonTypeConverter2 - $converterstreamMusicCodec = - const EnumNameConverter(SourceCodecs.values); - static JsonTypeConverter2 - $converterdownloadMusicCodec = - const EnumNameConverter(SourceCodecs.values); + static JsonTypeConverter2 + $converterelementType = + const EnumNameConverter(BlacklistedType.values); } -class PreferencesTableData extends DataClass - implements Insertable { +class BlacklistTableData extends DataClass + implements Insertable { final int id; - final SourceQualities audioQuality; - final bool albumColorSync; - final bool amoledDarkTheme; - final bool checkUpdate; - final bool normalizeAudio; - final bool showSystemTrayIcon; - final bool systemTitleBar; - final bool skipNonMusic; - final CloseBehavior closeBehavior; - final SpotubeColor accentColorScheme; - final LayoutMode layoutMode; - final Locale locale; - final Market market; - final SearchMode searchMode; - final String downloadLocation; - final List localLibraryLocation; - final String pipedInstance; - final ThemeMode themeMode; - final AudioSource audioSource; - final SourceCodecs streamMusicCodec; - final SourceCodecs downloadMusicCodec; - final bool discordPresence; - final bool endlessPlayback; - final bool enableConnect; - const PreferencesTableData( + final String name; + final BlacklistedType elementType; + final String elementId; + const BlacklistTableData( {required this.id, - required this.audioQuality, - required this.albumColorSync, - required this.amoledDarkTheme, - required this.checkUpdate, - required this.normalizeAudio, - required this.showSystemTrayIcon, - required this.systemTitleBar, - required this.skipNonMusic, - required this.closeBehavior, - required this.accentColorScheme, - required this.layoutMode, - required this.locale, - required this.market, - required this.searchMode, - required this.downloadLocation, - required this.localLibraryLocation, - required this.pipedInstance, - required this.themeMode, - required this.audioSource, - required this.streamMusicCodec, - required this.downloadMusicCodec, - required this.discordPresence, - required this.endlessPlayback, - required this.enableConnect}); + required this.name, + required this.elementType, + required this.elementId}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); + map['name'] = Variable(name); { - map['audio_quality'] = Variable( - $PreferencesTableTable.$converteraudioQuality.toSql(audioQuality)); - } - map['album_color_sync'] = Variable(albumColorSync); - map['amoled_dark_theme'] = Variable(amoledDarkTheme); - map['check_update'] = Variable(checkUpdate); - map['normalize_audio'] = Variable(normalizeAudio); - map['show_system_tray_icon'] = Variable(showSystemTrayIcon); - map['system_title_bar'] = Variable(systemTitleBar); - map['skip_non_music'] = Variable(skipNonMusic); - { - map['close_behavior'] = Variable( - $PreferencesTableTable.$convertercloseBehavior.toSql(closeBehavior)); - } - { - map['accent_color_scheme'] = Variable($PreferencesTableTable - .$converteraccentColorScheme - .toSql(accentColorScheme)); - } - { - map['layout_mode'] = Variable( - $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode)); - } - { - map['locale'] = Variable( - $PreferencesTableTable.$converterlocale.toSql(locale)); - } - { - map['market'] = Variable( - $PreferencesTableTable.$convertermarket.toSql(market)); - } - { - map['search_mode'] = Variable( - $PreferencesTableTable.$convertersearchMode.toSql(searchMode)); - } - map['download_location'] = Variable(downloadLocation); - { - map['local_library_location'] = Variable($PreferencesTableTable - .$converterlocalLibraryLocation - .toSql(localLibraryLocation)); - } - map['piped_instance'] = Variable(pipedInstance); - { - map['theme_mode'] = Variable( - $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); - } - { - map['audio_source'] = Variable( - $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); - } - { - map['stream_music_codec'] = Variable($PreferencesTableTable - .$converterstreamMusicCodec - .toSql(streamMusicCodec)); - } - { - map['download_music_codec'] = Variable($PreferencesTableTable - .$converterdownloadMusicCodec - .toSql(downloadMusicCodec)); + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType)); } - map['discord_presence'] = Variable(discordPresence); - map['endless_playback'] = Variable(endlessPlayback); - map['enable_connect'] = Variable(enableConnect); + map['element_id'] = Variable(elementId); return map; } - PreferencesTableCompanion toCompanion(bool nullToAbsent) { - return PreferencesTableCompanion( + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( id: Value(id), - audioQuality: Value(audioQuality), - albumColorSync: Value(albumColorSync), - amoledDarkTheme: Value(amoledDarkTheme), - checkUpdate: Value(checkUpdate), - normalizeAudio: Value(normalizeAudio), - showSystemTrayIcon: Value(showSystemTrayIcon), - systemTitleBar: Value(systemTitleBar), - skipNonMusic: Value(skipNonMusic), - closeBehavior: Value(closeBehavior), - accentColorScheme: Value(accentColorScheme), - layoutMode: Value(layoutMode), - locale: Value(locale), - market: Value(market), - searchMode: Value(searchMode), - downloadLocation: Value(downloadLocation), - localLibraryLocation: Value(localLibraryLocation), - pipedInstance: Value(pipedInstance), - themeMode: Value(themeMode), - audioSource: Value(audioSource), - streamMusicCodec: Value(streamMusicCodec), - downloadMusicCodec: Value(downloadMusicCodec), - discordPresence: Value(discordPresence), - endlessPlayback: Value(endlessPlayback), - enableConnect: Value(enableConnect), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), ); } - factory PreferencesTableData.fromJson(Map json, + factory BlacklistTableData.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return PreferencesTableData( + return BlacklistTableData( id: serializer.fromJson(json['id']), - audioQuality: $PreferencesTableTable.$converteraudioQuality - .fromJson(serializer.fromJson(json['audioQuality'])), - albumColorSync: serializer.fromJson(json['albumColorSync']), - amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), - checkUpdate: serializer.fromJson(json['checkUpdate']), - normalizeAudio: serializer.fromJson(json['normalizeAudio']), - showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), - systemTitleBar: serializer.fromJson(json['systemTitleBar']), - skipNonMusic: serializer.fromJson(json['skipNonMusic']), - closeBehavior: $PreferencesTableTable.$convertercloseBehavior - .fromJson(serializer.fromJson(json['closeBehavior'])), - accentColorScheme: - serializer.fromJson(json['accentColorScheme']), - layoutMode: $PreferencesTableTable.$converterlayoutMode - .fromJson(serializer.fromJson(json['layoutMode'])), - locale: serializer.fromJson(json['locale']), - market: $PreferencesTableTable.$convertermarket - .fromJson(serializer.fromJson(json['market'])), - searchMode: $PreferencesTableTable.$convertersearchMode - .fromJson(serializer.fromJson(json['searchMode'])), - downloadLocation: serializer.fromJson(json['downloadLocation']), - localLibraryLocation: - serializer.fromJson>(json['localLibraryLocation']), - pipedInstance: serializer.fromJson(json['pipedInstance']), - themeMode: $PreferencesTableTable.$converterthemeMode - .fromJson(serializer.fromJson(json['themeMode'])), - audioSource: $PreferencesTableTable.$converteraudioSource - .fromJson(serializer.fromJson(json['audioSource'])), - streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec - .fromJson(serializer.fromJson(json['streamMusicCodec'])), - downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec - .fromJson(serializer.fromJson(json['downloadMusicCodec'])), - discordPresence: serializer.fromJson(json['discordPresence']), - endlessPlayback: serializer.fromJson(json['endlessPlayback']), - enableConnect: serializer.fromJson(json['enableConnect']), + name: serializer.fromJson(json['name']), + elementType: $BlacklistTableTable.$converterelementType + .fromJson(serializer.fromJson(json['elementType'])), + elementId: serializer.fromJson(json['elementId']), ); } @override @@ -694,380 +412,90 @@ class PreferencesTableData extends DataClass serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'audioQuality': serializer.toJson( - $PreferencesTableTable.$converteraudioQuality.toJson(audioQuality)), - 'albumColorSync': serializer.toJson(albumColorSync), - 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), - 'checkUpdate': serializer.toJson(checkUpdate), - 'normalizeAudio': serializer.toJson(normalizeAudio), - 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), - 'systemTitleBar': serializer.toJson(systemTitleBar), - 'skipNonMusic': serializer.toJson(skipNonMusic), - 'closeBehavior': serializer.toJson( - $PreferencesTableTable.$convertercloseBehavior.toJson(closeBehavior)), - 'accentColorScheme': serializer.toJson(accentColorScheme), - 'layoutMode': serializer.toJson( - $PreferencesTableTable.$converterlayoutMode.toJson(layoutMode)), - 'locale': serializer.toJson(locale), - 'market': serializer.toJson( - $PreferencesTableTable.$convertermarket.toJson(market)), - 'searchMode': serializer.toJson( - $PreferencesTableTable.$convertersearchMode.toJson(searchMode)), - 'downloadLocation': serializer.toJson(downloadLocation), - 'localLibraryLocation': - serializer.toJson>(localLibraryLocation), - 'pipedInstance': serializer.toJson(pipedInstance), - 'themeMode': serializer.toJson( - $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), - 'audioSource': serializer.toJson( - $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), - 'streamMusicCodec': serializer.toJson($PreferencesTableTable - .$converterstreamMusicCodec - .toJson(streamMusicCodec)), - 'downloadMusicCodec': serializer.toJson($PreferencesTableTable - .$converterdownloadMusicCodec - .toJson(downloadMusicCodec)), - 'discordPresence': serializer.toJson(discordPresence), - 'endlessPlayback': serializer.toJson(endlessPlayback), - 'enableConnect': serializer.toJson(enableConnect), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson( + $BlacklistTableTable.$converterelementType.toJson(elementType)), + 'elementId': serializer.toJson(elementId), }; } - PreferencesTableData copyWith( + BlacklistTableData copyWith( {int? id, - SourceQualities? audioQuality, - bool? albumColorSync, - bool? amoledDarkTheme, - bool? checkUpdate, - bool? normalizeAudio, - bool? showSystemTrayIcon, - bool? systemTitleBar, - bool? skipNonMusic, - CloseBehavior? closeBehavior, - SpotubeColor? accentColorScheme, - LayoutMode? layoutMode, - Locale? locale, - Market? market, - SearchMode? searchMode, - String? downloadLocation, - List? localLibraryLocation, - String? pipedInstance, - ThemeMode? themeMode, - AudioSource? audioSource, - SourceCodecs? streamMusicCodec, - SourceCodecs? downloadMusicCodec, - bool? discordPresence, - bool? endlessPlayback, - bool? enableConnect}) => - PreferencesTableData( + String? name, + BlacklistedType? elementType, + String? elementId}) => + BlacklistTableData( id: id ?? this.id, - audioQuality: audioQuality ?? this.audioQuality, - albumColorSync: albumColorSync ?? this.albumColorSync, - amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, - checkUpdate: checkUpdate ?? this.checkUpdate, - normalizeAudio: normalizeAudio ?? this.normalizeAudio, - showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, - systemTitleBar: systemTitleBar ?? this.systemTitleBar, - skipNonMusic: skipNonMusic ?? this.skipNonMusic, - closeBehavior: closeBehavior ?? this.closeBehavior, - accentColorScheme: accentColorScheme ?? this.accentColorScheme, - layoutMode: layoutMode ?? this.layoutMode, - locale: locale ?? this.locale, - market: market ?? this.market, - searchMode: searchMode ?? this.searchMode, - downloadLocation: downloadLocation ?? this.downloadLocation, - localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - themeMode: themeMode ?? this.themeMode, - audioSource: audioSource ?? this.audioSource, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, - discordPresence: discordPresence ?? this.discordPresence, - endlessPlayback: endlessPlayback ?? this.endlessPlayback, - enableConnect: enableConnect ?? this.enableConnect, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, ); @override String toString() { - return (StringBuffer('PreferencesTableData(') + return (StringBuffer('BlacklistTableData(') ..write('id: $id, ') - ..write('audioQuality: $audioQuality, ') - ..write('albumColorSync: $albumColorSync, ') - ..write('amoledDarkTheme: $amoledDarkTheme, ') - ..write('checkUpdate: $checkUpdate, ') - ..write('normalizeAudio: $normalizeAudio, ') - ..write('showSystemTrayIcon: $showSystemTrayIcon, ') - ..write('systemTitleBar: $systemTitleBar, ') - ..write('skipNonMusic: $skipNonMusic, ') - ..write('closeBehavior: $closeBehavior, ') - ..write('accentColorScheme: $accentColorScheme, ') - ..write('layoutMode: $layoutMode, ') - ..write('locale: $locale, ') - ..write('market: $market, ') - ..write('searchMode: $searchMode, ') - ..write('downloadLocation: $downloadLocation, ') - ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('themeMode: $themeMode, ') - ..write('audioSource: $audioSource, ') - ..write('streamMusicCodec: $streamMusicCodec, ') - ..write('downloadMusicCodec: $downloadMusicCodec, ') - ..write('discordPresence: $discordPresence, ') - ..write('endlessPlayback: $endlessPlayback, ') - ..write('enableConnect: $enableConnect') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') ..write(')')) .toString(); } @override - int get hashCode => Object.hashAll([ - id, - audioQuality, - albumColorSync, - amoledDarkTheme, - checkUpdate, - normalizeAudio, - showSystemTrayIcon, - systemTitleBar, - skipNonMusic, - closeBehavior, - accentColorScheme, - layoutMode, - locale, - market, - searchMode, - downloadLocation, - localLibraryLocation, - pipedInstance, - themeMode, - audioSource, - streamMusicCodec, - downloadMusicCodec, - discordPresence, - endlessPlayback, - enableConnect - ]); + int get hashCode => Object.hash(id, name, elementType, elementId); @override bool operator ==(Object other) => identical(this, other) || - (other is PreferencesTableData && + (other is BlacklistTableData && other.id == this.id && - other.audioQuality == this.audioQuality && - other.albumColorSync == this.albumColorSync && - other.amoledDarkTheme == this.amoledDarkTheme && - other.checkUpdate == this.checkUpdate && - other.normalizeAudio == this.normalizeAudio && - other.showSystemTrayIcon == this.showSystemTrayIcon && - other.systemTitleBar == this.systemTitleBar && - other.skipNonMusic == this.skipNonMusic && - other.closeBehavior == this.closeBehavior && - other.accentColorScheme == this.accentColorScheme && - other.layoutMode == this.layoutMode && - other.locale == this.locale && - other.market == this.market && - other.searchMode == this.searchMode && - other.downloadLocation == this.downloadLocation && - other.localLibraryLocation == this.localLibraryLocation && - other.pipedInstance == this.pipedInstance && - other.themeMode == this.themeMode && - other.audioSource == this.audioSource && - other.streamMusicCodec == this.streamMusicCodec && - other.downloadMusicCodec == this.downloadMusicCodec && - other.discordPresence == this.discordPresence && - other.endlessPlayback == this.endlessPlayback && - other.enableConnect == this.enableConnect); + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); } -class PreferencesTableCompanion extends UpdateCompanion { +class BlacklistTableCompanion extends UpdateCompanion { final Value id; - final Value audioQuality; - final Value albumColorSync; - final Value amoledDarkTheme; - final Value checkUpdate; - final Value normalizeAudio; - final Value showSystemTrayIcon; - final Value systemTitleBar; - final Value skipNonMusic; - final Value closeBehavior; - final Value accentColorScheme; - final Value layoutMode; - final Value locale; - final Value market; - final Value searchMode; - final Value downloadLocation; - final Value> localLibraryLocation; - final Value pipedInstance; - final Value themeMode; - final Value audioSource; - final Value streamMusicCodec; - final Value downloadMusicCodec; - final Value discordPresence; - final Value endlessPlayback; - final Value enableConnect; - const PreferencesTableCompanion({ + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ this.id = const Value.absent(), - this.audioQuality = const Value.absent(), - this.albumColorSync = const Value.absent(), - this.amoledDarkTheme = const Value.absent(), - this.checkUpdate = const Value.absent(), - this.normalizeAudio = const Value.absent(), - this.showSystemTrayIcon = const Value.absent(), - this.systemTitleBar = const Value.absent(), - this.skipNonMusic = const Value.absent(), - this.closeBehavior = const Value.absent(), - this.accentColorScheme = const Value.absent(), - this.layoutMode = const Value.absent(), - this.locale = const Value.absent(), - this.market = const Value.absent(), - this.searchMode = const Value.absent(), - this.downloadLocation = const Value.absent(), - this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.themeMode = const Value.absent(), - this.audioSource = const Value.absent(), - this.streamMusicCodec = const Value.absent(), - this.downloadMusicCodec = const Value.absent(), - this.discordPresence = const Value.absent(), - this.endlessPlayback = const Value.absent(), - this.enableConnect = const Value.absent(), - }); - PreferencesTableCompanion.insert({ - this.id = const Value.absent(), - this.audioQuality = const Value.absent(), - this.albumColorSync = const Value.absent(), - this.amoledDarkTheme = const Value.absent(), - this.checkUpdate = const Value.absent(), - this.normalizeAudio = const Value.absent(), - this.showSystemTrayIcon = const Value.absent(), - this.systemTitleBar = const Value.absent(), - this.skipNonMusic = const Value.absent(), - this.closeBehavior = const Value.absent(), - this.accentColorScheme = const Value.absent(), - this.layoutMode = const Value.absent(), - this.locale = const Value.absent(), - this.market = const Value.absent(), - this.searchMode = const Value.absent(), - this.downloadLocation = const Value.absent(), - this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.themeMode = const Value.absent(), - this.audioSource = const Value.absent(), - this.streamMusicCodec = const Value.absent(), - this.downloadMusicCodec = const Value.absent(), - this.discordPresence = const Value.absent(), - this.endlessPlayback = const Value.absent(), - this.enableConnect = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), }); - static Insertable custom({ + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ Expression? id, - Expression? audioQuality, - Expression? albumColorSync, - Expression? amoledDarkTheme, - Expression? checkUpdate, - Expression? normalizeAudio, - Expression? showSystemTrayIcon, - Expression? systemTitleBar, - Expression? skipNonMusic, - Expression? closeBehavior, - Expression? accentColorScheme, - Expression? layoutMode, - Expression? locale, - Expression? market, - Expression? searchMode, - Expression? downloadLocation, - Expression? localLibraryLocation, - Expression? pipedInstance, - Expression? themeMode, - Expression? audioSource, - Expression? streamMusicCodec, - Expression? downloadMusicCodec, - Expression? discordPresence, - Expression? endlessPlayback, - Expression? enableConnect, + Expression? name, + Expression? elementType, + Expression? elementId, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (audioQuality != null) 'audio_quality': audioQuality, - if (albumColorSync != null) 'album_color_sync': albumColorSync, - if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, - if (checkUpdate != null) 'check_update': checkUpdate, - if (normalizeAudio != null) 'normalize_audio': normalizeAudio, - if (showSystemTrayIcon != null) - 'show_system_tray_icon': showSystemTrayIcon, - if (systemTitleBar != null) 'system_title_bar': systemTitleBar, - if (skipNonMusic != null) 'skip_non_music': skipNonMusic, - if (closeBehavior != null) 'close_behavior': closeBehavior, - if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, - if (layoutMode != null) 'layout_mode': layoutMode, - if (locale != null) 'locale': locale, - if (market != null) 'market': market, - if (searchMode != null) 'search_mode': searchMode, - if (downloadLocation != null) 'download_location': downloadLocation, - if (localLibraryLocation != null) - 'local_library_location': localLibraryLocation, - if (pipedInstance != null) 'piped_instance': pipedInstance, - if (themeMode != null) 'theme_mode': themeMode, - if (audioSource != null) 'audio_source': audioSource, - if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, - if (downloadMusicCodec != null) - 'download_music_codec': downloadMusicCodec, - if (discordPresence != null) 'discord_presence': discordPresence, - if (endlessPlayback != null) 'endless_playback': endlessPlayback, - if (enableConnect != null) 'enable_connect': enableConnect, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, }); } - PreferencesTableCompanion copyWith( + BlacklistTableCompanion copyWith( {Value? id, - Value? audioQuality, - Value? albumColorSync, - Value? amoledDarkTheme, - Value? checkUpdate, - Value? normalizeAudio, - Value? showSystemTrayIcon, - Value? systemTitleBar, - Value? skipNonMusic, - Value? closeBehavior, - Value? accentColorScheme, - Value? layoutMode, - Value? locale, - Value? market, - Value? searchMode, - Value? downloadLocation, - Value>? localLibraryLocation, - Value? pipedInstance, - Value? themeMode, - Value? audioSource, - Value? streamMusicCodec, - Value? downloadMusicCodec, - Value? discordPresence, - Value? endlessPlayback, - Value? enableConnect}) { - return PreferencesTableCompanion( + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( id: id ?? this.id, - audioQuality: audioQuality ?? this.audioQuality, - albumColorSync: albumColorSync ?? this.albumColorSync, - amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, - checkUpdate: checkUpdate ?? this.checkUpdate, - normalizeAudio: normalizeAudio ?? this.normalizeAudio, - showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, - systemTitleBar: systemTitleBar ?? this.systemTitleBar, - skipNonMusic: skipNonMusic ?? this.skipNonMusic, - closeBehavior: closeBehavior ?? this.closeBehavior, - accentColorScheme: accentColorScheme ?? this.accentColorScheme, - layoutMode: layoutMode ?? this.layoutMode, - locale: locale ?? this.locale, - market: market ?? this.market, - searchMode: searchMode ?? this.searchMode, - downloadLocation: downloadLocation ?? this.downloadLocation, - localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - themeMode: themeMode ?? this.themeMode, - audioSource: audioSource ?? this.audioSource, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, - discordPresence: discordPresence ?? this.discordPresence, - endlessPlayback: endlessPlayback ?? this.endlessPlayback, - enableConnect: enableConnect ?? this.enableConnect, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, ); } @@ -1077,139 +505,37 @@ class PreferencesTableCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (audioQuality.present) { - map['audio_quality'] = Variable($PreferencesTableTable - .$converteraudioQuality - .toSql(audioQuality.value)); - } - if (albumColorSync.present) { - map['album_color_sync'] = Variable(albumColorSync.value); - } - if (amoledDarkTheme.present) { - map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); - } - if (checkUpdate.present) { - map['check_update'] = Variable(checkUpdate.value); - } - if (normalizeAudio.present) { - map['normalize_audio'] = Variable(normalizeAudio.value); - } - if (showSystemTrayIcon.present) { - map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); - } - if (systemTitleBar.present) { - map['system_title_bar'] = Variable(systemTitleBar.value); - } - if (skipNonMusic.present) { - map['skip_non_music'] = Variable(skipNonMusic.value); - } - if (closeBehavior.present) { - map['close_behavior'] = Variable($PreferencesTableTable - .$convertercloseBehavior - .toSql(closeBehavior.value)); - } - if (accentColorScheme.present) { - map['accent_color_scheme'] = Variable($PreferencesTableTable - .$converteraccentColorScheme - .toSql(accentColorScheme.value)); - } - if (layoutMode.present) { - map['layout_mode'] = Variable( - $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode.value)); - } - if (locale.present) { - map['locale'] = Variable( - $PreferencesTableTable.$converterlocale.toSql(locale.value)); - } - if (market.present) { - map['market'] = Variable( - $PreferencesTableTable.$convertermarket.toSql(market.value)); - } - if (searchMode.present) { - map['search_mode'] = Variable( - $PreferencesTableTable.$convertersearchMode.toSql(searchMode.value)); - } - if (downloadLocation.present) { - map['download_location'] = Variable(downloadLocation.value); - } - if (localLibraryLocation.present) { - map['local_library_location'] = Variable($PreferencesTableTable - .$converterlocalLibraryLocation - .toSql(localLibraryLocation.value)); - } - if (pipedInstance.present) { - map['piped_instance'] = Variable(pipedInstance.value); - } - if (themeMode.present) { - map['theme_mode'] = Variable( - $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); - } - if (audioSource.present) { - map['audio_source'] = Variable($PreferencesTableTable - .$converteraudioSource - .toSql(audioSource.value)); - } - if (streamMusicCodec.present) { - map['stream_music_codec'] = Variable($PreferencesTableTable - .$converterstreamMusicCodec - .toSql(streamMusicCodec.value)); - } - if (downloadMusicCodec.present) { - map['download_music_codec'] = Variable($PreferencesTableTable - .$converterdownloadMusicCodec - .toSql(downloadMusicCodec.value)); - } - if (discordPresence.present) { - map['discord_presence'] = Variable(discordPresence.value); + if (name.present) { + map['name'] = Variable(name.value); } - if (endlessPlayback.present) { - map['endless_playback'] = Variable(endlessPlayback.value); + if (elementType.present) { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType.value)); } - if (enableConnect.present) { - map['enable_connect'] = Variable(enableConnect.value); + if (elementId.present) { + map['element_id'] = Variable(elementId.value); } return map; } @override String toString() { - return (StringBuffer('PreferencesTableCompanion(') + return (StringBuffer('BlacklistTableCompanion(') ..write('id: $id, ') - ..write('audioQuality: $audioQuality, ') - ..write('albumColorSync: $albumColorSync, ') - ..write('amoledDarkTheme: $amoledDarkTheme, ') - ..write('checkUpdate: $checkUpdate, ') - ..write('normalizeAudio: $normalizeAudio, ') - ..write('showSystemTrayIcon: $showSystemTrayIcon, ') - ..write('systemTitleBar: $systemTitleBar, ') - ..write('skipNonMusic: $skipNonMusic, ') - ..write('closeBehavior: $closeBehavior, ') - ..write('accentColorScheme: $accentColorScheme, ') - ..write('layoutMode: $layoutMode, ') - ..write('locale: $locale, ') - ..write('market: $market, ') - ..write('searchMode: $searchMode, ') - ..write('downloadLocation: $downloadLocation, ') - ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('themeMode: $themeMode, ') - ..write('audioSource: $audioSource, ') - ..write('streamMusicCodec: $streamMusicCodec, ') - ..write('downloadMusicCodec: $downloadMusicCodec, ') - ..write('discordPresence: $discordPresence, ') - ..write('endlessPlayback: $endlessPlayback, ') - ..write('enableConnect: $enableConnect') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') ..write(')')) .toString(); } } -class $SourceMatchTableTable extends SourceMatchTable - with TableInfo<$SourceMatchTableTable, SourceMatchTableData> { +class $PreferencesTableTable extends PreferencesTable + with TableInfo<$PreferencesTableTable, PreferencesTableData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $SourceMatchTableTable(this.attachedDatabase, [this._alias]); + $PreferencesTableTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -1219,251 +545,1056 @@ class $SourceMatchTableTable extends SourceMatchTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _trackIdMeta = - const VerificationMeta('trackId'); - @override - late final GeneratedColumn trackId = GeneratedColumn( - 'track_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _sourceIdMeta = - const VerificationMeta('sourceId'); - @override - late final GeneratedColumn sourceId = GeneratedColumn( - 'source_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _sourceTypeMeta = - const VerificationMeta('sourceType'); + static const VerificationMeta _audioQualityMeta = + const VerificationMeta('audioQuality'); @override - late final GeneratedColumnWithTypeConverter sourceType = - GeneratedColumn('source_type', aliasedName, false, + late final GeneratedColumnWithTypeConverter + audioQuality = GeneratedColumn( + 'audio_quality', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: Constant(SourceType.youtube.name)) - .withConverter( - $SourceMatchTableTable.$convertersourceType); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + defaultValue: Constant(SourceQualities.high.name)) + .withConverter( + $PreferencesTableTable.$converteraudioQuality); + static const VerificationMeta _albumColorSyncMeta = + const VerificationMeta('albumColorSync'); @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, requiredDuringInsert: false, - defaultValue: currentDateAndTime); + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _amoledDarkThemeMeta = + const VerificationMeta('amoledDarkTheme'); @override - List get $columns => - [id, trackId, sourceId, sourceType, createdAt]; + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _checkUpdateMeta = + const VerificationMeta('checkUpdate'); @override - String get aliasedName => _alias ?? actualTableName; + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _normalizeAudioMeta = + const VerificationMeta('normalizeAudio'); @override - String get actualTableName => $name; - static const String $name = 'source_match_table'; + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _showSystemTrayIconMeta = + const VerificationMeta('showSystemTrayIcon'); @override - VerificationContext validateIntegrity( - Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('track_id')) { - context.handle(_trackIdMeta, - trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); - } else if (isInserting) { - context.missing(_trackIdMeta); - } - if (data.containsKey('source_id')) { - context.handle(_sourceIdMeta, - sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta)); - } else if (isInserting) { - context.missing(_sourceIdMeta); - } - context.handle(_sourceTypeMeta, const VerificationResult.success()); - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); - } - return context; - } - + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _systemTitleBarMeta = + const VerificationMeta('systemTitleBar'); @override - Set get $primaryKey => {id}; + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _skipNonMusicMeta = + const VerificationMeta('skipNonMusic'); @override - SourceMatchTableData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return SourceMatchTableData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - trackId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, - sourceId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, - sourceType: $SourceMatchTableTable.$convertersourceType.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}source_type'])!), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - ); - } - + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _closeBehaviorMeta = + const VerificationMeta('closeBehavior'); @override - $SourceMatchTableTable createAlias(String alias) { - return $SourceMatchTableTable(attachedDatabase, alias); - } - - static JsonTypeConverter2 $convertersourceType = - const EnumNameConverter(SourceType.values); -} - -class SourceMatchTableData extends DataClass - implements Insertable { - final int id; - final String trackId; - final String sourceId; - final SourceType sourceType; - final DateTime createdAt; - const SourceMatchTableData( - {required this.id, - required this.trackId, - required this.sourceId, - required this.sourceType, - required this.createdAt}); + late final GeneratedColumnWithTypeConverter + closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)) + .withConverter( + $PreferencesTableTable.$convertercloseBehavior); + static const VerificationMeta _accentColorSchemeMeta = + const VerificationMeta('accentColorScheme'); @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['track_id'] = Variable(trackId); - map['source_id'] = Variable(sourceId); - { - map['source_type'] = Variable( - $SourceMatchTableTable.$convertersourceType.toSql(sourceType)); - } - map['created_at'] = Variable(createdAt); - return map; - } - - SourceMatchTableCompanion toCompanion(bool nullToAbsent) { - return SourceMatchTableCompanion( - id: Value(id), - trackId: Value(trackId), - sourceId: Value(sourceId), - sourceType: Value(sourceType), - createdAt: Value(createdAt), - ); - } - - factory SourceMatchTableData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return SourceMatchTableData( - id: serializer.fromJson(json['id']), - trackId: serializer.fromJson(json['trackId']), - sourceId: serializer.fromJson(json['sourceId']), - sourceType: $SourceMatchTableTable.$convertersourceType - .fromJson(serializer.fromJson(json['sourceType'])), - createdAt: serializer.fromJson(json['createdAt']), - ); - } + late final GeneratedColumnWithTypeConverter + accentColorScheme = GeneratedColumn( + 'accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Blue:0xFF2196F3")) + .withConverter( + $PreferencesTableTable.$converteraccentColorScheme); + static const VerificationMeta _layoutModeMeta = + const VerificationMeta('layoutMode'); @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'trackId': serializer.toJson(trackId), - 'sourceId': serializer.toJson(sourceId), - 'sourceType': serializer.toJson( - $SourceMatchTableTable.$convertersourceType.toJson(sourceType)), - 'createdAt': serializer.toJson(createdAt), + late final GeneratedColumnWithTypeConverter layoutMode = + GeneratedColumn('layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)) + .withConverter( + $PreferencesTableTable.$converterlayoutMode); + static const VerificationMeta _localeMeta = const VerificationMeta('locale'); + @override + late final GeneratedColumnWithTypeConverter locale = + GeneratedColumn('locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant( + '{"languageCode":"system","countryCode":"system"}')) + .withConverter($PreferencesTableTable.$converterlocale); + static const VerificationMeta _marketMeta = const VerificationMeta('market'); + @override + late final GeneratedColumnWithTypeConverter market = + GeneratedColumn('market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)) + .withConverter($PreferencesTableTable.$convertermarket); + static const VerificationMeta _searchModeMeta = + const VerificationMeta('searchMode'); + @override + late final GeneratedColumnWithTypeConverter searchMode = + GeneratedColumn('search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)) + .withConverter( + $PreferencesTableTable.$convertersearchMode); + static const VerificationMeta _downloadLocationMeta = + const VerificationMeta('downloadLocation'); + @override + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + static const VerificationMeta _localLibraryLocationMeta = + const VerificationMeta('localLibraryLocation'); + @override + late final GeneratedColumnWithTypeConverter, String> + localLibraryLocation = GeneratedColumn( + 'local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")) + .withConverter>( + $PreferencesTableTable.$converterlocalLibraryLocation); + static const VerificationMeta _pipedInstanceMeta = + const VerificationMeta('pipedInstance'); + @override + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + static const VerificationMeta _themeModeMeta = + const VerificationMeta('themeMode'); + @override + late final GeneratedColumnWithTypeConverter themeMode = + GeneratedColumn('theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)) + .withConverter($PreferencesTableTable.$converterthemeMode); + static const VerificationMeta _audioSourceMeta = + const VerificationMeta('audioSource'); + @override + late final GeneratedColumnWithTypeConverter audioSource = + GeneratedColumn('audio_source', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(AudioSource.youtube.name)) + .withConverter( + $PreferencesTableTable.$converteraudioSource); + static const VerificationMeta _streamMusicCodecMeta = + const VerificationMeta('streamMusicCodec'); + @override + late final GeneratedColumnWithTypeConverter + streamMusicCodec = GeneratedColumn( + 'stream_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.weba.name)) + .withConverter( + $PreferencesTableTable.$converterstreamMusicCodec); + static const VerificationMeta _downloadMusicCodecMeta = + const VerificationMeta('downloadMusicCodec'); + @override + late final GeneratedColumnWithTypeConverter + downloadMusicCodec = GeneratedColumn( + 'download_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.m4a.name)) + .withConverter( + $PreferencesTableTable.$converterdownloadMusicCodec); + static const VerificationMeta _discordPresenceMeta = + const VerificationMeta('discordPresence'); + @override + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _endlessPlaybackMeta = + const VerificationMeta('endlessPlayback'); + @override + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _enableConnectMeta = + const VerificationMeta('enableConnect'); + @override + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + @override + List get $columns => [ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + context.handle(_audioQualityMeta, const VerificationResult.success()); + if (data.containsKey('album_color_sync')) { + context.handle( + _albumColorSyncMeta, + albumColorSync.isAcceptableOrUnknown( + data['album_color_sync']!, _albumColorSyncMeta)); + } + if (data.containsKey('amoled_dark_theme')) { + context.handle( + _amoledDarkThemeMeta, + amoledDarkTheme.isAcceptableOrUnknown( + data['amoled_dark_theme']!, _amoledDarkThemeMeta)); + } + if (data.containsKey('check_update')) { + context.handle( + _checkUpdateMeta, + checkUpdate.isAcceptableOrUnknown( + data['check_update']!, _checkUpdateMeta)); + } + if (data.containsKey('normalize_audio')) { + context.handle( + _normalizeAudioMeta, + normalizeAudio.isAcceptableOrUnknown( + data['normalize_audio']!, _normalizeAudioMeta)); + } + if (data.containsKey('show_system_tray_icon')) { + context.handle( + _showSystemTrayIconMeta, + showSystemTrayIcon.isAcceptableOrUnknown( + data['show_system_tray_icon']!, _showSystemTrayIconMeta)); + } + if (data.containsKey('system_title_bar')) { + context.handle( + _systemTitleBarMeta, + systemTitleBar.isAcceptableOrUnknown( + data['system_title_bar']!, _systemTitleBarMeta)); + } + if (data.containsKey('skip_non_music')) { + context.handle( + _skipNonMusicMeta, + skipNonMusic.isAcceptableOrUnknown( + data['skip_non_music']!, _skipNonMusicMeta)); + } + context.handle(_closeBehaviorMeta, const VerificationResult.success()); + context.handle(_accentColorSchemeMeta, const VerificationResult.success()); + context.handle(_layoutModeMeta, const VerificationResult.success()); + context.handle(_localeMeta, const VerificationResult.success()); + context.handle(_marketMeta, const VerificationResult.success()); + context.handle(_searchModeMeta, const VerificationResult.success()); + if (data.containsKey('download_location')) { + context.handle( + _downloadLocationMeta, + downloadLocation.isAcceptableOrUnknown( + data['download_location']!, _downloadLocationMeta)); + } + context.handle( + _localLibraryLocationMeta, const VerificationResult.success()); + if (data.containsKey('piped_instance')) { + context.handle( + _pipedInstanceMeta, + pipedInstance.isAcceptableOrUnknown( + data['piped_instance']!, _pipedInstanceMeta)); + } + context.handle(_themeModeMeta, const VerificationResult.success()); + context.handle(_audioSourceMeta, const VerificationResult.success()); + context.handle(_streamMusicCodecMeta, const VerificationResult.success()); + context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); + if (data.containsKey('discord_presence')) { + context.handle( + _discordPresenceMeta, + discordPresence.isAcceptableOrUnknown( + data['discord_presence']!, _discordPresenceMeta)); + } + if (data.containsKey('endless_playback')) { + context.handle( + _endlessPlaybackMeta, + endlessPlayback.isAcceptableOrUnknown( + data['endless_playback']!, _endlessPlaybackMeta)); + } + if (data.containsKey('enable_connect')) { + context.handle( + _enableConnectMeta, + enableConnect.isAcceptableOrUnknown( + data['enable_connect']!, _enableConnectMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioQuality: $PreferencesTableTable.$converteraudioQuality.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}audio_quality'])!), + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: $PreferencesTableTable.$convertercloseBehavior.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}close_behavior'])!), + accentColorScheme: $PreferencesTableTable.$converteraccentColorScheme + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}accent_color_scheme'])!), + layoutMode: $PreferencesTableTable.$converterlayoutMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}layout_mode'])!), + locale: $PreferencesTableTable.$converterlocale.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!), + market: $PreferencesTableTable.$convertermarket.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!), + searchMode: $PreferencesTableTable.$convertersearchMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}search_mode'])!), + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: $PreferencesTableTable + .$converterlocalLibraryLocation + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!), + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), + audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}audio_source'])!), + streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}stream_music_codec'])!), + downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}download_music_codec'])!), + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + ); + } + + @override + $PreferencesTableTable createAlias(String alias) { + return $PreferencesTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converteraudioQuality = + const EnumNameConverter(SourceQualities.values); + static JsonTypeConverter2 + $convertercloseBehavior = + const EnumNameConverter(CloseBehavior.values); + static TypeConverter $converteraccentColorScheme = + const SpotubeColorConverter(); + static JsonTypeConverter2 $converterlayoutMode = + const EnumNameConverter(LayoutMode.values); + static TypeConverter $converterlocale = + const LocaleConverter(); + static JsonTypeConverter2 $convertermarket = + const EnumNameConverter(Market.values); + static JsonTypeConverter2 $convertersearchMode = + const EnumNameConverter(SearchMode.values); + static TypeConverter, String> $converterlocalLibraryLocation = + const StringListConverter(); + static JsonTypeConverter2 $converterthemeMode = + const EnumNameConverter(ThemeMode.values); + static JsonTypeConverter2 $converteraudioSource = + const EnumNameConverter(AudioSource.values); + static JsonTypeConverter2 + $converterstreamMusicCodec = + const EnumNameConverter(SourceCodecs.values); + static JsonTypeConverter2 + $converterdownloadMusicCodec = + const EnumNameConverter(SourceCodecs.values); +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final SourceQualities audioQuality; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final CloseBehavior closeBehavior; + final SpotubeColor accentColorScheme; + final LayoutMode layoutMode; + final Locale locale; + final Market market; + final SearchMode searchMode; + final String downloadLocation; + final List localLibraryLocation; + final String pipedInstance; + final ThemeMode themeMode; + final AudioSource audioSource; + final SourceCodecs streamMusicCodec; + final SourceCodecs downloadMusicCodec; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + const PreferencesTableData( + {required this.id, + required this.audioQuality, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.themeMode, + required this.audioSource, + required this.streamMusicCodec, + required this.downloadMusicCodec, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + map['audio_quality'] = Variable( + $PreferencesTableTable.$converteraudioQuality.toSql(audioQuality)); + } + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + { + map['close_behavior'] = Variable( + $PreferencesTableTable.$convertercloseBehavior.toSql(closeBehavior)); + } + { + map['accent_color_scheme'] = Variable($PreferencesTableTable + .$converteraccentColorScheme + .toSql(accentColorScheme)); + } + { + map['layout_mode'] = Variable( + $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode)); + } + { + map['locale'] = Variable( + $PreferencesTableTable.$converterlocale.toSql(locale)); + } + { + map['market'] = Variable( + $PreferencesTableTable.$convertermarket.toSql(market)); + } + { + map['search_mode'] = Variable( + $PreferencesTableTable.$convertersearchMode.toSql(searchMode)); + } + map['download_location'] = Variable(downloadLocation); + { + map['local_library_location'] = Variable($PreferencesTableTable + .$converterlocalLibraryLocation + .toSql(localLibraryLocation)); + } + map['piped_instance'] = Variable(pipedInstance); + { + map['theme_mode'] = Variable( + $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); + } + { + map['audio_source'] = Variable( + $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); + } + { + map['stream_music_codec'] = Variable($PreferencesTableTable + .$converterstreamMusicCodec + .toSql(streamMusicCodec)); + } + { + map['download_music_codec'] = Variable($PreferencesTableTable + .$converterdownloadMusicCodec + .toSql(downloadMusicCodec)); + } + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + audioQuality: Value(audioQuality), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + themeMode: Value(themeMode), + audioSource: Value(audioSource), + streamMusicCodec: Value(streamMusicCodec), + downloadMusicCodec: Value(downloadMusicCodec), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + audioQuality: $PreferencesTableTable.$converteraudioQuality + .fromJson(serializer.fromJson(json['audioQuality'])), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: $PreferencesTableTable.$convertercloseBehavior + .fromJson(serializer.fromJson(json['closeBehavior'])), + accentColorScheme: + serializer.fromJson(json['accentColorScheme']), + layoutMode: $PreferencesTableTable.$converterlayoutMode + .fromJson(serializer.fromJson(json['layoutMode'])), + locale: serializer.fromJson(json['locale']), + market: $PreferencesTableTable.$convertermarket + .fromJson(serializer.fromJson(json['market'])), + searchMode: $PreferencesTableTable.$convertersearchMode + .fromJson(serializer.fromJson(json['searchMode'])), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson>(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + themeMode: $PreferencesTableTable.$converterthemeMode + .fromJson(serializer.fromJson(json['themeMode'])), + audioSource: $PreferencesTableTable.$converteraudioSource + .fromJson(serializer.fromJson(json['audioSource'])), + streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec + .fromJson(serializer.fromJson(json['streamMusicCodec'])), + downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec + .fromJson(serializer.fromJson(json['downloadMusicCodec'])), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioQuality': serializer.toJson( + $PreferencesTableTable.$converteraudioQuality.toJson(audioQuality)), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson( + $PreferencesTableTable.$convertercloseBehavior.toJson(closeBehavior)), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson( + $PreferencesTableTable.$converterlayoutMode.toJson(layoutMode)), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson( + $PreferencesTableTable.$convertermarket.toJson(market)), + 'searchMode': serializer.toJson( + $PreferencesTableTable.$convertersearchMode.toJson(searchMode)), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': + serializer.toJson>(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'themeMode': serializer.toJson( + $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), + 'audioSource': serializer.toJson( + $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), + 'streamMusicCodec': serializer.toJson($PreferencesTableTable + .$converterstreamMusicCodec + .toJson(streamMusicCodec)), + 'downloadMusicCodec': serializer.toJson($PreferencesTableTable + .$converterdownloadMusicCodec + .toJson(downloadMusicCodec)), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), }; } - SourceMatchTableData copyWith( + PreferencesTableData copyWith( {int? id, - String? trackId, - String? sourceId, - SourceType? sourceType, - DateTime? createdAt}) => - SourceMatchTableData( + SourceQualities? audioQuality, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + CloseBehavior? closeBehavior, + SpotubeColor? accentColorScheme, + LayoutMode? layoutMode, + Locale? locale, + Market? market, + SearchMode? searchMode, + String? downloadLocation, + List? localLibraryLocation, + String? pipedInstance, + ThemeMode? themeMode, + AudioSource? audioSource, + SourceCodecs? streamMusicCodec, + SourceCodecs? downloadMusicCodec, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect}) => + PreferencesTableData( id: id ?? this.id, - trackId: trackId ?? this.trackId, - sourceId: sourceId ?? this.sourceId, - sourceType: sourceType ?? this.sourceType, - createdAt: createdAt ?? this.createdAt, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, ); @override String toString() { - return (StringBuffer('SourceMatchTableData(') + return (StringBuffer('PreferencesTableData(') ..write('id: $id, ') - ..write('trackId: $trackId, ') - ..write('sourceId: $sourceId, ') - ..write('sourceType: $sourceType, ') - ..write('createdAt: $createdAt') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + int get hashCode => Object.hashAll([ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]); @override bool operator ==(Object other) => identical(this, other) || - (other is SourceMatchTableData && + (other is PreferencesTableData && other.id == this.id && - other.trackId == this.trackId && - other.sourceId == this.sourceId && - other.sourceType == this.sourceType && - other.createdAt == this.createdAt); + other.audioQuality == this.audioQuality && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.themeMode == this.themeMode && + other.audioSource == this.audioSource && + other.streamMusicCodec == this.streamMusicCodec && + other.downloadMusicCodec == this.downloadMusicCodec && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect); } -class SourceMatchTableCompanion extends UpdateCompanion { +class PreferencesTableCompanion extends UpdateCompanion { final Value id; - final Value trackId; - final Value sourceId; - final Value sourceType; - final Value createdAt; - const SourceMatchTableCompanion({ + final Value audioQuality; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value> localLibraryLocation; + final Value pipedInstance; + final Value themeMode; + final Value audioSource; + final Value streamMusicCodec; + final Value downloadMusicCodec; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + const PreferencesTableCompanion({ this.id = const Value.absent(), - this.trackId = const Value.absent(), - this.sourceId = const Value.absent(), - this.sourceType = const Value.absent(), - this.createdAt = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), }); - SourceMatchTableCompanion.insert({ - this.id = const Value.absent(), - required String trackId, - required String sourceId, - this.sourceType = const Value.absent(), - this.createdAt = const Value.absent(), - }) : trackId = Value(trackId), - sourceId = Value(sourceId); - static Insertable custom({ + static Insertable custom({ Expression? id, - Expression? trackId, - Expression? sourceId, - Expression? sourceType, - Expression? createdAt, + Expression? audioQuality, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? themeMode, + Expression? audioSource, + Expression? streamMusicCodec, + Expression? downloadMusicCodec, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (trackId != null) 'track_id': trackId, - if (sourceId != null) 'source_id': sourceId, - if (sourceType != null) 'source_type': sourceType, - if (createdAt != null) 'created_at': createdAt, + if (audioQuality != null) 'audio_quality': audioQuality, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSource != null) 'audio_source': audioSource, + if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, + if (downloadMusicCodec != null) + 'download_music_codec': downloadMusicCodec, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, }); } - SourceMatchTableCompanion copyWith( + PreferencesTableCompanion copyWith( {Value? id, - Value? trackId, - Value? sourceId, - Value? sourceType, - Value? createdAt}) { - return SourceMatchTableCompanion( + Value? audioQuality, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value>? localLibraryLocation, + Value? pipedInstance, + Value? themeMode, + Value? audioSource, + Value? streamMusicCodec, + Value? downloadMusicCodec, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect}) { + return PreferencesTableCompanion( id: id ?? this.id, - trackId: trackId ?? this.trackId, - sourceId: sourceId ?? this.sourceId, - sourceType: sourceType ?? this.sourceType, - createdAt: createdAt ?? this.createdAt, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, ); } @@ -1473,30 +1604,128 @@ class SourceMatchTableCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (trackId.present) { - map['track_id'] = Variable(trackId.value); + if (audioQuality.present) { + map['audio_quality'] = Variable($PreferencesTableTable + .$converteraudioQuality + .toSql(audioQuality.value)); } - if (sourceId.present) { - map['source_id'] = Variable(sourceId.value); + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); } - if (sourceType.present) { - map['source_type'] = Variable( - $SourceMatchTableTable.$convertersourceType.toSql(sourceType.value)); + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable($PreferencesTableTable + .$convertercloseBehavior + .toSql(closeBehavior.value)); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable($PreferencesTableTable + .$converteraccentColorScheme + .toSql(accentColorScheme.value)); + } + if (layoutMode.present) { + map['layout_mode'] = Variable( + $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode.value)); + } + if (locale.present) { + map['locale'] = Variable( + $PreferencesTableTable.$converterlocale.toSql(locale.value)); + } + if (market.present) { + map['market'] = Variable( + $PreferencesTableTable.$convertermarket.toSql(market.value)); + } + if (searchMode.present) { + map['search_mode'] = Variable( + $PreferencesTableTable.$convertersearchMode.toSql(searchMode.value)); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = Variable($PreferencesTableTable + .$converterlocalLibraryLocation + .toSql(localLibraryLocation.value)); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable( + $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); + } + if (audioSource.present) { + map['audio_source'] = Variable($PreferencesTableTable + .$converteraudioSource + .toSql(audioSource.value)); + } + if (streamMusicCodec.present) { + map['stream_music_codec'] = Variable($PreferencesTableTable + .$converterstreamMusicCodec + .toSql(streamMusicCodec.value)); + } + if (downloadMusicCodec.present) { + map['download_music_codec'] = Variable($PreferencesTableTable + .$converterdownloadMusicCodec + .toSql(downloadMusicCodec.value)); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); } return map; } @override String toString() { - return (StringBuffer('SourceMatchTableCompanion(') + return (StringBuffer('PreferencesTableCompanion(') ..write('id: $id, ') - ..write('trackId: $trackId, ') - ..write('sourceId: $sourceId, ') - ..write('sourceType: $sourceType, ') - ..write('createdAt: $createdAt') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect') ..write(')')) .toString(); } @@ -1789,12 +2018,12 @@ class SkipSegmentTableCompanion extends UpdateCompanion { } } -class $BlacklistTableTable extends BlacklistTable - with TableInfo<$BlacklistTableTable, BlacklistTableData> { +class $SourceMatchTableTable extends SourceMatchTable + with TableInfo<$SourceMatchTableTable, SourceMatchTableData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $BlacklistTableTable(this.attachedDatabase, [this._alias]); + $SourceMatchTableTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -1804,52 +2033,69 @@ class $BlacklistTableTable extends BlacklistTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _nameMeta = const VerificationMeta('name'); + static const VerificationMeta _trackIdMeta = + const VerificationMeta('trackId'); @override - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _elementTypeMeta = - const VerificationMeta('elementType'); - @override - late final GeneratedColumnWithTypeConverter - elementType = GeneratedColumn('element_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter( - $BlacklistTableTable.$converterelementType); - static const VerificationMeta _elementIdMeta = - const VerificationMeta('elementId'); + static const VerificationMeta _sourceIdMeta = + const VerificationMeta('sourceId'); @override - late final GeneratedColumn elementId = GeneratedColumn( - 'element_id', aliasedName, false, + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _sourceTypeMeta = + const VerificationMeta('sourceType'); @override - List get $columns => [id, name, elementType, elementId]; + late final GeneratedColumnWithTypeConverter sourceType = + GeneratedColumn('source_type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceType.youtube.name)) + .withConverter( + $SourceMatchTableTable.$convertersourceType); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceId, sourceType, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'blacklist_table'; + static const String $name = 'source_match_table'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity( + Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } - if (data.containsKey('name')) { - context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + if (data.containsKey('track_id')) { + context.handle(_trackIdMeta, + trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); } else if (isInserting) { - context.missing(_nameMeta); + context.missing(_trackIdMeta); } - context.handle(_elementTypeMeta, const VerificationResult.success()); - if (data.containsKey('element_id')) { - context.handle(_elementIdMeta, - elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); + if (data.containsKey('source_id')) { + context.handle(_sourceIdMeta, + sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta)); } else if (isInserting) { - context.missing(_elementIdMeta); + context.missing(_sourceIdMeta); + } + context.handle(_sourceTypeMeta, const VerificationResult.success()); + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } return context; } @@ -1857,224 +2103,499 @@ class $BlacklistTableTable extends BlacklistTable @override Set get $primaryKey => {id}; @override - BlacklistTableData map(Map data, {String? tablePrefix}) { + SourceMatchTableData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return BlacklistTableData( + return SourceMatchTableData( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - elementType: $BlacklistTableTable.$converterelementType.fromSql( + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, + sourceType: $SourceMatchTableTable.$convertersourceType.fromSql( attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}element_type'])!), - elementId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + DriftSqlType.string, data['${effectivePrefix}source_type'])!), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, ); } @override - $BlacklistTableTable createAlias(String alias) { - return $BlacklistTableTable(attachedDatabase, alias); + $SourceMatchTableTable createAlias(String alias) { + return $SourceMatchTableTable(attachedDatabase, alias); } - static JsonTypeConverter2 - $converterelementType = - const EnumNameConverter(BlacklistedType.values); + static JsonTypeConverter2 $convertersourceType = + const EnumNameConverter(SourceType.values); } -class BlacklistTableData extends DataClass - implements Insertable { +class SourceMatchTableData extends DataClass + implements Insertable { final int id; - final String name; - final BlacklistedType elementType; - final String elementId; - const BlacklistTableData( + final String trackId; + final String sourceId; + final SourceType sourceType; + final DateTime createdAt; + const SourceMatchTableData( {required this.id, - required this.name, - required this.elementType, - required this.elementId}); + required this.trackId, + required this.sourceId, + required this.sourceType, + required this.createdAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - map['name'] = Variable(name); + map['track_id'] = Variable(trackId); + map['source_id'] = Variable(sourceId); { - map['element_type'] = Variable( - $BlacklistTableTable.$converterelementType.toSql(elementType)); + map['source_type'] = Variable( + $SourceMatchTableTable.$convertersourceType.toSql(sourceType)); } - map['element_id'] = Variable(elementId); + map['created_at'] = Variable(createdAt); return map; } - BlacklistTableCompanion toCompanion(bool nullToAbsent) { - return BlacklistTableCompanion( + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( id: Value(id), - name: Value(name), - elementType: Value(elementType), - elementId: Value(elementId), + trackId: Value(trackId), + sourceId: Value(sourceId), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceId: serializer.fromJson(json['sourceId']), + sourceType: $SourceMatchTableTable.$convertersourceType + .fromJson(serializer.fromJson(json['sourceType'])), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceId': serializer.toJson(sourceId), + 'sourceType': serializer.toJson( + $SourceMatchTableTable.$convertersourceType.toJson(sourceType)), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceId, + SourceType? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceId == this.sourceId && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceId; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceId = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceId, + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceId = Value(sourceId); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceId, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceId != null) 'source_id': sourceId, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceId, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, ); } - factory BlacklistTableData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return BlacklistTableData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - elementType: $BlacklistTableTable.$converterelementType - .fromJson(serializer.fromJson(json['elementType'])), - elementId: serializer.fromJson(json['elementId']), - ); - } @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'elementType': serializer.toJson( - $BlacklistTableTable.$converterelementType.toJson(elementType)), - 'elementId': serializer.toJson(elementId), - }; + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (sourceType.present) { + map['source_type'] = Variable( + $SourceMatchTableTable.$convertersourceType.toSql(sourceType.value)); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; } - BlacklistTableData copyWith( - {int? id, - String? name, - BlacklistedType? elementType, - String? elementId}) => - BlacklistTableData( - id: id ?? this.id, - name: name ?? this.name, - elementType: elementType ?? this.elementType, - elementId: elementId ?? this.elementId, - ); @override String toString() { - return (StringBuffer('BlacklistTableData(') + return (StringBuffer('SourceMatchTableCompanion(') ..write('id: $id, ') - ..write('name: $name, ') - ..write('elementType: $elementType, ') - ..write('elementId: $elementId') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') ..write(')')) .toString(); } +} +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + _$AppDatabaseManager get managers => _$AppDatabaseManager(this); + late final $AuthenticationTableTable authenticationTable = + $AuthenticationTableTable(this); + late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); + late final $PreferencesTableTable preferencesTable = + $PreferencesTableTable(this); + late final $SkipSegmentTableTable skipSegmentTable = + $SkipSegmentTableTable(this); + late final $SourceMatchTableTable sourceMatchTable = + $SourceMatchTableTable(this); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); @override - int get hashCode => Object.hash(id, name, elementType, elementId); + Iterable> get allTables => + allSchemaEntities.whereType>(); @override - bool operator ==(Object other) => - identical(this, other) || - (other is BlacklistTableData && - other.id == this.id && - other.name == this.name && - other.elementType == this.elementType && - other.elementId == this.elementId); + List get allSchemaEntities => [ + authenticationTable, + blacklistTable, + preferencesTable, + skipSegmentTable, + sourceMatchTable, + uniqueBlacklist, + uniqTrackMatch + ]; } -class BlacklistTableCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value elementType; - final Value elementId; - const BlacklistTableCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.elementType = const Value.absent(), - this.elementId = const Value.absent(), - }); - BlacklistTableCompanion.insert({ - this.id = const Value.absent(), - required String name, - required BlacklistedType elementType, - required String elementId, - }) : name = Value(name), - elementType = Value(elementType), - elementId = Value(elementId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? elementType, - Expression? elementId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (elementType != null) 'element_type': elementType, - if (elementId != null) 'element_id': elementId, - }); - } +typedef $$AuthenticationTableTableInsertCompanionBuilder + = AuthenticationTableCompanion Function({ + Value id, + required DecryptedText cookie, + required DecryptedText accessToken, + required DateTime expiration, +}); +typedef $$AuthenticationTableTableUpdateCompanionBuilder + = AuthenticationTableCompanion Function({ + Value id, + Value cookie, + Value accessToken, + Value expiration, +}); + +class $$AuthenticationTableTableTableManager extends RootTableManager< + _$AppDatabase, + $AuthenticationTableTable, + AuthenticationTableData, + $$AuthenticationTableTableFilterComposer, + $$AuthenticationTableTableOrderingComposer, + $$AuthenticationTableTableProcessedTableManager, + $$AuthenticationTableTableInsertCompanionBuilder, + $$AuthenticationTableTableUpdateCompanionBuilder> { + $$AuthenticationTableTableTableManager( + _$AppDatabase db, $AuthenticationTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: $$AuthenticationTableTableFilterComposer( + ComposerState(db, table)), + orderingComposer: $$AuthenticationTableTableOrderingComposer( + ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$AuthenticationTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value cookie = const Value.absent(), + Value accessToken = const Value.absent(), + Value expiration = const Value.absent(), + }) => + AuthenticationTableCompanion( + id: id, + cookie: cookie, + accessToken: accessToken, + expiration: expiration, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required DecryptedText cookie, + required DecryptedText accessToken, + required DateTime expiration, + }) => + AuthenticationTableCompanion.insert( + id: id, + cookie: cookie, + accessToken: accessToken, + expiration: expiration, + ), + )); +} + +class $$AuthenticationTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $AuthenticationTableTable, + AuthenticationTableData, + $$AuthenticationTableTableFilterComposer, + $$AuthenticationTableTableOrderingComposer, + $$AuthenticationTableTableProcessedTableManager, + $$AuthenticationTableTableInsertCompanionBuilder, + $$AuthenticationTableTableUpdateCompanionBuilder> { + $$AuthenticationTableTableProcessedTableManager(super.$state); +} + +class $$AuthenticationTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $AuthenticationTableTable> { + $$AuthenticationTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get cookie => $state.composableBuilder( + column: $state.table.cookie, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get accessToken => $state.composableBuilder( + column: $state.table.accessToken, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get expiration => $state.composableBuilder( + column: $state.table.expiration, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$AuthenticationTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $AuthenticationTableTable> { + $$AuthenticationTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get cookie => $state.composableBuilder( + column: $state.table.cookie, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get accessToken => $state.composableBuilder( + column: $state.table.accessToken, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get expiration => $state.composableBuilder( + column: $state.table.expiration, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + required String name, + required BlacklistedType elementType, + required String elementId, +}); +typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + Value name, + Value elementType, + Value elementId, +}); + +class $$BlacklistTableTableTableManager extends RootTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableTableManager( + _$AppDatabase db, $BlacklistTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$BlacklistTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$BlacklistTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$BlacklistTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value elementType = const Value.absent(), + Value elementId = const Value.absent(), + }) => + BlacklistTableCompanion( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) => + BlacklistTableCompanion.insert( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + )); +} + +class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableProcessedTableManager(super.$state); +} - BlacklistTableCompanion copyWith( - {Value? id, - Value? name, - Value? elementType, - Value? elementId}) { - return BlacklistTableCompanion( - id: id ?? this.id, - name: name ?? this.name, - elementType: elementType ?? this.elementType, - elementId: elementId ?? this.elementId, - ); - } +class $$BlacklistTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (elementType.present) { - map['element_type'] = Variable( - $BlacklistTableTable.$converterelementType.toSql(elementType.value)); - } - if (elementId.present) { - map['element_id'] = Variable(elementId.value); - } - return map; - } + ColumnFilters get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); - @override - String toString() { - return (StringBuffer('BlacklistTableCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('elementType: $elementType, ') - ..write('elementId: $elementId') - ..write(')')) - .toString(); - } + ColumnWithTypeConverterFilters + get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); } -abstract class _$AppDatabase extends GeneratedDatabase { - _$AppDatabase(QueryExecutor e) : super(e); - _$AppDatabaseManager get managers => _$AppDatabaseManager(this); - late final $PreferencesTableTable preferencesTable = - $PreferencesTableTable(this); - late final $SourceMatchTableTable sourceMatchTable = - $SourceMatchTableTable(this); - late final $SkipSegmentTableTable skipSegmentTable = - $SkipSegmentTableTable(this); - late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); - late final Index uniqTrackMatch = Index('uniq_track_match', - 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); - late final Index uniqueBlacklist = Index('unique_blacklist', - 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [ - preferencesTable, - sourceMatchTable, - skipSegmentTable, - blacklistTable, - uniqTrackMatch, - uniqueBlacklist - ]; +class $$BlacklistTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); } typedef $$PreferencesTableTableInsertCompanionBuilder @@ -2481,222 +3002,81 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings get closeBehavior => $state.composableBuilder( column: $state.table.closeBehavior, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get accentColorScheme => $state.composableBuilder( - column: $state.table.accentColorScheme, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get layoutMode => $state.composableBuilder( - column: $state.table.layoutMode, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get locale => $state.composableBuilder( - column: $state.table.locale, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get market => $state.composableBuilder( - column: $state.table.market, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get searchMode => $state.composableBuilder( - column: $state.table.searchMode, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get downloadLocation => $state.composableBuilder( - column: $state.table.downloadLocation, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get localLibraryLocation => $state.composableBuilder( - column: $state.table.localLibraryLocation, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get pipedInstance => $state.composableBuilder( - column: $state.table.pipedInstance, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get themeMode => $state.composableBuilder( - column: $state.table.themeMode, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get audioSource => $state.composableBuilder( - column: $state.table.audioSource, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get streamMusicCodec => $state.composableBuilder( - column: $state.table.streamMusicCodec, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get downloadMusicCodec => $state.composableBuilder( - column: $state.table.downloadMusicCodec, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get discordPresence => $state.composableBuilder( - column: $state.table.discordPresence, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get endlessPlayback => $state.composableBuilder( - column: $state.table.endlessPlayback, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get enableConnect => $state.composableBuilder( - column: $state.table.enableConnect, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); -} - -typedef $$SourceMatchTableTableInsertCompanionBuilder - = SourceMatchTableCompanion Function({ - Value id, - required String trackId, - required String sourceId, - Value sourceType, - Value createdAt, -}); -typedef $$SourceMatchTableTableUpdateCompanionBuilder - = SourceMatchTableCompanion Function({ - Value id, - Value trackId, - Value sourceId, - Value sourceType, - Value createdAt, -}); - -class $$SourceMatchTableTableTableManager extends RootTableManager< - _$AppDatabase, - $SourceMatchTableTable, - SourceMatchTableData, - $$SourceMatchTableTableFilterComposer, - $$SourceMatchTableTableOrderingComposer, - $$SourceMatchTableTableProcessedTableManager, - $$SourceMatchTableTableInsertCompanionBuilder, - $$SourceMatchTableTableUpdateCompanionBuilder> { - $$SourceMatchTableTableTableManager( - _$AppDatabase db, $SourceMatchTableTable table) - : super(TableManagerState( - db: db, - table: table, - filteringComposer: - $$SourceMatchTableTableFilterComposer(ComposerState(db, table)), - orderingComposer: - $$SourceMatchTableTableOrderingComposer(ComposerState(db, table)), - getChildManagerBuilder: (p) => - $$SourceMatchTableTableProcessedTableManager(p), - getUpdateCompanionBuilder: ({ - Value id = const Value.absent(), - Value trackId = const Value.absent(), - Value sourceId = const Value.absent(), - Value sourceType = const Value.absent(), - Value createdAt = const Value.absent(), - }) => - SourceMatchTableCompanion( - id: id, - trackId: trackId, - sourceId: sourceId, - sourceType: sourceType, - createdAt: createdAt, - ), - getInsertCompanionBuilder: ({ - Value id = const Value.absent(), - required String trackId, - required String sourceId, - Value sourceType = const Value.absent(), - Value createdAt = const Value.absent(), - }) => - SourceMatchTableCompanion.insert( - id: id, - trackId: trackId, - sourceId: sourceId, - sourceType: sourceType, - createdAt: createdAt, - ), - )); -} + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); -class $$SourceMatchTableTableProcessedTableManager - extends ProcessedTableManager< - _$AppDatabase, - $SourceMatchTableTable, - SourceMatchTableData, - $$SourceMatchTableTableFilterComposer, - $$SourceMatchTableTableOrderingComposer, - $$SourceMatchTableTableProcessedTableManager, - $$SourceMatchTableTableInsertCompanionBuilder, - $$SourceMatchTableTableUpdateCompanionBuilder> { - $$SourceMatchTableTableProcessedTableManager(super.$state); -} + ColumnOrderings get accentColorScheme => $state.composableBuilder( + column: $state.table.accentColorScheme, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); -class $$SourceMatchTableTableFilterComposer - extends FilterComposer<_$AppDatabase, $SourceMatchTableTable> { - $$SourceMatchTableTableFilterComposer(super.$state); - ColumnFilters get id => $state.composableBuilder( - column: $state.table.id, + ColumnOrderings get layoutMode => $state.composableBuilder( + column: $state.table.layoutMode, builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnFilters get trackId => $state.composableBuilder( - column: $state.table.trackId, + ColumnOrderings get locale => $state.composableBuilder( + column: $state.table.locale, builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnFilters get sourceId => $state.composableBuilder( - column: $state.table.sourceId, + ColumnOrderings get market => $state.composableBuilder( + column: $state.table.market, builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnWithTypeConverterFilters - get sourceType => $state.composableBuilder( - column: $state.table.sourceType, - builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( - column, - joinBuilders: joinBuilders)); + ColumnOrderings get searchMode => $state.composableBuilder( + column: $state.table.searchMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnFilters get createdAt => $state.composableBuilder( - column: $state.table.createdAt, + ColumnOrderings get downloadLocation => $state.composableBuilder( + column: $state.table.downloadLocation, builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); -} + ColumnOrderings(column, joinBuilders: joinBuilders)); -class $$SourceMatchTableTableOrderingComposer - extends OrderingComposer<_$AppDatabase, $SourceMatchTableTable> { - $$SourceMatchTableTableOrderingComposer(super.$state); - ColumnOrderings get id => $state.composableBuilder( - column: $state.table.id, + ColumnOrderings get localLibraryLocation => $state.composableBuilder( + column: $state.table.localLibraryLocation, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get trackId => $state.composableBuilder( - column: $state.table.trackId, + ColumnOrderings get pipedInstance => $state.composableBuilder( + column: $state.table.pipedInstance, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get sourceId => $state.composableBuilder( - column: $state.table.sourceId, + ColumnOrderings get themeMode => $state.composableBuilder( + column: $state.table.themeMode, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get sourceType => $state.composableBuilder( - column: $state.table.sourceType, + ColumnOrderings get audioSource => $state.composableBuilder( + column: $state.table.audioSource, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get createdAt => $state.composableBuilder( - column: $state.table.createdAt, + ColumnOrderings get streamMusicCodec => $state.composableBuilder( + column: $state.table.streamMusicCodec, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get downloadMusicCodec => $state.composableBuilder( + column: $state.table.downloadMusicCodec, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get discordPresence => $state.composableBuilder( + column: $state.table.discordPresence, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get endlessPlayback => $state.composableBuilder( + column: $state.table.endlessPlayback, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get enableConnect => $state.composableBuilder( + column: $state.table.enableConnect, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); } @@ -2840,126 +3220,143 @@ class $$SkipSegmentTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } -typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion - Function({ +typedef $$SourceMatchTableTableInsertCompanionBuilder + = SourceMatchTableCompanion Function({ Value id, - required String name, - required BlacklistedType elementType, - required String elementId, + required String trackId, + required String sourceId, + Value sourceType, + Value createdAt, }); -typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion - Function({ +typedef $$SourceMatchTableTableUpdateCompanionBuilder + = SourceMatchTableCompanion Function({ Value id, - Value name, - Value elementType, - Value elementId, + Value trackId, + Value sourceId, + Value sourceType, + Value createdAt, }); -class $$BlacklistTableTableTableManager extends RootTableManager< +class $$SourceMatchTableTableTableManager extends RootTableManager< _$AppDatabase, - $BlacklistTableTable, - BlacklistTableData, - $$BlacklistTableTableFilterComposer, - $$BlacklistTableTableOrderingComposer, - $$BlacklistTableTableProcessedTableManager, - $$BlacklistTableTableInsertCompanionBuilder, - $$BlacklistTableTableUpdateCompanionBuilder> { - $$BlacklistTableTableTableManager( - _$AppDatabase db, $BlacklistTableTable table) + $SourceMatchTableTable, + SourceMatchTableData, + $$SourceMatchTableTableFilterComposer, + $$SourceMatchTableTableOrderingComposer, + $$SourceMatchTableTableProcessedTableManager, + $$SourceMatchTableTableInsertCompanionBuilder, + $$SourceMatchTableTableUpdateCompanionBuilder> { + $$SourceMatchTableTableTableManager( + _$AppDatabase db, $SourceMatchTableTable table) : super(TableManagerState( db: db, table: table, filteringComposer: - $$BlacklistTableTableFilterComposer(ComposerState(db, table)), + $$SourceMatchTableTableFilterComposer(ComposerState(db, table)), orderingComposer: - $$BlacklistTableTableOrderingComposer(ComposerState(db, table)), + $$SourceMatchTableTableOrderingComposer(ComposerState(db, table)), getChildManagerBuilder: (p) => - $$BlacklistTableTableProcessedTableManager(p), + $$SourceMatchTableTableProcessedTableManager(p), getUpdateCompanionBuilder: ({ Value id = const Value.absent(), - Value name = const Value.absent(), - Value elementType = const Value.absent(), - Value elementId = const Value.absent(), + Value trackId = const Value.absent(), + Value sourceId = const Value.absent(), + Value sourceType = const Value.absent(), + Value createdAt = const Value.absent(), }) => - BlacklistTableCompanion( + SourceMatchTableCompanion( id: id, - name: name, - elementType: elementType, - elementId: elementId, + trackId: trackId, + sourceId: sourceId, + sourceType: sourceType, + createdAt: createdAt, ), getInsertCompanionBuilder: ({ Value id = const Value.absent(), - required String name, - required BlacklistedType elementType, - required String elementId, + required String trackId, + required String sourceId, + Value sourceType = const Value.absent(), + Value createdAt = const Value.absent(), }) => - BlacklistTableCompanion.insert( + SourceMatchTableCompanion.insert( id: id, - name: name, - elementType: elementType, - elementId: elementId, + trackId: trackId, + sourceId: sourceId, + sourceType: sourceType, + createdAt: createdAt, ), )); } -class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager< - _$AppDatabase, - $BlacklistTableTable, - BlacklistTableData, - $$BlacklistTableTableFilterComposer, - $$BlacklistTableTableOrderingComposer, - $$BlacklistTableTableProcessedTableManager, - $$BlacklistTableTableInsertCompanionBuilder, - $$BlacklistTableTableUpdateCompanionBuilder> { - $$BlacklistTableTableProcessedTableManager(super.$state); +class $$SourceMatchTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $SourceMatchTableTable, + SourceMatchTableData, + $$SourceMatchTableTableFilterComposer, + $$SourceMatchTableTableOrderingComposer, + $$SourceMatchTableTableProcessedTableManager, + $$SourceMatchTableTableInsertCompanionBuilder, + $$SourceMatchTableTableUpdateCompanionBuilder> { + $$SourceMatchTableTableProcessedTableManager(super.$state); } -class $$BlacklistTableTableFilterComposer - extends FilterComposer<_$AppDatabase, $BlacklistTableTable> { - $$BlacklistTableTableFilterComposer(super.$state); +class $$SourceMatchTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $SourceMatchTableTable> { + $$SourceMatchTableTableFilterComposer(super.$state); ColumnFilters get id => $state.composableBuilder( column: $state.table.id, builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnFilters get name => $state.composableBuilder( - column: $state.table.name, + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnWithTypeConverterFilters - get elementType => $state.composableBuilder( - column: $state.table.elementType, + ColumnFilters get sourceId => $state.composableBuilder( + column: $state.table.sourceId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get sourceType => $state.composableBuilder( + column: $state.table.sourceType, builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( column, joinBuilders: joinBuilders)); - ColumnFilters get elementId => $state.composableBuilder( - column: $state.table.elementId, + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); } -class $$BlacklistTableTableOrderingComposer - extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> { - $$BlacklistTableTableOrderingComposer(super.$state); +class $$SourceMatchTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $SourceMatchTableTable> { + $$SourceMatchTableTableOrderingComposer(super.$state); ColumnOrderings get id => $state.composableBuilder( column: $state.table.id, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get name => $state.composableBuilder( - column: $state.table.name, + ColumnOrderings get trackId => $state.composableBuilder( + column: $state.table.trackId, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get elementType => $state.composableBuilder( - column: $state.table.elementType, + ColumnOrderings get sourceId => $state.composableBuilder( + column: $state.table.sourceId, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get elementId => $state.composableBuilder( - column: $state.table.elementId, + ColumnOrderings get sourceType => $state.composableBuilder( + column: $state.table.sourceType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); } @@ -2967,12 +3364,14 @@ class $$BlacklistTableTableOrderingComposer class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); + $$AuthenticationTableTableTableManager get authenticationTable => + $$AuthenticationTableTableTableManager(_db, _db.authenticationTable); + $$BlacklistTableTableTableManager get blacklistTable => + $$BlacklistTableTableTableManager(_db, _db.blacklistTable); $$PreferencesTableTableTableManager get preferencesTable => $$PreferencesTableTableTableManager(_db, _db.preferencesTable); - $$SourceMatchTableTableTableManager get sourceMatchTable => - $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); $$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); - $$BlacklistTableTableTableManager get blacklistTable => - $$BlacklistTableTableTableManager(_db, _db.blacklistTable); + $$SourceMatchTableTableTableManager get sourceMatchTable => + $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); } diff --git a/lib/models/database/tables/authentication.dart b/lib/models/database/tables/authentication.dart new file mode 100644 index 000000000..960419527 --- /dev/null +++ b/lib/models/database/tables/authentication.dart @@ -0,0 +1,8 @@ +part of '../database.dart'; + +class AuthenticationTable extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get cookie => text().map(EncryptedTextConverter())(); + TextColumn get accessToken => text().map(EncryptedTextConverter())(); + DateTimeColumn get expiration => dateTime()(); +} diff --git a/lib/models/database/typeconverters/encrypted_text.dart b/lib/models/database/typeconverters/encrypted_text.dart index 27921788c..6afa82106 100644 --- a/lib/models/database/typeconverters/encrypted_text.dart +++ b/lib/models/database/typeconverters/encrypted_text.dart @@ -22,6 +22,11 @@ class DecryptedText { } String encrypt() { + _encrypter ??= Encrypter( + Salsa20( + Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync), + ), + ); return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64; } } diff --git a/lib/models/database/typeconverters/string_list.dart b/lib/models/database/typeconverters/string_list.dart index 5c30a9971..466ae4c4e 100644 --- a/lib/models/database/typeconverters/string_list.dart +++ b/lib/models/database/typeconverters/string_list.dart @@ -5,7 +5,7 @@ class StringListConverter extends TypeConverter, String> { @override List fromSql(String fromDb) { - return fromDb.split(","); + return fromDb.split(",").where((e) => e.isNotEmpty).toList(); } @override diff --git a/lib/modules/desktop_login/login_form.dart b/lib/modules/desktop_login/login_form.dart index 6091829c0..e5d31215d 100644 --- a/lib/modules/desktop_login/login_form.dart +++ b/lib/modules/desktop_login/login_form.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; class TokenLoginForm extends HookConsumerWidget { final void Function()? onDone; @@ -52,10 +52,7 @@ class TokenLoginForm extends HookConsumerWidget { final cookieHeader = "sp_dc=${directCodeController.text.trim()}"; - authenticationNotifier.setCredentials( - await AuthenticationCredentials.fromCookie( - cookieHeader), - ); + await authenticationNotifier.login(cookieHeader); if (context.mounted) { onDone?.call(); } diff --git a/lib/modules/home/sections/friends.dart b/lib/modules/home/sections/friends.dart index 85325f5a2..d6bed6a8b 100644 --- a/lib/modules/home/sections/friends.dart +++ b/lib/modules/home/sections/friends.dart @@ -8,7 +8,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/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/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomePageFriendsSection extends HookConsumerWidget { @@ -59,7 +59,7 @@ class HomePageFriendsSection extends HookConsumerWidget { if (friendsQuery.isLoading || friendsQuery.asData?.value.friends.isEmpty == true || - auth == null) { + auth.asData?.value == null) { return const SliverToBoxAdapter( child: SizedBox.shrink(), ); diff --git a/lib/modules/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart index 08b28138f..e2b327414 100644 --- a/lib/modules/home/sections/new_releases.dart +++ b/lib/modules/home/sections/new_releases.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomeNewReleasesSection extends HookConsumerWidget { @@ -18,7 +18,7 @@ class HomeNewReleasesSection extends HookConsumerWidget { final albums = ref.watch(userArtistAlbumReleasesProvider); - if (auth == null || + if (auth.asData?.value == null || newReleases.isLoading || newReleases.asData?.value.items.isEmpty == true) { return const SizedBox.shrink(); diff --git a/lib/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index 9408d0080..a5831fc21 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/library/local_folder/local_folder_item.dart @@ -46,7 +46,7 @@ class LocalFolderItem extends HookConsumerWidget { ...pathSegments.skip(pathSegments.length - 3).toList() ..removeLast(), ] - : pathSegments.take(pathSegments.length - 1).toList(); + : pathSegments.take(max(pathSegments.length - 1, 0)).toList(); final trackSnapshot = ref.watch( localTracksProvider.select( diff --git a/lib/modules/library/user_albums.dart b/lib/modules/library/user_albums.dart index 71e5b65a0..c2c91293e 100644 --- a/lib/modules/library/user_albums.dart +++ b/lib/modules/library/user_albums.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class UserAlbums extends HookConsumerWidget { @@ -46,7 +46,7 @@ class UserAlbums extends HookConsumerWidget { []; }, [albumsQuery.asData?.value, searchText.value]); - if (auth == null) { + if (auth.asData?.value == null) { return const AnonymousFallback(); } diff --git a/lib/modules/library/user_artists.dart b/lib/modules/library/user_artists.dart index dbdd8682c..dd097080a 100644 --- a/lib/modules/library/user_artists.dart +++ b/lib/modules/library/user_artists.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class UserArtists extends HookConsumerWidget { @@ -48,7 +48,7 @@ class UserArtists extends HookConsumerWidget { final controller = useScrollController(); - if (auth == null) { + if (auth.asData?.value == null) { return const AnonymousFallback(); } diff --git a/lib/modules/library/user_playlists.dart b/lib/modules/library/user_playlists.dart index e0c501bb3..577f96559 100644 --- a/lib/modules/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -17,7 +17,7 @@ import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -75,7 +75,7 @@ class UserPlaylists extends HookConsumerWidget { final controller = useScrollController(); - if (auth == null) { + if (auth.asData?.value == null) { return const AnonymousFallback(); } diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 6a8a3e52d..663447920 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -24,7 +24,7 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/volume_provider.dart'; diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 41de7388e..8fd434ade 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -13,7 +13,7 @@ import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/sleep_timer_provider.dart'; diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index 147841761..a77ab6fe6 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -18,7 +18,7 @@ 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'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index 592a3d905..ef7357985 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -20,7 +20,7 @@ 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/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -269,7 +269,7 @@ class SidebarFooter extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (auth != null && data == null) + if (auth.asData?.value != null && data == null) const CircularProgressIndicator() else if (data != null) Flexible( diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index a30535dd7..7d7fa8efc 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -11,7 +11,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/models/database/database.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/primitive_utils.dart'; diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index d78143e49..ec62543cc 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/links/hyper_link.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/home/home.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/service_utils.dart'; class LoginTutorial extends ConsumerWidget { @@ -18,8 +18,7 @@ class LoginTutorial extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - ref.watch(authenticationProvider); - final authenticationNotifier = ref.watch(authenticationProvider.notifier); + final auth = ref.watch(authenticationProvider); final key = GlobalKey>(); final theme = Theme.of(context); @@ -53,7 +52,7 @@ class LoginTutorial extends ConsumerWidget { ), showBackButton: true, overrideDone: FilledButton( - onPressed: authenticationNotifier.isLoggedIn + onPressed: auth.asData?.value != null ? () { ServiceUtils.pushNamed(context, HomePage.name); } @@ -91,7 +90,7 @@ class LoginTutorial extends ConsumerWidget { bodyWidget: Text(context.l10n.step_3_steps, textAlign: TextAlign.left), ), - if (authenticationNotifier.isLoggedIn) + if (auth.asData?.value != null) PageViewModel( decoration: pageDecoration.copyWith( bodyAlignment: Alignment.center, diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index f75c715c7..c484046ba 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -17,7 +17,7 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -84,7 +84,7 @@ class LyricsPage extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); - if (auth == null) { + if (auth.asData?.value == null) { return Scaffold( appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null, body: const AnonymousFallback(), diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index 603f90d30..f96595387 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -14,7 +14,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -48,7 +48,7 @@ class MiniLyricsPage extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); - if (auth == null) { + if (auth.asData?.value == null) { return const Scaffold( appBar: PageWindowTitleBar(), body: AnonymousFallback(), diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index 1f2df95a0..290c2b2f9 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -3,7 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/platform.dart'; class WebViewLogin extends HookConsumerWidget { @@ -53,9 +53,7 @@ class WebViewLogin extends HookConsumerWidget { final cookieHeader = "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; - authenticationNotifier.setCredentials( - await AuthenticationCredentials.fromCookie(cookieHeader), - ); + await authenticationNotifier.login(cookieHeader); if (context.mounted) { // ignore: use_build_context_synchronously GoRouter.of(context).go("/"); diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 4f53f8e6a..e28a5eff8 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -20,7 +20,7 @@ import 'package:spotube/pages/search/sections/albums.dart'; import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/sections/tracks.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; @@ -37,8 +37,7 @@ class SearchPage extends HookConsumerWidget { final searchTerm = ref.watch(searchTermStateProvider); final controller = useSearchController(); - ref.watch(authenticationProvider); - final authenticationNotifier = ref.watch(authenticationProvider.notifier); + final auth = ref.watch(authenticationProvider); final mediaQuery = MediaQuery.of(context); final searchTrack = ref.watch(searchProvider(SearchType.track)); @@ -91,7 +90,7 @@ class SearchPage extends HookConsumerWidget { appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar(automaticallyImplyLeading: true) : null, - body: !authenticationNotifier.isLoggedIn + body: auth.asData?.value == null ? const AnonymousFallback() : Column( children: [ diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index a007fbebc..1604f14bd 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -9,7 +9,7 @@ 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/authentication/authentication.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -35,7 +35,7 @@ class SettingsAccountSection extends HookConsumerWidget { return SectionCardWithHeading( heading: context.l10n.account, children: [ - if (auth != null) + if (auth.asData?.value != null) ListTile( leading: const Icon(SpotubeIcons.user), title: const Text("User Profile"), @@ -53,7 +53,7 @@ class SettingsAccountSection extends HookConsumerWidget { ServiceUtils.pushNamed(context, ProfilePage.name); }, ), - if (auth == null) + if (auth.asData?.value == null) LayoutBuilder(builder: (context, constrains) { return ListTile( leading: Icon( diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication/authentication.dart similarity index 51% rename from lib/provider/authentication_provider.dart rename to lib/provider/authentication/authentication.dart index 52c7f281a..3ea8693b2 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication/authentication.dart @@ -4,22 +4,30 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart' hide X509Certificate; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/utils/platform.dart'; -class AuthenticationCredentials { - String cookie; - String accessToken; - DateTime expiration; - +extension ExpirationAuthenticationTableData on AuthenticationTableData { bool get isExpired => DateTime.now().isAfter(expiration); + String? getCookie(String key) => cookie.value + .split("; ") + .firstWhereOrNull((c) => c.trim().startsWith("$key=")) + ?.trim() + .split("=") + .last + .replaceAll(";", ""); +} + +class AuthenticationNotifier extends AsyncNotifier { static final Dio dio = () { final dio = Dio(); @@ -32,13 +40,68 @@ class AuthenticationCredentials { return dio; }(); - AuthenticationCredentials({ - required this.cookie, - required this.accessToken, - required this.expiration, - }); + @override + build() async { + final database = ref.watch(databaseProvider); + + final data = await (database.select(database.authenticationTable) + ..where((s) => s.id.equals(0))) + .getSingleOrNull(); + + Timer? refreshTimer; + + ref.listenSelf((prevData, newData) async { + if (newData.asData?.value == null) return; + + if (newData.asData!.value!.isExpired) { + await refreshCredentials(); + } + + // set the refresh timer + refreshTimer?.cancel(); + refreshTimer = Timer( + newData.asData!.value!.expiration.difference(DateTime.now()), + () => refreshCredentials(), + ); + }); + + final subscription = + database.select(database.authenticationTable).watch().listen( + (event) { + state = AsyncData(event.isEmpty ? null : event.first); + }, + ); + + ref.onDispose(() { + subscription.cancel(); + refreshTimer?.cancel(); + }); + + return data; + } + + Future refreshCredentials() async { + final database = ref.read(databaseProvider); + final refreshedCredentials = + await credentialsFromCookie(state.asData!.value!.cookie.value); + + await database + .update(database.authenticationTable) + .replace(refreshedCredentials); + } + + Future login(String cookie) async { + final database = ref.read(databaseProvider); + final refreshedCredentials = await credentialsFromCookie(cookie); - static Future fromCookie(String cookie) async { + await database + .into(database.authenticationTable) + .insert(refreshedCredentials); + } + + Future credentialsFromCookie( + String cookie, + ) async { try { final spDc = cookie .split("; ") @@ -65,9 +128,10 @@ class AuthenticationCredentials { ); } - return AuthenticationCredentials( - cookie: "${res.headers["set-cookie"]?.join(";")}; $spDc", - accessToken: body['accessToken'], + return AuthenticationTableCompanion.insert( + id: const Value(0), + cookie: DecryptedText("${res.headers["set-cookie"]?.join(";")}; $spDc"), + accessToken: DecryptedText(body['accessToken']), expiration: DateTime.fromMillisecondsSinceEpoch( body['accessTokenExpirationTimestampMs'], ), @@ -86,102 +150,20 @@ class AuthenticationCredentials { } } - /// Returns the cookie value - String? getCookie(String key) => cookie - .split("; ") - .firstWhereOrNull((c) => c.trim().startsWith("$key=")) - ?.trim() - .split("=") - .last - .replaceAll(";", ""); - - factory AuthenticationCredentials.fromJson(Map json) { - return AuthenticationCredentials( - cookie: json['cookie'] as String, - accessToken: json['accessToken'] as String, - expiration: DateTime.parse(json['expiration'] as String), - ); - } - - Map toJson() { - return { - 'cookie': cookie, - 'accessToken': accessToken, - 'expiration': expiration.toIso8601String(), - }; - } - - AuthenticationCredentials copyWith({ - String? cookie, - String? accessToken, - DateTime? expiration, - }) { - return AuthenticationCredentials( - cookie: cookie ?? this.cookie, - accessToken: accessToken ?? this.accessToken, - expiration: expiration ?? this.expiration, - ); - } -} - -class AuthenticationNotifier - extends PersistedStateNotifier { - bool get isLoggedIn => state != null; - - AuthenticationNotifier() : super(null, "authentication", encrypted: true); - - Timer? _refreshTimer; - - @override - FutureOr onInit() async { - super.onInit(); - if (isLoggedIn && state!.isExpired) { - await refreshCredentials(); - } - - addListener((state) { - _refreshTimer?.cancel(); - if (isLoggedIn && !state!.isExpired) { - _refreshTimer = Timer( - state.expiration.difference(DateTime.now()), - () => refreshCredentials(), - ); - } - }); - } - - void setCredentials(AuthenticationCredentials credentials) { - state = credentials; - } - Future logout() async { - state = null; + state = const AsyncData(null); + final database = ref.read(databaseProvider); + await (database.delete(database.authenticationTable) + ..where((s) => s.id.equals(0))) + .go(); if (kIsMobile) { WebStorageManager.instance().deleteAllData(); CookieManager.instance().deleteAllCookies(); } } - - Future refreshCredentials() async { - if (!isLoggedIn) { - return; - } - - state = await AuthenticationCredentials.fromCookie(state!.cookie); - } - - @override - FutureOr fromJson(Map json) { - return AuthenticationCredentials.fromJson(json); - } - - @override - Map toJson() { - return state?.toJson() ?? {}; - } } final authenticationProvider = - StateNotifierProvider( - (ref) => AuthenticationNotifier(), + AsyncNotifierProvider( + () => AuthenticationNotifier(), ); diff --git a/lib/provider/custom_spotify_endpoint_provider.dart b/lib/provider/custom_spotify_endpoint_provider.dart index 4634549a4..ad0c389a4 100644 --- a/lib/provider/custom_spotify_endpoint_provider.dart +++ b/lib/provider/custom_spotify_endpoint_provider.dart @@ -1,10 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart'; final customSpotifyEndpointProvider = Provider((ref) { ref.watch(spotifyProvider); final auth = ref.watch(authenticationProvider); - return CustomSpotifyEndpoints(auth?.accessToken ?? ""); + return CustomSpotifyEndpoints(auth.asData?.value?.accessToken.value ?? ""); }); diff --git a/lib/provider/spotify/views/home.dart b/lib/provider/spotify/views/home.dart index 515869533..ad6a076ab 100644 --- a/lib/provider/spotify/views/home.dart +++ b/lib/provider/spotify/views/home.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -8,7 +8,7 @@ final homeViewProvider = FutureProvider((ref) async { userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( - authenticationProvider.select((s) => s?.getCookie("sp_t")), + authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")), ); if (spTCookie == null) return null; diff --git a/lib/provider/spotify/views/home_section.dart b/lib/provider/spotify/views/home_section.dart index 04c4cbd6e..5eb9183db 100644 --- a/lib/provider/spotify/views/home_section.dart +++ b/lib/provider/spotify/views/home_section.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/spotify/home_feed.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -11,7 +11,7 @@ final homeSectionViewProvider = userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( - authenticationProvider.select((s) => s?.getCookie("sp_t")), + authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")), ); if (spTCookie == null) return null; diff --git a/lib/provider/spotify_provider.dart b/lib/provider/spotify_provider.dart index f8b6e0441..5824cce0a 100644 --- a/lib/provider/spotify_provider.dart +++ b/lib/provider/spotify_provider.dart @@ -2,14 +2,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/env.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/primitive_utils.dart'; final spotifyProvider = Provider((ref) { final authState = ref.watch(authenticationProvider); final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets); - if (authState == null) { + if (authState.asData?.value == null) { return SpotifyApi( SpotifyApiCredentials( anonCred["clientId"], @@ -18,5 +18,5 @@ final spotifyProvider = Provider((ref) { ); } - return SpotifyApi.withAccessToken(authState.accessToken); + return SpotifyApi.withAccessToken(authState.asData!.value!.accessToken.value); }); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 8b96305fc..a730c313b 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -134,8 +134,11 @@ class UserPreferencesNotifier extends Notifier { void setLocalLibraryLocation(List localLibraryDirs) { //if (localLibraryDir.isEmpty) return; - setData(PreferencesTableCompanion( - localLibraryLocation: Value(localLibraryDirs))); + setData( + PreferencesTableCompanion( + localLibraryLocation: Value(localLibraryDirs), + ), + ); } void setLayoutMode(LayoutMode mode) { diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart index d8f69690b..ab4a750ec 100644 --- a/lib/services/kv_store/encrypted_kv_store.dart +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -1,6 +1,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:uuid/uuid.dart'; +import 'package:spotube/utils/platform.dart'; abstract class EncryptedKvStoreService { static const _storage = FlutterSecureStorage( @@ -9,15 +10,21 @@ abstract class EncryptedKvStoreService { ), ); - static late final String _encryptionKeySync; + static String? _encryptionKeySync; static Future initialize() async { _encryptionKeySync = await encryptionKey; } - static String get encryptionKeySync => _encryptionKeySync; + static String get encryptionKeySync => _encryptionKeySync!; + + static bool get isUnsupportedPlatform => + kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak); static Future get encryptionKey async { + if (isUnsupportedPlatform) { + return KVStoreService.encryptionKey; + } try { final value = await _storage.read(key: 'encryption'); final key = const Uuid().v4(); @@ -34,10 +41,17 @@ abstract class EncryptedKvStoreService { } static Future setEncryptionKey(String key) async { + if (isUnsupportedPlatform) { + await KVStoreService.setEncryptionKey(key); + return; + } + try { await _storage.write(key: 'encryption', value: key); } catch (e) { await KVStoreService.setEncryptionKey(key); + } finally { + _encryptionKeySync = key; } } } From b9b7d5c8aad013224ba749a31dcc9627ffa76e9e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 17 Jun 2024 18:08:57 +0600 Subject: [PATCH 23/92] refactor: lastfm scrobbling to drift db --- .../heart_button/use_track_toggle_like.dart | 2 +- lib/models/database/database.dart | 2 + lib/models/database/database.g.dart | 380 ++++++++++++++++++ lib/models/database/tables/scrobbler.dart | 8 + lib/pages/lastfm_login/lastfm_login.dart | 2 +- lib/pages/settings/sections/accounts.dart | 4 +- .../proxy_playlist_provider.dart | 2 +- lib/provider/scrobbler/scrobbler.dart | 130 ++++++ lib/provider/scrobbler_provider.dart | 129 ------ 9 files changed, 525 insertions(+), 134 deletions(-) create mode 100644 lib/models/database/tables/scrobbler.dart create mode 100644 lib/provider/scrobbler/scrobbler.dart delete mode 100644 lib/provider/scrobbler_provider.dart diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart index 2a886febc..ba5cbee11 100644 --- a/lib/components/heart_button/use_track_toggle_like.dart +++ b/lib/components/heart_button/use_track_toggle_like.dart @@ -1,7 +1,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; typedef UseTrackToggleLike = ({ diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 56f72ee73..e387291a3 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -22,6 +22,7 @@ part 'database.g.dart'; part 'tables/authentication.dart'; part 'tables/blacklist.dart'; part 'tables/preferences.dart'; +part 'tables/scrobbler.dart'; part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; @@ -35,6 +36,7 @@ part 'typeconverters/encrypted_text.dart'; AuthenticationTable, BlacklistTable, PreferencesTable, + ScrobblerTable, SkipSegmentTable, SourceMatchTable, ], diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 0ac7005e5..6bcfbf217 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1731,6 +1731,260 @@ class PreferencesTableCompanion extends UpdateCompanion { } } +class $ScrobblerTableTable extends ScrobblerTable + with TableInfo<$ScrobblerTableTable, ScrobblerTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ScrobblerTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _usernameMeta = + const VerificationMeta('username'); + @override + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _passwordHashMeta = + const VerificationMeta('passwordHash'); + @override + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('username')) { + context.handle(_usernameMeta, + username.isAcceptableOrUnknown(data['username']!, _usernameMeta)); + } else if (isInserting) { + context.missing(_usernameMeta); + } + if (data.containsKey('password_hash')) { + context.handle( + _passwordHashMeta, + passwordHash.isAcceptableOrUnknown( + data['password_hash']!, _passwordHashMeta)); + } else if (isInserting) { + context.missing(_passwordHashMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + $ScrobblerTableTable createAlias(String alias) { + return $ScrobblerTableTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + class $SkipSegmentTableTable extends SkipSegmentTable with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { @override @@ -2324,6 +2578,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); late final $PreferencesTableTable preferencesTable = $PreferencesTableTable(this); + late final $ScrobblerTableTable scrobblerTable = $ScrobblerTableTable(this); late final $SkipSegmentTableTable skipSegmentTable = $SkipSegmentTableTable(this); late final $SourceMatchTableTable sourceMatchTable = @@ -2340,6 +2595,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { authenticationTable, blacklistTable, preferencesTable, + scrobblerTable, skipSegmentTable, sourceMatchTable, uniqueBlacklist, @@ -3081,6 +3337,128 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$ScrobblerTableTableInsertCompanionBuilder = ScrobblerTableCompanion + Function({ + Value id, + Value createdAt, + required String username, + required String passwordHash, +}); +typedef $$ScrobblerTableTableUpdateCompanionBuilder = ScrobblerTableCompanion + Function({ + Value id, + Value createdAt, + Value username, + Value passwordHash, +}); + +class $$ScrobblerTableTableTableManager extends RootTableManager< + _$AppDatabase, + $ScrobblerTableTable, + ScrobblerTableData, + $$ScrobblerTableTableFilterComposer, + $$ScrobblerTableTableOrderingComposer, + $$ScrobblerTableTableProcessedTableManager, + $$ScrobblerTableTableInsertCompanionBuilder, + $$ScrobblerTableTableUpdateCompanionBuilder> { + $$ScrobblerTableTableTableManager( + _$AppDatabase db, $ScrobblerTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$ScrobblerTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$ScrobblerTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$ScrobblerTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value username = const Value.absent(), + Value passwordHash = const Value.absent(), + }) => + ScrobblerTableCompanion( + id: id, + createdAt: createdAt, + username: username, + passwordHash: passwordHash, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) => + ScrobblerTableCompanion.insert( + id: id, + createdAt: createdAt, + username: username, + passwordHash: passwordHash, + ), + )); +} + +class $$ScrobblerTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $ScrobblerTableTable, + ScrobblerTableData, + $$ScrobblerTableTableFilterComposer, + $$ScrobblerTableTableOrderingComposer, + $$ScrobblerTableTableProcessedTableManager, + $$ScrobblerTableTableInsertCompanionBuilder, + $$ScrobblerTableTableUpdateCompanionBuilder> { + $$ScrobblerTableTableProcessedTableManager(super.$state); +} + +class $$ScrobblerTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $ScrobblerTableTable> { + $$ScrobblerTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get username => $state.composableBuilder( + column: $state.table.username, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$ScrobblerTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $ScrobblerTableTable> { + $$ScrobblerTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get username => $state.composableBuilder( + column: $state.table.username, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + typedef $$SkipSegmentTableTableInsertCompanionBuilder = SkipSegmentTableCompanion Function({ Value id, @@ -3370,6 +3748,8 @@ class _$AppDatabaseManager { $$BlacklistTableTableTableManager(_db, _db.blacklistTable); $$PreferencesTableTableTableManager get preferencesTable => $$PreferencesTableTableTableManager(_db, _db.preferencesTable); + $$ScrobblerTableTableTableManager get scrobblerTable => + $$ScrobblerTableTableTableManager(_db, _db.scrobblerTable); $$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); $$SourceMatchTableTableTableManager get sourceMatchTable => diff --git a/lib/models/database/tables/scrobbler.dart b/lib/models/database/tables/scrobbler.dart new file mode 100644 index 000000000..481c441e9 --- /dev/null +++ b/lib/models/database/tables/scrobbler.dart @@ -0,0 +1,8 @@ +part of '../database.dart'; + +class ScrobblerTable extends Table { + IntColumn get id => integer().autoIncrement()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + TextColumn get username => text()(); + TextColumn get passwordHash => text().map(EncryptedTextConverter())(); +} diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index da2e4e131..8107e627c 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; class LastFMLoginPage extends HookConsumerWidget { static const name = "lastfm_login"; diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 1604f14bd..b06a67f66 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -119,7 +119,7 @@ class SettingsAccountSection extends HookConsumerWidget { ), ); }), - if (scrobbler == null) + if (scrobbler.asData?.value == null) ListTile( leading: const Icon(SpotubeIcons.lastFm), title: Text(context.l10n.login_with_lastfm), diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index d52073da4..067d8d44f 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -11,7 +11,7 @@ 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'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart new file mode 100644 index 000000000..d0b41c56e --- /dev/null +++ b/lib/provider/scrobbler/scrobbler.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:scrobblenaut/scrobblenaut.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/env.dart'; +import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/services/logger/logger.dart'; + +class ScrobblerNotifier extends AsyncNotifier { + final StreamController _scrobbleController = + StreamController.broadcast(); + @override + build() async { + final database = ref.watch(databaseProvider); + + final loginInfo = await (database.select(database.scrobblerTable) + ..where((t) => t.id.equals(0))) + .getSingleOrNull(); + + final subscription = + database.select(database.scrobblerTable).watch().listen((event) async { + if (event.isNotEmpty) { + state = await AsyncValue.guard( + () async => Scrobblenaut( + lastFM: await LastFM.authenticateWithPasswordHash( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: event.first.username, + passwordHash: event.first.passwordHash, + ), + ), + ); + } else { + state = const AsyncValue.data(null); + } + }); + + final scrobblerSubscription = + _scrobbleController.stream.listen((track) async { + try { + await state.asData?.value?.track.scrobble( + artist: track.artists!.first.name!, + track: track.name!, + album: track.album!.name!, + chosenByUser: true, + duration: track.duration, + timestamp: DateTime.now().toUtc(), + trackNumber: track.trackNumber, + ); + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + } + }); + + ref.onDispose(() { + subscription.cancel(); + scrobblerSubscription.cancel(); + }); + + if (loginInfo == null) { + return null; + } + + return Scrobblenaut( + lastFM: await LastFM.authenticateWithPasswordHash( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: loginInfo.username, + passwordHash: loginInfo.passwordHash, + ), + ); + } + + Future login( + String username, + String password, + ) async { + final database = ref.read(databaseProvider); + + final lastFm = await LastFM.authenticate( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: username, + password: password, + ); + + if (!lastFm.isAuth) throw Exception("Invalid credentials"); + + await database.into(database.scrobblerTable).insert( + ScrobblerTableCompanion.insert( + id: const Value(0), + username: username, + passwordHash: lastFm.passwordHash!, + ), + ); + } + + Future logout() async { + state = const AsyncValue.data(null); + final database = ref.read(databaseProvider); + await database.delete(database.scrobblerTable).go(); + } + + void scrobble(Track track) { + _scrobbleController.add(track); + } + + Future love(Track track) async { + await state.asData?.value?.track.love( + artist: track.artists!.asString(), + track: track.name!, + ); + } + + Future unlove(Track track) async { + await state.asData?.value?.track.unLove( + artist: track.artists!.asString(), + track: track.name!, + ); + } +} + +final scrobblerProvider = + AsyncNotifierProvider( + () => ScrobblerNotifier(), +); diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart deleted file mode 100644 index ab111ea45..000000000 --- a/lib/provider/scrobbler_provider.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:async'; - -import 'package:spotube/services/logger/logger.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:scrobblenaut/scrobblenaut.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/utils/persisted_state_notifier.dart'; - -class ScrobblerState { - final String username; - final String passwordHash; - - final Scrobblenaut scrobblenaut; - - ScrobblerState({ - required this.username, - required this.passwordHash, - required this.scrobblenaut, - }); - - Map toJson() { - return { - 'username': username, - 'passwordHash': passwordHash, - }; - } -} - -class ScrobblerNotifier extends PersistedStateNotifier { - final Scrobblenaut? scrobblenaut; - - /// Directly scrobbling in set state of [ProxyPlaylistNotifier] - /// brings extra latency in playback - final StreamController _scrobbleController = - StreamController.broadcast(); - - ScrobblerNotifier() - : scrobblenaut = null, - super(null, "scrobbler", encrypted: true) { - _scrobbleController.stream.listen((track) async { - try { - await state?.scrobblenaut.track.scrobble( - artist: track.artists!.first.name!, - track: track.name!, - album: track.album!.name!, - chosenByUser: true, - duration: track.duration, - timestamp: DateTime.now().toUtc(), - trackNumber: track.trackNumber, - ); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - } - }); - } - - Future login( - String username, - String password, - ) async { - final lastFm = await LastFM.authenticate( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: username, - password: password, - ); - if (!lastFm.isAuth) throw Exception("Invalid credentials"); - state = ScrobblerState( - username: username, - passwordHash: lastFm.passwordHash!, - scrobblenaut: Scrobblenaut(lastFM: lastFm), - ); - } - - Future logout() async { - state = null; - } - - void scrobble(Track track) { - _scrobbleController.add(track); - } - - Future love(Track track) async { - await state?.scrobblenaut.track.love( - artist: track.artists!.asString(), - track: track.name!, - ); - } - - Future unlove(Track track) async { - await state?.scrobblenaut.track.unLove( - artist: track.artists!.asString(), - track: track.name!, - ); - } - - @override - FutureOr fromJson(Map json) async { - if (json.isEmpty) { - return null; - } - - return ScrobblerState( - username: json['username'], - passwordHash: json['passwordHash'], - scrobblenaut: Scrobblenaut( - lastFM: await LastFM.authenticateWithPasswordHash( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: json["username"], - passwordHash: json["passwordHash"], - ), - ), - ); - } - - @override - Map toJson() { - return state?.toJson() ?? {}; - } -} - -final scrobblerProvider = - StateNotifierProvider( - (ref) => ScrobblerNotifier(), -); From 5936f08a92182a4cc3a0e72c2c044d4e480d2158 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 17 Jun 2024 18:13:41 +0600 Subject: [PATCH 24/92] refactor(volumeProvider): use notifier and kvstore for persistence --- lib/provider/volume_provider.dart | 29 ++++++++++------------------- lib/services/kv_store/kv_store.dart | 4 ++++ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/provider/volume_provider.dart b/lib/provider/volume_provider.dart index 464b5e424..ddd38fd98 100644 --- a/lib/provider/volume_provider.dart +++ b/lib/provider/volume_provider.dart @@ -2,31 +2,22 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; -class VolumeProvider extends PersistedStateNotifier { - VolumeProvider() : super(1, 'volume'); - - Future setVolume(double volume) async { - state = volume; - await audioPlayer.setVolume(volume); - } - - @override - FutureOr onInit() async { - await audioPlayer.setVolume(state); - } +class VolumeProvider extends Notifier { + VolumeProvider(); @override - FutureOr fromJson(Map json) { - return json['volume'] as double? ?? 0.0; + build() { + return KVStoreService.volume; } - @override - Map toJson() { - return {'volume': state}; + Future setVolume(double volume) async { + state = volume; + await audioPlayer.setVolume(volume); + KVStoreService.setVolume(volume); } } final volumeProvider = - StateNotifierProvider((ref) => VolumeProvider()); + NotifierProvider(() => VolumeProvider()); diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index 6b19c0322..2707ea4db 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -78,4 +78,8 @@ abstract class KVStoreService { static Future setIVKey(IV iv) async { await sharedPreferences.setString('iv', iv.base64); } + + static double get volume => sharedPreferences.getDouble('volume') ?? 1.0; + static Future setVolume(double value) async => + await sharedPreferences.setDouble('volume', value); } From 59041a2948b7dcaade4bb8cc9328d06a2c25f897 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 20 Jun 2024 23:30:41 +0600 Subject: [PATCH 25/92] chore: use .value for scrobble encrypted text --- lib/models/database/database.g.dart | 67 +++++++++++++++------------ lib/provider/scrobbler/scrobbler.dart | 6 +-- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 6bcfbf217..6f8990661 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1763,9 +1763,12 @@ class $ScrobblerTableTable extends ScrobblerTable static const VerificationMeta _passwordHashMeta = const VerificationMeta('passwordHash'); @override - late final GeneratedColumn passwordHash = GeneratedColumn( - 'password_hash', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumnWithTypeConverter + passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $ScrobblerTableTable.$converterpasswordHash); @override List get $columns => [id, createdAt, username, passwordHash]; @override @@ -1791,14 +1794,7 @@ class $ScrobblerTableTable extends ScrobblerTable } else if (isInserting) { context.missing(_usernameMeta); } - if (data.containsKey('password_hash')) { - context.handle( - _passwordHashMeta, - passwordHash.isAcceptableOrUnknown( - data['password_hash']!, _passwordHashMeta)); - } else if (isInserting) { - context.missing(_passwordHashMeta); - } + context.handle(_passwordHashMeta, const VerificationResult.success()); return context; } @@ -1814,8 +1810,9 @@ class $ScrobblerTableTable extends ScrobblerTable .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, username: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - passwordHash: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + passwordHash: $ScrobblerTableTable.$converterpasswordHash.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}password_hash'])!), ); } @@ -1823,6 +1820,9 @@ class $ScrobblerTableTable extends ScrobblerTable $ScrobblerTableTable createAlias(String alias) { return $ScrobblerTableTable(attachedDatabase, alias); } + + static TypeConverter $converterpasswordHash = + EncryptedTextConverter(); } class ScrobblerTableData extends DataClass @@ -1830,7 +1830,7 @@ class ScrobblerTableData extends DataClass final int id; final DateTime createdAt; final String username; - final String passwordHash; + final DecryptedText passwordHash; const ScrobblerTableData( {required this.id, required this.createdAt, @@ -1842,7 +1842,10 @@ class ScrobblerTableData extends DataClass map['id'] = Variable(id); map['created_at'] = Variable(createdAt); map['username'] = Variable(username); - map['password_hash'] = Variable(passwordHash); + { + map['password_hash'] = Variable( + $ScrobblerTableTable.$converterpasswordHash.toSql(passwordHash)); + } return map; } @@ -1862,7 +1865,7 @@ class ScrobblerTableData extends DataClass id: serializer.fromJson(json['id']), createdAt: serializer.fromJson(json['createdAt']), username: serializer.fromJson(json['username']), - passwordHash: serializer.fromJson(json['passwordHash']), + passwordHash: serializer.fromJson(json['passwordHash']), ); } @override @@ -1872,7 +1875,7 @@ class ScrobblerTableData extends DataClass 'id': serializer.toJson(id), 'createdAt': serializer.toJson(createdAt), 'username': serializer.toJson(username), - 'passwordHash': serializer.toJson(passwordHash), + 'passwordHash': serializer.toJson(passwordHash), }; } @@ -1880,7 +1883,7 @@ class ScrobblerTableData extends DataClass {int? id, DateTime? createdAt, String? username, - String? passwordHash}) => + DecryptedText? passwordHash}) => ScrobblerTableData( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -1914,7 +1917,7 @@ class ScrobblerTableCompanion extends UpdateCompanion { final Value id; final Value createdAt; final Value username; - final Value passwordHash; + final Value passwordHash; const ScrobblerTableCompanion({ this.id = const Value.absent(), this.createdAt = const Value.absent(), @@ -1925,7 +1928,7 @@ class ScrobblerTableCompanion extends UpdateCompanion { this.id = const Value.absent(), this.createdAt = const Value.absent(), required String username, - required String passwordHash, + required DecryptedText passwordHash, }) : username = Value(username), passwordHash = Value(passwordHash); static Insertable custom({ @@ -1946,7 +1949,7 @@ class ScrobblerTableCompanion extends UpdateCompanion { {Value? id, Value? createdAt, Value? username, - Value? passwordHash}) { + Value? passwordHash}) { return ScrobblerTableCompanion( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -1968,7 +1971,9 @@ class ScrobblerTableCompanion extends UpdateCompanion { map['username'] = Variable(username.value); } if (passwordHash.present) { - map['password_hash'] = Variable(passwordHash.value); + map['password_hash'] = Variable($ScrobblerTableTable + .$converterpasswordHash + .toSql(passwordHash.value)); } return map; } @@ -3342,14 +3347,14 @@ typedef $$ScrobblerTableTableInsertCompanionBuilder = ScrobblerTableCompanion Value id, Value createdAt, required String username, - required String passwordHash, + required DecryptedText passwordHash, }); typedef $$ScrobblerTableTableUpdateCompanionBuilder = ScrobblerTableCompanion Function({ Value id, Value createdAt, Value username, - Value passwordHash, + Value passwordHash, }); class $$ScrobblerTableTableTableManager extends RootTableManager< @@ -3376,7 +3381,7 @@ class $$ScrobblerTableTableTableManager extends RootTableManager< Value id = const Value.absent(), Value createdAt = const Value.absent(), Value username = const Value.absent(), - Value passwordHash = const Value.absent(), + Value passwordHash = const Value.absent(), }) => ScrobblerTableCompanion( id: id, @@ -3388,7 +3393,7 @@ class $$ScrobblerTableTableTableManager extends RootTableManager< Value id = const Value.absent(), Value createdAt = const Value.absent(), required String username, - required String passwordHash, + required DecryptedText passwordHash, }) => ScrobblerTableCompanion.insert( id: id, @@ -3429,10 +3434,12 @@ class $$ScrobblerTableTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnFilters get passwordHash => $state.composableBuilder( - column: $state.table.passwordHash, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnWithTypeConverterFilters + get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); } class $$ScrobblerTableTableOrderingComposer diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart index d0b41c56e..76559d698 100644 --- a/lib/provider/scrobbler/scrobbler.dart +++ b/lib/provider/scrobbler/scrobbler.dart @@ -30,7 +30,7 @@ class ScrobblerNotifier extends AsyncNotifier { apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: event.first.username, - passwordHash: event.first.passwordHash, + passwordHash: event.first.passwordHash.value, ), ), ); @@ -70,7 +70,7 @@ class ScrobblerNotifier extends AsyncNotifier { apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: loginInfo.username, - passwordHash: loginInfo.passwordHash, + passwordHash: loginInfo.passwordHash.value, ), ); } @@ -94,7 +94,7 @@ class ScrobblerNotifier extends AsyncNotifier { ScrobblerTableCompanion.insert( id: const Value(0), username: username, - passwordHash: lastFm.passwordHash!, + passwordHash: DecryptedText(lastFm.passwordHash!), ), ); } From f79fedefd48d4ac034960fcdc257e6be0a746022 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 23 Jun 2024 12:23:28 +0600 Subject: [PATCH 26/92] chore: create new audio player centric playback notifier with drift persistence --- lib/models/connect/connect.dart | 4 +- lib/models/connect/ws_event.dart | 12 +- lib/models/database/database.dart | 6 + lib/models/database/database.g.dart | 1305 +++++++++++++++++ .../database/tables/audio_player_state.dart | 27 + lib/models/database/typeconverters/map.dart | 15 + lib/modules/player/player_controls.dart | 27 +- lib/modules/player/use_progress.dart | 20 +- lib/pages/connect/control/control.dart | 23 +- lib/provider/audio_player/audio_player.dart | 225 +++ lib/provider/audio_player/state.dart | 42 + lib/provider/connect/connect.dart | 10 +- lib/provider/tray_manager/tray_menu.dart | 8 +- lib/services/audio_player/audio_player.dart | 18 +- .../audio_player/audio_player_impl.dart | 9 +- .../audio_players_streams_mixin.dart | 6 +- lib/services/audio_player/loop_mode.dart | 90 -- .../audio_services/mobile_audio_service.dart | 23 +- 18 files changed, 1694 insertions(+), 176 deletions(-) create mode 100644 lib/models/database/tables/audio_player_state.dart create mode 100644 lib/models/database/typeconverters/map.dart create mode 100644 lib/provider/audio_player/audio_player.dart create mode 100644 lib/provider/audio_player/state.dart delete mode 100644 lib/services/audio_player/loop_mode.dart diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart index 283860505..0a06be32f 100644 --- a/lib/models/connect/connect.dart +++ b/lib/models/connect/connect.dart @@ -4,9 +4,9 @@ import 'dart:async'; import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotify/spotify.dart'; +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; part 'connect.freezed.dart'; part 'connect.g.dart'; diff --git a/lib/models/connect/ws_event.dart b/lib/models/connect/ws_event.dart index 2d7213b1b..c3c29e762 100644 --- a/lib/models/connect/ws_event.dart +++ b/lib/models/connect/ws_event.dart @@ -183,7 +183,7 @@ class WebSocketEvent { if (type == WsEvent.loop) { await callback( WebSocketLoopEvent( - PlaybackLoopMode.fromString(data as String), + PlaylistMode.values.firstWhere((e) => e.name == data as String), ), ); } @@ -224,12 +224,16 @@ class WebSocketEvent { } } -class WebSocketLoopEvent extends WebSocketEvent { - WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data); +class WebSocketLoopEvent extends WebSocketEvent { + WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data); WebSocketLoopEvent.fromJson(Map json) : super( - WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String)); + WsEvent.loop, + PlaylistMode.values.firstWhere( + (e) => e.name == json["data"] as String, + ), + ); @override String toJson() { diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index e387291a3..98dc22dcc 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:encrypt/encrypt.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; @@ -25,11 +26,13 @@ part 'tables/preferences.dart'; part 'tables/scrobbler.dart'; part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; +part 'tables/audio_player_state.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; part 'typeconverters/encrypted_text.dart'; +part 'typeconverters/map.dart'; @DriftDatabase( tables: [ @@ -39,6 +42,9 @@ part 'typeconverters/encrypted_text.dart'; ScrobblerTable, SkipSegmentTable, SourceMatchTable, + AudioPlayerStateTable, + PlaylistTable, + PlaylistMediaTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 6f8990661..ca9d6d970 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -2575,6 +2575,839 @@ class SourceMatchTableCompanion extends UpdateCompanion { } } +class $AudioPlayerStateTableTable extends AudioPlayerStateTable + with TableInfo<$AudioPlayerStateTableTable, AudioPlayerStateTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AudioPlayerStateTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _playingMeta = + const VerificationMeta('playing'); + @override + late final GeneratedColumn playing = GeneratedColumn( + 'playing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); + static const VerificationMeta _volumeMeta = const VerificationMeta('volume'); + @override + late final GeneratedColumn volume = GeneratedColumn( + 'volume', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + static const VerificationMeta _loopModeMeta = + const VerificationMeta('loopMode'); + @override + late final GeneratedColumnWithTypeConverter loopMode = + GeneratedColumn('loop_mode', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $AudioPlayerStateTableTable.$converterloopMode); + static const VerificationMeta _shuffledMeta = + const VerificationMeta('shuffled'); + @override + late final GeneratedColumn shuffled = GeneratedColumn( + 'shuffled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + @override + List get $columns => + [id, playing, volume, loopMode, shuffled]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'audio_player_state_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('playing')) { + context.handle(_playingMeta, + playing.isAcceptableOrUnknown(data['playing']!, _playingMeta)); + } else if (isInserting) { + context.missing(_playingMeta); + } + if (data.containsKey('volume')) { + context.handle(_volumeMeta, + volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta)); + } else if (isInserting) { + context.missing(_volumeMeta); + } + context.handle(_loopModeMeta, const VerificationResult.success()); + if (data.containsKey('shuffled')) { + context.handle(_shuffledMeta, + shuffled.isAcceptableOrUnknown(data['shuffled']!, _shuffledMeta)); + } else if (isInserting) { + context.missing(_shuffledMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + AudioPlayerStateTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AudioPlayerStateTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, + volume: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}volume'])!, + loopMode: $AudioPlayerStateTableTable.$converterloopMode.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!), + shuffled: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + ); + } + + @override + $AudioPlayerStateTableTable createAlias(String alias) { + return $AudioPlayerStateTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $converterloopMode = + const EnumNameConverter(PlaylistMode.values); +} + +class AudioPlayerStateTableData extends DataClass + implements Insertable { + final int id; + final bool playing; + final double volume; + final PlaylistMode loopMode; + final bool shuffled; + const AudioPlayerStateTableData( + {required this.id, + required this.playing, + required this.volume, + required this.loopMode, + required this.shuffled}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playing'] = Variable(playing); + map['volume'] = Variable(volume); + { + map['loop_mode'] = Variable( + $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode)); + } + map['shuffled'] = Variable(shuffled); + return map; + } + + AudioPlayerStateTableCompanion toCompanion(bool nullToAbsent) { + return AudioPlayerStateTableCompanion( + id: Value(id), + playing: Value(playing), + volume: Value(volume), + loopMode: Value(loopMode), + shuffled: Value(shuffled), + ); + } + + factory AudioPlayerStateTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AudioPlayerStateTableData( + id: serializer.fromJson(json['id']), + playing: serializer.fromJson(json['playing']), + volume: serializer.fromJson(json['volume']), + loopMode: $AudioPlayerStateTableTable.$converterloopMode + .fromJson(serializer.fromJson(json['loopMode'])), + shuffled: serializer.fromJson(json['shuffled']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playing': serializer.toJson(playing), + 'volume': serializer.toJson(volume), + 'loopMode': serializer.toJson( + $AudioPlayerStateTableTable.$converterloopMode.toJson(loopMode)), + 'shuffled': serializer.toJson(shuffled), + }; + } + + AudioPlayerStateTableData copyWith( + {int? id, + bool? playing, + double? volume, + PlaylistMode? loopMode, + bool? shuffled}) => + AudioPlayerStateTableData( + id: id ?? this.id, + playing: playing ?? this.playing, + volume: volume ?? this.volume, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + ); + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableData(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('volume: $volume, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, playing, volume, loopMode, shuffled); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AudioPlayerStateTableData && + other.id == this.id && + other.playing == this.playing && + other.volume == this.volume && + other.loopMode == this.loopMode && + other.shuffled == this.shuffled); +} + +class AudioPlayerStateTableCompanion + extends UpdateCompanion { + final Value id; + final Value playing; + final Value volume; + final Value loopMode; + final Value shuffled; + const AudioPlayerStateTableCompanion({ + this.id = const Value.absent(), + this.playing = const Value.absent(), + this.volume = const Value.absent(), + this.loopMode = const Value.absent(), + this.shuffled = const Value.absent(), + }); + AudioPlayerStateTableCompanion.insert({ + this.id = const Value.absent(), + required bool playing, + required double volume, + required PlaylistMode loopMode, + required bool shuffled, + }) : playing = Value(playing), + volume = Value(volume), + loopMode = Value(loopMode), + shuffled = Value(shuffled); + static Insertable custom({ + Expression? id, + Expression? playing, + Expression? volume, + Expression? loopMode, + Expression? shuffled, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playing != null) 'playing': playing, + if (volume != null) 'volume': volume, + if (loopMode != null) 'loop_mode': loopMode, + if (shuffled != null) 'shuffled': shuffled, + }); + } + + AudioPlayerStateTableCompanion copyWith( + {Value? id, + Value? playing, + Value? volume, + Value? loopMode, + Value? shuffled}) { + return AudioPlayerStateTableCompanion( + id: id ?? this.id, + playing: playing ?? this.playing, + volume: volume ?? this.volume, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playing.present) { + map['playing'] = Variable(playing.value); + } + if (volume.present) { + map['volume'] = Variable(volume.value); + } + if (loopMode.present) { + map['loop_mode'] = Variable( + $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode.value)); + } + if (shuffled.present) { + map['shuffled'] = Variable(shuffled.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableCompanion(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('volume: $volume, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled') + ..write(')')) + .toString(); + } +} + +class $PlaylistTableTable extends PlaylistTable + with TableInfo<$PlaylistTableTable, PlaylistTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlaylistTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _audioPlayerStateIdMeta = + const VerificationMeta('audioPlayerStateId'); + @override + late final GeneratedColumn audioPlayerStateId = GeneratedColumn( + 'audio_player_state_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES audio_player_state_table (id)')); + static const VerificationMeta _indexMeta = const VerificationMeta('index'); + @override + late final GeneratedColumn index = GeneratedColumn( + 'index', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [id, audioPlayerStateId, index]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'playlist_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('audio_player_state_id')) { + context.handle( + _audioPlayerStateIdMeta, + audioPlayerStateId.isAcceptableOrUnknown( + data['audio_player_state_id']!, _audioPlayerStateIdMeta)); + } else if (isInserting) { + context.missing(_audioPlayerStateIdMeta); + } + if (data.containsKey('index')) { + context.handle( + _indexMeta, index.isAcceptableOrUnknown(data['index']!, _indexMeta)); + } else if (isInserting) { + context.missing(_indexMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PlaylistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlaylistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioPlayerStateId: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}audio_player_state_id'])!, + index: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}index'])!, + ); + } + + @override + $PlaylistTableTable createAlias(String alias) { + return $PlaylistTableTable(attachedDatabase, alias); + } +} + +class PlaylistTableData extends DataClass + implements Insertable { + final int id; + final int audioPlayerStateId; + final int index; + const PlaylistTableData( + {required this.id, + required this.audioPlayerStateId, + required this.index}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['audio_player_state_id'] = Variable(audioPlayerStateId); + map['index'] = Variable(index); + return map; + } + + PlaylistTableCompanion toCompanion(bool nullToAbsent) { + return PlaylistTableCompanion( + id: Value(id), + audioPlayerStateId: Value(audioPlayerStateId), + index: Value(index), + ); + } + + factory PlaylistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlaylistTableData( + id: serializer.fromJson(json['id']), + audioPlayerStateId: serializer.fromJson(json['audioPlayerStateId']), + index: serializer.fromJson(json['index']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioPlayerStateId': serializer.toJson(audioPlayerStateId), + 'index': serializer.toJson(index), + }; + } + + PlaylistTableData copyWith({int? id, int? audioPlayerStateId, int? index}) => + PlaylistTableData( + id: id ?? this.id, + audioPlayerStateId: audioPlayerStateId ?? this.audioPlayerStateId, + index: index ?? this.index, + ); + @override + String toString() { + return (StringBuffer('PlaylistTableData(') + ..write('id: $id, ') + ..write('audioPlayerStateId: $audioPlayerStateId, ') + ..write('index: $index') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, audioPlayerStateId, index); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlaylistTableData && + other.id == this.id && + other.audioPlayerStateId == this.audioPlayerStateId && + other.index == this.index); +} + +class PlaylistTableCompanion extends UpdateCompanion { + final Value id; + final Value audioPlayerStateId; + final Value index; + const PlaylistTableCompanion({ + this.id = const Value.absent(), + this.audioPlayerStateId = const Value.absent(), + this.index = const Value.absent(), + }); + PlaylistTableCompanion.insert({ + this.id = const Value.absent(), + required int audioPlayerStateId, + required int index, + }) : audioPlayerStateId = Value(audioPlayerStateId), + index = Value(index); + static Insertable custom({ + Expression? id, + Expression? audioPlayerStateId, + Expression? index, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (audioPlayerStateId != null) + 'audio_player_state_id': audioPlayerStateId, + if (index != null) 'index': index, + }); + } + + PlaylistTableCompanion copyWith( + {Value? id, Value? audioPlayerStateId, Value? index}) { + return PlaylistTableCompanion( + id: id ?? this.id, + audioPlayerStateId: audioPlayerStateId ?? this.audioPlayerStateId, + index: index ?? this.index, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (audioPlayerStateId.present) { + map['audio_player_state_id'] = Variable(audioPlayerStateId.value); + } + if (index.present) { + map['index'] = Variable(index.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlaylistTableCompanion(') + ..write('id: $id, ') + ..write('audioPlayerStateId: $audioPlayerStateId, ') + ..write('index: $index') + ..write(')')) + .toString(); + } +} + +class $PlaylistMediaTableTable extends PlaylistMediaTable + with TableInfo<$PlaylistMediaTableTable, PlaylistMediaTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlaylistMediaTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _playlistIdMeta = + const VerificationMeta('playlistId'); + @override + late final GeneratedColumn playlistId = GeneratedColumn( + 'playlist_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES playlist_table (id)')); + static const VerificationMeta _uriMeta = const VerificationMeta('uri'); + @override + late final GeneratedColumn uri = GeneratedColumn( + 'uri', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _extrasMeta = const VerificationMeta('extras'); + @override + late final GeneratedColumnWithTypeConverter?, String> + extras = GeneratedColumn('extras', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PlaylistMediaTableTable.$converterextrasn); + static const VerificationMeta _httpHeadersMeta = + const VerificationMeta('httpHeaders'); + @override + late final GeneratedColumnWithTypeConverter?, String> + httpHeaders = GeneratedColumn('http_headers', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PlaylistMediaTableTable.$converterhttpHeadersn); + @override + List get $columns => + [id, playlistId, uri, extras, httpHeaders]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'playlist_media_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('playlist_id')) { + context.handle( + _playlistIdMeta, + playlistId.isAcceptableOrUnknown( + data['playlist_id']!, _playlistIdMeta)); + } else if (isInserting) { + context.missing(_playlistIdMeta); + } + if (data.containsKey('uri')) { + context.handle( + _uriMeta, uri.isAcceptableOrUnknown(data['uri']!, _uriMeta)); + } else if (isInserting) { + context.missing(_uriMeta); + } + context.handle(_extrasMeta, const VerificationResult.success()); + context.handle(_httpHeadersMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PlaylistMediaTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlaylistMediaTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playlistId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}playlist_id'])!, + uri: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}uri'])!, + extras: $PlaylistMediaTableTable.$converterextrasn.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extras'])), + httpHeaders: $PlaylistMediaTableTable.$converterhttpHeadersn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}http_headers'])), + ); + } + + @override + $PlaylistMediaTableTable createAlias(String alias) { + return $PlaylistMediaTableTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converterextras = + const MapTypeConverter(); + static TypeConverter?, String?> $converterextrasn = + NullAwareTypeConverter.wrap($converterextras); + static TypeConverter, String> $converterhttpHeaders = + const MapTypeConverter(); + static TypeConverter?, String?> $converterhttpHeadersn = + NullAwareTypeConverter.wrap($converterhttpHeaders); +} + +class PlaylistMediaTableData extends DataClass + implements Insertable { + final int id; + final int playlistId; + final String uri; + final Map? extras; + final Map? httpHeaders; + const PlaylistMediaTableData( + {required this.id, + required this.playlistId, + required this.uri, + this.extras, + this.httpHeaders}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playlist_id'] = Variable(playlistId); + map['uri'] = Variable(uri); + if (!nullToAbsent || extras != null) { + map['extras'] = Variable( + $PlaylistMediaTableTable.$converterextrasn.toSql(extras)); + } + if (!nullToAbsent || httpHeaders != null) { + map['http_headers'] = Variable( + $PlaylistMediaTableTable.$converterhttpHeadersn.toSql(httpHeaders)); + } + return map; + } + + PlaylistMediaTableCompanion toCompanion(bool nullToAbsent) { + return PlaylistMediaTableCompanion( + id: Value(id), + playlistId: Value(playlistId), + uri: Value(uri), + extras: + extras == null && nullToAbsent ? const Value.absent() : Value(extras), + httpHeaders: httpHeaders == null && nullToAbsent + ? const Value.absent() + : Value(httpHeaders), + ); + } + + factory PlaylistMediaTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlaylistMediaTableData( + id: serializer.fromJson(json['id']), + playlistId: serializer.fromJson(json['playlistId']), + uri: serializer.fromJson(json['uri']), + extras: serializer.fromJson?>(json['extras']), + httpHeaders: + serializer.fromJson?>(json['httpHeaders']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playlistId': serializer.toJson(playlistId), + 'uri': serializer.toJson(uri), + 'extras': serializer.toJson?>(extras), + 'httpHeaders': serializer.toJson?>(httpHeaders), + }; + } + + PlaylistMediaTableData copyWith( + {int? id, + int? playlistId, + String? uri, + Value?> extras = const Value.absent(), + Value?> httpHeaders = const Value.absent()}) => + PlaylistMediaTableData( + id: id ?? this.id, + playlistId: playlistId ?? this.playlistId, + uri: uri ?? this.uri, + extras: extras.present ? extras.value : this.extras, + httpHeaders: httpHeaders.present ? httpHeaders.value : this.httpHeaders, + ); + @override + String toString() { + return (StringBuffer('PlaylistMediaTableData(') + ..write('id: $id, ') + ..write('playlistId: $playlistId, ') + ..write('uri: $uri, ') + ..write('extras: $extras, ') + ..write('httpHeaders: $httpHeaders') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, playlistId, uri, extras, httpHeaders); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlaylistMediaTableData && + other.id == this.id && + other.playlistId == this.playlistId && + other.uri == this.uri && + other.extras == this.extras && + other.httpHeaders == this.httpHeaders); +} + +class PlaylistMediaTableCompanion + extends UpdateCompanion { + final Value id; + final Value playlistId; + final Value uri; + final Value?> extras; + final Value?> httpHeaders; + const PlaylistMediaTableCompanion({ + this.id = const Value.absent(), + this.playlistId = const Value.absent(), + this.uri = const Value.absent(), + this.extras = const Value.absent(), + this.httpHeaders = const Value.absent(), + }); + PlaylistMediaTableCompanion.insert({ + this.id = const Value.absent(), + required int playlistId, + required String uri, + this.extras = const Value.absent(), + this.httpHeaders = const Value.absent(), + }) : playlistId = Value(playlistId), + uri = Value(uri); + static Insertable custom({ + Expression? id, + Expression? playlistId, + Expression? uri, + Expression? extras, + Expression? httpHeaders, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playlistId != null) 'playlist_id': playlistId, + if (uri != null) 'uri': uri, + if (extras != null) 'extras': extras, + if (httpHeaders != null) 'http_headers': httpHeaders, + }); + } + + PlaylistMediaTableCompanion copyWith( + {Value? id, + Value? playlistId, + Value? uri, + Value?>? extras, + Value?>? httpHeaders}) { + return PlaylistMediaTableCompanion( + id: id ?? this.id, + playlistId: playlistId ?? this.playlistId, + uri: uri ?? this.uri, + extras: extras ?? this.extras, + httpHeaders: httpHeaders ?? this.httpHeaders, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playlistId.present) { + map['playlist_id'] = Variable(playlistId.value); + } + if (uri.present) { + map['uri'] = Variable(uri.value); + } + if (extras.present) { + map['extras'] = Variable( + $PlaylistMediaTableTable.$converterextrasn.toSql(extras.value)); + } + if (httpHeaders.present) { + map['http_headers'] = Variable($PlaylistMediaTableTable + .$converterhttpHeadersn + .toSql(httpHeaders.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlaylistMediaTableCompanion(') + ..write('id: $id, ') + ..write('playlistId: $playlistId, ') + ..write('uri: $uri, ') + ..write('extras: $extras, ') + ..write('httpHeaders: $httpHeaders') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -2588,6 +3421,11 @@ abstract class _$AppDatabase extends GeneratedDatabase { $SkipSegmentTableTable(this); late final $SourceMatchTableTable sourceMatchTable = $SourceMatchTableTable(this); + late final $AudioPlayerStateTableTable audioPlayerStateTable = + $AudioPlayerStateTableTable(this); + late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this); + late final $PlaylistMediaTableTable playlistMediaTable = + $PlaylistMediaTableTable(this); late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', @@ -2603,6 +3441,9 @@ abstract class _$AppDatabase extends GeneratedDatabase { scrobblerTable, skipSegmentTable, sourceMatchTable, + audioPlayerStateTable, + playlistTable, + playlistMediaTable, uniqueBlacklist, uniqTrackMatch ]; @@ -3746,6 +4587,464 @@ class $$SourceMatchTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$AudioPlayerStateTableTableInsertCompanionBuilder + = AudioPlayerStateTableCompanion Function({ + Value id, + required bool playing, + required double volume, + required PlaylistMode loopMode, + required bool shuffled, +}); +typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder + = AudioPlayerStateTableCompanion Function({ + Value id, + Value playing, + Value volume, + Value loopMode, + Value shuffled, +}); + +class $$AudioPlayerStateTableTableTableManager extends RootTableManager< + _$AppDatabase, + $AudioPlayerStateTableTable, + AudioPlayerStateTableData, + $$AudioPlayerStateTableTableFilterComposer, + $$AudioPlayerStateTableTableOrderingComposer, + $$AudioPlayerStateTableTableProcessedTableManager, + $$AudioPlayerStateTableTableInsertCompanionBuilder, + $$AudioPlayerStateTableTableUpdateCompanionBuilder> { + $$AudioPlayerStateTableTableTableManager( + _$AppDatabase db, $AudioPlayerStateTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: $$AudioPlayerStateTableTableFilterComposer( + ComposerState(db, table)), + orderingComposer: $$AudioPlayerStateTableTableOrderingComposer( + ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$AudioPlayerStateTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value playing = const Value.absent(), + Value volume = const Value.absent(), + Value loopMode = const Value.absent(), + Value shuffled = const Value.absent(), + }) => + AudioPlayerStateTableCompanion( + id: id, + playing: playing, + volume: volume, + loopMode: loopMode, + shuffled: shuffled, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required bool playing, + required double volume, + required PlaylistMode loopMode, + required bool shuffled, + }) => + AudioPlayerStateTableCompanion.insert( + id: id, + playing: playing, + volume: volume, + loopMode: loopMode, + shuffled: shuffled, + ), + )); +} + +class $$AudioPlayerStateTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $AudioPlayerStateTableTable, + AudioPlayerStateTableData, + $$AudioPlayerStateTableTableFilterComposer, + $$AudioPlayerStateTableTableOrderingComposer, + $$AudioPlayerStateTableTableProcessedTableManager, + $$AudioPlayerStateTableTableInsertCompanionBuilder, + $$AudioPlayerStateTableTableUpdateCompanionBuilder> { + $$AudioPlayerStateTableTableProcessedTableManager(super.$state); +} + +class $$AudioPlayerStateTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $AudioPlayerStateTableTable> { + $$AudioPlayerStateTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get playing => $state.composableBuilder( + column: $state.table.playing, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get volume => $state.composableBuilder( + column: $state.table.volume, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get loopMode => $state.composableBuilder( + column: $state.table.loopMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get shuffled => $state.composableBuilder( + column: $state.table.shuffled, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ComposableFilter playlistTableRefs( + ComposableFilter Function($$PlaylistTableTableFilterComposer f) f) { + final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $state.db.playlistTable, + getReferencedColumn: (t) => t.audioPlayerStateId, + builder: (joinBuilder, parentComposers) => + $$PlaylistTableTableFilterComposer(ComposerState($state.db, + $state.db.playlistTable, joinBuilder, parentComposers))); + return f(composer); + } +} + +class $$AudioPlayerStateTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $AudioPlayerStateTableTable> { + $$AudioPlayerStateTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get playing => $state.composableBuilder( + column: $state.table.playing, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get volume => $state.composableBuilder( + column: $state.table.volume, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get loopMode => $state.composableBuilder( + column: $state.table.loopMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get shuffled => $state.composableBuilder( + column: $state.table.shuffled, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +typedef $$PlaylistTableTableInsertCompanionBuilder = PlaylistTableCompanion + Function({ + Value id, + required int audioPlayerStateId, + required int index, +}); +typedef $$PlaylistTableTableUpdateCompanionBuilder = PlaylistTableCompanion + Function({ + Value id, + Value audioPlayerStateId, + Value index, +}); + +class $$PlaylistTableTableTableManager extends RootTableManager< + _$AppDatabase, + $PlaylistTableTable, + PlaylistTableData, + $$PlaylistTableTableFilterComposer, + $$PlaylistTableTableOrderingComposer, + $$PlaylistTableTableProcessedTableManager, + $$PlaylistTableTableInsertCompanionBuilder, + $$PlaylistTableTableUpdateCompanionBuilder> { + $$PlaylistTableTableTableManager(_$AppDatabase db, $PlaylistTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$PlaylistTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$PlaylistTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$PlaylistTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value audioPlayerStateId = const Value.absent(), + Value index = const Value.absent(), + }) => + PlaylistTableCompanion( + id: id, + audioPlayerStateId: audioPlayerStateId, + index: index, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int audioPlayerStateId, + required int index, + }) => + PlaylistTableCompanion.insert( + id: id, + audioPlayerStateId: audioPlayerStateId, + index: index, + ), + )); +} + +class $$PlaylistTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $PlaylistTableTable, + PlaylistTableData, + $$PlaylistTableTableFilterComposer, + $$PlaylistTableTableOrderingComposer, + $$PlaylistTableTableProcessedTableManager, + $$PlaylistTableTableInsertCompanionBuilder, + $$PlaylistTableTableUpdateCompanionBuilder> { + $$PlaylistTableTableProcessedTableManager(super.$state); +} + +class $$PlaylistTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $PlaylistTableTable> { + $$PlaylistTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get index => $state.composableBuilder( + column: $state.table.index, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + $$AudioPlayerStateTableTableFilterComposer get audioPlayerStateId { + final $$AudioPlayerStateTableTableFilterComposer composer = + $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.audioPlayerStateId, + referencedTable: $state.db.audioPlayerStateTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$AudioPlayerStateTableTableFilterComposer(ComposerState( + $state.db, + $state.db.audioPlayerStateTable, + joinBuilder, + parentComposers))); + return composer; + } + + ComposableFilter playlistMediaTableRefs( + ComposableFilter Function($$PlaylistMediaTableTableFilterComposer f) f) { + final $$PlaylistMediaTableTableFilterComposer composer = $state + .composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $state.db.playlistMediaTable, + getReferencedColumn: (t) => t.playlistId, + builder: (joinBuilder, parentComposers) => + $$PlaylistMediaTableTableFilterComposer(ComposerState( + $state.db, + $state.db.playlistMediaTable, + joinBuilder, + parentComposers))); + return f(composer); + } +} + +class $$PlaylistTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $PlaylistTableTable> { + $$PlaylistTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get index => $state.composableBuilder( + column: $state.table.index, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + $$AudioPlayerStateTableTableOrderingComposer get audioPlayerStateId { + final $$AudioPlayerStateTableTableOrderingComposer composer = + $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.audioPlayerStateId, + referencedTable: $state.db.audioPlayerStateTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$AudioPlayerStateTableTableOrderingComposer(ComposerState( + $state.db, + $state.db.audioPlayerStateTable, + joinBuilder, + parentComposers))); + return composer; + } +} + +typedef $$PlaylistMediaTableTableInsertCompanionBuilder + = PlaylistMediaTableCompanion Function({ + Value id, + required int playlistId, + required String uri, + Value?> extras, + Value?> httpHeaders, +}); +typedef $$PlaylistMediaTableTableUpdateCompanionBuilder + = PlaylistMediaTableCompanion Function({ + Value id, + Value playlistId, + Value uri, + Value?> extras, + Value?> httpHeaders, +}); + +class $$PlaylistMediaTableTableTableManager extends RootTableManager< + _$AppDatabase, + $PlaylistMediaTableTable, + PlaylistMediaTableData, + $$PlaylistMediaTableTableFilterComposer, + $$PlaylistMediaTableTableOrderingComposer, + $$PlaylistMediaTableTableProcessedTableManager, + $$PlaylistMediaTableTableInsertCompanionBuilder, + $$PlaylistMediaTableTableUpdateCompanionBuilder> { + $$PlaylistMediaTableTableTableManager( + _$AppDatabase db, $PlaylistMediaTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$PlaylistMediaTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: $$PlaylistMediaTableTableOrderingComposer( + ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$PlaylistMediaTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value playlistId = const Value.absent(), + Value uri = const Value.absent(), + Value?> extras = const Value.absent(), + Value?> httpHeaders = const Value.absent(), + }) => + PlaylistMediaTableCompanion( + id: id, + playlistId: playlistId, + uri: uri, + extras: extras, + httpHeaders: httpHeaders, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int playlistId, + required String uri, + Value?> extras = const Value.absent(), + Value?> httpHeaders = const Value.absent(), + }) => + PlaylistMediaTableCompanion.insert( + id: id, + playlistId: playlistId, + uri: uri, + extras: extras, + httpHeaders: httpHeaders, + ), + )); +} + +class $$PlaylistMediaTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $PlaylistMediaTableTable, + PlaylistMediaTableData, + $$PlaylistMediaTableTableFilterComposer, + $$PlaylistMediaTableTableOrderingComposer, + $$PlaylistMediaTableTableProcessedTableManager, + $$PlaylistMediaTableTableInsertCompanionBuilder, + $$PlaylistMediaTableTableUpdateCompanionBuilder> { + $$PlaylistMediaTableTableProcessedTableManager(super.$state); +} + +class $$PlaylistMediaTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $PlaylistMediaTableTable> { + $$PlaylistMediaTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get uri => $state.composableBuilder( + column: $state.table.uri, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters?, Map, + String> + get extras => $state.composableBuilder( + column: $state.table.extras, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters?, Map, + String> + get httpHeaders => $state.composableBuilder( + column: $state.table.httpHeaders, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + $$PlaylistTableTableFilterComposer get playlistId { + final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playlistId, + referencedTable: $state.db.playlistTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$PlaylistTableTableFilterComposer(ComposerState($state.db, + $state.db.playlistTable, joinBuilder, parentComposers))); + return composer; + } +} + +class $$PlaylistMediaTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $PlaylistMediaTableTable> { + $$PlaylistMediaTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get uri => $state.composableBuilder( + column: $state.table.uri, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get extras => $state.composableBuilder( + column: $state.table.extras, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get httpHeaders => $state.composableBuilder( + column: $state.table.httpHeaders, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + $$PlaylistTableTableOrderingComposer get playlistId { + final $$PlaylistTableTableOrderingComposer composer = + $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playlistId, + referencedTable: $state.db.playlistTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$PlaylistTableTableOrderingComposer(ComposerState($state.db, + $state.db.playlistTable, joinBuilder, parentComposers))); + return composer; + } +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); @@ -3761,4 +5060,10 @@ class _$AppDatabaseManager { $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); $$SourceMatchTableTableTableManager get sourceMatchTable => $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); + $$AudioPlayerStateTableTableTableManager get audioPlayerStateTable => + $$AudioPlayerStateTableTableTableManager(_db, _db.audioPlayerStateTable); + $$PlaylistTableTableTableManager get playlistTable => + $$PlaylistTableTableTableManager(_db, _db.playlistTable); + $$PlaylistMediaTableTableTableManager get playlistMediaTable => + $$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable); } diff --git a/lib/models/database/tables/audio_player_state.dart b/lib/models/database/tables/audio_player_state.dart new file mode 100644 index 000000000..45f5ffd9d --- /dev/null +++ b/lib/models/database/tables/audio_player_state.dart @@ -0,0 +1,27 @@ +part of '../database.dart'; + +class AudioPlayerStateTable extends Table { + IntColumn get id => integer().autoIncrement()(); + BoolColumn get playing => boolean()(); + RealColumn get volume => real()(); + TextColumn get loopMode => textEnum()(); + BoolColumn get shuffled => boolean()(); +} + +class PlaylistTable extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get audioPlayerStateId => + integer().references(AudioPlayerStateTable, #id)(); + IntColumn get index => integer()(); +} + +class PlaylistMediaTable extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get playlistId => integer().references(PlaylistTable, #id)(); + + TextColumn get uri => text()(); + TextColumn get extras => + text().nullable().map(const MapTypeConverter())(); + TextColumn get httpHeaders => + text().nullable().map(const MapTypeConverter())(); +} diff --git a/lib/models/database/typeconverters/map.dart b/lib/models/database/typeconverters/map.dart new file mode 100644 index 000000000..0b0ff7e09 --- /dev/null +++ b/lib/models/database/typeconverters/map.dart @@ -0,0 +1,15 @@ +part of '../database.dart'; + +class MapTypeConverter extends TypeConverter, String> { + const MapTypeConverter(); + + @override + fromSql(String fromDb) { + return json.decode(fromDb) as Map; + } + + @override + toSql(value) { + return json.encode(value); + } +} diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index ba69560c2..a1a3ffcf9 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -12,7 +13,6 @@ import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.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'; class PlayerControls extends HookConsumerWidget { final PaletteGenerator? palette; @@ -234,38 +234,29 @@ class PlayerControls extends HookConsumerWidget { ? null : playlistNotifier.next, ), - StreamBuilder( + StreamBuilder( stream: audioPlayer.loopModeStream, builder: (context, snapshot) { - final loopMode = snapshot.data ?? PlaybackLoopMode.none; + final loopMode = snapshot.data ?? PlaylistMode.none; return IconButton( - tooltip: loopMode == PlaybackLoopMode.one + tooltip: loopMode == PlaylistMode.single ? context.l10n.loop_track - : loopMode == PlaybackLoopMode.all + : loopMode == PlaylistMode.loop ? context.l10n.repeat_playlist : null, icon: Icon( - loopMode == PlaybackLoopMode.one + loopMode == PlaylistMode.single ? SpotubeIcons.repeatOne : SpotubeIcons.repeat, ), - style: loopMode == PlaybackLoopMode.one || - loopMode == PlaybackLoopMode.all + style: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, onPressed: playlist.isFetching == true ? null : () async { - audioPlayer.setLoopMode( - switch (loopMode) { - PlaybackLoopMode.all => - PlaybackLoopMode.one, - PlaybackLoopMode.one => - PlaybackLoopMode.none, - PlaybackLoopMode.none => - PlaybackLoopMode.all, - }, - ); + await audioPlayer.setLoopMode(loopMode); }, ); }), diff --git a/lib/modules/player/use_progress.dart b/lib/modules/player/use_progress.dart index 15a979af7..eaea638e2 100644 --- a/lib/modules/player/use_progress.dart +++ b/lib/modules/player/use_progress.dart @@ -1,4 +1,3 @@ -import 'package:async/async.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -19,26 +18,13 @@ import 'package:spotube/services/audio_player/audio_player.dart'; final sliderValue = position.value.inSeconds; useEffect(() { - final durationOperation = - CancelableOperation.fromFuture(audioPlayer.duration); - durationOperation.then((value) { - if (value != null) { - duration.value = value; - } - }); + duration.value = audioPlayer.duration; final durationSubscription = audioPlayer.durationStream.listen((event) { duration.value = event; }); - final positionOperation = - CancelableOperation.fromFuture(audioPlayer.position); - - positionOperation.then((value) { - if (value != null) { - position.value = value; - } - }); + position.value = audioPlayer.position; var lastPosition = position.value; @@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart'; }); return () { - positionOperation.cancel(); positionSubscription.cancel(); - durationOperation.cancel(); durationSubscription.cancel(); }; }, []); diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index eb2c48c55..d27b7867e 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -16,7 +16,7 @@ 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'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/utils/service_utils.dart'; class RemotePlayerQueue extends ConsumerWidget { @@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget { : connectNotifier.next, ), IconButton( - tooltip: loopMode == PlaybackLoopMode.one + tooltip: loopMode == PlaylistMode.single ? context.l10n.loop_track - : loopMode == PlaybackLoopMode.all + : loopMode == PlaylistMode.loop ? context.l10n.repeat_playlist : null, icon: Icon( - loopMode == PlaybackLoopMode.one + loopMode == PlaylistMode.single ? SpotubeIcons.repeatOne : SpotubeIcons.repeat, ), - style: loopMode == PlaybackLoopMode.one || - loopMode == PlaybackLoopMode.all + style: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, onPressed: playlist.activeTrack == null @@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget { : () async { connectNotifier.setLoopMode( switch (loopMode) { - PlaybackLoopMode.all => - PlaybackLoopMode.one, - PlaybackLoopMode.one => - PlaybackLoopMode.none, - PlaybackLoopMode.none => - PlaybackLoopMode.all, + PlaylistMode.loop => + PlaylistMode.single, + PlaylistMode.single => + PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, }, ); }, diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart new file mode 100644 index 000000000..747c78e64 --- /dev/null +++ b/lib/provider/audio_player/audio_player.dart @@ -0,0 +1,225 @@ +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class AudioPlayerNotifier extends Notifier { + Future _syncSavedState() async { + final database = ref.read(databaseProvider); + + var playerState = + await database.select(database.audioPlayerStateTable).getSingleOrNull(); + + if (playerState == null) { + await database.into(database.audioPlayerStateTable).insert( + AudioPlayerStateTableCompanion.insert( + playing: audioPlayer.isPlaying, + volume: audioPlayer.volume, + loopMode: audioPlayer.loopMode, + shuffled: audioPlayer.isShuffled, + id: const Value(0), + ), + ); + + playerState = + await database.select(database.audioPlayerStateTable).getSingle(); + } else { + await audioPlayer.setVolume(playerState.volume); + await audioPlayer.setLoopMode(playerState.loopMode); + await audioPlayer.setShuffle(playerState.shuffled); + } + + var playlist = + await database.select(database.playlistTable).getSingleOrNull(); + var medias = await database.select(database.playlistMediaTable).get(); + + if (playlist == null) { + await database.into(database.playlistTable).insert( + PlaylistTableCompanion.insert( + audioPlayerStateId: 0, + index: audioPlayer.playlist.index, + id: const Value(0), + ), + ); + + playlist = await database.select(database.playlistTable).getSingle(); + } + + if (medias.isEmpty && audioPlayer.playlist.medias.isNotEmpty) { + await database.batch((batch) { + batch.insertAll( + database.playlistMediaTable, + [ + for (final media in audioPlayer.playlist.medias) + PlaylistMediaTableCompanion.insert( + playlistId: playlist!.id, + uri: media.uri, + extras: Value(media.extras), + httpHeaders: Value(media.httpHeaders), + ), + ], + ); + }); + } else { + await audioPlayer.openPlaylist( + medias + .map((media) => Media( + media.uri, + extras: media.extras, + httpHeaders: media.httpHeaders, + )) + .toList(), + initialIndex: playlist.index, + ); + } + } + + Future _updatePlayerState( + AudioPlayerStateTableCompanion companion, + ) async { + final database = ref.read(databaseProvider); + + await (database.update(database.audioPlayerStateTable) + ..where((tb) => tb.id.equals(0))) + .write(companion); + } + + Future _updatePlaylist( + Playlist playlist, + ) async { + final database = ref.read(databaseProvider); + + await database.batch((batch) { + batch.update( + database.playlistTable, + PlaylistTableCompanion(index: Value(playlist.index)), + where: (tb) => tb.id.equals(0), + ); + + batch.deleteAll(database.playlistMediaTable); + + if (playlist.medias.isEmpty) return; + batch.insertAll( + database.playlistMediaTable, + [ + for (final media in playlist.medias) + PlaylistMediaTableCompanion.insert( + playlistId: 0, + uri: media.uri, + extras: Value(media.extras), + httpHeaders: Value(media.httpHeaders), + ), + ], + ); + }); + } + + @override + build() { + final subscriptions = [ + audioPlayer.playingStream.listen((playing) async { + state = state.copyWith(playing: playing); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + playing: Value(playing), + ), + ); + }), + audioPlayer.volumeStream.listen((volume) async { + state = state.copyWith(volume: volume); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + volume: Value(volume), + ), + ); + }), + audioPlayer.loopModeStream.listen((loopMode) async { + state = state.copyWith(loopMode: loopMode); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + loopMode: Value(loopMode), + ), + ); + }), + audioPlayer.shuffledStream.listen((shuffled) async { + state = state.copyWith(shuffled: shuffled); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + shuffled: Value(shuffled), + ), + ); + }), + audioPlayer.playlistStream.listen((playlist) async { + state = state.copyWith(playlist: playlist); + + await _updatePlaylist(playlist); + }), + ]; + + _syncSavedState(); + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + + return AudioPlayerState( + loopMode: audioPlayer.loopMode, + playing: audioPlayer.isPlaying, + playlist: audioPlayer.playlist, + shuffled: audioPlayer.isShuffled, + volume: audioPlayer.volume, + ); + } + + // Tracks related methods + + Future addTrack(Track track) async { + await audioPlayer.addTrack(SpotubeMedia(track)); + } + + Future addTracks(Iterable tracks) async { + for (final track in tracks) { + await addTrack(track); + } + } + + Future removeTrack(Track track) async { + final index = state.tracks.indexWhere((element) => element == track); + + if (index == -1) return; + + await audioPlayer.removeTrack(index); + } + + Future removeTracks(Iterable tracks) async { + for (final track in tracks) { + await removeTrack(track); + } + } + + Future load( + List track, { + required int initialIndex, + bool autoPlay = false, + }) async { + await audioPlayer.openPlaylist( + track.map((t) => SpotubeMedia(t)).toList(), + initialIndex: initialIndex, + autoPlay: autoPlay, + ); + } +} + +final audioPlayerProvider = NotifierProvider( + () => AudioPlayerNotifier(), +); \ No newline at end of file diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart new file mode 100644 index 000000000..3c874011c --- /dev/null +++ b/lib/provider/audio_player/state.dart @@ -0,0 +1,42 @@ +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class AudioPlayerState { + final bool playing; + final double volume; + final PlaylistMode loopMode; + final bool shuffled; + final Playlist playlist; + + final List tracks; + + AudioPlayerState({ + required this.playing, + required this.volume, + required this.loopMode, + required this.shuffled, + required this.playlist, + List? tracks, + }) : tracks = tracks ?? + playlist.medias + .map((media) => SpotubeMedia.fromMedia(media).track) + .toList(); + + AudioPlayerState copyWith({ + bool? playing, + double? volume, + PlaylistMode? loopMode, + bool? shuffled, + Playlist? playlist, + }) { + return AudioPlayerState( + playing: playing ?? this.playing, + volume: volume ?? this.volume, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + playlist: playlist ?? this.playlist, + tracks: playlist == null ? tracks : null, + ); + } +} diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index feb9fbd2b..c60144450 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -1,13 +1,13 @@ import 'dart:convert'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -27,8 +27,8 @@ final shuffleProvider = StateProvider( (ref) => false, ); -final loopModeProvider = StateProvider( - (ref) => PlaybackLoopMode.none, +final loopModeProvider = StateProvider( + (ref) => PlaylistMode.none, ); final queueProvider = StateProvider( @@ -158,7 +158,7 @@ class ConnectNotifier extends AsyncNotifier { emit(WebSocketShuffleEvent(value)); } - Future setLoopMode(PlaybackLoopMode value) async { + Future setLoopMode(PlaylistMode value) async { emit(WebSocketLoopEvent(value)); } diff --git a/lib/provider/tray_manager/tray_menu.dart b/lib/provider/tray_manager/tray_menu.dart index cb793707e..35aca4f5f 100644 --- a/lib/provider/tray_manager/tray_menu.dart +++ b/lib/provider/tray_manager/tray_menu.dart @@ -3,11 +3,11 @@ 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:media_kit/media_kit.dart' hide Track; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; -final audioPlayerLoopMode = StreamProvider((ref) { +final audioPlayerLoopMode = StreamProvider((ref) { return audioPlayer.loopModeStream; }); @@ -23,7 +23,7 @@ final trayMenuProvider = Provider((ref) { final isPlaybackPlaying = ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null)); final isLoopOne = - ref.watch(audioPlayerLoopMode).asData?.value == PlaybackLoopMode.one; + ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single; final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false; final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false; @@ -75,7 +75,7 @@ final trayMenuProvider = Provider((ref) { checked: isLoopOne, onClick: (menuItem) { audioPlayer.setLoopMode( - isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one, + isLoopOne ? PlaylistMode.none : PlaylistMode.single, ); }, ), diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index df23039c9..713d518b5 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,16 +1,16 @@ import 'dart:io'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/local_track.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; import 'dart:async'; 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'; @@ -66,15 +66,19 @@ abstract class AudioPlayerInterface { bool get mkSupportedPlatform => _mkSupportedPlatform; - Future get duration async { + Duration get duration { return _mkPlayer.state.duration; } - Future get position async { + Playlist get playlist { + return _mkPlayer.state.playlist; + } + + Duration get position { return _mkPlayer.state.position; } - Future get bufferedPosition async { + Duration get bufferedPosition { return _mkPlayer.state.buffer; } @@ -111,8 +115,8 @@ abstract class AudioPlayerInterface { return _mkPlayer.shuffled; } - PlaybackLoopMode get loopMode { - return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode); + PlaylistMode get loopMode { + return _mkPlayer.state.playlistMode; } /// Returns the current volume of the player, between 0 and 1 diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 58868aed7..82c8c9067 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -65,7 +65,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface } String? get nextSource { - if (loopMode == PlaybackLoopMode.all && + if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == _mkPlayer.state.playlist.medias.length - 1) { return sources.first; @@ -77,8 +77,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface } String? get previousSource { - if (loopMode == PlaybackLoopMode.all && - _mkPlayer.state.playlist.index == 0) { + if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) { return sources.last; } @@ -125,8 +124,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface await _mkPlayer.setShuffle(shuffle); } - Future setLoopMode(PlaybackLoopMode loop) async { - await _mkPlayer.setPlaylistMode(loop.toPlaylistMode()); + Future setLoopMode(PlaylistMode loop) async { + await _mkPlayer.setPlaylistMode(loop); } Future setAudioNormalization(bool normalize) async { diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index f6fe06302..03ce0d5d5 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -71,12 +71,12 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { // } } - Stream get loopModeStream { + Stream get loopModeStream { // if (mkSupportedPlatform) { - return _mkPlayer.stream.playlistMode.map(PlaybackLoopMode.fromPlaylistMode); + return _mkPlayer.stream.playlistMode; // } else { // return _justAudio!.loopModeStream - // .map(PlaybackLoopMode.fromLoopMode) + // .map(PlaylistMode.fromLoopMode) // ; // } } diff --git a/lib/services/audio_player/loop_mode.dart b/lib/services/audio_player/loop_mode.dart deleted file mode 100644 index 78da43bae..000000000 --- a/lib/services/audio_player/loop_mode.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:media_kit/media_kit.dart'; - -/// An unified loop mode for both [LoopMode] and [PlaylistMode] -enum PlaybackLoopMode { - all, - one, - none; - - // static PlaybackLoopMode fromLoopMode(LoopMode loopMode) { - // switch (loopMode) { - // case LoopMode.all: - // return PlaybackLoopMode.all; - // case LoopMode.one: - // return PlaybackLoopMode.one; - // case LoopMode.off: - // return PlaybackLoopMode.none; - // } - // } - - // LoopMode toLoopMode() { - // switch (this) { - // case PlaybackLoopMode.all: - // return LoopMode.all; - // case PlaybackLoopMode.one: - // return LoopMode.one; - // case PlaybackLoopMode.none: - // return LoopMode.off; - // } - // } - - static PlaybackLoopMode fromPlaylistMode(PlaylistMode mode) { - switch (mode) { - case PlaylistMode.single: - return PlaybackLoopMode.one; - case PlaylistMode.loop: - return PlaybackLoopMode.all; - case PlaylistMode.none: - return PlaybackLoopMode.none; - } - } - - PlaylistMode toPlaylistMode() { - switch (this) { - case PlaybackLoopMode.all: - return PlaylistMode.loop; - case PlaybackLoopMode.one: - return PlaylistMode.single; - case PlaybackLoopMode.none: - return PlaylistMode.none; - } - } - - static PlaybackLoopMode fromAudioServiceRepeatMode( - AudioServiceRepeatMode mode) { - switch (mode) { - case AudioServiceRepeatMode.all: - case AudioServiceRepeatMode.group: - return PlaybackLoopMode.all; - case AudioServiceRepeatMode.one: - return PlaybackLoopMode.one; - case AudioServiceRepeatMode.none: - return PlaybackLoopMode.none; - } - } - - AudioServiceRepeatMode toAudioServiceRepeatMode() { - switch (this) { - case PlaybackLoopMode.all: - return AudioServiceRepeatMode.all; - case PlaybackLoopMode.one: - return AudioServiceRepeatMode.one; - case PlaybackLoopMode.none: - return AudioServiceRepeatMode.none; - } - } - - static PlaybackLoopMode fromString(String? value) { - switch (value) { - case 'all': - return PlaybackLoopMode.all; - case 'one': - return PlaybackLoopMode.one; - case 'none': - return PlaybackLoopMode.none; - default: - return PlaybackLoopMode.none; - } - } -} diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 62cc85520..3dbae18f3 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -5,7 +5,7 @@ import 'package:audio_session/audio_session.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.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:media_kit/media_kit.dart' hide Track; class MobileAudioService extends BaseAudioHandler { AudioSession? session; @@ -91,9 +91,13 @@ class MobileAudioService extends BaseAudioHandler { @override Future setRepeatMode(AudioServiceRepeatMode repeatMode) async { super.setRepeatMode(repeatMode); - audioPlayer.setLoopMode( - PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode), - ); + audioPlayer.setLoopMode(switch (repeatMode) { + AudioServiceRepeatMode.all || + AudioServiceRepeatMode.group => + PlaylistMode.loop, + AudioServiceRepeatMode.one => PlaylistMode.single, + _ => PlaylistMode.none, + }); } @override @@ -120,7 +124,6 @@ class MobileAudioService extends BaseAudioHandler { } Future _transformEvent() async { - final position = (await audioPlayer.position) ?? Duration.zero; return PlaybackState( controls: [ MediaControl.skipToPrevious, @@ -133,12 +136,16 @@ class MobileAudioService extends BaseAudioHandler { }, androidCompactActionIndices: const [0, 1, 2], playing: audioPlayer.isPlaying, - updatePosition: position, - bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero, + updatePosition: audioPlayer.position, + bufferedPosition: audioPlayer.bufferedPosition, shuffleMode: audioPlayer.isShuffled == true ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, - repeatMode: (audioPlayer.loopMode).toAudioServiceRepeatMode(), + repeatMode: switch (audioPlayer.loopMode) { + PlaylistMode.loop => AudioServiceRepeatMode.all, + PlaylistMode.single => AudioServiceRepeatMode.one, + _ => AudioServiceRepeatMode.none, + }, processingState: playlist.isFetching == true ? AudioProcessingState.loading : AudioProcessingState.ready, From a83dd64476486bdad88b0c0f2afb56e2b90e7f0f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 20:52:40 +0600 Subject: [PATCH 27/92] refactor: replace all instances of proxy playlist --- lib/collections/intents.dart | 8 +- lib/components/track_tile/track_options.dart | 14 +- lib/components/track_tile/track_tile.dart | 11 +- .../sections/body/track_view_body.dart | 6 +- .../sections/body/track_view_options.dart | 4 +- .../sections/header/header_actions.dart | 8 +- .../sections/header/header_buttons.dart | 6 +- .../configurators/use_endless_playback.dart | 16 +- lib/main.dart | 4 +- lib/models/connect/connect.dart | 2 +- lib/models/connect/ws_event.dart | 6 +- lib/models/database/database.g.dart | 128 ++++++----- .../database/tables/audio_player_state.dart | 2 +- lib/modules/album/album_card.dart | 8 +- lib/modules/player/player.dart | 21 +- lib/modules/player/player_actions.dart | 8 +- lib/modules/player/player_controls.dart | 24 +- lib/modules/player/player_overlay.dart | 16 +- lib/modules/player/player_queue.dart | 15 +- lib/modules/player/player_track_details.dart | 4 +- lib/modules/player/sibling_tracks_sheet.dart | 17 +- lib/modules/playlist/playlist_card.dart | 10 +- lib/modules/root/bottom_player.dart | 6 +- lib/pages/artist/section/header.dart | 2 +- lib/pages/artist/section/top_tracks.dart | 6 +- lib/pages/library/local_folder.dart | 8 +- .../playlist_generate_result.dart | 13 +- lib/pages/lyrics/lyrics.dart | 6 +- lib/pages/lyrics/mini_lyrics.dart | 11 +- lib/pages/lyrics/plain_lyrics.dart | 4 +- lib/pages/lyrics/synced_lyrics.dart | 6 +- lib/pages/root/root_app.dart | 8 +- lib/pages/search/sections/tracks.dart | 6 +- lib/pages/track/track.dart | 6 +- lib/provider/audio_player/audio_player.dart | 137 +++++++++-- .../audio_player_streams.dart} | 87 ++++--- lib/provider/audio_player/state.dart | 66 +++++- lib/provider/connect/connect.dart | 13 +- lib/provider/discord_provider.dart | 4 +- .../proxy_playlist/proxy_playlist.dart | 101 -------- .../proxy_playlist_provider.dart | 215 ------------------ lib/provider/server/active_sourced_track.dart | 4 +- lib/provider/server/routes/connect.dart | 28 +-- lib/provider/server/routes/playback.dart | 6 +- lib/provider/server/sourced_track.dart | 7 +- .../skip_segments.dart | 0 lib/provider/tray_manager/tray_menu.dart | 10 +- .../user_preferences_provider.dart | 5 +- .../audio_services/audio_services.dart | 4 +- .../audio_services/mobile_audio_service.dart | 20 +- .../audio_services/windows_audio_service.dart | 12 +- 51 files changed, 515 insertions(+), 624 deletions(-) rename lib/provider/{proxy_playlist/player_listeners.dart => audio_player/audio_player_streams.dart} (51%) delete mode 100644 lib/provider/proxy_playlist/proxy_playlist.dart delete mode 100644 lib/provider/proxy_playlist/proxy_playlist_provider.dart rename lib/provider/{proxy_playlist => skip_segments}/skip_segments.dart (100%) diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 6d6e643e0..1a44a8465 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -11,7 +11,7 @@ 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/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; @@ -96,8 +96,8 @@ class SeekIntent extends Intent { class SeekAction extends Action { @override invoke(intent) async { - final playlist = intent.ref.read(proxyPlaylistProvider); - if (playlist.isFetching) { + final playlist = intent.ref.read(audioPlayerProvider.notifier); + if (playlist.isFetching()) { DirectionalFocusAction().invoke( DirectionalFocusIntent( intent.forward ? TraversalDirection.right : TraversalDirection.left, @@ -105,7 +105,7 @@ class SeekAction extends Action { ); return null; } - final position = (await audioPlayer.position ?? Duration.zero).inSeconds; + final position = audioPlayer.position.inSeconds; await audioPlayer.seek( Duration( seconds: intent.forward ? position + 5 : position - 5, diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index d54a0c153..c6cfdd35a 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -24,7 +24,7 @@ import 'package:spotube/provider/authentication/authentication.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/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; @@ -96,8 +96,8 @@ class TrackOptions extends HookConsumerWidget { WidgetRef ref, Track track, ) async { - final playback = ref.read(proxyPlaylistProvider.notifier); - final playlist = ref.read(proxyPlaylistProvider); + final playback = ref.read(audioPlayerProvider.notifier); + final playlist = ref.read(audioPlayerProvider); final spotify = ref.read(spotifyProvider); final query = "${track.name} Radio"; final pages = @@ -160,8 +160,8 @@ class TrackOptions extends HookConsumerWidget { final router = GoRouter.of(context); final ThemeData(:colorScheme) = Theme.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playback = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playback = ref.watch(audioPlayerProvider.notifier); final auth = ref.watch(authenticationProvider); ref.watch(downloadManagerProvider); final downloadManager = ref.watch(downloadManagerProvider.notifier); @@ -364,7 +364,7 @@ class TrackOptions extends HookConsumerWidget { : context.l10n.save_as_favorite, ), ), - if (auth != null && !isLocalTrack) ...[ + if (auth.asData?.value != null && !isLocalTrack) ...[ PopSheetEntry( value: TrackOptionValue.startRadio, leading: const Icon(SpotubeIcons.radio), @@ -376,7 +376,7 @@ class TrackOptions extends HookConsumerWidget { title: Text(context.l10n.add_to_playlist), ), ], - if (userPlaylist && auth != null && !isLocalTrack) + if (userPlaylist && auth.asData?.value != null && !isLocalTrack) PopSheetEntry( value: TrackOptionValue.removeFromPlaylist, leading: const Icon(SpotubeIcons.removeFilled), diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index e2e7e2932..cdc18d9b9 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -17,8 +17,9 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; class TrackTile extends HookConsumerWidget { /// [index] will not be shown if null @@ -30,7 +31,7 @@ class TrackTile extends HookConsumerWidget { final VoidCallback? onLongPress; final bool userPlaylist; final String? playlistId; - final ProxyPlaylist playlist; + final AudioPlayerState playlist; final List? leadingActions; @@ -160,7 +161,11 @@ class TrackTile extends HookConsumerWidget { child: Skeleton.ignore( child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: (isPlaying && playlist.isFetching) || + child: (isPlaying && + ref + .watch(audioPlayerProvider + .notifier) + .isFetching()) || isLoading.value ? const SizedBox( width: 26, diff --git a/lib/components/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart index 0c3cca4ee..a6089cc3c 100644 --- a/lib/components/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -18,7 +18,7 @@ import 'package:spotube/components/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/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -27,8 +27,8 @@ class TrackViewBodySection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final props = InheritedTrackView.of(context); final trackViewState = ref.watch(trackViewProvider(props.tracks)); diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index 1accba347..98ddca259 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/database/database.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/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class TrackViewBodyOptions extends HookConsumerWidget { @@ -24,7 +24,7 @@ class TrackViewBodyOptions extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final audioSource = ref.watch(userPreferencesProvider.select((s) => s.audioSource)); diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index f20cd5533..6769ed523 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; class TrackViewHeaderActions extends HookConsumerWidget { const TrackViewHeaderActions({super.key}); @@ -20,8 +20,8 @@ class TrackViewHeaderActions extends HookConsumerWidget { Widget build(BuildContext context, ref) { final props = InheritedTrackView.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final isActive = playlist.collections.contains(props.collectionId); @@ -73,7 +73,7 @@ class TrackViewHeaderActions extends HookConsumerWidget { } }, ), - if (props.onHeart != null && auth != null) + if (props.onHeart != null && auth.asData?.value != null) HeartButton( isLiked: props.isLiked, icon: isUserPlaylist ? SpotubeIcons.trash : null, diff --git a/lib/components/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart index aa660f016..aabca20f1 100644 --- a/lib/components/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -13,7 +13,7 @@ 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/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class TrackViewHeaderButtons extends HookConsumerWidget { @@ -28,8 +28,8 @@ class TrackViewHeaderButtons extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final props = InheritedTrackView.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final isActive = playlist.collections.contains(props.collectionId); diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index 9b90b23dc..e2fb1e6ee 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -3,15 +3,15 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; void useEndlessPlayback(WidgetRef ref) { final auth = ref.watch(authenticationProvider); - final playback = ref.watch(proxyPlaylistProvider.notifier); - final playlist = ref.watch(proxyPlaylistProvider); + final playback = ref.watch(audioPlayerProvider.notifier); + final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist)); final spotify = ref.watch(spotifyProvider); final endlessPlayback = ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); @@ -22,7 +22,7 @@ void useEndlessPlayback(WidgetRef ref) { void listener(int index) async { try { - final playlist = ref.read(proxyPlaylistProvider); + final playlist = ref.read(audioPlayerProvider); if (index != playlist.tracks.length - 1) return; final track = playlist.tracks.last; @@ -56,7 +56,7 @@ void useEndlessPlayback(WidgetRef ref) { await playback.addTracks( tracks.toList() ..removeWhere((e) { - final playlist = ref.read(proxyPlaylistProvider); + final playlist = ref.read(audioPlayerProvider); final isDuplicate = playlist.tracks.any((t) => t.id == e.id); return e.id == track.id || isDuplicate; }), @@ -69,9 +69,9 @@ void useEndlessPlayback(WidgetRef ref) { // Sometimes user can change settings for which the currentIndexChanged // might not be called. So we need to check if the current track is the // last track and if it is then we need to call the listener manually. - if (playlist.active == playlist.tracks.length - 1 && + if (playlist.index == playlist.medias.length - 1 && audioPlayer.isPlaying) { - listener(playlist.active!); + listener(playlist.index); } final subscription = @@ -82,7 +82,7 @@ void useEndlessPlayback(WidgetRef ref) { [ spotify, playback, - playlist.tracks, + playlist.medias, endlessPlayback, auth, ], diff --git a/lib/main.dart b/lib/main.dart index 09db495c3..9b92a21df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,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/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; @@ -113,9 +114,10 @@ class Spotube extends HookConsumerWidget { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); - ref.listen(serverProvider, (_, __) {}); + ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); + ref.listen(serverProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); useDisableBatteryOptimizations(); diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart index 0a06be32f..a70520ad6 100644 --- a/lib/models/connect/connect.dart +++ b/lib/models/connect/connect.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotify/spotify.dart' hide Playlist; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; +import 'package:spotube/provider/audio_player/state.dart'; part 'connect.freezed.dart'; part 'connect.g.dart'; diff --git a/lib/models/connect/ws_event.dart b/lib/models/connect/ws_event.dart index c3c29e762..d10476461 100644 --- a/lib/models/connect/ws_event.dart +++ b/lib/models/connect/ws_event.dart @@ -325,12 +325,12 @@ class WebSocketErrorEvent extends WebSocketEvent { WebSocketErrorEvent(String data) : super(WsEvent.error, data); } -class WebSocketQueueEvent extends WebSocketEvent { - WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data); +class WebSocketQueueEvent extends WebSocketEvent { + WebSocketQueueEvent(AudioPlayerState data) : super(WsEvent.queue, data); factory WebSocketQueueEvent.fromJson(Map json) => WebSocketQueueEvent( - ProxyPlaylist.fromJsonRaw(json), + AudioPlayerState.fromJson(json), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index ca9d6d970..37cc930c0 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -2599,11 +2599,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); - static const VerificationMeta _volumeMeta = const VerificationMeta('volume'); - @override - late final GeneratedColumn volume = GeneratedColumn( - 'volume', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); static const VerificationMeta _loopModeMeta = const VerificationMeta('loopMode'); @override @@ -2621,9 +2616,17 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + static const VerificationMeta _collectionsMeta = + const VerificationMeta('collections'); + @override + late final GeneratedColumnWithTypeConverter, String> + collections = GeneratedColumn('collections', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $AudioPlayerStateTableTable.$convertercollections); @override List get $columns => - [id, playing, volume, loopMode, shuffled]; + [id, playing, loopMode, shuffled, collections]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2644,12 +2647,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable } else if (isInserting) { context.missing(_playingMeta); } - if (data.containsKey('volume')) { - context.handle(_volumeMeta, - volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta)); - } else if (isInserting) { - context.missing(_volumeMeta); - } context.handle(_loopModeMeta, const VerificationResult.success()); if (data.containsKey('shuffled')) { context.handle(_shuffledMeta, @@ -2657,6 +2654,7 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable } else if (isInserting) { context.missing(_shuffledMeta); } + context.handle(_collectionsMeta, const VerificationResult.success()); return context; } @@ -2671,13 +2669,14 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable .read(DriftSqlType.int, data['${effectivePrefix}id'])!, playing: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, - volume: attachedDatabase.typeMapping - .read(DriftSqlType.double, data['${effectivePrefix}volume'])!, loopMode: $AudioPlayerStateTableTable.$converterloopMode.fromSql( attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!), shuffled: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + collections: $AudioPlayerStateTableTable.$convertercollections.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}collections'])!), ); } @@ -2688,32 +2687,37 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable static JsonTypeConverter2 $converterloopMode = const EnumNameConverter(PlaylistMode.values); + static TypeConverter, String> $convertercollections = + const StringListConverter(); } class AudioPlayerStateTableData extends DataClass implements Insertable { final int id; final bool playing; - final double volume; final PlaylistMode loopMode; final bool shuffled; + final List collections; const AudioPlayerStateTableData( {required this.id, required this.playing, - required this.volume, required this.loopMode, - required this.shuffled}); + required this.shuffled, + required this.collections}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); map['playing'] = Variable(playing); - map['volume'] = Variable(volume); { map['loop_mode'] = Variable( $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode)); } map['shuffled'] = Variable(shuffled); + { + map['collections'] = Variable( + $AudioPlayerStateTableTable.$convertercollections.toSql(collections)); + } return map; } @@ -2721,9 +2725,9 @@ class AudioPlayerStateTableData extends DataClass return AudioPlayerStateTableCompanion( id: Value(id), playing: Value(playing), - volume: Value(volume), loopMode: Value(loopMode), shuffled: Value(shuffled), + collections: Value(collections), ); } @@ -2733,10 +2737,10 @@ class AudioPlayerStateTableData extends DataClass return AudioPlayerStateTableData( id: serializer.fromJson(json['id']), playing: serializer.fromJson(json['playing']), - volume: serializer.fromJson(json['volume']), loopMode: $AudioPlayerStateTableTable.$converterloopMode .fromJson(serializer.fromJson(json['loopMode'])), shuffled: serializer.fromJson(json['shuffled']), + collections: serializer.fromJson>(json['collections']), ); } @override @@ -2745,103 +2749,103 @@ class AudioPlayerStateTableData extends DataClass return { 'id': serializer.toJson(id), 'playing': serializer.toJson(playing), - 'volume': serializer.toJson(volume), 'loopMode': serializer.toJson( $AudioPlayerStateTableTable.$converterloopMode.toJson(loopMode)), 'shuffled': serializer.toJson(shuffled), + 'collections': serializer.toJson>(collections), }; } AudioPlayerStateTableData copyWith( {int? id, bool? playing, - double? volume, PlaylistMode? loopMode, - bool? shuffled}) => + bool? shuffled, + List? collections}) => AudioPlayerStateTableData( id: id ?? this.id, playing: playing ?? this.playing, - volume: volume ?? this.volume, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, ); @override String toString() { return (StringBuffer('AudioPlayerStateTableData(') ..write('id: $id, ') ..write('playing: $playing, ') - ..write('volume: $volume, ') ..write('loopMode: $loopMode, ') - ..write('shuffled: $shuffled') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, playing, volume, loopMode, shuffled); + int get hashCode => Object.hash(id, playing, loopMode, shuffled, collections); @override bool operator ==(Object other) => identical(this, other) || (other is AudioPlayerStateTableData && other.id == this.id && other.playing == this.playing && - other.volume == this.volume && other.loopMode == this.loopMode && - other.shuffled == this.shuffled); + other.shuffled == this.shuffled && + other.collections == this.collections); } class AudioPlayerStateTableCompanion extends UpdateCompanion { final Value id; final Value playing; - final Value volume; final Value loopMode; final Value shuffled; + final Value> collections; const AudioPlayerStateTableCompanion({ this.id = const Value.absent(), this.playing = const Value.absent(), - this.volume = const Value.absent(), this.loopMode = const Value.absent(), this.shuffled = const Value.absent(), + this.collections = const Value.absent(), }); AudioPlayerStateTableCompanion.insert({ this.id = const Value.absent(), required bool playing, - required double volume, required PlaylistMode loopMode, required bool shuffled, + required List collections, }) : playing = Value(playing), - volume = Value(volume), loopMode = Value(loopMode), - shuffled = Value(shuffled); + shuffled = Value(shuffled), + collections = Value(collections); static Insertable custom({ Expression? id, Expression? playing, - Expression? volume, Expression? loopMode, Expression? shuffled, + Expression? collections, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (playing != null) 'playing': playing, - if (volume != null) 'volume': volume, if (loopMode != null) 'loop_mode': loopMode, if (shuffled != null) 'shuffled': shuffled, + if (collections != null) 'collections': collections, }); } AudioPlayerStateTableCompanion copyWith( {Value? id, Value? playing, - Value? volume, Value? loopMode, - Value? shuffled}) { + Value? shuffled, + Value>? collections}) { return AudioPlayerStateTableCompanion( id: id ?? this.id, playing: playing ?? this.playing, - volume: volume ?? this.volume, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, ); } @@ -2854,9 +2858,6 @@ class AudioPlayerStateTableCompanion if (playing.present) { map['playing'] = Variable(playing.value); } - if (volume.present) { - map['volume'] = Variable(volume.value); - } if (loopMode.present) { map['loop_mode'] = Variable( $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode.value)); @@ -2864,6 +2865,11 @@ class AudioPlayerStateTableCompanion if (shuffled.present) { map['shuffled'] = Variable(shuffled.value); } + if (collections.present) { + map['collections'] = Variable($AudioPlayerStateTableTable + .$convertercollections + .toSql(collections.value)); + } return map; } @@ -2872,9 +2878,9 @@ class AudioPlayerStateTableCompanion return (StringBuffer('AudioPlayerStateTableCompanion(') ..write('id: $id, ') ..write('playing: $playing, ') - ..write('volume: $volume, ') ..write('loopMode: $loopMode, ') - ..write('shuffled: $shuffled') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections') ..write(')')) .toString(); } @@ -4591,17 +4597,17 @@ typedef $$AudioPlayerStateTableTableInsertCompanionBuilder = AudioPlayerStateTableCompanion Function({ Value id, required bool playing, - required double volume, required PlaylistMode loopMode, required bool shuffled, + required List collections, }); typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder = AudioPlayerStateTableCompanion Function({ Value id, Value playing, - Value volume, Value loopMode, Value shuffled, + Value> collections, }); class $$AudioPlayerStateTableTableTableManager extends RootTableManager< @@ -4627,30 +4633,30 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< getUpdateCompanionBuilder: ({ Value id = const Value.absent(), Value playing = const Value.absent(), - Value volume = const Value.absent(), Value loopMode = const Value.absent(), Value shuffled = const Value.absent(), + Value> collections = const Value.absent(), }) => AudioPlayerStateTableCompanion( id: id, playing: playing, - volume: volume, loopMode: loopMode, shuffled: shuffled, + collections: collections, ), getInsertCompanionBuilder: ({ Value id = const Value.absent(), required bool playing, - required double volume, required PlaylistMode loopMode, required bool shuffled, + required List collections, }) => AudioPlayerStateTableCompanion.insert( id: id, playing: playing, - volume: volume, loopMode: loopMode, shuffled: shuffled, + collections: collections, ), )); } @@ -4681,11 +4687,6 @@ class $$AudioPlayerStateTableTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnFilters get volume => $state.composableBuilder( - column: $state.table.volume, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnWithTypeConverterFilters get loopMode => $state.composableBuilder( column: $state.table.loopMode, @@ -4698,6 +4699,13 @@ class $$AudioPlayerStateTableTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnWithTypeConverterFilters, List, String> + get collections => $state.composableBuilder( + column: $state.table.collections, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + ComposableFilter playlistTableRefs( ComposableFilter Function($$PlaylistTableTableFilterComposer f) f) { final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder( @@ -4725,11 +4733,6 @@ class $$AudioPlayerStateTableTableOrderingComposer builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get volume => $state.composableBuilder( - column: $state.table.volume, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get loopMode => $state.composableBuilder( column: $state.table.loopMode, builder: (column, joinBuilders) => @@ -4739,6 +4742,11 @@ class $$AudioPlayerStateTableTableOrderingComposer column: $state.table.shuffled, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get collections => $state.composableBuilder( + column: $state.table.collections, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); } typedef $$PlaylistTableTableInsertCompanionBuilder = PlaylistTableCompanion diff --git a/lib/models/database/tables/audio_player_state.dart b/lib/models/database/tables/audio_player_state.dart index 45f5ffd9d..3e49cf6f6 100644 --- a/lib/models/database/tables/audio_player_state.dart +++ b/lib/models/database/tables/audio_player_state.dart @@ -3,9 +3,9 @@ part of '../database.dart'; class AudioPlayerStateTable extends Table { IntColumn get id => integer().autoIncrement()(); BoolColumn get playing => boolean()(); - RealColumn get volume => real()(); TextColumn get loopMode => textEnum()(); BoolColumn get shuffled => boolean()(); + TextColumn get collections => text().map(const StringListConverter())(); } class PlaylistTable extends Table { diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index a071ac047..f9f70c66a 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -12,7 +12,7 @@ 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/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -30,10 +30,10 @@ class AlbumCard extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.read(playbackHistoryProvider.notifier); bool isPlaylistPlaying = useMemoized( @@ -59,7 +59,7 @@ class AlbumCard extends HookConsumerWidget { ), margin: const EdgeInsets.symmetric(horizontal: 10), isPlaying: isPlaylistPlaying, - isLoading: (isPlaylistPlaying && playlist.isFetching == true) || + isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || updating.value, title: album.name!, description: diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 663447920..d75df7960 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -25,7 +25,7 @@ import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; @@ -47,7 +47,7 @@ class PlayerView extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider); final currentActiveTrack = - ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack)); + ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; final isLocalTrack = currentTrack is LocalTrack; final mediaQuery = MediaQuery.of(context); @@ -309,15 +309,13 @@ class PlayerView extends HookConsumerWidget { builder: (context) => Consumer( builder: (context, ref, _) { final playlist = ref.watch( - proxyPlaylistProvider, - ); - final playlistNotifier = - ref.read( - proxyPlaylistProvider - .notifier, + audioPlayerProvider, ); + final playlistNotifier = ref + .read(audioPlayerProvider + .notifier); return PlayerQueue - .fromProxyPlaylistNotifier( + .fromAudioPlayerNotifier( floating: false, playlist: playlist, notifier: playlistNotifier, @@ -328,8 +326,9 @@ class PlayerView extends HookConsumerWidget { } : null), ), - if (auth != null) const SizedBox(width: 10), - if (auth != null) + if (auth.asData?.value != null) + const SizedBox(width: 10), + if (auth.asData?.value != null) Expanded( child: OutlinedButton.icon( label: Text(context.l10n.lyrics), diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 8fd434ade..8a7b3e831 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -14,7 +14,7 @@ import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/sleep_timer_provider.dart'; class PlayerActions extends HookConsumerWidget { @@ -33,7 +33,7 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final isLocalTrack = playlist.activeTrack is LocalTrack; ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); @@ -129,7 +129,9 @@ class PlayerActions extends HookConsumerWidget { ? () => downloader.addToQueue(playlist.activeTrack!) : null, ), - if (playlist.activeTrack != null && !isLocalTrack && auth != null) + if (playlist.activeTrack != null && + !isLocalTrack && + auth.asData?.value != null) TrackHeartButton(track: playlist.activeTrack!), AdaptivePopSheetList( offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)), diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index a1a3ffcf9..c5ef82d63 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerControls extends HookConsumerWidget { @@ -43,8 +43,8 @@ class PlayerControls extends HookConsumerWidget { SeekIntent: SeekAction(), }, []); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; @@ -132,7 +132,7 @@ class PlayerControls extends HookConsumerWidget { // than total duration. Keeping it resolved value: progress.value.toDouble(), secondaryTrackValue: bufferProgress, - onChanged: playlist.isFetching == true + onChanged: playlistNotifier.isFetching() ? null : (v) { progress.value = v; @@ -183,7 +183,7 @@ class PlayerControls extends HookConsumerWidget { : context.l10n.shuffle_playlist, icon: const Icon(SpotubeIcons.shuffle), style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null : () { if (shuffled) { @@ -198,15 +198,15 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.previous_track, icon: const Icon(SpotubeIcons.skipBack), style: buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.previous, + : audioPlayer.skipToPrevious, ), IconButton( tooltip: playing ? context.l10n.pause_playback : context.l10n.resume_playback, - icon: playlist.isFetching == true + icon: playlistNotifier.isFetching() ? SizedBox( height: 20, width: 20, @@ -219,7 +219,7 @@ class PlayerControls extends HookConsumerWidget { playing ? SpotubeIcons.pause : SpotubeIcons.play, ), style: resumePauseStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null : Actions.handler( context, @@ -230,9 +230,9 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.next_track, icon: const Icon(SpotubeIcons.skipForward), style: buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.next, + : audioPlayer.skipToNext, ), StreamBuilder( stream: audioPlayer.loopModeStream, @@ -253,7 +253,7 @@ class PlayerControls extends HookConsumerWidget { loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null : () async { await audioPlayer.setLoopMode(loopMode); diff --git a/lib/modules/player/player_overlay.dart b/lib/modules/player/player_overlay.dart index 084de425f..c1b285eef 100644 --- a/lib/modules/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -11,7 +11,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/player.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerOverlay extends HookConsumerWidget { @@ -24,8 +24,8 @@ class PlayerOverlay extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); - final playlist = ref.watch(proxyPlaylistProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); final canShow = playlist.activeTrack != null; final playing = @@ -127,14 +127,14 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipBack, color: textColor, ), - onPressed: playlist.isFetching + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.previous, + : audioPlayer.skipToPrevious, ), Consumer( builder: (context, ref, _) { return IconButton( - icon: playlist.isFetching + icon: playlistNotifier.isFetching() ? const SizedBox( height: 20, width: 20, @@ -158,9 +158,9 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipForward, color: textColor, ), - onPressed: playlist.isFetching + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.next, + : audioPlayer.skipToNext, ), ], ), diff --git a/lib/modules/player/player_queue.dart b/lib/modules/player/player_queue.dart index cf16e9a39..2431d82ed 100644 --- a/lib/modules/player/player_queue.dart +++ b/lib/modules/player/player_queue.dart @@ -18,12 +18,12 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; class PlayerQueue extends HookConsumerWidget { final bool floating; - final ProxyPlaylist playlist; + final AudioPlayerState playlist; final Future Function(Track track) onJump; final Future Function(String trackId) onRemove; @@ -40,10 +40,10 @@ class PlayerQueue extends HookConsumerWidget { super.key, }); - PlayerQueue.fromProxyPlaylistNotifier({ + PlayerQueue.fromAudioPlayerNotifier({ this.floating = true, required this.playlist, - required ProxyPlaylistNotifier notifier, + required AudioPlayerNotifier notifier, super.key, }) : onJump = notifier.jumpToTrack, onRemove = notifier.removeTrack, @@ -93,11 +93,10 @@ class PlayerQueue extends HookConsumerWidget { ); useEffect(() { - if (playlist.active == null) return null; + if (playlist.activeTrack == null) return null; - if (playlist.active! < 0) return; controller.scrollToIndex( - playlist.active!, + playlist.playlist.index, preferPosition: AutoScrollPosition.middle, ); return null; diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index da58e3b19..d722830e2 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; class PlayerTrackDetails extends HookConsumerWidget { @@ -21,7 +21,7 @@ class PlayerTrackDetails extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final playback = ref.watch(proxyPlaylistProvider); + final playback = ref.watch(audioPlayerProvider); return Row( children: [ diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index a6136e621..8592f1e3f 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -15,7 +15,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/database/database.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -53,7 +53,8 @@ class SiblingTracksSheet extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final preferences = ref.watch(userPreferencesProvider); final isSearching = useState(false); @@ -129,13 +130,13 @@ class SiblingTracksSheet extends HookConsumerWidget { ]); final siblings = useMemoized( - () => playlist.isFetching == false + () => playlistNotifier.isFetching() ? [ (activeTrack as SourcedTrack).sourceInfo, ...activeTrack.siblings, ] : [], - [playlist.isFetching, activeTrack], + [activeTrack], ); final borderRadius = floating @@ -175,12 +176,12 @@ class SiblingTracksSheet extends HookConsumerWidget { Text(" • ${sourceInfo.artist}"), ], ), - enabled: playlist.isFetching != true, - selected: playlist.isFetching != true && + enabled: !playlistNotifier.isFetching(), + selected: !playlistNotifier.isFetching() && sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, selectedTileColor: theme.popupMenuTheme.color, onTap: () { - if (playlist.isFetching == false && + if (!playlistNotifier.isFetching() && sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { activeTrackNotifier.swapSibling(sourceInfo); Navigator.of(context).pop(); @@ -188,7 +189,7 @@ class SiblingTracksSheet extends HookConsumerWidget { }, ); }, - [playlist.isFetching, activeTrack, siblings], + [activeTrack, siblings], ); final mediaQuery = MediaQuery.of(context); diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index 7c11eca6d..c41647013 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -9,7 +9,7 @@ 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/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -22,8 +22,8 @@ class PlaylistCard extends HookConsumerWidget { }); @override Widget build(BuildContext context, ref) { - final playlistQueue = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistQueue = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.read(playbackHistoryProvider.notifier); final playing = @@ -65,8 +65,8 @@ class PlaylistCard extends HookConsumerWidget { placeholder: ImagePlaceholder.collection, ), isPlaying: isPlaylistPlaying, - isLoading: - (isPlaylistPlaying && playlistQueue.isFetching) || updating.value, + isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || + updating.value, isOwner: playlist.owner?.id == me.asData?.value.id && me.asData?.value.id != null, onTap: () { diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index a77ab6fe6..e7dbacd22 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -19,7 +19,7 @@ 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/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/volume_provider.dart'; @@ -33,7 +33,7 @@ class BottomPlayer extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(authenticationProvider); - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); @@ -91,7 +91,7 @@ class BottomPlayer extends HookConsumerWidget { children: [ PlayerActions( extraActions: [ - if (auth != null) + if (auth.asData?.value != null) IconButton( tooltip: context.l10n.mini_player, icon: const Icon(SpotubeIcons.miniPlayer), diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 7d7fa8efc..713e0d266 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -135,7 +135,7 @@ class ArtistPageHeader extends HookConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (auth != null) + if (auth.asData?.value != null) Consumer( builder: (context, ref, _) { final isFollowingQuery = ref diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index c9397c7bb..d52ed470f 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/track_tile/track_tile.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/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class ArtistPageTopTracks extends HookConsumerWidget { @@ -21,8 +21,8 @@ class ArtistPageTopTracks extends HookConsumerWidget { final theme = Theme.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final topTracksQuery = ref.watch(artistTopTracksProvider(artistId)); final isPlaylistPlaying = playlist.containsTracks( diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 830e8a5d0..16891bc18 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -17,7 +17,7 @@ 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/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; class LocalLibraryPage extends HookConsumerWidget { @@ -32,8 +32,8 @@ class LocalLibraryPage extends HookConsumerWidget { List tracks, { LocalTrack? currentTrack, }) async { - final playlist = ref.read(proxyPlaylistProvider); - final playback = ref.read(proxyPlaylistProvider.notifier); + final playlist = ref.read(audioPlayerProvider); + final playback = ref.read(audioPlayerProvider.notifier); currentTrack ??= tracks.first; final isPlaylistPlaying = playlist.containsTracks(tracks); if (!isPlaylistPlaying) { @@ -52,7 +52,7 @@ class LocalLibraryPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final sortBy = useState(SortBy.none); - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final trackSnapshot = ref.watch(localTracksProvider); final isPlaylistPlaying = playlist.containsTracks( trackSnapshot.asData?.value.values.flattened.toList() ?? []); diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 90838300c..3bdc3b52d 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/titlebar/titlebar.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/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class PlaylistGenerateResultPage extends HookConsumerWidget { @@ -28,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final router = GoRouter.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final generatedPlaylist = ref.watch(generatePlaylistProvider(state)); @@ -81,9 +81,12 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ? null : () async { await playlistNotifier.load( - generatedPlaylist.asData!.value.where( - (e) => selectedTracks.value.contains(e.id!), - ), + generatedPlaylist.asData!.value + .where( + (e) => selectedTracks.value + .contains(e.id!), + ) + .toList(), autoPlay: true, ); }, diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index c484046ba..18ce6e28a 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -18,7 +18,7 @@ import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -30,7 +30,7 @@ class LyricsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); String albumArt = useMemoized( () => (playlist.activeTrack?.album?.images).asUrlString( index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, @@ -62,7 +62,7 @@ class LyricsPage extends HookConsumerWidget { const Spacer(), Consumer( builder: (context, ref, child) { - final playback = ref.watch(proxyPlaylistProvider); + final playback = ref.watch(audioPlayerProvider); final lyric = ref.watch(syncedLyricsProvider(playback.activeTrack)); final providerName = lyric.asData?.value.provider; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index f96595387..d92220591 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -15,7 +15,7 @@ import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -31,7 +31,7 @@ class MiniLyricsPage extends HookConsumerWidget { final update = useForceUpdate(); final wasMaximized = useRef(false); - final playlistQueue = ref.watch(proxyPlaylistProvider); + final playlistQueue = ref.watch(audioPlayerProvider); final areaActive = useState(false); final hoverMode = useState(true); @@ -230,14 +230,13 @@ class MiniLyricsPage extends HookConsumerWidget { builder: (context) { return Consumer(builder: (context, ref, _) { final playlist = - ref.watch(proxyPlaylistProvider); + ref.watch(audioPlayerProvider); - return PlayerQueue - .fromProxyPlaylistNotifier( + return PlayerQueue.fromAudioPlayerNotifier( floating: true, playlist: playlist, notifier: ref - .read(proxyPlaylistProvider.notifier), + .read(audioPlayerProvider.notifier), ); }); }, diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 5340e8fd9..7c571d5f3 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class PlainLyrics extends HookConsumerWidget { @@ -27,7 +27,7 @@ class PlainLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack)); final mediaQuery = MediaQuery.of(context); final textTheme = Theme.of(context).textTheme; diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 8a2dd3564..3294bab50 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -12,7 +12,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/modules/lyrics/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -32,7 +32,7 @@ class SyncedLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final mediaQuery = MediaQuery.of(context); final controller = useAutoScrollController(); @@ -54,7 +54,7 @@ class SyncedLyrics extends HookConsumerWidget { final textTheme = Theme.of(context).textTheme; ref.listen( - proxyPlaylistProvider.select((s) => s.activeTrack), + audioPlayerProvider.select((s) => s.activeTrack), (previous, next) { controller.scrollToIndex(0); ref.read(syncedLyricsDelayProvider.notifier).state = 0; diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 93a84f0a6..322a8731a 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -16,7 +16,7 @@ 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/download_manager_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; @@ -201,11 +201,11 @@ class RootApp extends HookConsumerWidget { ), child: Consumer( builder: (context, ref, _) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = - ref.read(proxyPlaylistProvider.notifier); + ref.read(audioPlayerProvider.notifier); - return PlayerQueue.fromProxyPlaylistNotifier( + return PlayerQueue.fromAudioPlayerNotifier( floating: true, playlist: playlist, notifier: playlistNotifier, diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 1bde28727..6ec8f685e 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -8,7 +8,7 @@ import 'package:spotube/components/track_tile/track_tile.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/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class SearchTracksSection extends HookConsumerWidget { @@ -24,8 +24,8 @@ class SearchTracksSection extends HookConsumerWidget { ref.watch(searchProvider(SearchType.track).notifier); final tracks = searchTrack.asData?.value.items.cast() ?? []; - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); - final playlist = ref.watch(proxyPlaylistProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); final theme = Theme.of(context); return Column( diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 1e9b2067a..dc4defc87 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -34,8 +34,8 @@ class TrackPage extends HookConsumerWidget { final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isActive = playlist.activeTrack?.id == trackId; diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 747c78e64..258e15d88 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -1,13 +1,22 @@ +import 'dart:math'; + import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/discord_provider.dart'; +import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class AudioPlayerNotifier extends Notifier { + BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier); + Future _syncSavedState() async { final database = ref.read(databaseProvider); @@ -18,9 +27,9 @@ class AudioPlayerNotifier extends Notifier { await database.into(database.audioPlayerStateTable).insert( AudioPlayerStateTableCompanion.insert( playing: audioPlayer.isPlaying, - volume: audioPlayer.volume, loopMode: audioPlayer.loopMode, shuffled: audioPlayer.isShuffled, + collections: [], id: const Value(0), ), ); @@ -28,7 +37,6 @@ class AudioPlayerNotifier extends Notifier { playerState = await database.select(database.audioPlayerStateTable).getSingle(); } else { - await audioPlayer.setVolume(playerState.volume); await audioPlayer.setLoopMode(playerState.loopMode); await audioPlayer.setShuffle(playerState.shuffled); } @@ -130,15 +138,6 @@ class AudioPlayerNotifier extends Notifier { ), ); }), - audioPlayer.volumeStream.listen((volume) async { - state = state.copyWith(volume: volume); - - await _updatePlayerState( - AudioPlayerStateTableCompanion( - volume: Value(volume), - ), - ); - }), audioPlayer.loopModeStream.listen((loopMode) async { state = state.copyWith(loopMode: loopMode); @@ -177,49 +176,141 @@ class AudioPlayerNotifier extends Notifier { playing: audioPlayer.isPlaying, playlist: audioPlayer.playlist, shuffled: audioPlayer.isShuffled, - volume: audioPlayer.volume, + collections: [], + ); + } + + // Collection related methods + Future addCollections(List collectionIds) async { + state = state.copyWith(collections: [ + ...state.collections, + ...collectionIds, + ]); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + collections: Value(state.collections), + ), ); } + Future addCollection(String collectionId) async { + await addCollections([collectionId]); + } + + Future removeCollections(List collectionIds) async { + state = state.copyWith( + collections: state.collections + .where((element) => !collectionIds.contains(element)) + .toList(), + ); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + collections: Value(state.collections), + ), + ); + } + + Future removeCollection(String collectionId) async { + await removeCollections([collectionId]); + } + // Tracks related methods + Future addTracksAtFirst(Iterable tracks) async { + if (state.tracks.length == 1) { + return addTracks(tracks); + } + + tracks = _blacklist.filter(tracks).toList() as List; + + for (int i = 0; i < tracks.length; i++) { + final track = tracks.elementAt(i); + + await audioPlayer.addTrackAt( + SpotubeMedia(track), + max(state.playlist.index, 0) + i + 1, + ); + } + } + Future addTrack(Track track) async { + if (_blacklist.contains(track)) return; await audioPlayer.addTrack(SpotubeMedia(track)); } Future addTracks(Iterable tracks) async { + tracks = _blacklist.filter(tracks).toList() as List; for (final track in tracks) { - await addTrack(track); + await audioPlayer.addTrack(SpotubeMedia(track)); } } - Future removeTrack(Track track) async { - final index = state.tracks.indexWhere((element) => element == track); + Future removeTrack(String trackId) async { + final index = state.tracks.indexWhere((element) => element.id == trackId); if (index == -1) return; await audioPlayer.removeTrack(index); } - Future removeTracks(Iterable tracks) async { - for (final track in tracks) { - await removeTrack(track); + Future removeTracks(Iterable trackIds) async { + for (final trackId in trackIds) { + await removeTrack(trackId); } } Future load( - List track, { - required int initialIndex, + List tracks, { + int initialIndex = 0, bool autoPlay = false, }) async { + tracks = _blacklist.filter(tracks).toList() as List; + + // Giving the initial track a boost so MediaKit won't skip + // because of timeout + final intendedActiveTrack = tracks.elementAt(initialIndex); + if (intendedActiveTrack is! LocalTrack) { + await ref.read(sourcedTrackProvider(intendedActiveTrack).future); + } + await audioPlayer.openPlaylist( - track.map((t) => SpotubeMedia(t)).toList(), + tracks.asMediaList(), initialIndex: initialIndex, autoPlay: autoPlay, ); } + + Future jumpToTrack(Track track) async { + final index = + state.tracks.toList().indexWhere((element) => element.id == track.id); + if (index == -1) return; + await audioPlayer.jumpTo(index); + } + + Future moveTrack(int oldIndex, int newIndex) async { + if (oldIndex == newIndex || + newIndex < 0 || + oldIndex < 0 || + newIndex > state.tracks.length - 1 || + oldIndex > state.tracks.length - 1) return; + + await audioPlayer.moveTrack(oldIndex, newIndex); + } + + bool isFetching() { + if (state.activeTrack == null) return false; + return ref.read(sourcedTrackProvider(state.activeTrack!)).isLoading; + } + + Future stop() async { + await audioPlayer.stop(); + ref.read(discordProvider.notifier).clear(); + } } -final audioPlayerProvider = NotifierProvider( +final audioPlayerProvider = + NotifierProvider( () => AudioPlayerNotifier(), -); \ No newline at end of file +); diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/audio_player/audio_player_streams.dart similarity index 51% rename from lib/provider/proxy_playlist/player_listeners.dart rename to lib/provider/audio_player/audio_player_streams.dart index 2c1423a5c..d5473dd59 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -1,19 +1,53 @@ -// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - import 'dart:async'; -import 'package:spotube/services/logger/logger.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/provider/discord_provider.dart'; +import 'package:spotube/provider/history/history.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/skip_segments/skip_segments.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/server/sourced_track.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_services/audio_services.dart'; +import 'package:spotube/services/logger/logger.dart'; + +class AudioPlayerStreamListeners { + final Ref ref; + late final AudioServices notificationService; + AudioPlayerStreamListeners(this.ref) { + AudioServices.create(ref, ref.read(audioPlayerProvider.notifier)).then( + (value) => notificationService = value, + ); + + final subscriptions = [ + subscribeToPlaylist(), + subscribeToSkipSponsor(), + subscribeToScrobbleChanged(), + subscribeToPosition(), + subscribeToPlayerError(), + ]; + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + } + + ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); + UserPreferences get preferences => ref.read(userPreferencesProvider); + Discord get discord => ref.read(discordProvider); + AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider); + PlaybackHistoryNotifier get history => + ref.read(playbackHistoryProvider.notifier); -extension ProxyPlaylistListeners on ProxyPlaylistNotifier { Future updatePalette() async { final palette = ref.read(paletteProvider); if (!preferences.albumColorSync) { @@ -21,11 +55,12 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { return; } return Future.microtask(() async { - if (playlist.activeTrack == null) return; + final activeTrack = ref.read(audioPlayerProvider).activeTrack; + if (activeTrack == null) return; final palette = await PaletteGenerator.fromImageProvider( UniversalImage.imageProvider( - (playlist.activeTrack?.album?.images).asUrlString( + (activeTrack.album?.images).asUrlString( placeholder: ImagePlaceholder.albumArt, ), height: 50, @@ -38,15 +73,8 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { StreamSubscription subscribeToPlaylist() { return audioPlayer.playlistStream.listen((mpvPlaylist) { - state = playlist.copyWith( - tracks: mpvPlaylist.medias - .map((media) => SpotubeMedia.fromMedia(media).track) - .toSet(), - active: mpvPlaylist.index, - ); - - notificationService.addTrack(playlist.activeTrack!); - discord.updatePresence(playlist.activeTrack!); + notificationService.addTrack(audioPlayerState.activeTrack!); + discord.updatePresence(audioPlayerState.activeTrack!); updatePalette(); }); } @@ -72,18 +100,18 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { String? lastScrobbled; return audioPlayer.positionStream.listen((position) { try { - final uid = playlist.activeTrack is LocalTrack - ? (playlist.activeTrack as LocalTrack).path - : playlist.activeTrack?.id; + final uid = audioPlayerState.activeTrack is LocalTrack + ? (audioPlayerState.activeTrack as LocalTrack).path + : audioPlayerState.activeTrack?.id; - if (playlist.activeTrack == null || + if (audioPlayerState.activeTrack == null || lastScrobbled == uid || position.inSeconds < 30) { return; } - scrobbler.scrobble(playlist.activeTrack!); - history.addTrack(playlist.activeTrack!); + scrobbler.scrobble(audioPlayerState.activeTrack!); + history.addTrack(audioPlayerState.activeTrack!); lastScrobbled = uid; } catch (e, stack) { AppLogger.reportError(e, stack); @@ -95,9 +123,13 @@ 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) || - playlist.active == null || - playlist.active == playlist.tracks.length - 1) return; - final nextTrack = playlist.tracks.elementAt(playlist.active! + 1); + audioPlayerState.playlist.index == -1 || + audioPlayerState.playlist.index == + audioPlayerState.tracks.length - 1) { + return; + } + final nextTrack = audioPlayerState.tracks + .elementAt(audioPlayerState.playlist.index + 1); if (lastTrack == nextTrack.id || nextTrack is LocalTrack) return; @@ -113,3 +145,6 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { return audioPlayer.errorStream.listen((event) {}); } } + +final audioPlayerStreamListenersProvider = + Provider(AudioPlayerStreamListeners.new); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 3c874011c..685ce112d 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -4,39 +4,97 @@ import 'package:spotube/services/audio_player/audio_player.dart'; class AudioPlayerState { final bool playing; - final double volume; final PlaylistMode loopMode; final bool shuffled; final Playlist playlist; final List tracks; + final List collections; AudioPlayerState({ required this.playing, - required this.volume, required this.loopMode, required this.shuffled, required this.playlist, + required this.collections, List? tracks, }) : tracks = tracks ?? playlist.medias .map((media) => SpotubeMedia.fromMedia(media).track) .toList(); + factory AudioPlayerState.fromJson(Map json) { + return AudioPlayerState( + playing: json['playing'], + loopMode: PlaylistMode.values.firstWhere( + (e) => e.name == json['loopMode'], + orElse: () => audioPlayer.loopMode, + ), + shuffled: json['shuffled'], + playlist: Playlist( + json['playlist']['medias'] + .map((media) => Media( + media['uri'], + extras: media['extras'], + httpHeaders: media['httpHeaders'], + )) + .toList(), + index: json['playlist']['index'], + ), + collections: List.from(json['collections']), + ); + } + + Map toJson() { + return { + 'playing': playing, + 'loopMode': loopMode.name, + 'shuffled': shuffled, + 'playlist': { + 'medias': playlist.medias + .map((media) => { + 'uri': media.uri, + 'extras': media.extras, + 'httpHeaders': media.httpHeaders, + }) + .toList(), + 'index': playlist.index, + }, + 'collections': collections, + }; + } + AudioPlayerState copyWith({ bool? playing, - double? volume, PlaylistMode? loopMode, bool? shuffled, Playlist? playlist, + List? collections, }) { return AudioPlayerState( playing: playing ?? this.playing, - volume: volume ?? this.volume, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, playlist: playlist ?? this.playlist, + collections: collections ?? this.collections, tracks: playlist == null ? tracks : null, ); } + + Track? get activeTrack { + if (playlist.index == -1) return null; + return tracks.elementAtOrNull(playlist.index); + } + + bool containsTrack(Track track) { + return tracks.any((t) => t.id == track.id); + } + + bool containsTracks(List tracks) { + return tracks.every(containsTrack); + } + + bool containsCollection(String collectionId) { + return collections.contains(collectionId); + } } diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index c60144450..28eb131bc 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -1,13 +1,14 @@ import 'dart:convert'; import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/connect/clients.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -31,8 +32,14 @@ final loopModeProvider = StateProvider( (ref) => PlaylistMode.none, ); -final queueProvider = StateProvider( - (ref) => ProxyPlaylist({}), +final queueProvider = StateProvider( + (ref) => AudioPlayerState( + playing: audioPlayer.isPlaying, + loopMode: audioPlayer.loopMode, + shuffled: audioPlayer.isShuffled, + playlist: audioPlayer.playlist, + collections: [], + ), ); final volumeProvider = StateProvider( diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index f90db54ae..29c537625 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -4,7 +4,7 @@ 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/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; @@ -55,7 +55,7 @@ final discordProvider = ChangeNotifierProvider( (ref) { final isEnabled = ref.watch(userPreferencesProvider.select((s) => s.discordPresence)); - final playback = ref.read(proxyPlaylistProvider); + final playback = ref.read(audioPlayerProvider); final discord = Discord(isEnabled); if (playback.activeTrack != null) { diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart deleted file mode 100644 index 9f371b7a4..000000000 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/models/local_track.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; - -class ProxyPlaylist { - final Set tracks; - final Set collections; - final int? active; - - ProxyPlaylist(this.tracks, [this.active, this.collections = const {}]); - - factory ProxyPlaylist.fromJson( - Map json, - ) { - return ProxyPlaylist( - List.castFrom>( - json['tracks'] ?? >[], - ).map((t) => _makeAppropriateTrack(t)).toSet(), - json['active'] as int?, - json['collections'] == null - ? {} - : (json['collections'] as List).toSet().cast(), - ); - } - - factory ProxyPlaylist.fromJsonRaw(Map json) => ProxyPlaylist( - json['tracks'] == null - ? {} - : (json['tracks'] as List).map((t) => Track.fromJson(t)).toSet(), - json['active'] as int?, - json['collections'] == null - ? {} - : (json['collections'] as List).toSet().cast(), - ); - - Track? get activeTrack => - active == null || active == -1 ? null : tracks.elementAtOrNull(active!); - - bool get isFetching => activeTrack == null && tracks.isNotEmpty; - - bool containsCollection(String collection) { - return collections.contains(collection); - } - - bool containsTrack(TrackSimple track) { - 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) { - if (tracks.isEmpty) return false; - return tracks.every(containsTrack); - } - - static Track _makeAppropriateTrack(Map track) { - if (track.containsKey("path")) { - return LocalTrack.fromJson(track); - } else { - return Track.fromJson(track); - } - } - - /// 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) { - // ignore: unnecessary_cast - LocalTrack() => (track as LocalTrack).toJson(), - // ignore: unnecessary_cast - SourcedTrack() => (track as SourcedTrack).toJson(), - _ => track.toJson(), - }; - } - - Map toJson() { - return { - 'tracks': tracks.map(_makeAppropriateTrackJson).toList(), - 'active': active, - 'collections': collections.toList(), - }; - } - - ProxyPlaylist copyWith({ - Set? tracks, - int? active, - Set? collections, - }) { - return ProxyPlaylist( - tracks ?? this.tracks, - active ?? this.active, - collections ?? this.collections, - ); - } -} diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart deleted file mode 100644 index 067d8d44f..000000000 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.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'; -import 'package:spotube/provider/scrobbler/scrobbler.dart'; -import 'package:spotube/provider/server/sourced_track.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/audio_services/audio_services.dart'; -import 'package:spotube/provider/discord_provider.dart'; - -import 'package:spotube/utils/persisted_state_notifier.dart'; - -class ProxyPlaylistNotifier extends PersistedStateNotifier { - final Ref ref; - late final AudioServices notificationService; - - ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); - UserPreferences get preferences => ref.read(userPreferencesProvider); - 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 = []; - - ProxyPlaylistNotifier(this.ref) : super(ProxyPlaylist({}), "playlist") { - AudioServices.create(ref, this).then( - (value) => notificationService = value, - ); - - _subscriptions = [ - // These are subscription methods from player_listeners.dart - subscribeToPlaylist(), - subscribeToSkipSponsor(), - subscribeToPosition(), - subscribeToScrobbleChanged(), - ]; - } - // Basic methods for adding or removing tracks to playlist - - Future addTrack(Track track) async { - if (blacklist.contains(track)) return; - await audioPlayer.addTrack(SpotubeMedia(track)); - } - - Future addTracks(Iterable tracks) async { - tracks = blacklist.filter(tracks).toList() as List; - for (final track in tracks) { - await audioPlayer.addTrack(SpotubeMedia(track)); - } - } - - void addCollection(String collectionId) { - state = state.copyWith(collections: { - ...state.collections, - collectionId, - }); - } - - void removeCollection(String collectionId) { - state = state.copyWith(collections: { - ...state.collections..remove(collectionId), - }); - } - - Future removeTrack(String trackId) async { - final trackIndex = - state.tracks.toList().indexWhere((element) => element.id == trackId); - if (trackIndex == -1) return; - await audioPlayer.removeTrack(trackIndex); - } - - Future removeTracks(Iterable tracksIds) async { - final tracks = state.tracks.map((t) => t.id!).toList(); - - for (final track in tracks) { - final index = tracks.indexOf(track); - if (index == -1) continue; - await audioPlayer.removeTrack(index); - } - } - - Future load( - Iterable tracks, { - int initialIndex = 0, - bool autoPlay = false, - }) async { - tracks = blacklist.filter(tracks).toList() as List; - - state = state.copyWith(collections: {}); - - // Giving the initial track a boost so MediaKit won't skip - // because of timeout - final intendedActiveTrack = tracks.elementAt(initialIndex); - if (intendedActiveTrack is! LocalTrack) { - await ref.read(sourcedTrackProvider(intendedActiveTrack).future); - } - - await audioPlayer.openPlaylist( - tracks.asMediaList(), - initialIndex: initialIndex, - autoPlay: autoPlay, - ); - } - - Future jumpTo(int index) async { - await audioPlayer.jumpTo(index); - } - - Future jumpToTrack(Track track) async { - final index = - state.tracks.toList().indexWhere((element) => element.id == track.id); - if (index == -1) return; - await jumpTo(index); - } - - Future moveTrack(int oldIndex, int newIndex) async { - if (oldIndex == newIndex || - newIndex < 0 || - oldIndex < 0 || - newIndex > state.tracks.length - 1 || - oldIndex > state.tracks.length - 1) return; - - await audioPlayer.moveTrack(oldIndex, newIndex); - } - - Future addTracksAtFirst(Iterable tracks) async { - if (state.tracks.length == 1) { - return addTracks(tracks); - } - - tracks = blacklist.filter(tracks).toList() as List; - - for (int i = 0; i < tracks.length; i++) { - final track = tracks.elementAt(i); - - await audioPlayer.addTrackAt( - SpotubeMedia(track), - (state.active ?? 0) + i + 1, - ); - } - } - - Future next() async { - await audioPlayer.skipToNext(); - } - - Future previous() async { - await audioPlayer.skipToPrevious(); - } - - Future stop() async { - state = ProxyPlaylist({}); - await audioPlayer.stop(); - discord.clear(); - } - - @override - set state(state) { - super.state = state; - if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { - ref.read(paletteProvider.notifier).state = null; - } else { - updatePalette(); - } - } - - @override - onInit() async { - if (state.tracks.isEmpty) return null; - final oldCollections = state.collections; - await load( - state.tracks, - initialIndex: max(state.active ?? 0, 0), - autoPlay: false, - ); - state = state.copyWith(collections: oldCollections); - } - - @override - FutureOr fromJson(Map json) { - return ProxyPlaylist.fromJson(json); - } - - @override - Map toJson() { - final json = state.toJson(); - return json; - } - - @override - void dispose() { - for (final subscription in _subscriptions) { - subscription.cancel(); - } - super.dispose(); - } -} - -final proxyPlaylistProvider = - StateNotifierProvider( - (ref) => ProxyPlaylistNotifier(ref), -); diff --git a/lib/provider/server/active_sourced_track.dart b/lib/provider/server/active_sourced_track.dart index 410b788cb..685896ec0 100644 --- a/lib/provider/server/active_sourced_track.dart +++ b/lib/provider/server/active_sourced_track.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -28,7 +28,7 @@ class ActiveSourcedTrackNotifier extends Notifier { state = newTrack; await audioPlayer.pause(); - final playbackNotifier = ref.read(proxyPlaylistProvider.notifier); + final playbackNotifier = ref.read(audioPlayerProvider.notifier); final oldActiveIndex = audioPlayer.currentIndex; await playbackNotifier.addTracksAtFirst([newTrack]); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart index eee3365e9..a2fa70b80 100644 --- a/lib/provider/server/routes/connect.dart +++ b/lib/provider/server/routes/connect.dart @@ -9,7 +9,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -38,8 +38,8 @@ class ServerConnectRoutes { }); } - ProxyPlaylistNotifier get playbackNotifier => - ref.read(proxyPlaylistProvider.notifier); + AudioPlayerNotifier get audioPlayerNotifier => + ref.read(audioPlayerProvider.notifier); PlaybackHistoryNotifier get historyNotifier => ref.read(playbackHistoryProvider.notifier); Stream get connectClientStream => @@ -57,7 +57,7 @@ class ServerConnectRoutes { _connectClientStreamController.add(origin); ref.listen( - proxyPlaylistProvider, + audioPlayerProvider, (previous, next) { channel.sink.addEvent(WebSocketQueueEvent(next)); }, @@ -67,10 +67,10 @@ class ServerConnectRoutes { // because audioPlayer events doesn't fireImmediately channel.sink.addEvent(WebSocketPlayingEvent(audioPlayer.isPlaying)); channel.sink.addEvent( - WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero), + WebSocketPositionEvent(audioPlayer.position), ); channel.sink.addEvent( - WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero), + WebSocketDurationEvent(audioPlayer.duration), ); channel.sink.addEvent(WebSocketShuffleEvent(audioPlayer.isShuffled)); channel.sink.addEvent(WebSocketLoopEvent(audioPlayer.loopMode)); @@ -116,14 +116,14 @@ class ServerConnectRoutes { ); event.onLoad((event) async { - await playbackNotifier.load( + await audioPlayerNotifier.load( event.data.tracks, autoPlay: true, initialIndex: event.data.initialIndex ?? 0, ); if (event.data.collectionId == null) return; - playbackNotifier.addCollection(event.data.collectionId!); + audioPlayerNotifier.addCollection(event.data.collectionId!); if (event.data.collection is AlbumSimple) { historyNotifier .addAlbums([event.data.collection as AlbumSimple]); @@ -146,15 +146,15 @@ class ServerConnectRoutes { }); event.onNext((event) async { - await playbackNotifier.next(); + await audioPlayer.skipToNext(); }); event.onPrevious((event) async { - await playbackNotifier.previous(); + await audioPlayer.skipToPrevious(); }); event.onJump((event) async { - await playbackNotifier.jumpTo(event.data); + await audioPlayer.jumpTo(event.data); }); event.onSeek((event) async { @@ -170,15 +170,15 @@ class ServerConnectRoutes { }); event.onAddTrack((event) async { - await playbackNotifier.addTrack(event.data); + await audioPlayerNotifier.addTrack(event.data); }); event.onRemoveTrack((event) async { - await playbackNotifier.removeTrack(event.data); + await audioPlayerNotifier.removeTrack(event.data); }); event.onReorder((event) async { - await playbackNotifier.moveTrack( + await audioPlayerNotifier.moveTrack( event.data.oldIndex, event.data.newIndex, ); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 679f58b10..f29aecf48 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -2,8 +2,8 @@ import 'package:dio/dio.dart' hide Response; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelf/shelf.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -12,7 +12,7 @@ import 'package:spotube/services/logger/logger.dart'; class ServerPlaybackRoutes { final Ref ref; UserPreferences get userPreferences => ref.read(userPreferencesProvider); - ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider); + AudioPlayerState get playlist => ref.read(audioPlayerProvider); final Dio dio; ServerPlaybackRoutes(this.ref) : dio = Dio(); diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart index 82c7ddcd0..37c889b02 100644 --- a/lib/provider/server/sourced_track.dart +++ b/lib/provider/server/sourced_track.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; final sourcedTrackProvider = @@ -12,10 +12,9 @@ final sourcedTrackProvider = } ref.listen( - proxyPlaylistProvider, + audioPlayerProvider.select((value) => value.tracks), (old, next) { - if (next.tracks.isEmpty || - next.tracks.none((element) => element.id == track.id)) { + if (next.isEmpty || next.none((element) => element.id == track.id)) { ref.invalidateSelf(); } }, diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/skip_segments/skip_segments.dart similarity index 100% rename from lib/provider/proxy_playlist/skip_segments.dart rename to lib/provider/skip_segments/skip_segments.dart diff --git a/lib/provider/tray_manager/tray_menu.dart b/lib/provider/tray_manager/tray_menu.dart index 35aca4f5f..42a3f948e 100644 --- a/lib/provider/tray_manager/tray_menu.dart +++ b/lib/provider/tray_manager/tray_menu.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:tray_manager/tray_manager.dart'; @@ -19,9 +19,9 @@ final audioPlayerPlaying = StreamProvider((ref) { }); final trayMenuProvider = Provider((ref) { - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isPlaybackPlaying = - ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null)); + ref.watch(audioPlayerProvider.select((s) => s.activeTrack != null)); final isLoopOne = ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single; final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false; @@ -56,14 +56,14 @@ final trayMenuProvider = Provider((ref) { label: "Next", disabled: !isPlaybackPlaying, onClick: (menuItem) { - playlistNotifier.next(); + audioPlayer.skipToNext(); }, ), MenuItem( label: "Previous", disabled: !isPlaybackPlaying, onClick: (menuItem) { - playlistNotifier.previous(); + audioPlayer.skipToPrevious(); }, ), MenuItem.submenu( diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index a730c313b..a421e7d09 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -6,10 +6,9 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.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/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/platform.dart'; @@ -115,7 +114,7 @@ class UserPreferencesNotifier extends Notifier { if (!sync) { ref.read(paletteProvider.notifier).state = null; } else { - ref.read(proxyPlaylistProvider.notifier).updatePalette(); + ref.read(audioPlayerStreamListenersProvider).updatePalette(); } } diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index f42d6c4bf..63e43c4dd 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.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'; @@ -17,7 +17,7 @@ class AudioServices { static Future create( Ref ref, - ProxyPlaylistNotifier playback, + AudioPlayerNotifier playback, ) async { final mobile = kIsMobile || kIsMacOS || kIsLinux ? await AudioService.init( diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 3dbae18f3..cdd16138e 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -2,19 +2,19 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:media_kit/media_kit.dart' hide Track; class MobileAudioService extends BaseAudioHandler { AudioSession? session; - final ProxyPlaylistNotifier playlistNotifier; + final AudioPlayerNotifier audioPlayerNotifier; // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - ProxyPlaylist get playlist => playlistNotifier.state; + AudioPlayerState get playlist => audioPlayerNotifier.state; - MobileAudioService(this.playlistNotifier) { + MobileAudioService(this.audioPlayerNotifier) { AudioSession.instance.then((s) { session = s; session?.configure(const AudioSessionConfiguration.music()); @@ -102,24 +102,24 @@ class MobileAudioService extends BaseAudioHandler { @override Future stop() async { - await playlistNotifier.stop(); + await audioPlayerNotifier.stop(); } @override Future skipToNext() async { - await playlistNotifier.next(); + await audioPlayer.skipToNext(); await super.skipToNext(); } @override Future skipToPrevious() async { - await playlistNotifier.previous(); + await audioPlayer.skipToPrevious(); await super.skipToPrevious(); } @override Future onTaskRemoved() async { - await playlistNotifier.stop(); + await audioPlayerNotifier.stop(); return super.onTaskRemoved(); } @@ -146,7 +146,7 @@ class MobileAudioService extends BaseAudioHandler { PlaylistMode.single => AudioServiceRepeatMode.one, _ => AudioServiceRepeatMode.none, }, - processingState: playlist.isFetching == true + processingState: audioPlayer.isBuffering ? AudioProcessingState.loading : AudioProcessingState.ready, ); diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index a3ee31e14..0b3113fc3 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -5,18 +5,18 @@ import 'package:smtc_windows/smtc_windows.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; class WindowsAudioService { final SMTCWindows smtc; final Ref ref; - final ProxyPlaylistNotifier playlistNotifier; + final AudioPlayerNotifier audioPlayerNotifier; final subscriptions = []; - WindowsAudioService(this.ref, this.playlistNotifier) + WindowsAudioService(this.ref, this.audioPlayerNotifier) : smtc = SMTCWindows(enabled: false) { smtc.setPlaybackStatus(PlaybackStatus.Stopped); final buttonStream = smtc.buttonPressStream.listen((event) { @@ -28,13 +28,13 @@ class WindowsAudioService { audioPlayer.pause(); break; case PressedButton.next: - playlistNotifier.next(); + audioPlayer.skipToNext(); break; case PressedButton.previous: - playlistNotifier.previous(); + audioPlayer.skipToPrevious(); break; case PressedButton.stop: - playlistNotifier.stop(); + audioPlayerNotifier.stop(); break; default: break; From 75173e5096209104763059f812961982bd3755e6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 21:01:09 +0600 Subject: [PATCH 28/92] refactor: use provider based is track loading implementation --- lib/collections/intents.dart | 6 +- lib/components/track_tile/track_tile.dart | 325 +++++++++--------- lib/modules/album/album_card.dart | 6 +- lib/modules/player/player_controls.dart | 22 +- lib/modules/player/player_overlay.dart | 9 +- lib/modules/player/sibling_tracks_sheet.dart | 11 +- lib/modules/playlist/playlist_card.dart | 5 +- lib/provider/audio_player/audio_player.dart | 5 - .../audio_player/querying_track_info.dart | 12 + 9 files changed, 205 insertions(+), 196 deletions(-) create mode 100644 lib/provider/audio_player/querying_track_info.dart diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 1a44a8465..ac0451ac9 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -11,7 +11,7 @@ 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/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; @@ -96,8 +96,8 @@ class SeekIntent extends Intent { class SeekAction extends Action { @override invoke(intent) async { - final playlist = intent.ref.read(audioPlayerProvider.notifier); - if (playlist.isFetching()) { + final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider); + if (isFetchingActiveTrack) { DirectionalFocusAction().invoke( DirectionalFocusIntent( intent.forward ? TraversalDirection.right : TraversalDirection.left, diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index cdc18d9b9..0e8d2cd0b 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -17,7 +17,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; @@ -84,191 +84,190 @@ class TrackTile extends HookConsumerWidget { }, child: HoverBuilder( permanentState: isSelected || constrains.smAndDown ? true : null, - builder: (context, isHovering) { - return ListTile( - selected: isSelected, - onTap: () async { - try { - isLoading.value = true; - await onTap?.call(); - } finally { - if (context.mounted) { - isLoading.value = false; - } + builder: (context, isHovering) => ListTile( + selected: isSelected, + onTap: () async { + try { + isLoading.value = true; + await onTap?.call(); + } finally { + if (context.mounted) { + isLoading.value = false; } - }, - onLongPress: onLongPress, - enabled: !isBlackListed, - contentPadding: EdgeInsets.zero, - tileColor: - isBlackListed ? theme.colorScheme.errorContainer : null, - horizontalTitleGap: 12, - leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ...?leadingActions, - if (index != null && onChanged == null && constrains.mdAndUp) - SizedBox( - width: 50, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - '${(index ?? 0) + 1}', - maxLines: 1, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), + } + }, + onLongPress: onLongPress, + enabled: !isBlackListed, + contentPadding: EdgeInsets.zero, + tileColor: isBlackListed ? theme.colorScheme.errorContainer : null, + horizontalTitleGap: 12, + leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...?leadingActions, + if (index != null && onChanged == null && constrains.mdAndUp) + SizedBox( + width: 50, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + '${(index ?? 0) + 1}', + maxLines: 1, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, ), - ) - else if (constrains.smAndDown) - const SizedBox(width: 16), - if (onChanged != null) - Checkbox( - value: selected, - onChanged: onChanged, ), - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: AspectRatio( - aspectRatio: 1, - child: UniversalImage( - path: (track.album?.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - ), - fit: BoxFit.cover, + ) + else if (constrains.smAndDown) + const SizedBox(width: 16), + if (onChanged != null) + Checkbox( + value: selected, + onChanged: onChanged, + ), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: AspectRatio( + aspectRatio: 1, + child: UniversalImage( + path: (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, ), + fit: BoxFit.cover, ), ), - Positioned.fill( - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: isHovering - ? Colors.black.withOpacity(0.4) - : Colors.transparent, - ), + ), + Positioned.fill( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: isHovering + ? Colors.black.withOpacity(0.4) + : Colors.transparent, ), ), - Positioned.fill( - child: Center( - child: IconTheme( - data: theme.iconTheme - .copyWith(size: 26, color: Colors.white), - child: Skeleton.ignore( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: (isPlaying && - ref - .watch(audioPlayerProvider - .notifier) - .isFetching()) || - isLoading.value - ? const SizedBox( - width: 26, - height: 26, - child: CircularProgressIndicator( - strokeWidth: 1.5, - color: Colors.white, - ), - ) - : isPlaying - ? Icon( - SpotubeIcons.pause, - color: theme.colorScheme.primary, - ) - : !isHovering - ? const SizedBox.shrink() - : const Icon(SpotubeIcons.play), - ), + ), + Positioned.fill( + child: Center( + child: IconTheme( + data: theme.iconTheme + .copyWith(size: 26, color: Colors.white), + child: Skeleton.ignore( + child: Consumer( + builder: (context, ref, _) { + final isFetchingActiveTrack = + ref.watch(queryingTrackInfoProvider); + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: (isPlaying && isFetchingActiveTrack) || + isLoading.value + ? const SizedBox( + width: 26, + height: 26, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: Colors.white, + ), + ) + : isPlaying + ? Icon( + SpotubeIcons.pause, + color: theme.colorScheme.primary, + ) + : !isHovering + ? const SizedBox.shrink() + : const Icon(SpotubeIcons.play), + ); + }, ), ), ), ), - ], - ), - ], - ), - title: Row( - children: [ + ), + ], + ), + ], + ), + title: Row( + children: [ + Expanded( + flex: 6, + 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: 6, + flex: 4, child: switch (track) { LocalTrack() => Text( - track.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - _ => LinkText( - track.name!, - "/track/${track.id}", - push: true, + track.album!.name!, maxLines: 1, overflow: TextOverflow.ellipsis, ), - }, - ), - if (constrains.mdAndUp) ...[ - const SizedBox(width: 8), - Expanded( - flex: 4, - child: switch (track) { - LocalTrack() => Text( + _ => Align( + alignment: Alignment.centerLeft, + child: LinkText( track.album!.name!, - maxLines: 1, + "/album/${track.album?.id}", + extra: track.album, + push: true, overflow: TextOverflow.ellipsis, ), - _ => Align( - alignment: Alignment.centerLeft, - child: LinkText( - track.album!.name!, - "/album/${track.album?.id}", - extra: track.album, - push: true, - overflow: TextOverflow.ellipsis, - ), - ) - }, - ), - ], - ], - ), - subtitle: Align( - alignment: Alignment.centerLeft, - child: track is LocalTrack - ? Text( - track.artists?.asString() ?? '', - ) - : ClipRect( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 40), - child: ArtistLink(artists: track.artists ?? []), - ), - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 8), - Text( - Duration(milliseconds: track.durationMs ?? 0) - .toHumanReadableString(padZero: false), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - TrackOptions( - track: track, - playlistId: playlistId, - userPlaylist: userPlaylist, - showMenuCbRef: showOptionCbRef, + ) + }, ), ], - ), - ); - }, + ], + ), + subtitle: Align( + alignment: Alignment.centerLeft, + child: track is LocalTrack + ? Text( + track.artists?.asString() ?? '', + ) + : ClipRect( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 40), + child: ArtistLink(artists: track.artists ?? []), + ), + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + Text( + Duration(milliseconds: track.durationMs ?? 0) + .toHumanReadableString(padZero: false), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + TrackOptions( + track: track, + playlistId: playlistId, + userPlaylist: userPlaylist, + showMenuCbRef: showOptionCbRef, + ), + ], + ), + ), ), ); }); diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index f9f70c66a..de7aa5f8f 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -10,6 +10,7 @@ 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/audio_player/querying_track_info.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -35,6 +36,7 @@ class AlbumCard extends HookConsumerWidget { useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); bool isPlaylistPlaying = useMemoized( () => playlist.containsCollection(album.id!), @@ -59,8 +61,8 @@ class AlbumCard extends HookConsumerWidget { ), margin: const EdgeInsets.symmetric(horizontal: 10), isPlaying: isPlaylistPlaying, - isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || - updating.value, + isLoading: + (isPlaylistPlaying && isFetchingActiveTrack) || updating.value, title: album.name!, description: "${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}", diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index c5ef82d63..25080b669 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerControls extends HookConsumerWidget { @@ -43,8 +43,7 @@ class PlayerControls extends HookConsumerWidget { SeekIntent: SeekAction(), }, []); - ref.watch(audioPlayerProvider); - final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; @@ -132,7 +131,7 @@ class PlayerControls extends HookConsumerWidget { // than total duration. Keeping it resolved value: progress.value.toDouble(), secondaryTrackValue: bufferProgress, - onChanged: playlistNotifier.isFetching() + onChanged: isFetchingActiveTrack ? null : (v) { progress.value = v; @@ -183,7 +182,7 @@ class PlayerControls extends HookConsumerWidget { : context.l10n.shuffle_playlist, icon: const Icon(SpotubeIcons.shuffle), style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : () { if (shuffled) { @@ -198,7 +197,7 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.previous_track, icon: const Icon(SpotubeIcons.skipBack), style: buttonStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToPrevious, ), @@ -206,7 +205,7 @@ class PlayerControls extends HookConsumerWidget { tooltip: playing ? context.l10n.pause_playback : context.l10n.resume_playback, - icon: playlistNotifier.isFetching() + icon: isFetchingActiveTrack ? SizedBox( height: 20, width: 20, @@ -219,7 +218,7 @@ class PlayerControls extends HookConsumerWidget { playing ? SpotubeIcons.pause : SpotubeIcons.play, ), style: resumePauseStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : Actions.handler( context, @@ -230,9 +229,8 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.next_track, icon: const Icon(SpotubeIcons.skipForward), style: buttonStyle, - onPressed: playlistNotifier.isFetching() - ? null - : audioPlayer.skipToNext, + onPressed: + isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), StreamBuilder( stream: audioPlayer.loopModeStream, @@ -253,7 +251,7 @@ class PlayerControls extends HookConsumerWidget { loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : () async { await audioPlayer.setLoopMode(loopMode); diff --git a/lib/modules/player/player_overlay.dart b/lib/modules/player/player_overlay.dart index c1b285eef..2322bcbac 100644 --- a/lib/modules/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -12,6 +12,7 @@ import 'package:spotube/collections/intents.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerOverlay extends HookConsumerWidget { @@ -24,7 +25,7 @@ class PlayerOverlay extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final playlist = ref.watch(audioPlayerProvider); final canShow = playlist.activeTrack != null; @@ -127,14 +128,14 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipBack, color: textColor, ), - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToPrevious, ), Consumer( builder: (context, ref, _) { return IconButton( - icon: playlistNotifier.isFetching() + icon: isFetchingActiveTrack ? const SizedBox( height: 20, width: 20, @@ -158,7 +159,7 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipForward, color: textColor, ), - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 8592f1e3f..ddc77b153 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -16,6 +16,7 @@ import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -54,7 +55,7 @@ class SiblingTracksSheet extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final playlist = ref.watch(audioPlayerProvider); - final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final preferences = ref.watch(userPreferencesProvider); final isSearching = useState(false); @@ -130,7 +131,7 @@ class SiblingTracksSheet extends HookConsumerWidget { ]); final siblings = useMemoized( - () => playlistNotifier.isFetching() + () => isFetchingActiveTrack ? [ (activeTrack as SourcedTrack).sourceInfo, ...activeTrack.siblings, @@ -176,12 +177,12 @@ class SiblingTracksSheet extends HookConsumerWidget { Text(" • ${sourceInfo.artist}"), ], ), - enabled: !playlistNotifier.isFetching(), - selected: !playlistNotifier.isFetching() && + enabled: !isFetchingActiveTrack, + selected: !isFetchingActiveTrack && sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, selectedTileColor: theme.popupMenuTheme.color, onTap: () { - if (!playlistNotifier.isFetching() && + if (!isFetchingActiveTrack && sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { activeTrackNotifier.swapSibling(sourceInfo); Navigator.of(context).pop(); diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index c41647013..4e81a2547 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -7,6 +7,7 @@ import 'package:spotube/components/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/audio_player/querying_track_info.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -24,6 +25,7 @@ class PlaylistCard extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlistQueue = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final historyNotifier = ref.read(playbackHistoryProvider.notifier); final playing = @@ -65,8 +67,7 @@ class PlaylistCard extends HookConsumerWidget { placeholder: ImagePlaceholder.collection, ), isPlaying: isPlaylistPlaying, - isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || - updating.value, + isLoading: (isPlaylistPlaying && isFetchingActiveTrack) || updating.value, isOwner: playlist.owner?.id == me.asData?.value.id && me.asData?.value.id != null, onTap: () { diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 258e15d88..06081b19c 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -299,11 +299,6 @@ class AudioPlayerNotifier extends Notifier { await audioPlayer.moveTrack(oldIndex, newIndex); } - bool isFetching() { - if (state.activeTrack == null) return false; - return ref.read(sourcedTrackProvider(state.activeTrack!)).isLoading; - } - Future stop() async { await audioPlayer.stop(); ref.read(discordProvider.notifier).clear(); diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart new file mode 100644 index 000000000..4069523ba --- /dev/null +++ b/lib/provider/audio_player/querying_track_info.dart @@ -0,0 +1,12 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/server/sourced_track.dart'; + +final queryingTrackInfoProvider = Provider((ref) { + final activeTrack = + ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); + + if (activeTrack == null) return false; + + return ref.read(sourcedTrackProvider(activeTrack)).isLoading; +}); From a621a45f0bbbb4dfc4b342388996827fb64dfad8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 21:43:09 +0600 Subject: [PATCH 29/92] chore: fix alternative track sources not showing up --- lib/modules/player/player_controls.dart | 57 +++++++++++-------- lib/modules/player/sibling_tracks_sheet.dart | 4 +- .../audio_player/querying_track_info.dart | 12 +++- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index 25080b669..1b9d9f86f 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -11,6 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -232,32 +233,38 @@ class PlayerControls extends HookConsumerWidget { onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), - StreamBuilder( - stream: audioPlayer.loopModeStream, - builder: (context, snapshot) { - final loopMode = snapshot.data ?? PlaylistMode.none; - return IconButton( - tooltip: loopMode == PlaylistMode.single - ? context.l10n.loop_track - : loopMode == PlaylistMode.loop - ? context.l10n.repeat_playlist - : null, - icon: Icon( - loopMode == PlaylistMode.single - ? SpotubeIcons.repeatOne - : SpotubeIcons.repeat, - ), - style: loopMode == PlaylistMode.single || - loopMode == PlaylistMode.loop - ? activeButtonStyle - : buttonStyle, - onPressed: isFetchingActiveTrack - ? null - : () async { - await audioPlayer.setLoopMode(loopMode); + Consumer(builder: (context, ref, _) { + final loopMode = ref + .watch(audioPlayerProvider.select((s) => s.loopMode)); + + return IconButton( + tooltip: loopMode == PlaylistMode.single + ? context.l10n.loop_track + : loopMode == PlaylistMode.loop + ? context.l10n.repeat_playlist + : null, + icon: Icon( + loopMode == PlaylistMode.single + ? SpotubeIcons.repeatOne + : SpotubeIcons.repeat, + ), + style: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop + ? activeButtonStyle + : buttonStyle, + onPressed: isFetchingActiveTrack + ? null + : () async { + await audioPlayer.setLoopMode( + switch (loopMode) { + PlaylistMode.loop => PlaylistMode.single, + PlaylistMode.single => PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, }, - ); - }), + ); + }, + ); + }), ], ), const SizedBox(height: 5) diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index ddc77b153..092d631f8 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -131,13 +131,13 @@ class SiblingTracksSheet extends HookConsumerWidget { ]); final siblings = useMemoized( - () => isFetchingActiveTrack + () => !isFetchingActiveTrack ? [ (activeTrack as SourcedTrack).sourceInfo, ...activeTrack.siblings, ] : [], - [activeTrack], + [activeTrack, isFetchingActiveTrack], ); final borderRadius = floating diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index 4069523ba..f03efd9e9 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -1,12 +1,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/sourced_track.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; final queryingTrackInfoProvider = Provider((ref) { + final media = audioPlayer.playlist.index == -1 + ? null + : audioPlayer.playlist.medias.elementAtOrNull(audioPlayer.playlist.index); + final audioPlayerActiveTrack = + media == null ? null : SpotubeMedia.fromMedia(media).track; + final activeTrack = - ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); + ref.watch(audioPlayerProvider.select((s) => s.activeTrack)) ?? + audioPlayerActiveTrack; if (activeTrack == null) return false; - return ref.read(sourcedTrackProvider(activeTrack)).isLoading; + return ref.watch(sourcedTrackProvider(activeTrack)).isLoading; }); From 1b420e661beae8af86b4f639ce6c3569ffbc5f48 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 22:26:44 +0600 Subject: [PATCH 30/92] chore: player skipping all tracks from cache --- lib/provider/audio_player/audio_player.dart | 10 +++++++--- lib/provider/audio_player/state.dart | 12 +++++++----- lib/provider/server/server.dart | 7 +++---- lib/services/audio_player/audio_player.dart | 16 +++++++++++++--- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 06081b19c..e5db78c09 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -72,14 +72,18 @@ class AudioPlayerNotifier extends Notifier { ], ); }); - } else { + } else if (medias.isNotEmpty) { await audioPlayer.openPlaylist( medias - .map((media) => Media( + .map( + (media) => SpotubeMedia.fromMedia( + Media( media.uri, extras: media.extras, httpHeaders: media.httpHeaders, - )) + ), + ), + ) .toList(), initialIndex: playlist.index, ); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 685ce112d..3572e2894 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -33,11 +33,13 @@ class AudioPlayerState { shuffled: json['shuffled'], playlist: Playlist( json['playlist']['medias'] - .map((media) => Media( - media['uri'], - extras: media['extras'], - httpHeaders: media['httpHeaders'], - )) + .map( + (media) => SpotubeMedia.fromMedia(Media( + media['uri'], + extras: media['extras'], + httpHeaders: media['httpHeaders'], + )), + ) .toList(), index: json['playlist']['index'], ), diff --git a/lib/provider/server/server.dart b/lib/provider/server/server.dart index 5232bb17b..131f1ea47 100644 --- a/lib/provider/server/server.dart +++ b/lib/provider/server/server.dart @@ -5,15 +5,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelf/shelf_io.dart'; import 'package:spotube/provider/server/pipeline.dart'; import 'package:spotube/provider/server/router.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; -int serverPort = 0; final serverProvider = FutureProvider( (ref) async { final pipeline = ref.watch(pipelineProvider); final router = ref.watch(serverRouterProvider); + final port = Random().nextInt(17500) + 5000; - final port = Random().nextInt(17000) + 1500; + SpotubeMedia.serverPort = port; final server = await serve( pipeline.addHandler(router.call), @@ -28,8 +29,6 @@ final serverProvider = FutureProvider( server.close(); }); - serverPort = port; - return (server: server, port: port); }, ); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 713d518b5..0b62c0684 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:media_kit/media_kit.dart' hide Track; -import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotify/spotify.dart' hide Playlist; @@ -20,9 +19,11 @@ part 'audio_player_impl.dart'; class SpotubeMedia extends mk.Media { final Track track; + static int serverPort = 0; + SpotubeMedia( this.track, { - Map? extras, + Map? extras, super.httpHeaders, }) : super( track is LocalTrack @@ -38,11 +39,20 @@ class SpotubeMedia extends mk.Media { }, ); + @override + String get uri => track is LocalTrack + ? (track as LocalTrack).path + : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; + factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http") ? Track.fromJson(media.extras?["track"]) : LocalTrack.fromJson(media.extras?["track"]); - return SpotubeMedia(track); + return SpotubeMedia( + track, + extras: media.extras, + httpHeaders: media.httpHeaders, + ); } } From 6c5cab9899080450bbd077c206bb8dd41c8e9b5d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 25 Jun 2024 20:36:23 +0600 Subject: [PATCH 31/92] chore: fix use SpotubeMedia to avoid duplicate sourceTrackProvider instances --- lib/provider/audio_player/audio_player.dart | 8 +++++--- .../audio_player/audio_player_streams.dart | 10 ++++++---- .../audio_player/querying_track_info.dart | 18 +++++++++++------- lib/provider/audio_player/state.dart | 5 +++++ lib/provider/server/routes/playback.dart | 3 ++- lib/provider/server/sourced_track.dart | 5 +++-- lib/services/audio_player/audio_player.dart | 15 +++++++++++++++ 7 files changed, 47 insertions(+), 17 deletions(-) diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index e5db78c09..9dfc2c0a1 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -86,6 +86,7 @@ class AudioPlayerNotifier extends Notifier { ) .toList(), initialIndex: playlist.index, + autoPlay: false, ); } } @@ -270,17 +271,18 @@ class AudioPlayerNotifier extends Notifier { int initialIndex = 0, bool autoPlay = false, }) async { - tracks = _blacklist.filter(tracks).toList() as List; + final medias = + (_blacklist.filter(tracks).toList() as List).asMediaList(); // Giving the initial track a boost so MediaKit won't skip // because of timeout - final intendedActiveTrack = tracks.elementAt(initialIndex); + final intendedActiveTrack = medias.elementAt(initialIndex); if (intendedActiveTrack is! LocalTrack) { await ref.read(sourcedTrackProvider(intendedActiveTrack).future); } await audioPlayer.openPlaylist( - tracks.asMediaList(), + medias, initialIndex: initialIndex, autoPlay: autoPlay, ); diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index d5473dd59..429440758 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -128,15 +128,17 @@ class AudioPlayerStreamListeners { audioPlayerState.tracks.length - 1) { return; } - final nextTrack = audioPlayerState.tracks - .elementAt(audioPlayerState.playlist.index + 1); + final nextTrack = SpotubeMedia.fromMedia(audioPlayerState.playlist.medias + .elementAt(audioPlayerState.playlist.index + 1)); - if (lastTrack == nextTrack.id || nextTrack is LocalTrack) return; + if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { + return; + } try { await ref.read(sourcedTrackProvider(nextTrack).future); } finally { - lastTrack = nextTrack.id!; + lastTrack = nextTrack.track.id!; } }); } diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index f03efd9e9..55590d481 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -4,17 +4,21 @@ import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; final queryingTrackInfoProvider = Provider((ref) { - final media = audioPlayer.playlist.index == -1 + final media = audioPlayer.playlist.index == -1 || + audioPlayer.playlist.medias.isEmpty ? null : audioPlayer.playlist.medias.elementAtOrNull(audioPlayer.playlist.index); final audioPlayerActiveTrack = - media == null ? null : SpotubeMedia.fromMedia(media).track; + media == null ? null : SpotubeMedia.fromMedia(media); - final activeTrack = - ref.watch(audioPlayerProvider.select((s) => s.activeTrack)) ?? - audioPlayerActiveTrack; + final activeMedia = ref.watch(audioPlayerProvider.select( + (s) => s.activeMedia == null + ? null + : SpotubeMedia.fromMedia(s.activeMedia!), + )) ?? + audioPlayerActiveTrack; - if (activeTrack == null) return false; + if (activeMedia == null) return false; - return ref.watch(sourcedTrackProvider(activeTrack)).isLoading; + return ref.watch(sourcedTrackProvider(activeMedia)).isLoading; }); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 3572e2894..387c2e30a 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -88,6 +88,11 @@ class AudioPlayerState { return tracks.elementAtOrNull(playlist.index); } + Media? get activeMedia { + if (playlist.index == -1 || playlist.medias.isEmpty) return null; + return playlist.medias.elementAt(playlist.index); + } + bool containsTrack(Track track) { return tracks.any((t) => t.id == track.id); } diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index f29aecf48..aa380d01d 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -7,6 +7,7 @@ import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; class ServerPlaybackRoutes { @@ -25,7 +26,7 @@ class ServerPlaybackRoutes { final activeSourcedTrack = ref.read(activeSourcedTrackProvider); final sourcedTrack = activeSourcedTrack?.id == track.id ? activeSourcedTrack - : await ref.read(sourcedTrackProvider(track).future); + : await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future); ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart index 37c889b02..53a040232 100644 --- a/lib/provider/server/sourced_track.dart +++ b/lib/provider/server/sourced_track.dart @@ -1,12 +1,13 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; final sourcedTrackProvider = - FutureProvider.family((ref, track) async { + FutureProvider.family((ref, media) async { + final track = media?.track; if (track == null || track is LocalTrack) { return null; } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 0b62c0684..bb1a62036 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -54,6 +54,21 @@ class SpotubeMedia extends mk.Media { httpHeaders: media.httpHeaders, ); } + + @override + operator ==(Object other) { + if (other is! SpotubeMedia) return false; + + final isLocal = track is LocalTrack && other.track is LocalTrack; + return isLocal + ? (other.track as LocalTrack).path == (track as LocalTrack).path + : other.track.id == track.id; + } + + @override + int get hashCode => track is LocalTrack + ? (track as LocalTrack).path.hashCode + : track.id.hashCode; } abstract class AudioPlayerInterface { From 44418868ad10e4f1043777973f70e1d2745c7fcc Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 25 Jun 2024 20:38:40 +0600 Subject: [PATCH 32/92] chore: fix volume not being set after launch --- lib/provider/volume_provider.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/provider/volume_provider.dart b/lib/provider/volume_provider.dart index ddd38fd98..64bcfe1a0 100644 --- a/lib/provider/volume_provider.dart +++ b/lib/provider/volume_provider.dart @@ -9,6 +9,7 @@ class VolumeProvider extends Notifier { @override build() { + audioPlayer.setVolume(KVStoreService.volume); return KVStoreService.volume; } From 08ac29c97936e4015ef23ee4b7890ab0cfe31766 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 29 Jun 2024 17:05:06 +0600 Subject: [PATCH 33/92] refactor(stats): migrate stats to use drift db --- build.yaml | 7 + lib/collections/fake.dart | 34 ++ .../sections/body/track_view_body.dart | 2 +- .../sections/body/track_view_options.dart | 2 +- .../sections/header/header_actions.dart | 2 +- .../sections/header/header_buttons.dart | 2 +- lib/models/database/database.dart | 6 +- lib/models/database/database.g.dart | 441 ++++++++++++++++++ lib/models/database/tables/history.dart | 25 + lib/modules/album/album_card.dart | 2 +- lib/modules/home/sections/recent.dart | 35 +- lib/modules/playlist/playlist_card.dart | 2 +- lib/modules/stats/summary/summary.dart | 156 ++++--- lib/modules/stats/top/albums.dart | 30 +- lib/modules/stats/top/artists.dart | 8 +- lib/modules/stats/top/tracks.dart | 8 +- lib/pages/lyrics/synced_lyrics.dart | 6 +- lib/pages/stats/albums/albums.dart | 12 +- lib/pages/stats/artists/artists.dart | 8 +- lib/pages/stats/fees/fees.dart | 8 +- lib/pages/stats/minutes/minutes.dart | 8 +- lib/pages/stats/playlists/playlists.dart | 10 +- lib/pages/stats/streams/streams.dart | 8 +- .../audio_player/audio_player_streams.dart | 4 +- lib/provider/history/history.dart | 161 ++----- lib/provider/history/recent.dart | 89 ++-- lib/provider/history/summary.dart | 257 +++++++--- lib/provider/history/top.dart | 291 ++++++++---- lib/provider/server/routes/connect.dart | 4 +- 29 files changed, 1187 insertions(+), 441 deletions(-) create mode 100644 lib/models/database/tables/history.dart diff --git a/build.yaml b/build.yaml index d83d6a202..8dbfe45d6 100644 --- a/build.yaml +++ b/build.yaml @@ -8,3 +8,10 @@ targets: options: any_map: true explicit_to_json: true + drift_dev: + options: + sql: + dialect: sqlite + options: + modules: + - json1 diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index 7391d3a06..31f97e0c9 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -1,6 +1,8 @@ import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/spotify/home_feed.dart'; import 'package:spotube/models/spotify_friends.dart'; +import 'package:spotube/provider/history/summary.dart'; abstract class FakeData { static final Image image = Image() @@ -222,4 +224,36 @@ abstract class FakeData { ) ], ); + + static const historySummary = PlaybackHistorySummary( + albums: 1, + artists: 1, + duration: Duration(seconds: 1), + playlists: 1, + tracks: 1, + fees: 1, + ); + + static final historyRecentlyPlayedPlaylist = HistoryTableData( + id: 0, + type: HistoryEntryType.track, + createdAt: DateTime.now(), + itemId: "1", + data: playlist.toJson(), + ); + + static final historyRecentlyPlayedAlbum = HistoryTableData( + id: 0, + type: HistoryEntryType.track, + createdAt: DateTime.now(), + itemId: "1", + data: album.toJson(), + ); + + static final historyRecentlyPlayedItems = List.generate( + 10, + (index) => index % 2 == 0 + ? historyRecentlyPlayedPlaylist + : historyRecentlyPlayedAlbum, + ); } diff --git a/lib/components/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart index a6089cc3c..df841b8d4 100644 --- a/lib/components/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -29,7 +29,7 @@ class TrackViewBodySection extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final props = InheritedTrackView.of(context); final trackViewState = ref.watch(trackViewProvider(props.tracks)); diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index 98ddca259..23198aec6 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -25,7 +25,7 @@ class TrackViewBodyOptions extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final audioSource = ref.watch(userPreferencesProvider.select((s) => s.audioSource)); diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index 6769ed523..94f0baa2d 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -22,7 +22,7 @@ class TrackViewHeaderActions extends HookConsumerWidget { final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final isActive = playlist.collections.contains(props.collectionId); diff --git a/lib/components/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart index aabca20f1..e9fbf6bb6 100644 --- a/lib/components/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -30,7 +30,7 @@ class TrackViewHeaderButtons extends HookConsumerWidget { final props = InheritedTrackView.of(context); final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final isActive = playlist.collections.contains(props.collectionId); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 98dc22dcc..9b47aaab1 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -5,10 +5,10 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:encrypt/encrypt.dart'; -import 'package:media_kit/media_kit.dart'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -27,6 +27,7 @@ part 'tables/scrobbler.dart'; part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; part 'tables/audio_player_state.dart'; +part 'tables/history.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; @@ -45,6 +46,7 @@ part 'typeconverters/map.dart'; AudioPlayerStateTable, PlaylistTable, PlaylistMediaTable, + HistoryTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 37cc930c0..bb7d2fb64 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3414,6 +3414,301 @@ class PlaylistMediaTableCompanion } } +class $HistoryTableTable extends HistoryTable + with TableInfo<$HistoryTableTable, HistoryTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $HistoryTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($HistoryTableTable.$convertertype); + static const VerificationMeta _itemIdMeta = const VerificationMeta('itemId'); + @override + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _dataMeta = const VerificationMeta('data'); + @override + late final GeneratedColumnWithTypeConverter, String> + data = GeneratedColumn('data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $HistoryTableTable.$converterdata); + @override + List get $columns => [id, createdAt, type, itemId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + context.handle(_typeMeta, const VerificationResult.success()); + if (data.containsKey('item_id')) { + context.handle(_itemIdMeta, + itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta)); + } else if (isInserting) { + context.missing(_itemIdMeta); + } + context.handle(_dataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + HistoryTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + type: $HistoryTableTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + itemId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}item_id'])!, + data: $HistoryTableTable.$converterdata.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!), + ); + } + + @override + $HistoryTableTable createAlias(String alias) { + return $HistoryTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(HistoryEntryType.values); + static TypeConverter, String> $converterdata = + const MapTypeConverter(); +} + +class HistoryTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final HistoryEntryType type; + final String itemId; + final Map data; + const HistoryTableData( + {required this.id, + required this.createdAt, + required this.type, + required this.itemId, + required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + { + map['type'] = + Variable($HistoryTableTable.$convertertype.toSql(type)); + } + map['item_id'] = Variable(itemId); + { + map['data'] = + Variable($HistoryTableTable.$converterdata.toSql(data)); + } + return map; + } + + HistoryTableCompanion toCompanion(bool nullToAbsent) { + return HistoryTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + type: Value(type), + itemId: Value(itemId), + data: Value(data), + ); + } + + factory HistoryTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + type: $HistoryTableTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + itemId: serializer.fromJson(json['itemId']), + data: serializer.fromJson>(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'type': serializer + .toJson($HistoryTableTable.$convertertype.toJson(type)), + 'itemId': serializer.toJson(itemId), + 'data': serializer.toJson>(data), + }; + } + + HistoryTableData copyWith( + {int? id, + DateTime? createdAt, + HistoryEntryType? type, + String? itemId, + Map? data}) => + HistoryTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + @override + String toString() { + return (StringBuffer('HistoryTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, type, itemId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.type == this.type && + other.itemId == this.itemId && + other.data == this.data); +} + +class HistoryTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value type; + final Value itemId; + final Value> data; + const HistoryTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.type = const Value.absent(), + this.itemId = const Value.absent(), + this.data = const Value.absent(), + }); + HistoryTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required HistoryEntryType type, + required String itemId, + required Map data, + }) : type = Value(type), + itemId = Value(itemId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? type, + Expression? itemId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (type != null) 'type': type, + if (itemId != null) 'item_id': itemId, + if (data != null) 'data': data, + }); + } + + HistoryTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? type, + Value? itemId, + Value>? data}) { + return HistoryTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (type.present) { + map['type'] = + Variable($HistoryTableTable.$convertertype.toSql(type.value)); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (data.present) { + map['data'] = + Variable($HistoryTableTable.$converterdata.toSql(data.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -3432,6 +3727,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this); late final $PlaylistMediaTableTable playlistMediaTable = $PlaylistMediaTableTable(this); + late final $HistoryTableTable historyTable = $HistoryTableTable(this); late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', @@ -3450,6 +3746,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { audioPlayerStateTable, playlistTable, playlistMediaTable, + historyTable, uniqueBlacklist, uniqTrackMatch ]; @@ -5053,6 +5350,148 @@ class $$PlaylistMediaTableTableOrderingComposer } } +typedef $$HistoryTableTableInsertCompanionBuilder = HistoryTableCompanion + Function({ + Value id, + Value createdAt, + required HistoryEntryType type, + required String itemId, + required Map data, +}); +typedef $$HistoryTableTableUpdateCompanionBuilder = HistoryTableCompanion + Function({ + Value id, + Value createdAt, + Value type, + Value itemId, + Value> data, +}); + +class $$HistoryTableTableTableManager extends RootTableManager< + _$AppDatabase, + $HistoryTableTable, + HistoryTableData, + $$HistoryTableTableFilterComposer, + $$HistoryTableTableOrderingComposer, + $$HistoryTableTableProcessedTableManager, + $$HistoryTableTableInsertCompanionBuilder, + $$HistoryTableTableUpdateCompanionBuilder> { + $$HistoryTableTableTableManager(_$AppDatabase db, $HistoryTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$HistoryTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$HistoryTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$HistoryTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value type = const Value.absent(), + Value itemId = const Value.absent(), + Value> data = const Value.absent(), + }) => + HistoryTableCompanion( + id: id, + createdAt: createdAt, + type: type, + itemId: itemId, + data: data, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + required HistoryEntryType type, + required String itemId, + required Map data, + }) => + HistoryTableCompanion.insert( + id: id, + createdAt: createdAt, + type: type, + itemId: itemId, + data: data, + ), + )); +} + +class $$HistoryTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $HistoryTableTable, + HistoryTableData, + $$HistoryTableTableFilterComposer, + $$HistoryTableTableOrderingComposer, + $$HistoryTableTableProcessedTableManager, + $$HistoryTableTableInsertCompanionBuilder, + $$HistoryTableTableUpdateCompanionBuilder> { + $$HistoryTableTableProcessedTableManager(super.$state); +} + +class $$HistoryTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $HistoryTableTable> { + $$HistoryTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get type => $state.composableBuilder( + column: $state.table.type, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get itemId => $state.composableBuilder( + column: $state.table.itemId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters, Map, + String> + get data => $state.composableBuilder( + column: $state.table.data, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); +} + +class $$HistoryTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $HistoryTableTable> { + $$HistoryTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get type => $state.composableBuilder( + column: $state.table.type, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get itemId => $state.composableBuilder( + column: $state.table.itemId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get data => $state.composableBuilder( + column: $state.table.data, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); @@ -5074,4 +5513,6 @@ class _$AppDatabaseManager { $$PlaylistTableTableTableManager(_db, _db.playlistTable); $$PlaylistMediaTableTableTableManager get playlistMediaTable => $$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable); + $$HistoryTableTableTableManager get historyTable => + $$HistoryTableTableTableManager(_db, _db.historyTable); } diff --git a/lib/models/database/tables/history.dart b/lib/models/database/tables/history.dart new file mode 100644 index 000000000..23c16f17e --- /dev/null +++ b/lib/models/database/tables/history.dart @@ -0,0 +1,25 @@ +part of '../database.dart'; + +enum HistoryEntryType { + playlist, + album, + track, +} + +class HistoryTable extends Table { + IntColumn get id => integer().autoIncrement()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + TextColumn get type => textEnum()(); + TextColumn get itemId => text()(); + TextColumn get data => + text().map(const MapTypeConverter())(); +} + +extension HistoryItemParseExtension on HistoryTableData { + PlaylistSimple? get playlist => + type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null; + AlbumSimple? get album => + type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null; + Track? get track => + type == HistoryEntryType.track ? Track.fromJson(data) : null; +} diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index de7aa5f8f..dd914fad9 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -35,7 +35,7 @@ class AlbumCard extends HookConsumerWidget { final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final historyNotifier = ref.read(playbackHistoryActionsProvider); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); bool isPlaylistPlaying = useMemoized( diff --git a/lib/modules/home/sections/recent.dart b/lib/modules/home/sections/recent.dart index 5be2fcc22..b26c0e164 100644 --- a/lib/modules/home/sections/recent.dart +++ b/lib/modules/home/sections/recent.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/history/recent.dart'; -import 'package:spotube/provider/history/state.dart'; class HomeRecentlyPlayedSection extends HookConsumerWidget { const HomeRecentlyPlayedSection({super.key}); @@ -10,23 +12,28 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final history = ref.watch(recentlyPlayedItems); + final historyData = + history.asData?.value ?? FakeData.historyRecentlyPlayedItems; - if (history.isEmpty) { + if (history.asData?.value.isEmpty == true) { 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: () {}, + return Skeletonizer( + enabled: history.isLoading, + child: HorizontalPlaybuttonCardView( + title: const Text('Recently Played'), + items: [ + for (final item in historyData) + if (item.playlist != null) + item.playlist + else if (item.album != null) + item.album + ], + hasNextPage: false, + isLoadingNextPage: false, + onFetchMore: () {}, + ), ); } } diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index 4e81a2547..d6ea2a460 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -26,7 +26,7 @@ class PlaylistCard extends HookConsumerWidget { final playlistQueue = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); - final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final historyNotifier = ref.read(playbackHistoryActionsProvider); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; diff --git a/lib/modules/stats/summary/summary.dart b/lib/modules/stats/summary/summary.dart index 0b6c60409..ef8aa1b02 100644 --- a/lib/modules/stats/summary/summary.dart +++ b/lib/modules/stats/summary/summary.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/summary/summary_card.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -18,83 +20,87 @@ class StatsPageSummarySection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final summary = ref.watch(playbackHistorySummaryProvider); + final summaryData = summary.asData?.value ?? FakeData.historySummary; - 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); - }, + return Skeletonizer.sliver( + enabled: summary.isLoading, + child: 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, ), - 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); - }, - ), - ]), - ); - }), + delegate: SliverChildListDelegate([ + SummaryCard( + title: summaryData.duration.inMinutes.toDouble(), + unit: "minutes", + description: 'Listened to music', + color: Colors.purple, + onTap: () { + ServiceUtils.pushNamed(context, StatsMinutesPage.name); + }, + ), + SummaryCard( + title: summaryData.tracks.toDouble(), + unit: "songs", + description: 'Streamed overall', + color: Colors.lightBlue, + onTap: () { + ServiceUtils.pushNamed(context, StatsStreamsPage.name); + }, + ), + SummaryCard.unformatted( + title: usdFormatter.format(summaryData.fees.toDouble()), + unit: "", + description: 'Owed to artists\nthis month', + color: Colors.green, + onTap: () { + ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); + }, + ), + SummaryCard( + title: summaryData.artists.toDouble(), + unit: "artist's", + description: 'Music reached you', + color: Colors.yellow, + onTap: () { + ServiceUtils.pushNamed(context, StatsArtistsPage.name); + }, + ), + SummaryCard( + title: summaryData.albums.toDouble(), + unit: "full albums", + description: 'Got your love', + color: Colors.pink, + onTap: () { + ServiceUtils.pushNamed(context, StatsAlbumsPage.name); + }, + ), + SummaryCard( + title: summaryData.playlists.toDouble(), + unit: "playlists", + description: 'Were on repeat', + color: Colors.teal, + onTap: () { + ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); + }, + ), + ]), + ); + }), + ), ); } } diff --git a/lib/modules/stats/top/albums.dart b/lib/modules/stats/top/albums.dart index 808a58a4c..bcaa75c54 100644 --- a/lib/modules/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; @@ -11,19 +12,24 @@ class TopAlbums extends HookConsumerWidget { Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final albums = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.albums)); + .select((value) => value.whenData((s) => s.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", - ), - ); - }, + final albumsData = albums.asData?.value ?? []; + + return Skeletonizer( + enabled: albums.isLoading, + child: SliverList.builder( + itemCount: albumsData.length, + itemBuilder: (context, index) { + final album = albumsData[index]; + return StatsAlbumItem( + album: album.album, + info: Text( + "${compactNumberFormatter.format(album.count)} plays", + ), + ); + }, + ), ); } } diff --git a/lib/modules/stats/top/artists.dart b/lib/modules/stats/top/artists.dart index 24e976010..094353f2a 100644 --- a/lib/modules/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -11,12 +11,14 @@ class TopArtists extends HookConsumerWidget { Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final artists = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.artists)); + .select((value) => value.whenData((s) => s.artists))); + + final artistsData = artists.asData?.value ?? []; return SliverList.builder( - itemCount: artists.length, + itemCount: artistsData.length, itemBuilder: (context, index) { - final artist = artists[index]; + final artist = artistsData[index]; return StatsArtistItem( artist: artist.artist, info: Text("${compactNumberFormatter.format(artist.count)} plays"), diff --git a/lib/modules/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart index ee37af3b6..8bffa800b 100644 --- a/lib/modules/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -12,13 +12,15 @@ class TopTracks extends HookConsumerWidget { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final tracks = ref.watch( playbackHistoryTopProvider(historyDuration) - .select((value) => value.tracks), + .select((value) => value.whenData((s) => s.tracks)), ); + final tracksData = tracks.asData?.value ?? []; + return SliverList.builder( - itemCount: tracks.length, + itemCount: tracksData.length, itemBuilder: (context, index) { - final track = tracks[index]; + final track = tracksData[index]; return StatsTrackItem( track: track.track, info: Text( diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 3294bab50..217967255 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -139,14 +139,12 @@ class SyncedLyrics extends HookConsumerWidget { textAlign: TextAlign.center, child: InkWell( onTap: () async { - final duration = - await audioPlayer.duration ?? - Duration.zero; final time = Duration( seconds: lyricSlice.time.inSeconds - delay, ); - if (time > duration || time.isNegative) { + if (time > audioPlayer.duration || + time.isNegative) { return; } audioPlayer.seek(time); diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 868f068a5..a13e500b8 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -12,10 +12,10 @@ class StatsAlbumsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final albums = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.albums), - ); + final albums = ref.watch(playbackHistoryTopProvider(HistoryDuration.allTime) + .select((value) => value.whenData((s) => s.albums))); + + final albumsData = albums.asData?.value ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -24,9 +24,9 @@ class StatsAlbumsPage extends HookConsumerWidget { title: Text("Albums"), ), body: ListView.builder( - itemCount: albums.length, + itemCount: albumsData.length, itemBuilder: (context, index) { - final album = albums[index]; + final album = albumsData[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 index b3f8c2401..9ebdbe5d9 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -14,9 +14,11 @@ class StatsArtistsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final artists = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.artists), + .select((s) => s.whenData((s) => s.artists)), ); + final artistsData = artists.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -24,9 +26,9 @@ class StatsArtistsPage extends HookConsumerWidget { title: Text("Artists"), ), body: ListView.builder( - itemCount: artists.length, + itemCount: artistsData.length, itemBuilder: (context, index) { - final artist = artists[index]; + final artist = artistsData[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 index ee1414757..e881ec70c 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -18,9 +18,11 @@ class StatsStreamFeesPage extends HookConsumerWidget { final artists = ref.watch( playbackHistoryTopProvider(HistoryDuration.days30) - .select((value) => value.artists), + .select((value) => value.whenData((s) => s.artists)), ); + final artistsData = artists.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -49,9 +51,9 @@ class StatsStreamFeesPage extends HookConsumerWidget { ), ), SliverList.builder( - itemCount: artists.length, + itemCount: artistsData.length, itemBuilder: (context, index) { - final artist = artists[index]; + final artist = artistsData[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 index ea0a0c10d..1d6a5844d 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -16,9 +16,11 @@ class StatsMinutesPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final topTracks = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.tracks), + .select((s) => s.whenData((s) => s.tracks)), ); + final topTracksData = topTracks.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( title: Text("Minutes listened"), @@ -27,9 +29,9 @@ class StatsMinutesPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracks.length, + itemCount: topTracksData.length, itemBuilder: (context, index) { - final (:track, :count) = topTracks[index]; + final (:track, :count) = topTracksData[index]; return StatsTrackItem( track: track, diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index d31f1dfa0..94f8ce9d1 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -14,9 +14,11 @@ class StatsPlaylistsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlists = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.playlists), + .select((s) => s.whenData((s) => s.playlists)), ); + final playlistsData = playlists.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -24,11 +26,11 @@ class StatsPlaylistsPage extends HookConsumerWidget { title: Text("Playlists"), ), body: ListView.builder( - itemCount: playlists.length, + itemCount: playlistsData.length, itemBuilder: (context, index) { - final playlist = playlists[index]; + final playlist = playlistsData[index]; return StatsPlaylistItem( - playlist: playlist.playlist.playlist, + playlist: playlist.playlist, info: Text("${compactNumberFormatter.format(playlist.count)} plays"), ); diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 3df344836..41f2d33a0 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -16,9 +16,11 @@ class StatsStreamsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final topTracks = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.tracks), + .select((s) => s.whenData((s) => s.tracks)), ); + final topTracksData = topTracks.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( title: Text("Streamed songs"), @@ -27,9 +29,9 @@ class StatsStreamsPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracks.length, + itemCount: topTracksData.length, itemBuilder: (context, index) { - final (:track, :count) = topTracks[index]; + final (:track, :count) = topTracksData[index]; return StatsTrackItem( track: track, diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 429440758..368fc6d96 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -45,8 +45,8 @@ class AudioPlayerStreamListeners { UserPreferences get preferences => ref.read(userPreferencesProvider); Discord get discord => ref.read(discordProvider); AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider); - PlaybackHistoryNotifier get history => - ref.read(playbackHistoryProvider.notifier); + PlaybackHistoryActions get history => + ref.read(playbackHistoryActionsProvider); Future updatePalette() async { final palette = ref.read(paletteProvider); diff --git a/lib/provider/history/history.dart b/lib/provider/history/history.dart index 4436626d5..0c20a9e5c 100644 --- a/lib/provider/history/history.dart +++ b/lib/provider/history/history.dart @@ -1,129 +1,68 @@ -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() ?? - [], - ); - } +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; - Map toJson() { - return { - "items": items.map((s) => s.toJson()).toList(), - }; - } - - PlaybackHistoryState copyWith({ - List? items, - }) { - return PlaybackHistoryState(items: items ?? this.items); - } -} - -class PlaybackHistoryNotifier - extends PersistedStateNotifier { +class PlaybackHistoryActions { final Ref ref; - PlaybackHistoryNotifier(this.ref) - : super(const PlaybackHistoryState(), "playback_history"); - - SpotifyApi get spotify => ref.read(spotifyProvider); + AppDatabase get _db => ref.read(databaseProvider); - @override - FutureOr fromJson(Map json) => - PlaybackHistoryState.fromJson(json); + PlaybackHistoryActions(this.ref); - @override - Map toJson() { - return state.toJson(); + Future _batchInsertHistoryEntries( + List entries) async { + await _db.batch((batch) { + batch.insertAll(_db.historyTable, entries); + }); } - void addPlaylists(List playlists) { - state = state.copyWith( - items: [ - ...state.items, - for (final playlist in playlists) - PlaybackHistoryItem.playlist( - date: DateTime.now(), playlist: playlist), - ], - ); + Future addPlaylists(List playlists) async { + await _batchInsertHistoryEntries([ + for (final playlist in playlists) + HistoryTableCompanion.insert( + type: HistoryEntryType.playlist, + itemId: playlist.id!, + data: playlist.toJson(), + ), + ]); } - void addAlbums(List albums) { - state = state.copyWith( - items: [ - ...state.items, - for (final album in albums) - PlaybackHistoryItem.album(date: DateTime.now(), album: album), - ], - ); + Future addAlbums(List albums) async { + await _batchInsertHistoryEntries([ + for (final albums in albums) + HistoryTableCompanion.insert( + type: HistoryEntryType.album, + itemId: albums.id!, + data: albums.toJson(), + ), + ]); } - 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(); + Future addTracks(List tracks) async { + await _batchInsertHistoryEntries([ + for (final track in tracks) + HistoryTableCompanion.insert( + type: HistoryEntryType.track, + itemId: track.id!, + data: track.toJson(), + ), + ]); + } - state = state.copyWith( - items: [ - ...state.items, - PlaybackHistoryItem.track(date: DateTime.now(), track: track), - ], - ); + Future addTrack(Track track) async { + await _db.into(_db.historyTable).insert( + HistoryTableCompanion.insert( + type: HistoryEntryType.track, + itemId: track.id!, + data: track.toJson(), + ), + ); } - void clear() { - state = state.copyWith(items: []); + Future clear() async { + _db.delete(_db.historyTable).go(); } } -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, - ); -}); +final playbackHistoryActionsProvider = + Provider((ref) => PlaybackHistoryActions(ref)); diff --git a/lib/provider/history/recent.dart b/lib/provider/history/recent.dart index 9953858d8..4e4455001 100644 --- a/lib/provider/history/recent.dart +++ b/lib/provider/history/recent.dart @@ -1,40 +1,55 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.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/models/database/database.dart'; +import 'package:spotube/provider/database/database.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(), - ), - ); -}); +class RecentlyPlayedItemNotifier extends AsyncNotifier> { + @override + build() async { + final database = ref.watch(databaseProvider); + + final uniqueItemIds = + await (database.selectOnly(database.historyTable, distinct: true) + ..addColumns([database.historyTable.itemId]) + ..where( + database.historyTable.type.isIn([ + HistoryEntryType.playlist.name, + HistoryEntryType.album.name, + ]), + ) + ..limit(10)) + .map((row) => row.read(database.historyTable.itemId)) + .get() + .then((value) => value.whereNotNull().toList()); + + final query = database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.isIn([ + HistoryEntryType.playlist.name, + HistoryEntryType.album.name, + ]) & + tbl.itemId.isIn(uniqueItemIds), + ) + ..orderBy([ + (tbl) => OrderingTerm( + expression: tbl.createdAt, + mode: OrderingMode.desc, + ), + ]); + + final subscription = query.watch().listen((event) { + state = AsyncData(event); + }); + + ref.onDispose(() => subscription.cancel()); + + return await query.get(); + } +} + +final recentlyPlayedItems = + AsyncNotifierProvider>( + () => RecentlyPlayedItemNotifier(), +); diff --git a/lib/provider/history/summary.dart b/lib/provider/history/summary.dart index 2aa86ac9f..99df4c11a 100644 --- a/lib/provider/history/summary.dart +++ b/lib/provider/history/summary.dart @@ -1,62 +1,197 @@ -import 'package:collection/collection.dart'; +import 'dart:async'; +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:drift/extensions/json1.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, - ); -}); +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; + +class PlaybackHistorySummary { + final Duration duration; + final int tracks; + final int artists; + final double fees; + final int albums; + final int playlists; + + const PlaybackHistorySummary({ + required this.duration, + required this.tracks, + required this.artists, + required this.fees, + required this.albums, + required this.playlists, + }); + + PlaybackHistorySummary copyWith({ + Duration? duration, + int? tracks, + int? artists, + double? fees, + int? albums, + int? playlists, + }) { + return PlaybackHistorySummary( + duration: duration ?? this.duration, + tracks: tracks ?? this.tracks, + artists: artists ?? this.artists, + fees: fees ?? this.fees, + albums: albums ?? this.albums, + playlists: playlists ?? this.playlists, + ); + } +} + +class PlaybackHistorySummaryNotifier + extends AsyncNotifier { + @override + build() async { + final database = ref.watch(databaseProvider); + + final uniqItemIdCountingCol = + database.historyTable.itemId.count(distinct: true); + final itemIdCountingCol = database.historyTable.itemId.count(); + final durationSumJsonColumn = + database.historyTable.data.jsonExtract(r"$.duration_ms").sum(); + final artistCountingCol = + database.historyTable.data.jsonExtract(r"$.artists"); + + final totalTracksListenedQuery = (database.selectOnly(database.historyTable) + ..addColumns([uniqItemIdCountingCol]) + ..where( + database.historyTable.type.equals(HistoryEntryType.track.name))) + .map((row) => row.read(uniqItemIdCountingCol)); + + final totalDurationListenedQuery = (database + .selectOnly(database.historyTable) + ..addColumns([durationSumJsonColumn]) + ..where( + database.historyTable.type.equals(HistoryEntryType.track.name))) + .map( + (row) => Duration(milliseconds: row.read(durationSumJsonColumn) ?? 0), + ); + + final totalArtistsListenedQuery = + (database.selectOnly(database.historyTable) + ..addColumns([artistCountingCol]) + ..where( + database.historyTable.type.equals(HistoryEntryType.track.name), + )) + .map( + (row) { + final data = jsonDecode(row.read(artistCountingCol)!) as List; + return data.map((e) => e['id'] as String).cast().toList(); + }, + ); + + final totalAlbumsListenedQuery = (database.selectOnly(database.historyTable) + ..addColumns([uniqItemIdCountingCol]) + ..where( + database.historyTable.type.equals(HistoryEntryType.album.name))) + .map((row) => row.read(uniqItemIdCountingCol)); + + final totalPlaylistsListenedQuery = + (database.selectOnly(database.historyTable) + ..addColumns([uniqItemIdCountingCol]) + ..where( + database.historyTable.type + .equals(HistoryEntryType.playlist.name), + )) + .map((row) => row.read(uniqItemIdCountingCol)); + + final oldestDate = DateTime.now().copyWith(day: 1, hour: 0, minute: 0); + final newestDate = DateTime.now().copyWith(day: 30, hour: 23, minute: 59); + final totalTracksListenedThisMonthQuery = + (database.selectOnly(database.historyTable) + ..addColumns([itemIdCountingCol]) + ..where( + database.historyTable.type.equals( + HistoryEntryType.track.name, + ) & + database.historyTable.createdAt + .isBetweenValues(oldestDate, newestDate), + )) + .map((row) => row.read(itemIdCountingCol)); + + final subscriptions = [ + totalTracksListenedQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + tracks: event, + )); + }), + totalDurationListenedQuery.watchSingle().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + duration: event, + )); + }), + totalArtistsListenedQuery.watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + artists: event.expand((e) => e).toSet().length, + )); + }), + totalAlbumsListenedQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + albums: event, + )); + }), + totalPlaylistsListenedQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + playlists: event, + )); + }), + totalTracksListenedThisMonthQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + fees: event * 0.005, + )); + }), + ]; + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + + return database.transaction(() async { + final totalTracksListened = + await totalTracksListenedQuery.getSingle() ?? 0; + + final totalDurationListened = + await totalDurationListenedQuery.getSingle(); + + final totalArtistsListened = await totalArtistsListenedQuery + .get() + .then((value) => value.expand((e) => e).toSet().length); + + final totalAlbumsListened = + await totalAlbumsListenedQuery.getSingle() ?? 0; + + final totalPlaylistsListened = + await totalPlaylistsListenedQuery.getSingle() ?? 0; + + final totalTracksListenedThisMonth = + await totalTracksListenedThisMonthQuery.getSingle() ?? 0; + + return PlaybackHistorySummary( + duration: totalDurationListened, + tracks: totalTracksListened, + artists: totalArtistsListened, + fees: totalTracksListenedThisMonth * 0.005, + albums: totalAlbumsListened, + playlists: totalPlaylistsListened, + ); + }); + } +} + +final playbackHistorySummaryProvider = AsyncNotifierProvider< + PlaybackHistorySummaryNotifier, PlaybackHistorySummary>( + () => PlaybackHistorySummaryNotifier(), +); diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index 7d4594f08..aa12c9b3c 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -1,95 +1,212 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/history/history.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.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, - ); -}); +typedef PlaybackHistoryTrack = ({int count, Track track}); +typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); +typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); +typedef PlaybackHistoryArtist = ({int count, Artist artist}); + +class PlaybackHistoryTopState { + final List tracks; + final List albums; + final List playlists; + final List artists; + + const PlaybackHistoryTopState({ + required this.tracks, + required this.albums, + required this.playlists, + required this.artists, + }); + + PlaybackHistoryTopState copyWith({ + List? tracks, + List? albums, + List? playlists, + List? artists, + }) { + return PlaybackHistoryTopState( + tracks: tracks ?? this.tracks, + albums: albums ?? this.albums, + playlists: playlists ?? this.playlists, + artists: artists ?? this.artists, + ); + } +} + +class PlaybackHistoryTopNotifier + extends FamilyAsyncNotifier { + @override + build(arg) async { + final database = ref.watch(databaseProvider); + + final duration = switch (arg) { + 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 tracksQuery = (database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.track) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(duration), + ), + )); + + final albumsQuery = database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.album) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(duration), + ), + ); + + final playlistsQuery = database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.playlist) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(duration), + ), + ); + + final subscriptions = [ + tracksQuery.watch().listen((event) { + if (state.asData == null) return; + final artists = event + .map((track) => track.track!.artists) + .expand((e) => e ?? []); + state = AsyncData(state.asData!.value.copyWith( + tracks: getTracksWithCount(event), + artists: getArtistsWithCount(artists), + )); + }), + albumsQuery.watch().listen((event) async { + if (state.asData == null) return; + final tracks = await tracksQuery.get(); + + final albumsWithTrackAlbums = [ + for (final historicAlbum in event) historicAlbum.album!, + for (final track in tracks) track.track!.album! + ]; + + state = AsyncData(state.asData!.value.copyWith( + albums: getAlbumsWithCount(albumsWithTrackAlbums), + )); + }), + playlistsQuery.watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + playlists: getPlaylistsWithCount(event), + )); + }), + ]; + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + + return database.transaction(() async { + final tracks = await tracksQuery.get(); + final albums = await albumsQuery.get(); + final playlists = await playlistsQuery.get(); + + final tracksWithCount = getTracksWithCount(tracks); + + final albumsWithTrackAlbums = [ + for (final historicAlbum in albums) historicAlbum.album!, + for (final track in tracks) track.track!.album! + ]; + + final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums); + + final artists = tracks + .map((track) => track.track!.artists) + .expand((e) => e ?? []); + + final artistsWithCount = getArtistsWithCount(artists); + + final playlistsWithCount = getPlaylistsWithCount(playlists); + + return PlaybackHistoryTopState( + tracks: tracksWithCount, + albums: albumsWithCount, + artists: artistsWithCount, + playlists: playlistsWithCount, + ); + }); + } + + List getTracksWithCount(List tracks) { + return 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(); + } + + List getAlbumsWithCount( + List albumsWithTrackAlbums, + ) { + return 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(); + } + + List getArtistsWithCount(Iterable artists) { + return 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(); + } + + List getPlaylistsWithCount( + List playlists, + ) { + return groupBy(playlists, (playlist) => playlist.playlist!.id!) + .entries + .map((entry) { + return ( + count: entry.value.length, + playlist: entry.value.first.playlist!, + ); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } +} + +final playbackHistoryTopProvider = AsyncNotifierProviderFamily< + PlaybackHistoryTopNotifier, + PlaybackHistoryTopState, + HistoryDuration>(PlaybackHistoryTopNotifier.new); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart index a2fa70b80..8e75a87ec 100644 --- a/lib/provider/server/routes/connect.dart +++ b/lib/provider/server/routes/connect.dart @@ -40,8 +40,8 @@ class ServerConnectRoutes { AudioPlayerNotifier get audioPlayerNotifier => ref.read(audioPlayerProvider.notifier); - PlaybackHistoryNotifier get historyNotifier => - ref.read(playbackHistoryProvider.notifier); + PlaybackHistoryActions get historyNotifier => + ref.read(playbackHistoryActionsProvider); Stream get connectClientStream => _connectClientStreamController.stream; From b495ed4ac02a7a24a9a9b0c79f8aa1cd0363438b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 29 Jun 2024 17:09:58 +0600 Subject: [PATCH 34/92] fix: null exception in album page navigated from /home --- lib/pages/album/album.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index dcdc2ce79..0c6cfd693 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -46,7 +46,8 @@ class AlbumPage extends HookConsumerWidget { }, ), routePath: "/album/${album.id}", - shareUrl: album.externalUrls!.spotify!, + shareUrl: album.externalUrls?.spotify ?? + "https://open.spotify.com/album/${album.id}", isLiked: isSavedAlbum.asData?.value ?? false, onHeart: isSavedAlbum.asData?.value == null ? null From 1cfd377c298e98f460a082e1196c441e71291079 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 11:01:40 +0600 Subject: [PATCH 35/92] refactor: synced lyric cache to use drift db --- .../sections/header/header_buttons.dart | 4 +- lib/main.dart | 5 - lib/models/database/database.dart | 4 + lib/models/database/database.g.dart | 323 ++++++++++++++++++ lib/models/database/tables/lyrics.dart | 8 + .../database/typeconverters/subtitle.dart | 13 + lib/pages/root/root_app.dart | 9 - lib/provider/spotify/lyrics/synced.dart | 38 ++- lib/provider/spotify/playlist/liked.dart | 18 +- lib/provider/spotify/spotify.dart | 5 +- lib/provider/spotify/utils/json_cast.dart | 21 ++ lib/provider/spotify/utils/persistence.dart | 2 +- lib/utils/persisted_change_notifier.dart | 55 --- lib/utils/persisted_state_notifier.dart | 164 --------- 14 files changed, 401 insertions(+), 268 deletions(-) create mode 100644 lib/models/database/tables/lyrics.dart create mode 100644 lib/models/database/typeconverters/subtitle.dart create mode 100644 lib/provider/spotify/utils/json_cast.dart delete mode 100644 lib/utils/persisted_change_notifier.dart delete mode 100644 lib/utils/persisted_state_notifier.dart diff --git a/lib/components/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart index e9fbf6bb6..54e0f0cf7 100644 --- a/lib/components/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -131,7 +131,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget { ); } } finally { - isLoading.value = false; + if (context.mounted) { + isLoading.value = false; + } } } diff --git a/lib/main.dart b/lib/main.dart index 9b92a21df..69c890628 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,7 +33,6 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/logger/logger.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'; @@ -85,10 +84,6 @@ Future main(List rawArgs) async { Hive.init(hiveCacheDir); - await PersistedStateNotifier.initializeBoxes( - path: hiveCacheDir, - ); - if (kIsDesktop) { await localNotifier.setup(appName: "Spotube"); await WindowManagerTools.initialize(); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 9b47aaab1..609d6771e 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -9,6 +9,7 @@ import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/models/lyrics.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -28,12 +29,14 @@ part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; part 'tables/audio_player_state.dart'; part 'tables/history.dart'; +part 'tables/lyrics.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; part 'typeconverters/encrypted_text.dart'; part 'typeconverters/map.dart'; +part 'typeconverters/subtitle.dart'; @DriftDatabase( tables: [ @@ -47,6 +50,7 @@ part 'typeconverters/map.dart'; PlaylistTable, PlaylistMediaTable, HistoryTable, + LyricsTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index bb7d2fb64..1e585fa87 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3709,6 +3709,218 @@ class HistoryTableCompanion extends UpdateCompanion { } } +class $LyricsTableTable extends LyricsTable + with TableInfo<$LyricsTableTable, LyricsTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $LyricsTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _trackIdMeta = + const VerificationMeta('trackId'); + @override + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _dataMeta = const VerificationMeta('data'); + @override + late final GeneratedColumnWithTypeConverter data = + GeneratedColumn('data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($LyricsTableTable.$converterdata); + @override + List get $columns => [id, trackId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lyrics_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('track_id')) { + context.handle(_trackIdMeta, + trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); + } else if (isInserting) { + context.missing(_trackIdMeta); + } + context.handle(_dataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + LyricsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LyricsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + data: $LyricsTableTable.$converterdata.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!), + ); + } + + @override + $LyricsTableTable createAlias(String alias) { + return $LyricsTableTable(attachedDatabase, alias); + } + + static TypeConverter $converterdata = + SubtitleTypeConverter(); +} + +class LyricsTableData extends DataClass implements Insertable { + final int id; + final String trackId; + final SubtitleSimple data; + const LyricsTableData( + {required this.id, required this.trackId, required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + { + map['data'] = + Variable($LyricsTableTable.$converterdata.toSql(data)); + } + return map; + } + + LyricsTableCompanion toCompanion(bool nullToAbsent) { + return LyricsTableCompanion( + id: Value(id), + trackId: Value(trackId), + data: Value(data), + ); + } + + factory LyricsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LyricsTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'data': serializer.toJson(data), + }; + } + + LyricsTableData copyWith({int? id, String? trackId, SubtitleSimple? data}) => + LyricsTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + @override + String toString() { + return (StringBuffer('LyricsTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LyricsTableData && + other.id == this.id && + other.trackId == this.trackId && + other.data == this.data); +} + +class LyricsTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value data; + const LyricsTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.data = const Value.absent(), + }); + LyricsTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required SubtitleSimple data, + }) : trackId = Value(trackId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (data != null) 'data': data, + }); + } + + LyricsTableCompanion copyWith( + {Value? id, Value? trackId, Value? data}) { + return LyricsTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (data.present) { + map['data'] = + Variable($LyricsTableTable.$converterdata.toSql(data.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LyricsTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -3728,6 +3940,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $PlaylistMediaTableTable playlistMediaTable = $PlaylistMediaTableTable(this); late final $HistoryTableTable historyTable = $HistoryTableTable(this); + late final $LyricsTableTable lyricsTable = $LyricsTableTable(this); late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', @@ -3747,6 +3960,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { playlistTable, playlistMediaTable, historyTable, + lyricsTable, uniqueBlacklist, uniqTrackMatch ]; @@ -5492,6 +5706,113 @@ class $$HistoryTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$LyricsTableTableInsertCompanionBuilder = LyricsTableCompanion + Function({ + Value id, + required String trackId, + required SubtitleSimple data, +}); +typedef $$LyricsTableTableUpdateCompanionBuilder = LyricsTableCompanion + Function({ + Value id, + Value trackId, + Value data, +}); + +class $$LyricsTableTableTableManager extends RootTableManager< + _$AppDatabase, + $LyricsTableTable, + LyricsTableData, + $$LyricsTableTableFilterComposer, + $$LyricsTableTableOrderingComposer, + $$LyricsTableTableProcessedTableManager, + $$LyricsTableTableInsertCompanionBuilder, + $$LyricsTableTableUpdateCompanionBuilder> { + $$LyricsTableTableTableManager(_$AppDatabase db, $LyricsTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$LyricsTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$LyricsTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$LyricsTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value trackId = const Value.absent(), + Value data = const Value.absent(), + }) => + LyricsTableCompanion( + id: id, + trackId: trackId, + data: data, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String trackId, + required SubtitleSimple data, + }) => + LyricsTableCompanion.insert( + id: id, + trackId: trackId, + data: data, + ), + )); +} + +class $$LyricsTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $LyricsTableTable, + LyricsTableData, + $$LyricsTableTableFilterComposer, + $$LyricsTableTableOrderingComposer, + $$LyricsTableTableProcessedTableManager, + $$LyricsTableTableInsertCompanionBuilder, + $$LyricsTableTableUpdateCompanionBuilder> { + $$LyricsTableTableProcessedTableManager(super.$state); +} + +class $$LyricsTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $LyricsTableTable> { + $$LyricsTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get data => $state.composableBuilder( + column: $state.table.data, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); +} + +class $$LyricsTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $LyricsTableTable> { + $$LyricsTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get data => $state.composableBuilder( + column: $state.table.data, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); @@ -5515,4 +5836,6 @@ class _$AppDatabaseManager { $$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable); $$HistoryTableTableTableManager get historyTable => $$HistoryTableTableTableManager(_db, _db.historyTable); + $$LyricsTableTableTableManager get lyricsTable => + $$LyricsTableTableTableManager(_db, _db.lyricsTable); } diff --git a/lib/models/database/tables/lyrics.dart b/lib/models/database/tables/lyrics.dart new file mode 100644 index 000000000..7c4c7f8f3 --- /dev/null +++ b/lib/models/database/tables/lyrics.dart @@ -0,0 +1,8 @@ +part of '../database.dart'; + +class LyricsTable extends Table { + IntColumn get id => integer().autoIncrement()(); + + TextColumn get trackId => text()(); + TextColumn get data => text().map(SubtitleTypeConverter())(); +} diff --git a/lib/models/database/typeconverters/subtitle.dart b/lib/models/database/typeconverters/subtitle.dart new file mode 100644 index 000000000..25fa4ad51 --- /dev/null +++ b/lib/models/database/typeconverters/subtitle.dart @@ -0,0 +1,13 @@ +part of '../database.dart'; + +class SubtitleTypeConverter extends TypeConverter { + @override + SubtitleSimple fromSql(String fromDb) { + return SubtitleSimple.fromJson(jsonDecode(fromDb)); + } + + @override + String toSql(SubtitleSimple value) { + return jsonEncode(value.toJson()); + } +} diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 322a8731a..402a7cf05 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; @@ -19,7 +18,6 @@ import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/routes/connect.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'; @@ -41,13 +39,6 @@ class RootApp extends HookConsumerWidget { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { ServiceUtils.checkForUpdates(context, ref); - - final sharedPreferences = await SharedPreferences.getInstance(); - - if (sharedPreferences.getBool(kIsUsingEncryption) == false && - context.mounted) { - await PersistedStateNotifier.showNoEncryptionDialog(context); - } }); final subscriptions = [ diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index ef83a1a18..bcf2a1626 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -1,11 +1,6 @@ part of '../spotify.dart'; -class SyncedLyricsNotifier extends FamilyAsyncNotifier - with Persistence { - SyncedLyricsNotifier() { - load(); - } - +class SyncedLyricsNotifier extends FamilyAsyncNotifier { Track get _track => arg!; Future getSpotifyLyrics(String? token) async { @@ -128,12 +123,25 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier @override FutureOr build(track) async { try { + final database = ref.watch(databaseProvider); final spotify = ref.watch(spotifyProvider); + if (track == null) { throw "No track currently"; } + + final cachedLyrics = await (database.select(database.lyricsTable) + ..where((tbl) => tbl.trackId.equals(track.id!))) + .map((row) => row.data) + .getSingleOrNull(); + + SubtitleSimple? lyrics = cachedLyrics; + final token = await spotify.getCredentials(); - SubtitleSimple lyrics = await getSpotifyLyrics(token.accessToken); + + if (lyrics == null || lyrics.lyrics.isEmpty) { + lyrics = await getSpotifyLyrics(token.accessToken); + } if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) { lyrics = await getLRCLibLyrics(); @@ -143,19 +151,21 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier throw Exception("Unable to find lyrics"); } + if (cachedLyrics == null || cachedLyrics.lyrics.isEmpty) { + await database.into(database.lyricsTable).insertOnConflictUpdate( + LyricsTableCompanion.insert( + trackId: track.id!, + data: lyrics, + ), + ); + } + return lyrics; } catch (e, stackTrace) { AppLogger.reportError(e, stackTrace); rethrow; } } - - @override - FutureOr fromJson(Map json) => - SubtitleSimple.fromJson(json.castKeyDeep()); - - @override - Map toJson(SubtitleSimple data) => data.toJson(); } final syncedLyricsDelayProvider = StateProvider((ref) => 0); diff --git a/lib/provider/spotify/playlist/liked.dart b/lib/provider/spotify/playlist/liked.dart index 52463d3de..27c3e2b65 100644 --- a/lib/provider/spotify/playlist/liked.dart +++ b/lib/provider/spotify/playlist/liked.dart @@ -1,10 +1,6 @@ part of '../spotify.dart'; -class LikedTracksNotifier extends AsyncNotifier> with Persistence { - LikedTracksNotifier() { - load(); - } - +class LikedTracksNotifier extends AsyncNotifier> { @override FutureOr> build() async { final spotify = ref.watch(spotifyProvider); @@ -29,18 +25,6 @@ class LikedTracksNotifier extends AsyncNotifier> with Persistence { } }); } - - @override - FutureOr> fromJson(Map json) { - return (json['tracks'] as List).map((e) => Track.fromJson(e)).toList(); - } - - @override - Map toJson(List data) { - return { - 'tracks': data.map((e) => e.toJson()).toList(), - }; - } } final likedTracksProvider = diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index e592e93b3..63a8ed385 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -2,6 +2,9 @@ library spotify; import 'dart:async'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/spotify/utils/json_cast.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; @@ -15,7 +18,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; // ignore: depend_on_referenced_packages, implementation_imports import 'package:riverpod/src/async_notifier.dart'; import 'package:spotube/extensions/album_simple.dart'; -import 'package:spotube/extensions/map.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart'; @@ -25,7 +27,6 @@ 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:wikipedia_api/wikipedia_api.dart'; diff --git a/lib/provider/spotify/utils/json_cast.dart b/lib/provider/spotify/utils/json_cast.dart new file mode 100644 index 000000000..307009714 --- /dev/null +++ b/lib/provider/spotify/utils/json_cast.dart @@ -0,0 +1,21 @@ +Map castNestedJson(Map map) { + return Map.castFrom( + map.map((key, value) { + if (value is Map) { + return MapEntry( + key, + castNestedJson(value), + ); + } else if (value is Iterable) { + return MapEntry( + key, + value.map((e) { + if (e is Map) return castNestedJson(e); + return e; + }).toList(), + ); + } + return MapEntry(key, value); + }), + ); +} diff --git a/lib/provider/spotify/utils/persistence.dart b/lib/provider/spotify/utils/persistence.dart index 14d3c9408..57f41dec0 100644 --- a/lib/provider/spotify/utils/persistence.dart +++ b/lib/provider/spotify/utils/persistence.dart @@ -16,7 +16,7 @@ mixin Persistence on BuildlessAsyncNotifier { (json is List && json.isNotEmpty)) { state = AsyncData( await fromJson( - PersistedStateNotifier.castNestedJson(json), + castNestedJson(json), ), ); } diff --git a/lib/utils/persisted_change_notifier.dart b/lib/utils/persisted_change_notifier.dart deleted file mode 100644 index d48cb67a3..000000000 --- a/lib/utils/persisted_change_notifier.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -abstract class PersistedChangeNotifier extends ChangeNotifier { - late SharedPreferences _localStorage; - PersistedChangeNotifier() { - SharedPreferences.getInstance().then((value) => _localStorage = value).then( - (_) async { - final persistedMap = (await toMap()) - .entries - .toList() - .fold>({}, (acc, entry) { - if (entry.value != null) { - if (entry.value is bool) { - acc[entry.key] = _localStorage.getBool(entry.key); - } else if (entry.value is int) { - acc[entry.key] = _localStorage.getInt(entry.key); - } else if (entry.value is double) { - acc[entry.key] = _localStorage.getDouble(entry.key); - } else if (entry.value is String) { - acc[entry.key] = _localStorage.getString(entry.key); - } - } else { - acc[entry.key] = _localStorage.get(entry.key); - } - return acc; - }); - await loadFromLocal(persistedMap); - notifyListeners(); - }, - ); - } - - FutureOr loadFromLocal(Map map); - - FutureOr> toMap(); - - Future updatePersistence({bool clearNullEntries = false}) async { - for (final entry in (await toMap()).entries) { - if (entry.value is bool) { - await _localStorage.setBool(entry.key, entry.value); - } else if (entry.value is int) { - await _localStorage.setInt(entry.key, entry.value); - } else if (entry.value is double) { - await _localStorage.setDouble(entry.key, entry.value); - } else if (entry.value is String) { - await _localStorage.setString(entry.key, entry.value); - } else if (entry.value == null && clearNullEntries) { - _localStorage.remove(entry.key); - } - } - } -} diff --git a/lib/utils/persisted_state_notifier.dart b/lib/utils/persisted_state_notifier.dart deleted file mode 100644 index 450bb6640..000000000 --- a/lib/utils/persisted_state_notifier.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:hive/hive.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotube/components/dialogs/prompt_dialog.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/utils/platform.dart'; -import 'package:spotube/utils/primitive_utils.dart'; - -const secureStorage = FlutterSecureStorage( - aOptions: AndroidOptions( - encryptedSharedPreferences: true, - ), -); - -const kKeyBoxName = "spotube_box_name"; -const kNoEncryptionWarningShownKey = "showedNoEncryptionWarning"; -const kIsUsingEncryption = "isUsingEncryption"; -String getBoxKey(String boxName) => "spotube_box_$boxName"; - -abstract class PersistedStateNotifier extends StateNotifier { - final String cacheKey; - final bool encrypted; - - FutureOr onInit() {} - - PersistedStateNotifier( - super.state, - this.cacheKey, { - this.encrypted = false, - }) { - _load().then((_) => onInit()); - } - - static late LazyBox _box; - static late LazyBox _encryptedBox; - - static Future showNoEncryptionDialog(BuildContext context) async { - final localStorage = await SharedPreferences.getInstance(); - final wasShownAlready = - localStorage.getBool(kNoEncryptionWarningShownKey) == true; - - if (wasShownAlready || !context.mounted) { - return; - } - - await showPromptDialog( - context: context, - title: context.l10n.failed_to_encrypt, - message: context.l10n.encryption_failed_warning, - cancelText: null, - ); - await localStorage.setBool(kNoEncryptionWarningShownKey, true); - } - - static Future read(String key) async { - final localStorage = await SharedPreferences.getInstance(); - if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) { - return localStorage.getString(key); - } - - try { - await localStorage.setBool(kIsUsingEncryption, true); - return await secureStorage.read(key: key); - } catch (e) { - await localStorage.setBool(kIsUsingEncryption, false); - return localStorage.getString(key); - } - } - - static Future write(String key, String value) async { - final localStorage = await SharedPreferences.getInstance(); - if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) { - await localStorage.setString(key, value); - return; - } - - try { - await localStorage.setBool(kIsUsingEncryption, true); - await secureStorage.write(key: key, value: value); - } catch (e) { - await localStorage.setBool(kIsUsingEncryption, false); - await localStorage.setString(key, value); - } - } - - static Future initializeBoxes({required String? path}) async { - String? boxName = await read(kKeyBoxName); - - if (boxName == null) { - boxName = "spotube-${PrimitiveUtils.uuid.v4()}"; - await write(kKeyBoxName, boxName); - } - - String? encryptionKey = await read(getBoxKey(boxName)); - - if (encryptionKey == null) { - encryptionKey = base64Url.encode(Hive.generateSecureKey()); - await write(getBoxKey(boxName), encryptionKey); - } - - _encryptedBox = await Hive.openLazyBox( - boxName, - encryptionCipher: HiveAesCipher(base64Url.decode(encryptionKey)), - ); - - _box = await Hive.openLazyBox( - "spotube_cache", - path: path, - ); - } - - LazyBox get box => encrypted ? _encryptedBox : _box; - - Future _load() async { - final json = await box.get(cacheKey); - - if (json != null || - (json is Map && json.entries.isNotEmpty) || - (json is List && json.isNotEmpty)) { - state = await fromJson(castNestedJson(json)); - } - } - - static Map castNestedJson(Map map) { - return Map.castFrom( - map.map((key, value) { - if (value is Map) { - return MapEntry( - key, - castNestedJson(value), - ); - } else if (value is Iterable) { - return MapEntry( - key, - value.map((e) { - if (e is Map) return castNestedJson(e); - return e; - }).toList(), - ); - } - return MapEntry(key, value); - }), - ); - } - - void save() async { - await box.put(cacheKey, toJson()); - } - - FutureOr fromJson(Map json); - Map toJson(); - - @override - set state(T value) { - if (state == value) return; - super.state = value; - save(); - } -} From a3021e4c52b24c5a6870eeb89fa00e3f8bd51636 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 14:14:02 +0600 Subject: [PATCH 36/92] chore: removed unused files --- lib/collections/cache_keys.dart | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 lib/collections/cache_keys.dart diff --git a/lib/collections/cache_keys.dart b/lib/collections/cache_keys.dart deleted file mode 100644 index bca133229..000000000 --- a/lib/collections/cache_keys.dart +++ /dev/null @@ -1,21 +0,0 @@ -abstract class LocalStorageKeys { - static String saveTrackLyrics = 'save_track_lyrics'; - static String recommendationMarket = 'recommendation_market'; - static String ytSearchFormate = 'youtube_search_format'; - - static String clientId = 'clientId'; - static String clientSecret = 'clientSecret'; - static String accessToken = 'accessToken'; - static String refreshToken = 'refreshToken'; - static String expiration = "expiration"; - static String geniusAccessToken = "genius_access_token"; - - static String themeMode = "theme_mode"; - static String nextTrackHotKey = "next_track_hot_key"; - static String prevTrackHotKey = "prev_track_hot_key"; - static String playPauseHotKey = "play_pause_hot_key"; - - static String volume = "volume"; - - static String windowSizeInfo = "window_size_info"; -} From ffb3a3377fe84947213b11aed100d2dd049bd13f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 15:44:24 +0600 Subject: [PATCH 37/92] chore: add migration script to migrate hive to drift --- lib/main.dart | 16 +- lib/modules/stats/top/top.dart | 2 +- lib/pages/stats/albums/albums.dart | 2 +- lib/pages/stats/artists/artists.dart | 2 +- lib/pages/stats/fees/fees.dart | 2 +- lib/pages/stats/minutes/minutes.dart | 2 +- lib/pages/stats/playlists/playlists.dart | 2 +- lib/pages/stats/streams/streams.dart | 2 +- lib/provider/history/state.dart | 35 - lib/provider/history/state.freezed.dart | 644 -------- lib/provider/history/state.g.dart | 55 - lib/provider/history/top.dart | 10 +- lib/services/kv_store/encrypted_kv_store.dart | 2 + lib/services/kv_store/kv_store.dart | 5 + lib/utils/migrations/adapters.dart | 320 ++++ lib/utils/migrations/adapters.freezed.dart | 1380 +++++++++++++++++ lib/utils/migrations/adapters.g.dart | 600 +++++++ lib/utils/migrations/cache_box.dart | 100 ++ lib/utils/migrations/hive.dart | 316 ++++ 19 files changed, 2754 insertions(+), 743 deletions(-) delete mode 100644 lib/provider/history/state.dart delete mode 100644 lib/provider/history/state.freezed.dart delete mode 100644 lib/provider/history/state.g.dart create mode 100644 lib/utils/migrations/adapters.dart create mode 100644 lib/utils/migrations/adapters.freezed.dart create mode 100644 lib/utils/migrations/adapters.g.dart create mode 100644 lib/utils/migrations/cache_box.dart create mode 100644 lib/utils/migrations/hive.dart diff --git a/lib/main.dart b/lib/main.dart index 69c890628..cb5531156 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,9 @@ 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/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player_streams.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; @@ -33,6 +35,7 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/themes/theme.dart'; +import 'package:spotube/utils/migrations/hive.dart'; import 'package:spotube/utils/platform.dart'; import 'package:system_theme/system_theme.dart'; import 'package:path_provider/path_provider.dart'; @@ -84,12 +87,23 @@ Future main(List rawArgs) async { Hive.init(hiveCacheDir); + final database = AppDatabase(); + + await migrateFromHiveToDrift(database); + if (kIsDesktop) { await localNotifier.setup(appName: "Spotube"); await WindowManagerTools.initialize(); } - runApp(const ProviderScope(child: Spotube())); + runApp( + ProviderScope( + overrides: [ + databaseProvider.overrideWith((ref) => database), + ], + child: const Spotube(), + ), + ); }); } diff --git a/lib/modules/stats/top/top.dart b/lib/modules/stats/top/top.dart index 52529f3ff..ea52c5173 100644 --- a/lib/modules/stats/top/top.dart +++ b/lib/modules/stats/top/top.dart @@ -5,7 +5,7 @@ import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/tracks.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsPageTopSection extends HookConsumerWidget { diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index a13e500b8..859eaf26c 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsAlbumsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index 9ebdbe5d9..e6dadd950 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsArtistsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index e881ec70c..e1d701eb0 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -4,7 +4,7 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsStreamFeesPage extends HookConsumerWidget { diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 1d6a5844d..587e90079 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsMinutesPage extends HookConsumerWidget { diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index 94f8ce9d1..f5ee62d06 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsPlaylistsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 41f2d33a0..20e8ff966 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsStreamsPage extends HookConsumerWidget { diff --git a/lib/provider/history/state.dart b/lib/provider/history/state.dart deleted file mode 100644 index 67658502f..000000000 --- a/lib/provider/history/state.dart +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index e2ee94210..000000000 --- a/lib/provider/history/state.freezed.dart +++ /dev/null @@ -1,644 +0,0 @@ -// 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 deleted file mode 100644 index dfd01c2cd..000000000 --- a/lib/provider/history/state.g.dart +++ /dev/null @@ -1,55 +0,0 @@ -// 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/top.dart b/lib/provider/history/top.dart index aa12c9b3c..965fb3ad8 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -6,7 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/history/state.dart'; + +enum HistoryDuration { + allTime, + days7, + days30, + months6, + year, + years2, +} final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart index ab4a750ec..4eca00070 100644 --- a/lib/services/kv_store/encrypted_kv_store.dart +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -10,6 +10,8 @@ abstract class EncryptedKvStoreService { ), ); + static FlutterSecureStorage get storage => _storage; + static String? _encryptionKeySync; static Future initialize() async { diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index 2707ea4db..efe83abf8 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -82,4 +82,9 @@ abstract class KVStoreService { static double get volume => sharedPreferences.getDouble('volume') ?? 1.0; static Future setVolume(double value) async => await sharedPreferences.setDouble('volume', value); + + static bool get hasMigratedToDrift => + sharedPreferences.getBool('hasMigratedToDrift') ?? false; + static Future setHasMigratedToDrift(bool value) async => + await sharedPreferences.setBool('hasMigratedToDrift', value); } diff --git a/lib/utils/migrations/adapters.dart b/lib/utils/migrations/adapters.dart new file mode 100644 index 000000000..f7f6350be --- /dev/null +++ b/lib/utils/migrations/adapters.dart @@ -0,0 +1,320 @@ +import 'package:hive/hive.dart'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; + +part 'adapters.g.dart'; +part 'adapters.freezed.dart'; + +@HiveType(typeId: 2) +class SkipSegment { + @HiveField(0) + final int start; + @HiveField(1) + final int end; + SkipSegment(this.start, this.end); + + static String version = 'v1'; + static final boxName = "oss.krtirtho.spotube.skip_segments.$version"; + static LazyBox get box => Hive.lazyBox(boxName); + + SkipSegment.fromJson(Map json) + : start = json['start'], + end = json['end']; + + Map toJson() => { + 'start': start, + 'end': end, + }; +} + +@JsonEnum() +@HiveType(typeId: 5) +enum SourceType { + @HiveField(0) + youtube._("YouTube"), + + @HiveField(1) + youtubeMusic._("YouTube Music"), + + @HiveField(2) + jiosaavn._("JioSaavn"); + + final String label; + + const SourceType._(this.label); +} + +@JsonSerializable() +@HiveType(typeId: 6) +class SourceMatch { + @HiveField(0) + String id; + + @HiveField(1) + String sourceId; + + @HiveField(2) + SourceType sourceType; + + @HiveField(3) + DateTime createdAt; + + SourceMatch({ + required this.id, + required this.sourceId, + required this.sourceType, + required this.createdAt, + }); + + factory SourceMatch.fromJson(Map json) => + _$SourceMatchFromJson(json); + + Map toJson() => _$SourceMatchToJson(this); + + static String version = 'v1'; + static final boxName = "oss.krtirtho.spotube.source_matches.$version"; + + static LazyBox get box => Hive.lazyBox(boxName); +} + +@JsonSerializable() +class AuthenticationCredentials { + String cookie; + String accessToken; + DateTime expiration; + + AuthenticationCredentials({ + required this.cookie, + required this.accessToken, + required this.expiration, + }); + + factory AuthenticationCredentials.fromJson(Map json) { + return AuthenticationCredentials( + cookie: json['cookie'] as String, + accessToken: json['accessToken'] as String, + expiration: DateTime.parse(json['expiration'] as String), + ); + } + + Map toJson() { + return { + 'cookie': cookie, + 'accessToken': accessToken, + 'expiration': expiration.toIso8601String(), + }; + } +} + +@JsonEnum() +enum LayoutMode { + compact, + extended, + adaptive, +} + +@JsonEnum() +enum CloseBehavior { + minimizeToTray, + close, +} + +@JsonEnum() +enum AudioSource { + youtube, + piped, + jiosaavn; + + String get label => name[0].toUpperCase() + name.substring(1); +} + +@JsonEnum() +enum MusicCodec { + m4a._("M4a (Best for downloaded music)"), + weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); + + final String label; + const MusicCodec._(this.label); +} + +@JsonEnum() +enum SearchMode { + youtube._("YouTube"), + youtubeMusic._("YouTube Music"); + + final String label; + + const SearchMode._(this.label); + + factory SearchMode.fromString(String key) { + return SearchMode.values.firstWhere((e) => e.name == key); + } +} + +@freezed +class UserPreferences with _$UserPreferences { + const factory UserPreferences({ + @Default(SourceQualities.high) SourceQualities audioQuality, + @Default(true) bool albumColorSync, + @Default(false) bool amoledDarkTheme, + @Default(true) bool checkUpdate, + @Default(false) bool normalizeAudio, + @Default(false) bool showSystemTrayIcon, + @Default(false) bool skipNonMusic, + @Default(false) bool systemTitleBar, + @Default(CloseBehavior.close) CloseBehavior closeBehavior, + @Default(SpotubeColor(0xFF2196F3, name: "Blue")) + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue, + ) + SpotubeColor accentColorScheme, + @Default(LayoutMode.adaptive) LayoutMode layoutMode, + @Default(Locale("system", "system")) + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue, + ) + Locale locale, + @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, + @Default(SourceCodecs.weba) SourceCodecs streamMusicCodec, + @Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec, + @Default(true) bool discordPresence, + @Default(true) bool endlessPlayback, + @Default(false) bool enableConnect, + }) = _UserPreferences; + factory UserPreferences.fromJson(Map json) => + _$UserPreferencesFromJson(json); + + factory UserPreferences.withDefaults() => UserPreferences.fromJson({}); + + static SpotubeColor _accentColorSchemeFromJson(Map json) { + return SpotubeColor.fromString(json["color"]); + } + + static Map? _accentColorSchemeReadValue( + Map json, String key) { + if (json[key] is String) { + return {"color": json[key]}; + } + + return json[key] as Map?; + } + + static Map _accentColorSchemeToJson(SpotubeColor color) { + return {"color": color.toString()}; + } + + static Locale _localeFromJson(Map json) { + return Locale(json["languageCode"], json["countryCode"]); + } + + static Map _localeToJson(Locale locale) { + return { + "languageCode": locale.languageCode, + "countryCode": locale.countryCode, + }; + } + + static Map? _localeReadValue( + Map json, String key) { + if (json[key] is String) { + final map = jsonDecode(json[key]); + return { + "languageCode": map["lc"], + "countryCode": map["cc"], + }; + } + + return json[key] as Map?; + } +} + +enum BlacklistedType { + artist, + track; + + static BlacklistedType fromName(String name) => + BlacklistedType.values.firstWhere((e) => e.name == name); +} + +class BlacklistedElement { + final String id; + final String name; + final BlacklistedType type; + + BlacklistedElement.fromJson(Map json) + : id = json['id'], + name = json['name'], + type = BlacklistedType.fromName(json['type']); + + Map toJson() => {'id': id, 'type': type.name, 'name': name}; +} + +@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); +} + +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() ?? + [], + ); + } +} + +class ScrobblerState { + final String username; + final String passwordHash; + + ScrobblerState({ + required this.username, + required this.passwordHash, + }); + + factory ScrobblerState.fromJson(Map json) { + return ScrobblerState( + username: json["username"], + passwordHash: json["passwordHash"], + ); + } +} diff --git a/lib/utils/migrations/adapters.freezed.dart b/lib/utils/migrations/adapters.freezed.dart new file mode 100644 index 000000000..339ec0e5b --- /dev/null +++ b/lib/utils/migrations/adapters.freezed.dart @@ -0,0 +1,1380 @@ +// 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 'adapters.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'); + +UserPreferences _$UserPreferencesFromJson(Map json) { + return _UserPreferences.fromJson(json); +} + +/// @nodoc +mixin _$UserPreferences { + SourceQualities get audioQuality => throw _privateConstructorUsedError; + bool get albumColorSync => throw _privateConstructorUsedError; + bool get amoledDarkTheme => throw _privateConstructorUsedError; + bool get checkUpdate => throw _privateConstructorUsedError; + bool get normalizeAudio => throw _privateConstructorUsedError; + bool get showSystemTrayIcon => throw _privateConstructorUsedError; + bool get skipNonMusic => throw _privateConstructorUsedError; + bool get systemTitleBar => throw _privateConstructorUsedError; + CloseBehavior get closeBehavior => throw _privateConstructorUsedError; + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor get accentColorScheme => throw _privateConstructorUsedError; + LayoutMode get layoutMode => throw _privateConstructorUsedError; + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale get locale => throw _privateConstructorUsedError; + 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; + SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError; + SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError; + bool get discordPresence => throw _privateConstructorUsedError; + bool get endlessPlayback => throw _privateConstructorUsedError; + bool get enableConnect => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UserPreferencesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserPreferencesCopyWith<$Res> { + factory $UserPreferencesCopyWith( + UserPreferences value, $Res Function(UserPreferences) then) = + _$UserPreferencesCopyWithImpl<$Res, UserPreferences>; + @useResult + $Res call( + {SourceQualities audioQuality, + bool albumColorSync, + bool amoledDarkTheme, + bool checkUpdate, + bool normalizeAudio, + bool showSystemTrayIcon, + bool skipNonMusic, + bool systemTitleBar, + CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor accentColorScheme, + LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale locale, + Market recommendationMarket, + SearchMode searchMode, + String downloadLocation, + List localLibraryLocation, + String pipedInstance, + ThemeMode themeMode, + AudioSource audioSource, + SourceCodecs streamMusicCodec, + SourceCodecs downloadMusicCodec, + bool discordPresence, + bool endlessPlayback, + bool enableConnect}); +} + +/// @nodoc +class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> + implements $UserPreferencesCopyWith<$Res> { + _$UserPreferencesCopyWithImpl(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? audioQuality = null, + Object? albumColorSync = null, + Object? amoledDarkTheme = null, + Object? checkUpdate = null, + Object? normalizeAudio = null, + Object? showSystemTrayIcon = null, + Object? skipNonMusic = null, + Object? systemTitleBar = null, + Object? closeBehavior = null, + Object? accentColorScheme = null, + Object? layoutMode = null, + Object? locale = null, + Object? recommendationMarket = null, + Object? searchMode = null, + Object? downloadLocation = null, + Object? localLibraryLocation = null, + Object? pipedInstance = null, + Object? themeMode = null, + Object? audioSource = null, + Object? streamMusicCodec = null, + Object? downloadMusicCodec = null, + Object? discordPresence = null, + Object? endlessPlayback = null, + Object? enableConnect = null, + }) { + return _then(_value.copyWith( + audioQuality: null == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as SourceQualities, + albumColorSync: null == albumColorSync + ? _value.albumColorSync + : albumColorSync // ignore: cast_nullable_to_non_nullable + as bool, + amoledDarkTheme: null == amoledDarkTheme + ? _value.amoledDarkTheme + : amoledDarkTheme // ignore: cast_nullable_to_non_nullable + as bool, + checkUpdate: null == checkUpdate + ? _value.checkUpdate + : checkUpdate // ignore: cast_nullable_to_non_nullable + as bool, + normalizeAudio: null == normalizeAudio + ? _value.normalizeAudio + : normalizeAudio // ignore: cast_nullable_to_non_nullable + as bool, + showSystemTrayIcon: null == showSystemTrayIcon + ? _value.showSystemTrayIcon + : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable + as bool, + skipNonMusic: null == skipNonMusic + ? _value.skipNonMusic + : skipNonMusic // ignore: cast_nullable_to_non_nullable + as bool, + systemTitleBar: null == systemTitleBar + ? _value.systemTitleBar + : systemTitleBar // ignore: cast_nullable_to_non_nullable + as bool, + closeBehavior: null == closeBehavior + ? _value.closeBehavior + : closeBehavior // ignore: cast_nullable_to_non_nullable + as CloseBehavior, + accentColorScheme: null == accentColorScheme + ? _value.accentColorScheme + : accentColorScheme // ignore: cast_nullable_to_non_nullable + as SpotubeColor, + layoutMode: null == layoutMode + ? _value.layoutMode + : layoutMode // ignore: cast_nullable_to_non_nullable + as LayoutMode, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + recommendationMarket: null == recommendationMarket + ? _value.recommendationMarket + : recommendationMarket // ignore: cast_nullable_to_non_nullable + as Market, + searchMode: null == searchMode + ? _value.searchMode + : searchMode // ignore: cast_nullable_to_non_nullable + as SearchMode, + downloadLocation: null == downloadLocation + ? _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 + as String, + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + audioSource: null == audioSource + ? _value.audioSource + : audioSource // ignore: cast_nullable_to_non_nullable + as AudioSource, + streamMusicCodec: null == streamMusicCodec + ? _value.streamMusicCodec + : streamMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + downloadMusicCodec: null == downloadMusicCodec + ? _value.downloadMusicCodec + : downloadMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + discordPresence: null == discordPresence + ? _value.discordPresence + : discordPresence // ignore: cast_nullable_to_non_nullable + as bool, + endlessPlayback: null == endlessPlayback + ? _value.endlessPlayback + : endlessPlayback // ignore: cast_nullable_to_non_nullable + as bool, + enableConnect: null == enableConnect + ? _value.enableConnect + : enableConnect // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserPreferencesImplCopyWith<$Res> + implements $UserPreferencesCopyWith<$Res> { + factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value, + $Res Function(_$UserPreferencesImpl) then) = + __$$UserPreferencesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SourceQualities audioQuality, + bool albumColorSync, + bool amoledDarkTheme, + bool checkUpdate, + bool normalizeAudio, + bool showSystemTrayIcon, + bool skipNonMusic, + bool systemTitleBar, + CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor accentColorScheme, + LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale locale, + Market recommendationMarket, + SearchMode searchMode, + String downloadLocation, + List localLibraryLocation, + String pipedInstance, + ThemeMode themeMode, + AudioSource audioSource, + SourceCodecs streamMusicCodec, + SourceCodecs downloadMusicCodec, + bool discordPresence, + bool endlessPlayback, + bool enableConnect}); +} + +/// @nodoc +class __$$UserPreferencesImplCopyWithImpl<$Res> + extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl> + implements _$$UserPreferencesImplCopyWith<$Res> { + __$$UserPreferencesImplCopyWithImpl( + _$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? audioQuality = null, + Object? albumColorSync = null, + Object? amoledDarkTheme = null, + Object? checkUpdate = null, + Object? normalizeAudio = null, + Object? showSystemTrayIcon = null, + Object? skipNonMusic = null, + Object? systemTitleBar = null, + Object? closeBehavior = null, + Object? accentColorScheme = null, + Object? layoutMode = null, + Object? locale = null, + Object? recommendationMarket = null, + Object? searchMode = null, + Object? downloadLocation = null, + Object? localLibraryLocation = null, + Object? pipedInstance = null, + Object? themeMode = null, + Object? audioSource = null, + Object? streamMusicCodec = null, + Object? downloadMusicCodec = null, + Object? discordPresence = null, + Object? endlessPlayback = null, + Object? enableConnect = null, + }) { + return _then(_$UserPreferencesImpl( + audioQuality: null == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as SourceQualities, + albumColorSync: null == albumColorSync + ? _value.albumColorSync + : albumColorSync // ignore: cast_nullable_to_non_nullable + as bool, + amoledDarkTheme: null == amoledDarkTheme + ? _value.amoledDarkTheme + : amoledDarkTheme // ignore: cast_nullable_to_non_nullable + as bool, + checkUpdate: null == checkUpdate + ? _value.checkUpdate + : checkUpdate // ignore: cast_nullable_to_non_nullable + as bool, + normalizeAudio: null == normalizeAudio + ? _value.normalizeAudio + : normalizeAudio // ignore: cast_nullable_to_non_nullable + as bool, + showSystemTrayIcon: null == showSystemTrayIcon + ? _value.showSystemTrayIcon + : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable + as bool, + skipNonMusic: null == skipNonMusic + ? _value.skipNonMusic + : skipNonMusic // ignore: cast_nullable_to_non_nullable + as bool, + systemTitleBar: null == systemTitleBar + ? _value.systemTitleBar + : systemTitleBar // ignore: cast_nullable_to_non_nullable + as bool, + closeBehavior: null == closeBehavior + ? _value.closeBehavior + : closeBehavior // ignore: cast_nullable_to_non_nullable + as CloseBehavior, + accentColorScheme: null == accentColorScheme + ? _value.accentColorScheme + : accentColorScheme // ignore: cast_nullable_to_non_nullable + as SpotubeColor, + layoutMode: null == layoutMode + ? _value.layoutMode + : layoutMode // ignore: cast_nullable_to_non_nullable + as LayoutMode, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + recommendationMarket: null == recommendationMarket + ? _value.recommendationMarket + : recommendationMarket // ignore: cast_nullable_to_non_nullable + as Market, + searchMode: null == searchMode + ? _value.searchMode + : searchMode // ignore: cast_nullable_to_non_nullable + as SearchMode, + downloadLocation: null == downloadLocation + ? _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 + as String, + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + audioSource: null == audioSource + ? _value.audioSource + : audioSource // ignore: cast_nullable_to_non_nullable + as AudioSource, + streamMusicCodec: null == streamMusicCodec + ? _value.streamMusicCodec + : streamMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + downloadMusicCodec: null == downloadMusicCodec + ? _value.downloadMusicCodec + : downloadMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + discordPresence: null == discordPresence + ? _value.discordPresence + : discordPresence // ignore: cast_nullable_to_non_nullable + as bool, + endlessPlayback: null == endlessPlayback + ? _value.endlessPlayback + : endlessPlayback // ignore: cast_nullable_to_non_nullable + as bool, + enableConnect: null == enableConnect + ? _value.enableConnect + : enableConnect // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserPreferencesImpl implements _UserPreferences { + const _$UserPreferencesImpl( + {this.audioQuality = SourceQualities.high, + this.albumColorSync = true, + this.amoledDarkTheme = false, + this.checkUpdate = true, + this.normalizeAudio = false, + this.showSystemTrayIcon = false, + this.skipNonMusic = false, + this.systemTitleBar = false, + this.closeBehavior = CloseBehavior.close, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"), + this.layoutMode = LayoutMode.adaptive, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + this.locale = const Locale("system", "system"), + 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, + this.streamMusicCodec = SourceCodecs.weba, + this.downloadMusicCodec = SourceCodecs.m4a, + this.discordPresence = true, + this.endlessPlayback = true, + this.enableConnect = false}) + : _localLibraryLocation = localLibraryLocation; + + factory _$UserPreferencesImpl.fromJson(Map json) => + _$$UserPreferencesImplFromJson(json); + + @override + @JsonKey() + final SourceQualities audioQuality; + @override + @JsonKey() + final bool albumColorSync; + @override + @JsonKey() + final bool amoledDarkTheme; + @override + @JsonKey() + final bool checkUpdate; + @override + @JsonKey() + final bool normalizeAudio; + @override + @JsonKey() + final bool showSystemTrayIcon; + @override + @JsonKey() + final bool skipNonMusic; + @override + @JsonKey() + final bool systemTitleBar; + @override + @JsonKey() + final CloseBehavior closeBehavior; + @override + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + final SpotubeColor accentColorScheme; + @override + @JsonKey() + final LayoutMode layoutMode; + @override + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + final Locale locale; + @override + @JsonKey() + final Market recommendationMarket; + @override + @JsonKey() + final SearchMode searchMode; + @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; + @override + @JsonKey() + final ThemeMode themeMode; + @override + @JsonKey() + final AudioSource audioSource; + @override + @JsonKey() + final SourceCodecs streamMusicCodec; + @override + @JsonKey() + final SourceCodecs downloadMusicCodec; + @override + @JsonKey() + final bool discordPresence; + @override + @JsonKey() + final bool endlessPlayback; + @override + @JsonKey() + final bool enableConnect; + + @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, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserPreferencesImpl && + (identical(other.audioQuality, audioQuality) || + other.audioQuality == audioQuality) && + (identical(other.albumColorSync, albumColorSync) || + other.albumColorSync == albumColorSync) && + (identical(other.amoledDarkTheme, amoledDarkTheme) || + other.amoledDarkTheme == amoledDarkTheme) && + (identical(other.checkUpdate, checkUpdate) || + other.checkUpdate == checkUpdate) && + (identical(other.normalizeAudio, normalizeAudio) || + other.normalizeAudio == normalizeAudio) && + (identical(other.showSystemTrayIcon, showSystemTrayIcon) || + other.showSystemTrayIcon == showSystemTrayIcon) && + (identical(other.skipNonMusic, skipNonMusic) || + other.skipNonMusic == skipNonMusic) && + (identical(other.systemTitleBar, systemTitleBar) || + other.systemTitleBar == systemTitleBar) && + (identical(other.closeBehavior, closeBehavior) || + other.closeBehavior == closeBehavior) && + (identical(other.accentColorScheme, accentColorScheme) || + other.accentColorScheme == accentColorScheme) && + (identical(other.layoutMode, layoutMode) || + other.layoutMode == layoutMode) && + (identical(other.locale, locale) || other.locale == locale) && + (identical(other.recommendationMarket, recommendationMarket) || + other.recommendationMarket == recommendationMarket) && + (identical(other.searchMode, searchMode) || + 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) || + other.themeMode == themeMode) && + (identical(other.audioSource, audioSource) || + other.audioSource == audioSource) && + (identical(other.streamMusicCodec, streamMusicCodec) || + other.streamMusicCodec == streamMusicCodec) && + (identical(other.downloadMusicCodec, downloadMusicCodec) || + other.downloadMusicCodec == downloadMusicCodec) && + (identical(other.discordPresence, discordPresence) || + other.discordPresence == discordPresence) && + (identical(other.endlessPlayback, endlessPlayback) || + other.endlessPlayback == endlessPlayback) && + (identical(other.enableConnect, enableConnect) || + other.enableConnect == enableConnect)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hashAll([ + runtimeType, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + skipNonMusic, + systemTitleBar, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + recommendationMarket, + searchMode, + downloadLocation, + const DeepCollectionEquality().hash(_localLibraryLocation), + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => + __$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$UserPreferencesImplToJson( + this, + ); + } +} + +abstract class _UserPreferences implements UserPreferences { + const factory _UserPreferences( + {final SourceQualities audioQuality, + final bool albumColorSync, + final bool amoledDarkTheme, + final bool checkUpdate, + final bool normalizeAudio, + final bool showSystemTrayIcon, + final bool skipNonMusic, + final bool systemTitleBar, + final CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + final SpotubeColor accentColorScheme, + final LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + final Locale locale, + final Market recommendationMarket, + final SearchMode searchMode, + final String downloadLocation, + final List localLibraryLocation, + final String pipedInstance, + final ThemeMode themeMode, + final AudioSource audioSource, + final SourceCodecs streamMusicCodec, + final SourceCodecs downloadMusicCodec, + final bool discordPresence, + final bool endlessPlayback, + final bool enableConnect}) = _$UserPreferencesImpl; + + factory _UserPreferences.fromJson(Map json) = + _$UserPreferencesImpl.fromJson; + + @override + SourceQualities get audioQuality; + @override + bool get albumColorSync; + @override + bool get amoledDarkTheme; + @override + bool get checkUpdate; + @override + bool get normalizeAudio; + @override + bool get showSystemTrayIcon; + @override + bool get skipNonMusic; + @override + bool get systemTitleBar; + @override + CloseBehavior get closeBehavior; + @override + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor get accentColorScheme; + @override + LayoutMode get layoutMode; + @override + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale get locale; + @override + Market get recommendationMarket; + @override + SearchMode get searchMode; + @override + String get downloadLocation; + @override + List get localLibraryLocation; + @override + String get pipedInstance; + @override + ThemeMode get themeMode; + @override + AudioSource get audioSource; + @override + SourceCodecs get streamMusicCodec; + @override + SourceCodecs get downloadMusicCodec; + @override + bool get discordPresence; + @override + bool get endlessPlayback; + @override + bool get enableConnect; + @override + @JsonKey(ignore: true) + _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => + throw _privateConstructorUsedError; +} + +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/utils/migrations/adapters.g.dart b/lib/utils/migrations/adapters.g.dart new file mode 100644 index 000000000..ca95a8404 --- /dev/null +++ b/lib/utils/migrations/adapters.g.dart @@ -0,0 +1,600 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'adapters.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SkipSegmentAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + SkipSegment read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SkipSegment( + fields[0] as int, + fields[1] as int, + ); + } + + @override + void write(BinaryWriter writer, SkipSegment obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.start) + ..writeByte(1) + ..write(obj.end); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SkipSegmentAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SourceMatchAdapter extends TypeAdapter { + @override + final int typeId = 6; + + @override + SourceMatch read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SourceMatch( + id: fields[0] as String, + sourceId: fields[1] as String, + sourceType: fields[2] as SourceType, + createdAt: fields[3] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, SourceMatch obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.sourceId) + ..writeByte(2) + ..write(obj.sourceType) + ..writeByte(3) + ..write(obj.createdAt); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SourceMatchAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SourceTypeAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + SourceType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return SourceType.youtube; + case 1: + return SourceType.youtubeMusic; + case 2: + return SourceType.jiosaavn; + default: + return SourceType.youtube; + } + } + + @override + void write(BinaryWriter writer, SourceType obj) { + switch (obj) { + case SourceType.youtube: + writer.writeByte(0); + break; + case SourceType.youtubeMusic: + writer.writeByte(1); + break; + case SourceType.jiosaavn: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SourceTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( + id: json['id'] as String, + sourceId: json['sourceId'] as String, + sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']), + createdAt: DateTime.parse(json['createdAt'] as String), + ); + +Map _$SourceMatchToJson(SourceMatch instance) => + { + 'id': instance.id, + 'sourceId': instance.sourceId, + 'sourceType': _$SourceTypeEnumMap[instance.sourceType]!, + 'createdAt': instance.createdAt.toIso8601String(), + }; + +const _$SourceTypeEnumMap = { + SourceType.youtube: 'youtube', + SourceType.youtubeMusic: 'youtubeMusic', + SourceType.jiosaavn: 'jiosaavn', +}; + +AuthenticationCredentials _$AuthenticationCredentialsFromJson(Map json) => + AuthenticationCredentials( + cookie: json['cookie'] as String, + accessToken: json['accessToken'] as String, + expiration: DateTime.parse(json['expiration'] as String), + ); + +Map _$AuthenticationCredentialsToJson( + AuthenticationCredentials instance) => + { + 'cookie': instance.cookie, + 'accessToken': instance.accessToken, + 'expiration': instance.expiration.toIso8601String(), + }; + +_$UserPreferencesImpl _$$UserPreferencesImplFromJson(Map json) => + _$UserPreferencesImpl( + audioQuality: + $enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ?? + SourceQualities.high, + albumColorSync: json['albumColorSync'] as bool? ?? true, + amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, + checkUpdate: json['checkUpdate'] as bool? ?? true, + normalizeAudio: json['normalizeAudio'] as bool? ?? false, + showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? false, + skipNonMusic: json['skipNonMusic'] as bool? ?? false, + systemTitleBar: json['systemTitleBar'] as bool? ?? false, + closeBehavior: + $enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ?? + CloseBehavior.close, + accentColorScheme: UserPreferences._accentColorSchemeReadValue( + json, 'accentColorScheme') == + null + ? const SpotubeColor(0xFF2196F3, name: "Blue") + : UserPreferences._accentColorSchemeFromJson( + UserPreferences._accentColorSchemeReadValue( + json, 'accentColorScheme') as Map), + layoutMode: + $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ?? + LayoutMode.adaptive, + locale: UserPreferences._localeReadValue(json, 'locale') == null + ? const Locale("system", "system") + : UserPreferences._localeFromJson( + UserPreferences._localeReadValue(json, 'locale') + as Map), + recommendationMarket: + $enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ?? + Market.US, + searchMode: + $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']) ?? + ThemeMode.system, + audioSource: + $enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ?? + AudioSource.youtube, + streamMusicCodec: $enumDecodeNullable( + _$SourceCodecsEnumMap, json['streamMusicCodec']) ?? + SourceCodecs.weba, + downloadMusicCodec: $enumDecodeNullable( + _$SourceCodecsEnumMap, json['downloadMusicCodec']) ?? + SourceCodecs.m4a, + discordPresence: json['discordPresence'] as bool? ?? true, + endlessPlayback: json['endlessPlayback'] as bool? ?? true, + enableConnect: json['enableConnect'] as bool? ?? false, + ); + +Map _$$UserPreferencesImplToJson( + _$UserPreferencesImpl instance) => + { + 'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!, + 'albumColorSync': instance.albumColorSync, + 'amoledDarkTheme': instance.amoledDarkTheme, + 'checkUpdate': instance.checkUpdate, + 'normalizeAudio': instance.normalizeAudio, + 'showSystemTrayIcon': instance.showSystemTrayIcon, + 'skipNonMusic': instance.skipNonMusic, + 'systemTitleBar': instance.systemTitleBar, + 'closeBehavior': _$CloseBehaviorEnumMap[instance.closeBehavior]!, + 'accentColorScheme': + UserPreferences._accentColorSchemeToJson(instance.accentColorScheme), + 'layoutMode': _$LayoutModeEnumMap[instance.layoutMode]!, + 'locale': UserPreferences._localeToJson(instance.locale), + '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]!, + 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, + 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, + 'discordPresence': instance.discordPresence, + 'endlessPlayback': instance.endlessPlayback, + 'enableConnect': instance.enableConnect, + }; + +const _$SourceQualitiesEnumMap = { + SourceQualities.high: 'high', + SourceQualities.medium: 'medium', + SourceQualities.low: 'low', +}; + +const _$CloseBehaviorEnumMap = { + CloseBehavior.minimizeToTray: 'minimizeToTray', + CloseBehavior.close: 'close', +}; + +const _$LayoutModeEnumMap = { + LayoutMode.compact: 'compact', + LayoutMode.extended: 'extended', + LayoutMode.adaptive: 'adaptive', +}; + +const _$MarketEnumMap = { + Market.AD: 'AD', + Market.AE: 'AE', + Market.AF: 'AF', + Market.AG: 'AG', + Market.AI: 'AI', + Market.AL: 'AL', + Market.AM: 'AM', + Market.AO: 'AO', + Market.AQ: 'AQ', + Market.AR: 'AR', + Market.AS: 'AS', + Market.AT: 'AT', + Market.AU: 'AU', + Market.AW: 'AW', + Market.AX: 'AX', + Market.AZ: 'AZ', + Market.BA: 'BA', + Market.BB: 'BB', + Market.BD: 'BD', + Market.BE: 'BE', + Market.BF: 'BF', + Market.BG: 'BG', + Market.BH: 'BH', + Market.BI: 'BI', + Market.BJ: 'BJ', + Market.BL: 'BL', + Market.BM: 'BM', + Market.BN: 'BN', + Market.BO: 'BO', + Market.BQ: 'BQ', + Market.BR: 'BR', + Market.BS: 'BS', + Market.BT: 'BT', + Market.BV: 'BV', + Market.BW: 'BW', + Market.BY: 'BY', + Market.BZ: 'BZ', + Market.CA: 'CA', + Market.CC: 'CC', + Market.CD: 'CD', + Market.CF: 'CF', + Market.CG: 'CG', + Market.CH: 'CH', + Market.CI: 'CI', + Market.CK: 'CK', + Market.CL: 'CL', + Market.CM: 'CM', + Market.CN: 'CN', + Market.CO: 'CO', + Market.CR: 'CR', + Market.CU: 'CU', + Market.CV: 'CV', + Market.CW: 'CW', + Market.CX: 'CX', + Market.CY: 'CY', + Market.CZ: 'CZ', + Market.DE: 'DE', + Market.DJ: 'DJ', + Market.DK: 'DK', + Market.DM: 'DM', + Market.DO: 'DO', + Market.DZ: 'DZ', + Market.EC: 'EC', + Market.EE: 'EE', + Market.EG: 'EG', + Market.EH: 'EH', + Market.ER: 'ER', + Market.ES: 'ES', + Market.ET: 'ET', + Market.FI: 'FI', + Market.FJ: 'FJ', + Market.FK: 'FK', + Market.FM: 'FM', + Market.FO: 'FO', + Market.FR: 'FR', + Market.GA: 'GA', + Market.GB: 'GB', + Market.GD: 'GD', + Market.GE: 'GE', + Market.GF: 'GF', + Market.GG: 'GG', + Market.GH: 'GH', + Market.GI: 'GI', + Market.GL: 'GL', + Market.GM: 'GM', + Market.GN: 'GN', + Market.GP: 'GP', + Market.GQ: 'GQ', + Market.GR: 'GR', + Market.GS: 'GS', + Market.GT: 'GT', + Market.GU: 'GU', + Market.GW: 'GW', + Market.GY: 'GY', + Market.HK: 'HK', + Market.HM: 'HM', + Market.HN: 'HN', + Market.HR: 'HR', + Market.HT: 'HT', + Market.HU: 'HU', + Market.ID: 'ID', + Market.IE: 'IE', + Market.IL: 'IL', + Market.IM: 'IM', + Market.IN: 'IN', + Market.IO: 'IO', + Market.IQ: 'IQ', + Market.IR: 'IR', + Market.IS: 'IS', + Market.IT: 'IT', + Market.JE: 'JE', + Market.JM: 'JM', + Market.JO: 'JO', + Market.JP: 'JP', + Market.KE: 'KE', + Market.KG: 'KG', + Market.KH: 'KH', + Market.KI: 'KI', + Market.KM: 'KM', + Market.KN: 'KN', + Market.KP: 'KP', + Market.KR: 'KR', + Market.KW: 'KW', + Market.KY: 'KY', + Market.KZ: 'KZ', + Market.LA: 'LA', + Market.LB: 'LB', + Market.LC: 'LC', + Market.LI: 'LI', + Market.LK: 'LK', + Market.LR: 'LR', + Market.LS: 'LS', + Market.LT: 'LT', + Market.LU: 'LU', + Market.LV: 'LV', + Market.LY: 'LY', + Market.MA: 'MA', + Market.MC: 'MC', + Market.MD: 'MD', + Market.ME: 'ME', + Market.MF: 'MF', + Market.MG: 'MG', + Market.MH: 'MH', + Market.MK: 'MK', + Market.ML: 'ML', + Market.MM: 'MM', + Market.MN: 'MN', + Market.MO: 'MO', + Market.MP: 'MP', + Market.MQ: 'MQ', + Market.MR: 'MR', + Market.MS: 'MS', + Market.MT: 'MT', + Market.MU: 'MU', + Market.MV: 'MV', + Market.MW: 'MW', + Market.MX: 'MX', + Market.MY: 'MY', + Market.MZ: 'MZ', + Market.NA: 'NA', + Market.NC: 'NC', + Market.NE: 'NE', + Market.NF: 'NF', + Market.NG: 'NG', + Market.NI: 'NI', + Market.NL: 'NL', + Market.NO: 'NO', + Market.NP: 'NP', + Market.NR: 'NR', + Market.NU: 'NU', + Market.NZ: 'NZ', + Market.OM: 'OM', + Market.PA: 'PA', + Market.PE: 'PE', + Market.PF: 'PF', + Market.PG: 'PG', + Market.PH: 'PH', + Market.PK: 'PK', + Market.PL: 'PL', + Market.PM: 'PM', + Market.PN: 'PN', + Market.PR: 'PR', + Market.PS: 'PS', + Market.PT: 'PT', + Market.PW: 'PW', + Market.PY: 'PY', + Market.QA: 'QA', + Market.RE: 'RE', + Market.RO: 'RO', + Market.RS: 'RS', + Market.RU: 'RU', + Market.RW: 'RW', + Market.SA: 'SA', + Market.SB: 'SB', + Market.SC: 'SC', + Market.SD: 'SD', + Market.SE: 'SE', + Market.SG: 'SG', + Market.SH: 'SH', + Market.SI: 'SI', + Market.SJ: 'SJ', + Market.SK: 'SK', + Market.SL: 'SL', + Market.SM: 'SM', + Market.SN: 'SN', + Market.SO: 'SO', + Market.SR: 'SR', + Market.SS: 'SS', + Market.ST: 'ST', + Market.SV: 'SV', + Market.SX: 'SX', + Market.SY: 'SY', + Market.SZ: 'SZ', + Market.TC: 'TC', + Market.TD: 'TD', + Market.TF: 'TF', + Market.TG: 'TG', + Market.TH: 'TH', + Market.TJ: 'TJ', + Market.TK: 'TK', + Market.TL: 'TL', + Market.TM: 'TM', + Market.TN: 'TN', + Market.TO: 'TO', + Market.TR: 'TR', + Market.TT: 'TT', + Market.TV: 'TV', + Market.TW: 'TW', + Market.TZ: 'TZ', + Market.UA: 'UA', + Market.UG: 'UG', + Market.UM: 'UM', + Market.US: 'US', + Market.UY: 'UY', + Market.UZ: 'UZ', + Market.VA: 'VA', + Market.VC: 'VC', + Market.VE: 'VE', + Market.VG: 'VG', + Market.VI: 'VI', + Market.VN: 'VN', + Market.VU: 'VU', + Market.WF: 'WF', + Market.WS: 'WS', + Market.XK: 'XK', + Market.YE: 'YE', + Market.YT: 'YT', + Market.ZA: 'ZA', + Market.ZM: 'ZM', + Market.ZW: 'ZW', +}; + +const _$SearchModeEnumMap = { + SearchMode.youtube: 'youtube', + SearchMode.youtubeMusic: 'youtubeMusic', +}; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +const _$AudioSourceEnumMap = { + AudioSource.youtube: 'youtube', + AudioSource.piped: 'piped', + AudioSource.jiosaavn: 'jiosaavn', +}; + +const _$SourceCodecsEnumMap = { + SourceCodecs.m4a: 'm4a', + SourceCodecs.weba: 'weba', +}; + +_$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/utils/migrations/cache_box.dart b/lib/utils/migrations/cache_box.dart new file mode 100644 index 000000000..dfe1947bd --- /dev/null +++ b/lib/utils/migrations/cache_box.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:hive/hive.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/provider/spotify/utils/json_cast.dart'; +import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/primitive_utils.dart'; + +const kKeyBoxName = "spotube_box_name"; +const kNoEncryptionWarningShownKey = "showedNoEncryptionWarning"; +const kIsUsingEncryption = "isUsingEncryption"; +String getBoxKey(String boxName) => "spotube_box_$boxName"; + +class PersistenceCacheBox { + static late LazyBox _box; + static late LazyBox _encryptedBox; + + final String cacheKey; + final bool encrypted; + + final T Function(Map) fromJson; + + PersistenceCacheBox( + this.cacheKey, { + required this.fromJson, + this.encrypted = false, + }); + + static Future read(String key) async { + final localStorage = await SharedPreferences.getInstance(); + if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) { + return localStorage.getString(key); + } + + try { + await localStorage.setBool(kIsUsingEncryption, true); + return await EncryptedKvStoreService.storage.read(key: key); + } catch (e) { + await localStorage.setBool(kIsUsingEncryption, false); + return localStorage.getString(key); + } + } + + static Future write(String key, String value) async { + final localStorage = await SharedPreferences.getInstance(); + if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) { + await localStorage.setString(key, value); + return; + } + + try { + await localStorage.setBool(kIsUsingEncryption, true); + await EncryptedKvStoreService.storage.write(key: key, value: value); + } catch (e) { + await localStorage.setBool(kIsUsingEncryption, false); + await localStorage.setString(key, value); + } + } + + static Future initializeBoxes({required String? path}) async { + String? boxName = await read(kKeyBoxName); + + if (boxName == null) { + boxName = "spotube-${PrimitiveUtils.uuid.v4()}"; + await write(kKeyBoxName, boxName); + } + + String? encryptionKey = await read(getBoxKey(boxName)); + + if (encryptionKey == null) { + encryptionKey = base64Url.encode(Hive.generateSecureKey()); + await write(getBoxKey(boxName), encryptionKey); + } + + _encryptedBox = await Hive.openLazyBox( + boxName, + encryptionCipher: HiveAesCipher(base64Url.decode(encryptionKey)), + ); + + _box = await Hive.openLazyBox( + "spotube_cache", + path: path, + ); + } + + LazyBox get box => encrypted ? _encryptedBox : _box; + + Future getData() async { + final json = await box.get(cacheKey); + + if (json != null || + (json is Map && json.entries.isNotEmpty) || + (json is List && json.isNotEmpty)) { + return fromJson(castNestedJson(json)); + } + + return null; + } +} diff --git a/lib/utils/migrations/hive.dart b/lib/utils/migrations/hive.dart new file mode 100644 index 000000000..e43df1d80 --- /dev/null +++ b/lib/utils/migrations/hive.dart @@ -0,0 +1,316 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotube/models/database/database.dart' + hide + SourceType, + AudioSource, + CloseBehavior, + MusicCodec, + LayoutMode, + SearchMode, + BlacklistedType; +import 'package:spotube/models/database/database.dart' as db; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/utils/migrations/adapters.dart'; +import 'package:spotube/utils/migrations/cache_box.dart'; + +late AppDatabase _database; + +Future getHiveCacheDir() async => + kIsWeb ? null : (await getApplicationSupportDirectory()).path; + +Future migrateAuthenticationInfo() async { + AppLogger.log.i("🔵 Migrating authentication info.."); + + final box = PersistenceCacheBox( + "authentication", + encrypted: true, + fromJson: (json) => AuthenticationCredentials.fromJson(json), + ); + + final credentials = await box.getData(); + + if (credentials == null) return; + + await _database.into(_database.authenticationTable).insertOnConflictUpdate( + AuthenticationTableCompanion.insert( + accessToken: DecryptedText(credentials.accessToken), + cookie: DecryptedText(credentials.cookie), + expiration: credentials.expiration, + id: const Value(0), + ), + ); + + AppLogger.log.i("✅ Migrated authentication info"); +} + +Future migratePreferences() async { + AppLogger.log.i("🔵 Migrating preferences.."); + final box = PersistenceCacheBox( + "preferences", + fromJson: (json) => UserPreferences.fromJson(json), + ); + + final preferences = await box.getData(); + + if (preferences == null) return; + + await _database.into(_database.preferencesTable).insertOnConflictUpdate( + PreferencesTableCompanion.insert( + id: const Value(0), + accentColorScheme: Value(preferences.accentColorScheme), + albumColorSync: Value(preferences.albumColorSync), + amoledDarkTheme: Value(preferences.amoledDarkTheme), + audioQuality: Value(preferences.audioQuality), + audioSource: Value( + switch (preferences.audioSource) { + AudioSource.youtube => db.AudioSource.youtube, + AudioSource.piped => db.AudioSource.piped, + AudioSource.jiosaavn => db.AudioSource.jiosaavn, + }, + ), + checkUpdate: Value(preferences.checkUpdate), + closeBehavior: Value( + switch (preferences.closeBehavior) { + CloseBehavior.minimizeToTray => db.CloseBehavior.minimizeToTray, + CloseBehavior.close => db.CloseBehavior.close, + }, + ), + discordPresence: Value(preferences.discordPresence), + downloadLocation: Value(preferences.downloadLocation), + downloadMusicCodec: Value(preferences.downloadMusicCodec), + enableConnect: Value(preferences.enableConnect), + endlessPlayback: Value(preferences.endlessPlayback), + layoutMode: Value( + switch (preferences.layoutMode) { + LayoutMode.adaptive => db.LayoutMode.adaptive, + LayoutMode.compact => db.LayoutMode.compact, + LayoutMode.extended => db.LayoutMode.extended, + }, + ), + localLibraryLocation: Value(preferences.localLibraryLocation), + locale: Value(preferences.locale), + market: Value(preferences.recommendationMarket), + normalizeAudio: Value(preferences.normalizeAudio), + pipedInstance: Value(preferences.pipedInstance), + searchMode: Value( + switch (preferences.searchMode) { + SearchMode.youtube => db.SearchMode.youtube, + SearchMode.youtubeMusic => db.SearchMode.youtubeMusic, + }, + ), + showSystemTrayIcon: Value(preferences.showSystemTrayIcon), + skipNonMusic: Value(preferences.skipNonMusic), + streamMusicCodec: Value(preferences.streamMusicCodec), + systemTitleBar: Value(preferences.systemTitleBar), + themeMode: Value(preferences.themeMode), + ), + ); + + AppLogger.log.i("✅ Migrated preferences"); +} + +Future migrateSkipSegment() async { + AppLogger.log.i("🔵 Migrating skip segments.."); + Hive.registerAdapter(SkipSegmentAdapter()); + + final box = await Hive.openLazyBox( + SkipSegment.boxName, + path: await getHiveCacheDir(), + ); + + final skipSegments = await Future.wait( + box.keys.map( + (key) async => ( + id: key as String, + data: await box.get(key), + ), + ), + ); + + await _database.batch((batch) { + batch.insertAll( + _database.skipSegmentTable, + skipSegments + .where((element) => element.data != null) + .expand((element) => (element.data as List).map( + (segment) => SkipSegmentTableCompanion.insert( + trackId: element.id, + start: segment["start"], + end: segment["end"], + ), + )) + .toList(), + ); + }); + + AppLogger.log.i("✅ Migrated skip segments"); +} + +Future migrateSourceMatches() async { + AppLogger.log.i("🔵 Migrating source matches.."); + + Hive.registerAdapter(SourceMatchAdapter()); + Hive.registerAdapter(SourceTypeAdapter()); + + final box = await Hive.openBox( + SourceMatch.boxName, + path: await getHiveCacheDir(), + ); + + final sourceMatches = + box.keys.map((key) => (data: box.get(key), trackId: key)); + + await _database.batch((batch) { + batch.insertAll( + _database.sourceMatchTable, + sourceMatches + .where((element) => element.data != null) + .map( + (sourceMatch) => SourceMatchTableCompanion.insert( + sourceId: sourceMatch.data!.sourceId, + trackId: sourceMatch.trackId, + sourceType: Value( + switch (sourceMatch.data!.sourceType) { + SourceType.jiosaavn => db.SourceType.jiosaavn, + SourceType.youtube => db.SourceType.youtube, + SourceType.youtubeMusic => db.SourceType.youtubeMusic, + }, + ), + ), + ) + .toList(), + ); + }); + + AppLogger.log.i("✅ Migrated source matches"); +} + +Future migrateBlacklist() async { + AppLogger.log.i("🔵 Migrating blacklist.."); + + final box = PersistenceCacheBox>( + "blacklist", + fromJson: (json) => (json["blacklist"] as List) + .map((e) => BlacklistedElement.fromJson(e)) + .toSet(), + ); + + final data = await box.getData(); + + if (data == null) return; + + await _database.batch((batch) { + batch.insertAll( + _database.blacklistTable, + data.map( + (element) => BlacklistTableCompanion.insert( + name: element.name, + elementId: element.id, + elementType: switch (element.type) { + BlacklistedType.artist => db.BlacklistedType.artist, + BlacklistedType.track => db.BlacklistedType.track, + }, + ), + ), + ); + }); + + AppLogger.log.i("✅ Migrated blacklist"); +} + +Future migrateLastFmCredentials() async { + AppLogger.log.i("🔵 Migrating Last.fm credentials.."); + + final box = PersistenceCacheBox( + "scrobbler", + fromJson: (json) => ScrobblerState.fromJson(json), + encrypted: true, + ); + + final data = await box.getData(); + + if (data == null) return; + + await _database.into(_database.scrobblerTable).insertOnConflictUpdate( + ScrobblerTableCompanion.insert( + id: const Value(0), + passwordHash: DecryptedText(data.passwordHash), + username: data.username, + ), + ); + + AppLogger.log.i("✅ Migrated Last.fm credentials"); +} + +Future migratePlaybackHistory() async { + AppLogger.log.i("🔵 Migrating playback history.."); + + final box = PersistenceCacheBox( + "playback_history", + fromJson: (json) => PlaybackHistoryState.fromJson(json), + ); + + final data = await box.getData(); + + if (data == null) return; + + await _database.batch((batch) { + batch.insertAll( + _database.historyTable, + data.items.map( + (item) => switch (item) { + PlaybackHistoryAlbum() => HistoryTableCompanion.insert( + createdAt: Value(item.date), + itemId: item.album.id!, + data: item.album.toJson(), + type: db.HistoryEntryType.album, + ), + PlaybackHistoryPlaylist() => HistoryTableCompanion.insert( + createdAt: Value(item.date), + itemId: item.playlist.id!, + data: item.playlist.toJson(), + type: db.HistoryEntryType.playlist, + ), + PlaybackHistoryTrack() => HistoryTableCompanion.insert( + createdAt: Value(item.date), + itemId: item.track.id!, + data: item.track.toJson(), + type: db.HistoryEntryType.track, + ), + _ => throw Exception("Unknown history item type"), + }, + ), + ); + }); + + AppLogger.log.i("✅ Migrated playback history"); +} + +Future migrateFromHiveToDrift(AppDatabase database) async { + if (KVStoreService.hasMigratedToDrift) return; + + await PersistenceCacheBox.initializeBoxes( + path: await getHiveCacheDir(), + ); + + _database = database; + + await migrateAuthenticationInfo(); + await migratePreferences(); + + await migrateSkipSegment(); + await migrateSourceMatches(); + + await migrateBlacklist(); + await migratePlaybackHistory(); + + await migrateLastFmCredentials(); + + await KVStoreService.setHasMigratedToDrift(true); + + AppLogger.log.i("🚀 Migrated all data to Drift"); +} From 261e1b6685f207df1808af30044a63dbafbefb4c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 18:00:50 +0600 Subject: [PATCH 38/92] chore: fix queue collections not being loaded --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- lib/provider/audio_player/audio_player.dart | 6 +++ lib/provider/history/recent.dart | 45 ++++++++++++--------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 64ee89d2c..fed668501 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,7 +53,7 @@ body: description: Where did you install Spotube from? multiple: true options: - - "Website (spotube.netlify.app) or (spotube.krtirtho.dev)" + - "Website (spotube.krtirtho.dev)" - "GitHub Releases (Binary)" - "GitHub Actions (Nightly Binary)" - "Play Store (Android)" @@ -77,4 +77,4 @@ body: description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions! options: - label: I'm ready to work on this issue! - required: false \ No newline at end of file + required: false diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 9dfc2c0a1..da22b2ce5 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -89,6 +89,12 @@ class AudioPlayerNotifier extends Notifier { autoPlay: false, ); } + + if (playerState.collections.isNotEmpty) { + state = state.copyWith( + collections: playerState.collections, + ); + } } Future _updatePlayerState( diff --git a/lib/provider/history/recent.dart b/lib/provider/history/recent.dart index 4e4455001..8894b7138 100644 --- a/lib/provider/history/recent.dart +++ b/lib/provider/history/recent.dart @@ -9,28 +9,31 @@ class RecentlyPlayedItemNotifier extends AsyncNotifier> { build() async { final database = ref.watch(databaseProvider); - final uniqueItemIds = - await (database.selectOnly(database.historyTable, distinct: true) - ..addColumns([database.historyTable.itemId]) - ..where( - database.historyTable.type.isIn([ - HistoryEntryType.playlist.name, - HistoryEntryType.album.name, - ]), - ) - ..limit(10)) - .map((row) => row.read(database.historyTable.itemId)) - .get() - .then((value) => value.whereNotNull().toList()); + final uniqueItemIds = await (database.selectOnly(database.historyTable, + distinct: true) + ..addColumns([database.historyTable.itemId, database.historyTable.id]) + ..where( + database.historyTable.type.isIn([ + HistoryEntryType.playlist.name, + HistoryEntryType.album.name, + ]), + ) + ..limit(10) + ..orderBy([ + OrderingTerm( + expression: database.historyTable.createdAt, + mode: OrderingMode.desc, + ), + ])) + .map( + (row) => row.read(database.historyTable.id), + ) + .get() + .then((value) => value.whereNotNull().toList()); final query = database.select(database.historyTable) ..where( - (tbl) => - tbl.type.isIn([ - HistoryEntryType.playlist.name, - HistoryEntryType.album.name, - ]) & - tbl.itemId.isIn(uniqueItemIds), + (tbl) => tbl.id.isIn(uniqueItemIds), ) ..orderBy([ (tbl) => OrderingTerm( @@ -45,7 +48,9 @@ class RecentlyPlayedItemNotifier extends AsyncNotifier> { ref.onDispose(() => subscription.cancel()); - return await query.get(); + final items = await query.get(); + + return items; } } From 4c5564fd2f6bb7e6f42896a01e95fe197c362b50 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 18:31:57 +0600 Subject: [PATCH 39/92] chore: use enum properties for history duration in top stats --- lib/provider/history/top.dart | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index 965fb3ad8..826812674 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -8,20 +8,24 @@ import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; enum HistoryDuration { - allTime, - days7, - days30, - months6, - year, - years2, + allTime(Duration(days: 365 * 2003)), + days7(Duration(days: 7)), + days30(Duration(days: 30)), + months6(Duration(days: 30 * 6)), + year(Duration(days: 365)), + years2(Duration(days: 365 * 2)); + + final Duration duration; + + const HistoryDuration(this.duration); } final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); -typedef PlaybackHistoryTrack = ({int count, Track track}); typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); +typedef PlaybackHistoryTrack = ({int count, Track track}); typedef PlaybackHistoryArtist = ({int count, Artist artist}); class PlaybackHistoryTopState { @@ -58,14 +62,7 @@ class PlaybackHistoryTopNotifier build(arg) async { final database = ref.watch(databaseProvider); - final duration = switch (arg) { - 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 duration = arg.duration; final tracksQuery = (database.select(database.historyTable) ..where( From 3bdc46da4d05e675be11a41064029eb3c98baa5d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 21:08:29 +0600 Subject: [PATCH 40/92] feat(stats): add lazy loading support --- lib/models/database/database.dart | 3 +- lib/modules/stats/top/albums.dart | 22 ++- lib/modules/stats/top/artists.dart | 42 +++-- lib/modules/stats/top/tracks.dart | 44 +++-- lib/pages/stats/albums/albums.dart | 41 +++-- lib/pages/stats/artists/artists.dart | 43 +++-- lib/pages/stats/fees/fees.dart | 42 +++-- lib/pages/stats/minutes/minutes.dart | 45 +++-- lib/pages/stats/playlists/playlists.dart | 44 +++-- lib/pages/stats/streams/streams.dart | 45 +++-- lib/provider/history/top.dart | 200 ----------------------- lib/provider/history/top/albums.dart | 135 +++++++++++++++ lib/provider/history/top/playlists.dart | 104 ++++++++++++ lib/provider/history/top/tracks.dart | 119 ++++++++++++++ 14 files changed, 610 insertions(+), 319 deletions(-) create mode 100644 lib/provider/history/top/albums.dart create mode 100644 lib/provider/history/top/playlists.dart create mode 100644 lib/provider/history/top/tracks.dart diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 609d6771e..1c233f845 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; +import 'package:drift/extensions/json1.dart'; import 'package:encrypt/encrypt.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; @@ -13,7 +14,7 @@ import 'package:spotube/models/lyrics.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:flutter/material.dart' hide Table, Key; +import 'package:flutter/material.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; import 'package:sqlite3/sqlite3.dart'; diff --git a/lib/modules/stats/top/albums.dart b/lib/modules/stats/top/albums.dart index bcaa75c54..4329b871e 100644 --- a/lib/modules/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -4,6 +4,9 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/albums.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class TopAlbums extends HookConsumerWidget { const TopAlbums({super.key}); @@ -11,14 +14,21 @@ class TopAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); - final albums = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.whenData((s) => s.albums))); + final topAlbums = ref.watch(historyTopAlbumsProvider(historyDuration)); + final topAlbumsNotifier = + ref.watch(historyTopAlbumsProvider(historyDuration).notifier); - final albumsData = albums.asData?.value ?? []; + final albumsData = topAlbums.asData?.value.items ?? []; - return Skeletonizer( - enabled: albums.isLoading, - child: SliverList.builder( + return Skeletonizer.sliver( + enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topAlbumsNotifier.fetchMore(); + }, + hasError: topAlbums.hasError, + isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + hasReachedMax: topAlbums.asData?.value.hasMore ?? true, itemCount: albumsData.length, itemBuilder: (context, index) { final album = albumsData[index]; diff --git a/lib/modules/stats/top/artists.dart b/lib/modules/stats/top/artists.dart index 094353f2a..d5eb2d0e7 100644 --- a/lib/modules/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -1,8 +1,13 @@ 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/formatters.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class TopArtists extends HookConsumerWidget { const TopArtists({super.key}); @@ -10,20 +15,33 @@ class TopArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); - final artists = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.whenData((s) => s.artists))); + final topTracks = ref.watch( + historyTopTracksProvider(historyDuration), + ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(historyDuration).notifier); - final artistsData = artists.asData?.value ?? []; + final artistsData = useMemoized( + () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); - return SliverList.builder( - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text("${compactNumberFormatter.format(artist.count)} plays"), - ); - }, + return Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text("${compactNumberFormatter.format(artist.count)} plays"), + ); + }, + ), ); } } diff --git a/lib/modules/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart index 8bffa800b..be457b2e3 100644 --- a/lib/modules/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class TopTracks extends HookConsumerWidget { const TopTracks({super.key}); @@ -10,24 +14,34 @@ class TopTracks extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); - final tracks = ref.watch( - playbackHistoryTopProvider(historyDuration) - .select((value) => value.whenData((s) => s.tracks)), + final topTracks = ref.watch( + historyTopTracksProvider(historyDuration), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(historyDuration).notifier); - final tracksData = tracks.asData?.value ?? []; + final tracksData = topTracks.asData?.value.items ?? []; - return SliverList.builder( - itemCount: tracksData.length, - itemBuilder: (context, index) { - final track = tracksData[index]; - return StatsTrackItem( - track: track.track, - info: Text( - "${compactNumberFormatter.format(track.count)} plays", - ), - ); - }, + return Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count)} plays", + ), + ); + }, + ), ); } } diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 859eaf26c..db0eedf6b 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/albums.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsAlbumsPage extends HookConsumerWidget { static const name = "stats_albums"; @@ -12,10 +16,12 @@ class StatsAlbumsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final albums = ref.watch(playbackHistoryTopProvider(HistoryDuration.allTime) - .select((value) => value.whenData((s) => s.albums))); + final topAlbums = + ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime)); + final topAlbumsNotifier = + ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime).notifier); - final albumsData = albums.asData?.value ?? []; + final albumsData = topAlbums.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -23,15 +29,26 @@ class StatsAlbumsPage extends HookConsumerWidget { centerTitle: false, title: Text("Albums"), ), - body: ListView.builder( - itemCount: albumsData.length, - itemBuilder: (context, index) { - final album = albumsData[index]; - return StatsAlbumItem( - album: album.album, - info: Text("${compactNumberFormatter.format(album.count)} plays"), - ); - }, + body: Skeletonizer( + enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topAlbumsNotifier.fetchMore(); + }, + hasError: topAlbums.hasError, + isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + hasReachedMax: topAlbums.asData?.value.hasMore ?? true, + itemCount: albumsData.length, + itemBuilder: (context, index) { + final album = albumsData[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 index e6dadd950..80ff5f235 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -1,10 +1,15 @@ 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/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsArtistsPage extends HookConsumerWidget { static const name = "stats_artists"; @@ -12,12 +17,14 @@ class StatsArtistsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final artists = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.artists)), + final topTracks = ref.watch( + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final artistsData = artists.asData?.value ?? []; + final artistsData = useMemoized( + () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); return Scaffold( appBar: const PageWindowTitleBar( @@ -25,15 +32,25 @@ class StatsArtistsPage extends HookConsumerWidget { centerTitle: false, title: Text("Artists"), ), - body: ListView.builder( - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text("${compactNumberFormatter.format(artist.count)} plays"), - ); - }, + body: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[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 index e1d701eb0..0e25c00b9 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -1,11 +1,16 @@ 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:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsStreamFeesPage extends HookConsumerWidget { static const name = "stats_stream_fees"; @@ -16,12 +21,14 @@ class StatsStreamFeesPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final ThemeData(:textTheme, :hintColor) = Theme.of(context); - final artists = ref.watch( - playbackHistoryTopProvider(HistoryDuration.days30) - .select((value) => value.whenData((s) => s.artists)), + final topTracks = ref.watch( + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final artistsData = artists.asData?.value ?? []; + final artistsData = useMemoized( + () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); return Scaffold( appBar: const PageWindowTitleBar( @@ -50,15 +57,24 @@ class StatsStreamFeesPage extends HookConsumerWidget { ), ), ), - SliverList.builder( - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text(usdFormatter.format(artist.count * 0.005)), - ); - }, + Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[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 index 587e90079..ea3048ef4 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsMinutesPage extends HookConsumerWidget { static const name = "stats_minutes"; @@ -15,11 +19,12 @@ class StatsMinutesPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final topTracks = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.tracks)), + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final topTracksData = topTracks.asData?.value ?? []; + final tracksData = topTracks.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -27,19 +32,27 @@ class StatsMinutesPage extends HookConsumerWidget { centerTitle: false, automaticallyImplyLeading: true, ), - body: ListView.separated( - separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracksData.length, - itemBuilder: (context, index) { - final (:track, :count) = topTracksData[index]; - - return StatsTrackItem( - track: track, - info: Text( - "${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins", - ), - ); - }, + body: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + separatorBuilder: (context, index) => const Gap(8), + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count)} plays", + ), + ); + }, + ), ), ); } diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index f5ee62d06..a6db3e1cf 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/playlists.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsPlaylistsPage extends HookConsumerWidget { static const name = "stats_playlists"; @@ -12,12 +16,13 @@ class StatsPlaylistsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlists = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.playlists)), - ); + final topPlaylists = + ref.watch(historyTopPlaylistsProvider(HistoryDuration.allTime)); + + final topPlaylistsNotifier = ref + .watch(historyTopPlaylistsProvider(HistoryDuration.allTime).notifier); - final playlistsData = playlists.asData?.value ?? []; + final playlistsData = topPlaylists.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -25,16 +30,25 @@ class StatsPlaylistsPage extends HookConsumerWidget { centerTitle: false, title: Text("Playlists"), ), - body: ListView.builder( - itemCount: playlistsData.length, - itemBuilder: (context, index) { - final playlist = playlistsData[index]; - return StatsPlaylistItem( - playlist: playlist.playlist, - info: - Text("${compactNumberFormatter.format(playlist.count)} plays"), - ); - }, + body: Skeletonizer( + enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topPlaylistsNotifier.fetchMore(); + }, + hasError: topPlaylists.hasError, + isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, + hasReachedMax: topPlaylists.asData?.value.hasMore ?? true, + itemCount: playlistsData.length, + itemBuilder: (context, index) { + final playlist = playlistsData[index]; + return StatsPlaylistItem( + playlist: playlist.playlist, + info: Text( + "${compactNumberFormatter.format(playlist.count)} plays"), + ); + }, + ), ), ); } diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 20e8ff966..dd5856d0a 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsStreamsPage extends HookConsumerWidget { static const name = "stats_streams"; @@ -15,11 +19,12 @@ class StatsStreamsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final topTracks = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.tracks)), + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final topTracksData = topTracks.asData?.value ?? []; + final tracksData = topTracks.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -27,19 +32,27 @@ class StatsStreamsPage extends HookConsumerWidget { centerTitle: false, automaticallyImplyLeading: true, ), - body: ListView.separated( - separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracksData.length, - itemBuilder: (context, index) { - final (:track, :count) = topTracksData[index]; - - return StatsTrackItem( - track: track, - info: Text( - "${compactNumberFormatter.format(count)} streams", - ), - ); - }, + body: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + separatorBuilder: (context, index) => const Gap(8), + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count * track.track.duration!.inMinutes)} mins", + ), + ); + }, + ), ), ); } diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index 826812674..b52e65e20 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -1,11 +1,4 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/provider/database/database.dart'; enum HistoryDuration { allTime(Duration(days: 365 * 2003)), @@ -22,196 +15,3 @@ enum HistoryDuration { final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); - -typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); -typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); -typedef PlaybackHistoryTrack = ({int count, Track track}); -typedef PlaybackHistoryArtist = ({int count, Artist artist}); - -class PlaybackHistoryTopState { - final List tracks; - final List albums; - final List playlists; - final List artists; - - const PlaybackHistoryTopState({ - required this.tracks, - required this.albums, - required this.playlists, - required this.artists, - }); - - PlaybackHistoryTopState copyWith({ - List? tracks, - List? albums, - List? playlists, - List? artists, - }) { - return PlaybackHistoryTopState( - tracks: tracks ?? this.tracks, - albums: albums ?? this.albums, - playlists: playlists ?? this.playlists, - artists: artists ?? this.artists, - ); - } -} - -class PlaybackHistoryTopNotifier - extends FamilyAsyncNotifier { - @override - build(arg) async { - final database = ref.watch(databaseProvider); - - final duration = arg.duration; - - final tracksQuery = (database.select(database.historyTable) - ..where( - (tbl) => - tbl.type.equalsValue(HistoryEntryType.track) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(duration), - ), - )); - - final albumsQuery = database.select(database.historyTable) - ..where( - (tbl) => - tbl.type.equalsValue(HistoryEntryType.album) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(duration), - ), - ); - - final playlistsQuery = database.select(database.historyTable) - ..where( - (tbl) => - tbl.type.equalsValue(HistoryEntryType.playlist) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(duration), - ), - ); - - final subscriptions = [ - tracksQuery.watch().listen((event) { - if (state.asData == null) return; - final artists = event - .map((track) => track.track!.artists) - .expand((e) => e ?? []); - state = AsyncData(state.asData!.value.copyWith( - tracks: getTracksWithCount(event), - artists: getArtistsWithCount(artists), - )); - }), - albumsQuery.watch().listen((event) async { - if (state.asData == null) return; - final tracks = await tracksQuery.get(); - - final albumsWithTrackAlbums = [ - for (final historicAlbum in event) historicAlbum.album!, - for (final track in tracks) track.track!.album! - ]; - - state = AsyncData(state.asData!.value.copyWith( - albums: getAlbumsWithCount(albumsWithTrackAlbums), - )); - }), - playlistsQuery.watch().listen((event) { - if (state.asData == null) return; - state = AsyncData(state.asData!.value.copyWith( - playlists: getPlaylistsWithCount(event), - )); - }), - ]; - - ref.onDispose(() { - for (final subscription in subscriptions) { - subscription.cancel(); - } - }); - - return database.transaction(() async { - final tracks = await tracksQuery.get(); - final albums = await albumsQuery.get(); - final playlists = await playlistsQuery.get(); - - final tracksWithCount = getTracksWithCount(tracks); - - final albumsWithTrackAlbums = [ - for (final historicAlbum in albums) historicAlbum.album!, - for (final track in tracks) track.track!.album! - ]; - - final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums); - - final artists = tracks - .map((track) => track.track!.artists) - .expand((e) => e ?? []); - - final artistsWithCount = getArtistsWithCount(artists); - - final playlistsWithCount = getPlaylistsWithCount(playlists); - - return PlaybackHistoryTopState( - tracks: tracksWithCount, - albums: albumsWithCount, - artists: artistsWithCount, - playlists: playlistsWithCount, - ); - }); - } - - List getTracksWithCount(List tracks) { - return 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(); - } - - List getAlbumsWithCount( - List albumsWithTrackAlbums, - ) { - return 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(); - } - - List getArtistsWithCount(Iterable artists) { - return 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(); - } - - List getPlaylistsWithCount( - List playlists, - ) { - return groupBy(playlists, (playlist) => playlist.playlist!.id!) - .entries - .map((entry) { - return ( - count: entry.value.length, - playlist: entry.value.first.playlist!, - ); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); - } -} - -final playbackHistoryTopProvider = AsyncNotifierProviderFamily< - PlaybackHistoryTopNotifier, - PlaybackHistoryTopState, - HistoryDuration>(PlaybackHistoryTopNotifier.new); diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart new file mode 100644 index 000000000..84518418c --- /dev/null +++ b/lib/provider/history/top/albums.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); + +class HistoryTopAlbumsState extends PaginatedState { + HistoryTopAlbumsState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + HistoryTopAlbumsState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return HistoryTopAlbumsState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< + PlaybackHistoryAlbum, HistoryTopAlbumsState, HistoryDuration> { + HistoryTopAlbumsNotifier() : super(); + + Selectable createAlbumsQuery({int? limit, int? offset}) { + final database = ref.read(databaseProvider); + + final duration = switch (arg) { + HistoryDuration.allTime => '0', + HistoryDuration.days7 => "strftime('%s', 'now', 'weekday 0', '-7 days')", + HistoryDuration.days30 => "strftime('%s', 'now', 'start of month')", + HistoryDuration.months6 => + "strftime('%s', 'start of month', '-5 months')", + HistoryDuration.year => "strftime('%s', 'start of year')", + HistoryDuration.years2 => "strftime('%s', 'start of year', '-1 year')", + }; + + return database.customSelect( + """ + SELECT + history_table.created_at, + """ + r""" + json_extract(history_table.data, '$.album') as data, + json_extract(history_table.data, '$.album.id') as item_id, + json_extract(history_table.data, '$.album.type') as type + """ + """ + FROM history_table + WHERE type = 'track' AND + created_at >= $duration + UNION ALL + SELECT + history_table.created_at, + history_table.data, + history_table.item_id, + history_table.type + FROM history_table + WHERE type = 'album' AND + created_at >= $duration + ORDER BY created_at desc + ${limit != null && offset != null ? 'LIMIT $limit OFFSET $offset' : ''} + """, + readsFrom: {database.historyTable}, + ).map((row) { + final data = row.read('data'); + final album = AlbumSimple.fromJson(jsonDecode(data)); + return album; + }); + } + + @override + fetch(arg, offset, limit) async { + final albumsQuery = createAlbumsQuery(limit: limit, offset: offset); + + return getAlbumsWithCount(await albumsQuery.get()); + } + + @override + build(arg) async { + final albums = await fetch(arg, 0, 20); + + final subscription = createAlbumsQuery().watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + items: getAlbumsWithCount(event), + hasMore: false, + )); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + + return HistoryTopAlbumsState( + items: albums, + offset: albums.length, + limit: 20, + hasMore: true, + ); + } + + List getAlbumsWithCount( + List albumsWithTrackAlbums, + ) { + return 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 historyTopAlbumsProvider = AsyncNotifierProviderFamily< + HistoryTopAlbumsNotifier, HistoryTopAlbumsState, HistoryDuration>( + () => HistoryTopAlbumsNotifier(), +); diff --git a/lib/provider/history/top/playlists.dart b/lib/provider/history/top/playlists.dart new file mode 100644 index 000000000..04071f7a4 --- /dev/null +++ b/lib/provider/history/top/playlists.dart @@ -0,0 +1,104 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); + +class HistoryTopPlaylistsState extends PaginatedState { + HistoryTopPlaylistsState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + HistoryTopPlaylistsState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return HistoryTopPlaylistsState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< + PlaybackHistoryPlaylist, HistoryTopPlaylistsState, HistoryDuration> { + HistoryTopPlaylistsNotifier() : super(); + + SimpleSelectStatement<$HistoryTableTable, HistoryTableData> + createPlaylistsQuery() { + final database = ref.read(databaseProvider); + + return database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.playlist) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(arg.duration), + ), + ); + } + + @override + fetch(arg, offset, limit) async { + final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset); + + return getPlaylistsWithCount(await playlistsQuery.get()); + } + + @override + build(arg) async { + final playlists = await fetch(arg, 0, 20); + + final subscription = createPlaylistsQuery().watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + items: getPlaylistsWithCount(event), + hasMore: false, + )); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + + return HistoryTopPlaylistsState( + items: playlists, + offset: playlists.length, + limit: 20, + hasMore: true, + ); + } + + List getPlaylistsWithCount( + List playlists, + ) { + return groupBy(playlists, (playlist) => playlist.playlist!.id!) + .entries + .map((entry) { + return ( + count: entry.value.length, + playlist: entry.value.first.playlist!, + ); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } +} + +final historyTopPlaylistsProvider = AsyncNotifierProviderFamily< + HistoryTopPlaylistsNotifier, HistoryTopPlaylistsState, HistoryDuration>( + () => HistoryTopPlaylistsNotifier(), +); diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart new file mode 100644 index 000000000..6c4e44b7b --- /dev/null +++ b/lib/provider/history/top/tracks.dart @@ -0,0 +1,119 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef PlaybackHistoryTrack = ({int count, Track track}); +typedef PlaybackHistoryArtist = ({int count, Artist artist}); + +class HistoryTopTracksState extends PaginatedState { + HistoryTopTracksState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + List get artists { + return getArtistsWithCount( + items.expand((e) => e.track.artists ?? []), + ); + } + + List getArtistsWithCount(Iterable artists) { + return 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(); + } + + @override + HistoryTopTracksState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return HistoryTopTracksState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< + PlaybackHistoryTrack, HistoryTopTracksState, HistoryDuration> { + HistoryTopTracksNotifier() : super(); + + SimpleSelectStatement<$HistoryTableTable, HistoryTableData> + createTracksQuery() { + final database = ref.read(databaseProvider); + + return database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.track) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(arg.duration), + ), + ); + } + + @override + fetch(arg, offset, limit) async { + final tracksQuery = createTracksQuery()..limit(limit, offset: offset); + + return getTracksWithCount(await tracksQuery.get()); + } + + @override + build(arg) async { + final tracks = await fetch(arg, 0, 20); + + final subscription = createTracksQuery().watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + items: getTracksWithCount(event), + hasMore: false, + )); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + + return HistoryTopTracksState( + items: tracks, + offset: tracks.length, + limit: 20, + hasMore: true, + ); + } + + List getTracksWithCount(List tracks) { + return 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 historyTopTracksProvider = AsyncNotifierProviderFamily< + HistoryTopTracksNotifier, HistoryTopTracksState, HistoryDuration>( + () => HistoryTopTracksNotifier(), +); From 7927a3e404cce1b828d4bc079911f85884d44a34 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 1 Jul 2024 13:25:28 +0600 Subject: [PATCH 41/92] chore: fix top album and track invalid time frame operations --- lib/models/database/database.dart | 1 - lib/modules/stats/common/album_item.dart | 2 +- lib/pages/stats/fees/fees.dart | 96 +++++++++++++++++++----- lib/provider/history/top/albums.dart | 9 ++- lib/provider/history/top/tracks.dart | 23 +++++- 5 files changed, 103 insertions(+), 28 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 1c233f845..74f588abd 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; -import 'package:drift/extensions/json1.dart'; import 'package:encrypt/encrypt.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; diff --git a/lib/modules/stats/common/album_item.dart b/lib/modules/stats/common/album_item.dart index 0424ca70c..58604c454 100644 --- a/lib/modules/stats/common/album_item.dart +++ b/lib/modules/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/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 0e25c00b9..33d223aed 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -20,16 +20,25 @@ class StatsStreamFeesPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final ThemeData(:textTheme, :hintColor) = Theme.of(context); + final duration = useState(HistoryDuration.days30); final topTracks = ref.watch( - historyTopTracksProvider(HistoryDuration.allTime), + historyTopTracksProvider(duration.value), ); final topTracksNotifier = - ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); + ref.watch(historyTopTracksProvider(duration.value).notifier); final artistsData = useMemoized( () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); + final total = useMemoized( + () => artistsData.fold( + 0, + (previousValue, element) => previousValue + element.count * 0.005, + ), + [artistsData], + ); + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -57,23 +66,72 @@ class StatsStreamFeesPage extends HookConsumerWidget { ), ), ), - Skeletonizer.sliver( - enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, - child: SliverInfiniteList( - onFetchData: () async { - await topTracksNotifier.fetchMore(); - }, - hasError: topTracks.hasError, - isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, - hasReachedMax: topTracks.asData?.value.hasMore ?? true, - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text(usdFormatter.format(artist.count * 0.005)), - ); - }, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total ${usdFormatter.format(total)}", + style: textTheme.titleLarge, + ), + DropdownButton( + value: duration.value, + onChanged: (value) { + if (value == null) return; + duration.value = value; + }, + 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"), + ), + ], + ), + ], + ), + ), + ), + SliverSafeArea( + sliver: Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(usdFormatter.format(artist.count * 0.005)), + ); + }, + ), ), ), ], diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart index 84518418c..7448a8496 100644 --- a/lib/provider/history/top/albums.dart +++ b/lib/provider/history/top/albums.dart @@ -46,9 +46,10 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< HistoryDuration.days7 => "strftime('%s', 'now', 'weekday 0', '-7 days')", HistoryDuration.days30 => "strftime('%s', 'now', 'start of month')", HistoryDuration.months6 => - "strftime('%s', 'start of month', '-5 months')", - HistoryDuration.year => "strftime('%s', 'start of year')", - HistoryDuration.years2 => "strftime('%s', 'start of year', '-1 year')", + "strftime('%s', date('now', '-5 months', 'start of month'))", + HistoryDuration.year => "strftime('%s', date('now', 'start of year'))", + HistoryDuration.years2 => + "strftime('%s', date('now', '-1 years', 'start of year'))", }; return database.customSelect( @@ -59,7 +60,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< r""" json_extract(history_table.data, '$.album') as data, json_extract(history_table.data, '$.album.id') as item_id, - json_extract(history_table.data, '$.album.type') as type + 'album' as type """ """ FROM history_table diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart index 6c4e44b7b..56795cc6c 100644 --- a/lib/provider/history/top/tracks.dart +++ b/lib/provider/history/top/tracks.dart @@ -62,9 +62,26 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< ..where( (tbl) => tbl.type.equalsValue(HistoryEntryType.track) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(arg.duration), - ), + tbl.createdAt.isBiggerOrEqualValue(switch (arg) { + HistoryDuration.allTime => DateTime(1970), + // from start of the week + HistoryDuration.days7 => DateTime.now() + .subtract(Duration(days: DateTime.now().weekday - 1)), + // from start of the month + HistoryDuration.days30 => + DateTime.now().subtract(Duration(days: DateTime.now().day - 1)), + // from start of the 6th month + HistoryDuration.months6 => DateTime.now() + .subtract(Duration(days: DateTime.now().day - 1)) + .subtract(const Duration(days: 30 * 6)), + // from start of the year + HistoryDuration.year => DateTime.now() + .subtract(Duration(days: DateTime.now().day - 1)) + .subtract(const Duration(days: 30 * 12)), + HistoryDuration.years2 => DateTime.now() + .subtract(Duration(days: DateTime.now().day - 1)) + .subtract(const Duration(days: 30 * 12 * 2)), + }), ); } From cb6b6f142e44771109b708dd7fb7d30f89bbb38e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 1 Jul 2024 19:21:12 +0600 Subject: [PATCH 42/92] chore: playback not working in windows due to using loop back ipv4 address --- lib/provider/server/routes/playback.dart | 1 + lib/services/audio_player/audio_player.dart | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index aa380d01d..30322a6fc 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -23,6 +23,7 @@ class ServerPlaybackRoutes { try { final track = playlist.tracks.firstWhere((element) => element.id == trackId); + final activeSourcedTrack = ref.read(activeSourcedTrackProvider); final sourcedTrack = activeSourcedTrack?.id == track.id ? activeSourcedTrack diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index bb1a62036..7915dc3bd 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -12,6 +12,7 @@ import 'package:media_kit/media_kit.dart' as mk; import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/utils/platform.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; @@ -28,7 +29,7 @@ class SpotubeMedia extends mk.Media { }) : super( track is LocalTrack ? track.path - : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", + : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", extras: { ...?extras, "track": switch (track) { @@ -42,7 +43,7 @@ class SpotubeMedia extends mk.Media { @override String get uri => track is LocalTrack ? (track as LocalTrack).path - : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; + : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http") From 15bd58a95597925637bf66866af59c2780aeff97 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 11:21:32 +0600 Subject: [PATCH 43/92] feat(desktop): implement webview based login --- lib/collections/routes.dart | 13 +- lib/main.dart | 2 +- lib/modules/desktop_login/login_form.dart | 69 ---------- lib/pages/desktop_login/desktop_login.dart | 78 ----------- lib/pages/desktop_login/login_tutorial.dart | 125 ------------------ lib/pages/settings/sections/accounts.dart | 44 ++++-- .../authentication/authentication.dart | 4 + linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec.lock | 9 ++ pubspec.yaml | 5 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 15 files changed, 73 insertions(+), 293 deletions(-) delete mode 100644 lib/modules/desktop_login/login_form.dart delete mode 100644 lib/pages/desktop_login/desktop_login.dart delete mode 100644 lib/pages/desktop_login/login_tutorial.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index b3cba5810..3bf1d883d 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -34,12 +34,9 @@ import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/spotube_page_route.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/pages/library/library.dart'; -import 'package:spotube/pages/desktop_login/login_tutorial.dart'; -import 'package:spotube/pages/desktop_login/desktop_login.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/root/root_app.dart'; import 'package:spotube/pages/settings/settings.dart'; @@ -313,16 +310,8 @@ final routerProvider = Provider((ref) { path: "/login", name: WebViewLogin.name, parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => SpotubePage( - child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(), - ), - ), - GoRoute( - path: "/login-tutorial", - name: LoginTutorial.name, - parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) => const SpotubePage( - child: LoginTutorial(), + child: WebViewLogin(), ), ), GoRoute( diff --git a/lib/main.dart b/lib/main.dart index cb5531156..b3a3132f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -65,7 +65,7 @@ Future main(List rawArgs) async { await FlutterDisplayMode.setHighRefreshRate(); } - if (kIsDesktop) { + if (kIsDesktop && !kIsMacOS) { await windowManager.setPreventClose(true); } diff --git a/lib/modules/desktop_login/login_form.dart b/lib/modules/desktop_login/login_form.dart deleted file mode 100644 index e5d31215d..000000000 --- a/lib/modules/desktop_login/login_form.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/extensions/context.dart'; - -import 'package:spotube/provider/authentication/authentication.dart'; - -class TokenLoginForm extends HookConsumerWidget { - final void Function()? onDone; - const TokenLoginForm({ - super.key, - this.onDone, - }); - - @override - Widget build(BuildContext context, ref) { - final authenticationNotifier = ref.watch(authenticationProvider.notifier); - final directCodeController = useTextEditingController(); - - final isLoading = useState(false); - - return ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Column( - children: [ - TextField( - controller: directCodeController, - decoration: InputDecoration( - hintText: context.l10n.spotify_cookie("\"sp_dc\""), - labelText: context.l10n.cookie_name_cookie("sp_dc"), - ), - keyboardType: TextInputType.visiblePassword, - ), - const SizedBox(height: 10), - FilledButton( - onPressed: isLoading.value - ? null - : () async { - try { - isLoading.value = true; - if (directCodeController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.fill_in_all_fields), - behavior: SnackBarBehavior.floating, - ), - ); - return; - } - final cookieHeader = - "sp_dc=${directCodeController.text.trim()}"; - - await authenticationNotifier.login(cookieHeader); - if (context.mounted) { - onDone?.call(); - } - } finally { - isLoading.value = false; - } - }, - child: Text(context.l10n.submit), - ) - ], - ), - ); - } -} diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart deleted file mode 100644 index 805488987..000000000 --- a/lib/pages/desktop_login/desktop_login.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/modules/desktop_login/login_form.dart'; -import 'package:spotube/components/titlebar/titlebar.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 - Widget build(BuildContext context, ref) { - final mediaQuery = MediaQuery.of(context); - final theme = Theme.of(context); - final color = theme.colorScheme.surfaceVariant.withOpacity(.3); - - return SafeArea( - child: Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), - ), - body: SingleChildScrollView( - child: Center( - child: Container( - margin: const EdgeInsets.all(10), - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(10), - ), - child: Column( - children: [ - Assets.spotubeLogoPng.image( - width: MediaQuery.of(context).size.width * - (mediaQuery.mdAndDown ? .5 : .3), - ), - Text( - context.l10n.add_spotify_credentials, - style: theme.textTheme.titleMedium, - ), - Text( - context.l10n.credentials_will_not_be_shared_disclaimer, - style: theme.textTheme.labelMedium, - ), - const SizedBox(height: 10), - TokenLoginForm( - onDone: () => GoRouter.of(context).go("/"), - ), - const SizedBox(height: 10), - Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text(context.l10n.know_how_to_login), - TextButton( - child: Text( - context.l10n.follow_step_by_step_guide, - ), - onPressed: () => GoRouter.of(context).push( - "/login-tutorial", - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart deleted file mode 100644 index ec62543cc..000000000 --- a/lib/pages/desktop_login/login_tutorial.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:introduction_screen/introduction_screen.dart'; - -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/modules/desktop_login/login_form.dart'; -import 'package:spotube/components/links/hyper_link.dart'; -import 'package:spotube/components/titlebar/titlebar.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/pages/home/home.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/utils/service_utils.dart'; - -class LoginTutorial extends ConsumerWidget { - static const name = "login_tutorial"; - const LoginTutorial({super.key}); - - @override - Widget build(BuildContext context, ref) { - final auth = ref.watch(authenticationProvider); - final key = GlobalKey>(); - final theme = Theme.of(context); - - final pageDecoration = PageDecoration( - bodyTextStyle: theme.textTheme.bodyMedium!, - titleTextStyle: theme.textTheme.headlineMedium!, - ); - return Scaffold( - appBar: PageWindowTitleBar( - leading: TextButton( - child: Text(context.l10n.exit), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - body: IntroductionScreen( - key: key, - globalBackgroundColor: theme.scaffoldBackgroundColor, - overrideBack: OutlinedButton( - child: Center(child: Text(context.l10n.previous)), - onPressed: () { - (key.currentState as IntroductionScreenState).previous(); - }, - ), - overrideNext: FilledButton( - child: Center(child: Text(context.l10n.next)), - onPressed: () { - (key.currentState as IntroductionScreenState).next(); - }, - ), - showBackButton: true, - overrideDone: FilledButton( - onPressed: auth.asData?.value != null - ? () { - ServiceUtils.pushNamed(context, HomePage.name); - } - : null, - child: Center(child: Text(context.l10n.done)), - ), - pages: [ - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_1, - image: Assets.tutorial.step1.image(), - bodyWidget: Wrap( - children: [ - Text(context.l10n.first_go_to), - const SizedBox(width: 5), - const Hyperlink( - "accounts.spotify.com ", - "https://accounts.spotify.com", - ), - Text(context.l10n.login_if_not_logged_in), - ], - ), - ), - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_2, - image: Assets.tutorial.step2.image(), - bodyWidget: - Text(context.l10n.step_2_steps, textAlign: TextAlign.left), - ), - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_3, - image: Assets.tutorial.step3.image(), - bodyWidget: - Text(context.l10n.step_3_steps, textAlign: TextAlign.left), - ), - if (auth.asData?.value != null) - PageViewModel( - decoration: pageDecoration.copyWith( - bodyAlignment: Alignment.center, - ), - title: context.l10n.success_emoji, - image: Assets.success.image(), - body: context.l10n.success_message, - ) - else - PageViewModel( - decoration: pageDecoration, - title: context.l10n.step_4, - bodyWidget: Column( - children: [ - Text( - context.l10n.step_4_steps, - style: theme.textTheme.labelMedium, - ), - const SizedBox(height: 10), - TokenLoginForm( - onDone: () { - GoRouter.of(context).go("/"); - }, - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index b06a67f66..1ec488c96 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,10 +9,12 @@ import 'package:spotube/components/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/mobile_login/mobile_login.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; class SettingsAccountSection extends HookConsumerWidget { @@ -23,6 +26,7 @@ class SettingsAccountSection extends HookConsumerWidget { final router = GoRouter.of(context); final auth = ref.watch(authenticationProvider); + final authNotifier = ref.watch(authenticationProvider.notifier); final scrobbler = ref.watch(scrobblerProvider); final me = ref.watch(meProvider); final meData = me.asData?.value; @@ -32,6 +36,36 @@ class SettingsAccountSection extends HookConsumerWidget { foregroundColor: Colors.white, ); + void onLogin() async { + if (kIsMobile) { + router.pushNamed(WebViewLogin.name); + return; + } + + final webview = await WebviewWindow.create(); + + webview.setOnUrlRequestCallback((url) { + final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); + + if (exp.hasMatch(url)) { + webview.getAllCookies().then((cookies) async { + final cookieHeader = + "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; + + await authNotifier.login(cookieHeader); + webview.close(); + if (context.mounted) { + context.go("/"); + } + }); + return true; + } + return false; + }); + + webview.launch("https://accounts.spotify.com/"); + } + return SectionCardWithHeading( heading: context.l10n.account, children: [ @@ -70,17 +104,11 @@ class SettingsAccountSection extends HookConsumerWidget { ), ), ), - onTap: constrains.mdAndUp - ? null - : () { - router.push("/login"); - }, + onTap: constrains.mdAndUp ? null : onLogin, trailing: constrains.smAndDown ? null : FilledButton( - onPressed: () { - router.push("/login"); - }, + onPressed: onLogin, style: ButtonStyle( shape: MaterialStateProperty.all( RoundedRectangleBorder( diff --git a/lib/provider/authentication/authentication.dart b/lib/provider/authentication/authentication.dart index 3ea8693b2..08e658e86 100644 --- a/lib/provider/authentication/authentication.dart +++ b/lib/provider/authentication/authentication.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:drift/drift.dart'; @@ -160,6 +161,9 @@ class AuthenticationNotifier extends AsyncNotifier { WebStorageManager.instance().deleteAllData(); CookieManager.instance().deleteAllCookies(); } + if (kIsDesktop) { + await WebviewWindow.clearAll(); + } } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index b8e26367b..2218d1108 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -24,6 +25,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dart_discord_rpc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DartDiscordRpcPlugin"); dart_discord_rpc_plugin_register_with_registrar(dart_discord_rpc_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 20d4a4dda..bb0776b51 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dart_discord_rpc + desktop_webview_window file_selector_linux flutter_secure_storage_linux gtk diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 545467052..8a65bb53c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import app_links import audio_service import audio_session import bonsoir_darwin +import desktop_webview_window import device_info_plus import file_selector_macos import flutter_inappwebview_macos @@ -32,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 58d09cd9f..9ab2ee38b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,8 @@ PODS: - bonsoir_darwin (0.0.1): - Flutter - FlutterMacOS + - desktop_webview_window (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -70,6 +72,7 @@ DEPENDENCIES: - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - bonsoir_darwin (from `Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin`) + - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -105,6 +108,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos bonsoir_darwin: :path: Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin + desktop_webview_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: @@ -151,6 +156,7 @@ SPEC CHECKSUMS: audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 + desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d diff --git a/pubspec.lock b/pubspec.lock index 70b0655ce..14f2a5b09 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -474,6 +474,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + desktop_webview_window: + dependency: "direct main" + description: + path: "packages/desktop_webview_window" + ref: "feat/cookies" + resolved-ref: f20e433d4a948515b35089d40069f7dd9bced9e4 + url: "https://github.com/KRTirtho/flutter-plugins.git" + source: git + version: "0.2.4" device_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a923f5a3b..a94907465 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,11 @@ dependencies: collection: ^1.15.0 curved_navigation_bar: ^1.0.3 dbus: ^0.7.8 + desktop_webview_window: + git: + url: https://github.com/KRTirtho/flutter-plugins.git + ref: feat/cookies + path: packages/desktop_webview_window device_info_plus: ^10.1.0 dio: ^5.4.3+1 disable_battery_optimization: ^1.1.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b978edb94..4fcf30199 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi")); DartDiscordRpcPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4fcc467a0..d0dd6751a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links bonsoir_windows dart_discord_rpc + desktop_webview_window file_selector_windows flutter_secure_storage_windows local_notifier From 1284b409e72d2e9e5ac2ed94f7884e38bd19657c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 11:31:47 +0600 Subject: [PATCH 44/92] chore: add linux dependencies and update CI + docker config --- .github/Dockerfile.flutter_distributor | 2 +- CONTRIBUTION.md | 6 +++--- cli/commands/install-dependencies.dart | 2 +- linux/packaging/deb/make_config.yaml | 2 ++ linux/packaging/rpm/make_config.yaml | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/Dockerfile.flutter_distributor b/.github/Dockerfile.flutter_distributor index 952b9158e..d842e533f 100644 --- a/.github/Dockerfile.flutter_distributor +++ b/.github/Dockerfile.flutter_distributor @@ -4,7 +4,7 @@ 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 && \ + 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 libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev && \ rm -rf /var/lib/apt/lists/* WORKDIR /home/flutter diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 0cfff0cab..d4746a1a9 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -123,16 +123,16 @@ Do the following: - Install Development dependencies in linux - Debian (>=12/Bookworm)/Ubuntu ```bash - $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan + $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev ``` - Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04) - Arch/Manjaro ```bash - yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan + yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan webkit2gtk-4.1 libsoup3 ``` - Fedora ```bash - dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns + dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns webkit2gtk4.1 webkit2gtk4.1-devel libsoup3 libsoup3-devel ``` - Clone the Repo - Create a `.env` in root of the project following the `.env.example` template diff --git a/cli/commands/install-dependencies.dart b/cli/commands/install-dependencies.dart index 75df28dfa..6875e35fb 100644 --- a/cli/commands/install-dependencies.dart +++ b/cli/commands/install-dependencies.dart @@ -37,7 +37,7 @@ class InstallDependenciesCommand extends Command { 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 + 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 libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev """, ); break; diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index 95777f567..a7bea1aa4 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -23,6 +23,8 @@ dependencies: - avahi-utils - libnss-mdns - mdns-scan + - libwebkit2gtk-4.1-0 | libwebkit2gtk-4.0-0 + - libsoup-3.0-0 | libsoup-2.4-0 essential: false icon: assets/spotube-logo.png diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml index 12b4473e5..3d4a3b7ed 100644 --- a/linux/packaging/rpm/make_config.yaml +++ b/linux/packaging/rpm/make_config.yaml @@ -16,6 +16,8 @@ requires: - avahi - mdns-scan - nss-mdns + - webkit2gtk4.1 + - libsoup3 display_name: Spotube From 79b842dad32d64ee5bd968f0c3552634b25f8daa Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 11:55:04 +0600 Subject: [PATCH 45/92] chore: use flutter 3.19.6 to avoid window stretching error in windows --- .github/workflows/spotube-release-binary.yml | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 5b74c9b59..e99aebab9 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/pubspec.yaml b/pubspec.yaml index a94907465..58ca0ae96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -132,7 +132,7 @@ dependencies: encrypt: ^5.0.3 dev_dependencies: - build_runner: ^2.4.11 + build_runner: ^2.4.9 crypto: ^3.0.3 envied_generator: ^0.5.4+1 flutter_gen_runner: ^5.4.0 From f2f35bd2fbdd046441f4aa5da7f7572292a40b1a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 13:32:07 +0600 Subject: [PATCH 46/92] chore: fix windows webview2 trying to store data in admin folders --- lib/main.dart | 3 ++- lib/models/database/database.dart | 2 +- lib/pages/settings/sections/accounts.dart | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b3a3132f7..e0e342544 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:dart_discord_rpc/dart_discord_rpc.dart'; +import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -65,7 +66,7 @@ Future main(List rawArgs) async { await FlutterDisplayMode.setHighRefreshRate(); } - if (kIsDesktop && !kIsMacOS) { + if (kIsDesktop) { await windowManager.setPreventClose(true); } diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 1c233f845..fbfc59dbc 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -66,7 +66,7 @@ LazyDatabase _openConnection() { return LazyDatabase(() async { // put the database file, called db.sqlite here, into the documents folder // for your app. - final dbFolder = await getApplicationDocumentsDirectory(); + final dbFolder = await getApplicationSupportDirectory(); final file = File(join(dbFolder.path, 'db.sqlite')); // Also work around limitations on old Android versions diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 1ec488c96..0c2f3c5e5 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:auto_size_text/auto_size_text.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/image/universal_image.dart'; @@ -42,7 +46,18 @@ class SettingsAccountSection extends HookConsumerWidget { return; } - final webview = await WebviewWindow.create(); + final applicationSupportDir = await getApplicationSupportDirectory(); + final userDataFolder = Directory( + join(applicationSupportDir.path, "webview_window_Webview2")); + + if (!await userDataFolder.exists()) { + await userDataFolder.create(); + } + + final webview = await WebviewWindow.create( + configuration: + CreateConfiguration(userDataFolderWindows: userDataFolder.path), + ); webview.setOnUrlRequestCallback((url) { final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); From 7dd76d24c3c8082a0ac37a4018d807aaeca1c3d2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 14:54:49 +0600 Subject: [PATCH 47/92] chore: fix windows cookie invalid characters --- lib/main.dart | 6 ++++ lib/pages/settings/sections/accounts.dart | 44 ++++++++++++----------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e0e342544..db7773f9e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,6 +47,12 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:window_manager/window_manager.dart'; Future main(List rawArgs) async { + WidgetsFlutterBinding.ensureInitialized(); + + if (runWebViewTitleBarWidget(rawArgs)) { + return; + } + final arguments = await startCLI(rawArgs); AppLogger.initialize(arguments["verbose"]); diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 0c2f3c5e5..596599be9 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -46,6 +46,7 @@ class SettingsAccountSection extends HookConsumerWidget { return; } + final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); final applicationSupportDir = await getApplicationSupportDirectory(); final userDataFolder = Directory( join(applicationSupportDir.path, "webview_window_Webview2")); @@ -55,30 +56,33 @@ class SettingsAccountSection extends HookConsumerWidget { } final webview = await WebviewWindow.create( - configuration: - CreateConfiguration(userDataFolderWindows: userDataFolder.path), + configuration: CreateConfiguration( + title: "Spotify Login", + titleBarTopPadding: kIsMacOS ? 20 : 0, + windowHeight: 720, + windowWidth: 1280, + userDataFolderWindows: userDataFolder.path, + ), ); + webview + ..setBrightness(theme.colorScheme.brightness) + ..launch("https://accounts.spotify.com/") + ..setOnUrlRequestCallback((url) { + if (exp.hasMatch(url)) { + webview.getAllCookies().then((cookies) async { + final cookieHeader = + "sp_dc=${cookies.firstWhere((element) => element.name.contains("sp_dc")).value.replaceAll("\u0000", "")}"; - webview.setOnUrlRequestCallback((url) { - final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); + await authNotifier.login(cookieHeader); + webview.close(); + if (context.mounted) { + context.go("/"); + } + }); + } - if (exp.hasMatch(url)) { - webview.getAllCookies().then((cookies) async { - final cookieHeader = - "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; - - await authNotifier.login(cookieHeader); - webview.close(); - if (context.mounted) { - context.go("/"); - } - }); return true; - } - return false; - }); - - webview.launch("https://accounts.spotify.com/"); + }); } return SectionCardWithHeading( From 359b918e6bb0a2c4792492b8cc84761af1c8aea4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jul 2024 15:32:18 +0600 Subject: [PATCH 48/92] chore: fix windows playback not working for loop back ipv4 --- lib/main.dart | 10 ++++------ lib/services/audio_player/audio_player.dart | 5 +++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index db7773f9e..45f4462d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,18 +47,16 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:window_manager/window_manager.dart'; Future main(List rawArgs) async { - WidgetsFlutterBinding.ensureInitialized(); - - if (runWebViewTitleBarWidget(rawArgs)) { - return; - } - final arguments = await startCLI(rawArgs); AppLogger.initialize(arguments["verbose"]); AppLogger.runZoned(() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + if (runWebViewTitleBarWidget(rawArgs)) { + return; + } + await registerWindowsScheme("spotify"); tz.initializeTimeZones(); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index bb1a62036..7915dc3bd 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -12,6 +12,7 @@ import 'package:media_kit/media_kit.dart' as mk; import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/utils/platform.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; @@ -28,7 +29,7 @@ class SpotubeMedia extends mk.Media { }) : super( track is LocalTrack ? track.path - : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", + : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", extras: { ...?extras, "track": switch (track) { @@ -42,7 +43,7 @@ class SpotubeMedia extends mk.Media { @override String get uri => track is LocalTrack ? (track as LocalTrack).path - : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; + : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http") From 2f46fa32f13da3d500a3593e445c43f9a88ac54d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 6 Jul 2024 18:31:17 +0600 Subject: [PATCH 49/92] chore: fix webview and app window freezing after successful login --- lib/main.dart | 10 ++++++---- lib/pages/settings/sections/accounts.dart | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 45f4462d7..7e8da0f27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,16 +47,18 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:window_manager/window_manager.dart'; Future main(List rawArgs) async { + if (rawArgs.contains("web_view_title_bar")) { + WidgetsFlutterBinding.ensureInitialized(); + if (runWebViewTitleBarWidget(rawArgs)) { + return; + } + } final arguments = await startCLI(rawArgs); AppLogger.initialize(arguments["verbose"]); AppLogger.runZoned(() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - if (runWebViewTitleBarWidget(rawArgs)) { - return; - } - await registerWindowsScheme("spotify"); tz.initializeTimeZones(); diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 596599be9..7e37b68be 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -74,6 +74,7 @@ class SettingsAccountSection extends HookConsumerWidget { "sp_dc=${cookies.firstWhere((element) => element.name.contains("sp_dc")).value.replaceAll("\u0000", "")}"; await authNotifier.login(cookieHeader); + webview.close(); if (context.mounted) { context.go("/"); From 2ce4853fd1e12ab4616a52d7827484bffd5012a6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 6 Jul 2024 19:26:59 +0600 Subject: [PATCH 50/92] chore: fix while loading playlists/album already playing ones doesn't get cleared --- lib/provider/audio_player/audio_player.dart | 4 ++++ lib/provider/authentication/authentication.dart | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index da22b2ce5..5323f3c0c 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -287,6 +287,10 @@ class AudioPlayerNotifier extends Notifier { await ref.read(sourcedTrackProvider(intendedActiveTrack).future); } + if(medias.isEmpty) return; + + await removeCollections(state.collections); + await audioPlayer.openPlaylist( medias, initialIndex: initialIndex, diff --git a/lib/provider/authentication/authentication.dart b/lib/provider/authentication/authentication.dart index 08e658e86..f7339ef06 100644 --- a/lib/provider/authentication/authentication.dart +++ b/lib/provider/authentication/authentication.dart @@ -97,7 +97,7 @@ class AuthenticationNotifier extends AsyncNotifier { await database .into(database.authenticationTable) - .insert(refreshedCredentials); + .insertOnConflictUpdate(refreshedCredentials); } Future credentialsFromCookie( From ccea4a003d84853a68bffe39796b3d70bd34f0be Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 6 Jul 2024 21:35:56 +0600 Subject: [PATCH 51/92] fix: changed source doesn't get saved and uses the wrong once again --- lib/services/sourced_track/sources/jiosaavn.dart | 11 ++++++++++- lib/services/sourced_track/sources/piped.dart | 11 ++++++++++- lib/services/sourced_track/sources/youtube.dart | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index 865e3d634..1434e4f73 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -43,7 +43,12 @@ class JioSaavnSourcedTrack extends SourcedTrack { }) async { final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!))) + ..where((s) => s.trackId.equals(track.id!)) + ..limit(1) + ..orderBy([ + (s) => + OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), + ])) .getSingleOrNull(); if (cachedSource == null || @@ -215,7 +220,11 @@ class JioSaavnSourcedTrack extends SourcedTrack { trackId: id!, sourceId: info.id, sourceType: const Value(SourceType.jiosaavn), + // Because we're sorting by createdAt in the query + // we have to update it to indicate priority + createdAt: Value(DateTime.now()), ), + mode: InsertMode.replace, ); return JioSaavnSourcedTrack( diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index d156b26e5..d24f110f3 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -52,7 +52,12 @@ class PipedSourcedTrack extends SourcedTrack { }) async { final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!))) + ..where((s) => s.trackId.equals(track.id!)) + ..limit(1) + ..orderBy([ + (s) => + OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), + ])) .getSingleOrNull(); final preferences = ref.read(userPreferencesProvider); final pipedClient = ref.read(pipedProvider); @@ -278,7 +283,11 @@ class PipedSourcedTrack extends SourcedTrack { trackId: id!, sourceId: newSourceInfo.id, sourceType: const Value(SourceType.youtube), + // Because we're sorting by createdAt in the query + // we have to update it to indicate priority + createdAt: Value(DateTime.now()), ), + mode: InsertMode.replace, ); return PipedSourcedTrack( diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 0501a499d..0b5ee71bb 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -50,8 +50,14 @@ class YoutubeSourcedTrack extends SourcedTrack { }) async { final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!))) - .getSingleOrNull(); + ..where((s) => s.trackId.equals(track.id!)) + ..limit(1) + ..orderBy([ + (s) => + OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), + ])) + .get() + .then((s) => s.firstOrNull); if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { final siblings = await fetchSiblings(ref: ref, track: track); @@ -287,12 +293,17 @@ class YoutubeSourcedTrack extends SourcedTrack { ); final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( trackId: id!, sourceId: newSourceInfo.id, sourceType: const Value(SourceType.youtube), + // Because we're sorting by createdAt in the query + // we have to update it to indicate priority + createdAt: Value(DateTime.now()), ), + mode: InsertMode.replace, ); return YoutubeSourcedTrack( From 86f5b80177b5c1d3da2ff9fbe7694165c48ac190 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 6 Jul 2024 21:38:36 +0600 Subject: [PATCH 52/92] chore: fix insert failing to invalid conflict check --- lib/provider/authentication/authentication.dart | 2 +- lib/provider/spotify/lyrics/synced.dart | 3 ++- lib/provider/spotify/spotify.dart | 1 + lib/utils/migrations/hive.dart | 9 ++++++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/provider/authentication/authentication.dart b/lib/provider/authentication/authentication.dart index f7339ef06..05a05972c 100644 --- a/lib/provider/authentication/authentication.dart +++ b/lib/provider/authentication/authentication.dart @@ -97,7 +97,7 @@ class AuthenticationNotifier extends AsyncNotifier { await database .into(database.authenticationTable) - .insertOnConflictUpdate(refreshedCredentials); + .insert(refreshedCredentials, mode: InsertMode.replace); } Future credentialsFromCookie( diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index bcf2a1626..085fccb7f 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -152,11 +152,12 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { } if (cachedLyrics == null || cachedLyrics.lyrics.isEmpty) { - await database.into(database.lyricsTable).insertOnConflictUpdate( + await database.into(database.lyricsTable).insert( LyricsTableCompanion.insert( trackId: track.id!, data: lyrics, ), + mode: InsertMode.replace, ); } diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index 63a8ed385..5997a47a6 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -2,6 +2,7 @@ library spotify; import 'dart:async'; +import 'package:drift/drift.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/spotify/utils/json_cast.dart'; diff --git a/lib/utils/migrations/hive.dart b/lib/utils/migrations/hive.dart index e43df1d80..e57819314 100644 --- a/lib/utils/migrations/hive.dart +++ b/lib/utils/migrations/hive.dart @@ -35,13 +35,14 @@ Future migrateAuthenticationInfo() async { if (credentials == null) return; - await _database.into(_database.authenticationTable).insertOnConflictUpdate( + await _database.into(_database.authenticationTable).insert( AuthenticationTableCompanion.insert( accessToken: DecryptedText(credentials.accessToken), cookie: DecryptedText(credentials.cookie), expiration: credentials.expiration, id: const Value(0), ), + mode: InsertMode.insertOrReplace, ); AppLogger.log.i("✅ Migrated authentication info"); @@ -58,7 +59,7 @@ Future migratePreferences() async { if (preferences == null) return; - await _database.into(_database.preferencesTable).insertOnConflictUpdate( + await _database.into(_database.preferencesTable).insert( PreferencesTableCompanion.insert( id: const Value(0), accentColorScheme: Value(preferences.accentColorScheme), @@ -108,6 +109,7 @@ Future migratePreferences() async { systemTitleBar: Value(preferences.systemTitleBar), themeMode: Value(preferences.themeMode), ), + mode: InsertMode.replace, ); AppLogger.log.i("✅ Migrated preferences"); @@ -235,12 +237,13 @@ Future migrateLastFmCredentials() async { if (data == null) return; - await _database.into(_database.scrobblerTable).insertOnConflictUpdate( + await _database.into(_database.scrobblerTable).insert( ScrobblerTableCompanion.insert( id: const Value(0), passwordHash: DecryptedText(data.passwordHash), username: data.username, ), + mode: InsertMode.replace, ); AppLogger.log.i("✅ Migrated Last.fm credentials"); From 86ee64c6066b3aa3c5910c89ffff7367826e823c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 6 Jul 2024 22:02:31 +0600 Subject: [PATCH 53/92] chore: remove old logger --- lib/collections/intents.dart | 3 - lib/extensions/list.dart | 99 ------------------- lib/models/logger.dart | 74 -------------- lib/modules/artist/artist_album_list.dart | 6 +- lib/modules/player/player_actions.dart | 5 +- lib/modules/player/player_controls.dart | 5 +- lib/modules/root/bottom_player.dart | 4 +- lib/pages/artist/artist.dart | 5 +- lib/pages/settings/logs.dart | 4 +- lib/provider/connect/connect.dart | 8 +- lib/provider/server/routes/connect.dart | 8 +- lib/services/cli/cli.dart | 8 -- .../download_manager/chunked_download.dart | 7 -- .../download_manager/download_manager.dart | 13 +-- lib/utils/service_utils.dart | 9 +- 15 files changed, 19 insertions(+), 239 deletions(-) delete mode 100644 lib/extensions/list.dart delete mode 100644 lib/models/logger.dart diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index ac0451ac9..4f446831a 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/modules/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'; @@ -21,8 +20,6 @@ class PlayPauseIntent extends Intent { } class PlayPauseAction extends Action { - final logger = getLogger(PlayPauseAction); - @override invoke(intent) async { if (PlayerControls.focusNode.canRequestFocus) { diff --git a/lib/extensions/list.dart b/lib/extensions/list.dart deleted file mode 100644 index 6ecf6cf69..000000000 --- a/lib/extensions/list.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:spotube/models/logger.dart'; - -final logger = getLogger("List"); - -extension MultiSortListMap on List { - /// [preference] - List of properties in which you want to sort the list - /// i.e. - /// ``` - /// List preference = ['property1','property2']; - /// ``` - /// This will first sort the list by property1 then by property2 - /// - /// [criteria] - List of booleans that specifies the criteria of sort - /// i.e., For ascending order `true` and for descending order `false`. - /// ``` - /// List criteria = [true. false]; - /// ``` - List sortByProperties(List criteria, List preference) { - if (preference.isEmpty || criteria.isEmpty || isEmpty) { - return this; - } - if (preference.length != criteria.length) { - logger.d('Criteria length is not equal to preference'); - return this; - } - - int compare(int i, Map a, Map b) { - if (a[preference[i]] == b[preference[i]]) { - return 0; - } else if (a[preference[i]] > b[preference[i]]) { - return criteria[i] ? 1 : -1; - } else { - return criteria[i] ? -1 : 1; - } - } - - int sortAll(Map a, Map b) { - int i = 0; - int result = 0; - while (i < preference.length) { - result = compare(i, a, b); - if (result != 0) break; - i++; - } - return result; - } - - return sorted((a, b) => sortAll(a, b)); - } -} - -extension MultiSortListTupleMap on List<(Map, V)> { - /// [preference] - List of properties in which you want to sort the list - /// i.e. - /// ``` - /// List preference = ['property1','property2']; - /// ``` - /// This will first sort the list by property1 then by property2 - /// - /// [criteria] - List of booleans that specifies the criteria of sort - /// i.e., For ascending order `true` and for descending order `false`. - /// ``` - /// List criteria = [true. false]; - /// ``` - List<(Map, V)> sortByProperties( - List criteria, List preference) { - if (preference.isEmpty || criteria.isEmpty || isEmpty) { - return this; - } - if (preference.length != criteria.length) { - logger.d('Criteria length is not equal to preference'); - return this; - } - - int compare(int i, (Map, V) a, (Map, V) b) { - if (a.$1[preference[i]] == b.$1[preference[i]]) { - return 0; - } else if (a.$1[preference[i]] > b.$1[preference[i]]) { - return criteria[i] ? 1 : -1; - } else { - return criteria[i] ? -1 : 1; - } - } - - int sortAll((Map, V) a, (Map, V) b) { - int i = 0; - int result = 0; - while (i < preference.length) { - result = compare(i, a, b); - if (result != 0) break; - i++; - } - return result; - } - - return sorted((a, b) => sortAll(a, b)); - } -} diff --git a/lib/models/logger.dart b/lib/models/logger.dart deleted file mode 100644 index 3236028dc..000000000 --- a/lib/models/logger.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:logger/logger.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:path/path.dart' as path; -import 'package:spotube/utils/platform.dart'; - -final _loggerFactory = SpotubeLogger(); -final logEnv = { - if (!kIsWeb) ...Platform.environment, -}; - -SpotubeLogger getLogger(T owner) { - _loggerFactory.owner = owner is String ? owner : owner.toString(); - return _loggerFactory; -} - -Future getLogsPath() async { - String dir = (await getApplicationDocumentsDirectory()).path; - if (kIsAndroid) { - dir = (await getExternalStorageDirectory())?.path ?? ""; - } - - if (kIsMacOS) { - dir = path.join((await getLibraryDirectory()).path, "Logs"); - } - final file = File(path.join(dir, ".spotube_logs")); - if (!await file.exists()) { - await file.create(recursive: true); - } - return file; -} - -class SpotubeLogger extends Logger { - String? owner; - SpotubeLogger([this.owner]) : super(filter: _SpotubeLogFilter()); - - @override - void log(Level level, dynamic message, - {Object? error, StackTrace? stackTrace, DateTime? time}) async { - if (!kIsWeb) { - if (level == Level.error) { - String dir = (await getApplicationDocumentsDirectory()).path; - - if (kIsAndroid) { - dir = (await getExternalStorageDirectory())?.path ?? ""; - } - - if (kIsMacOS) { - dir = path.join((await getLibraryDirectory()).path, "Logs"); - } - - await File(path.join(dir, ".spotube_logs")).writeAsString( - "[${DateTime.now()}]\n$message\n$stackTrace", - mode: FileMode.writeOnlyAppend); - } - } - - super.log(level, "[$owner] $message", error: error, stackTrace: stackTrace); - } -} - -class _SpotubeLogFilter extends DevelopmentFilter { - @override - bool shouldLog(LogEvent event) { - if ((logEnv["DEBUG"] == "true" && event.level == Level.debug) || - (logEnv["VERBOSE"] == "true" && event.level == Level.trace) || - (logEnv["ERROR"] == "true" && event.level == Level.error)) { - return true; - } - return super.shouldLog(event); - } -} diff --git a/lib/modules/artist/artist_album_list.dart b/lib/modules/artist/artist_album_list.dart index 9bb65804a..a2dd80066 100644 --- a/lib/modules/artist/artist_album_list.dart +++ b/lib/modules/artist/artist_album_list.dart @@ -3,18 +3,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class ArtistAlbumList extends HookConsumerWidget { final String artistId; - ArtistAlbumList( + + const ArtistAlbumList( this.artistId, { super.key, }); - final logger = getLogger(ArtistAlbumList); - @override Widget build(BuildContext context, ref) { final albumsQuery = ref.watch(artistAlbumsProvider(artistId)); diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 8a7b3e831..a47c992d7 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -11,7 +11,6 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -22,14 +21,14 @@ class PlayerActions extends HookConsumerWidget { final bool floatingQueue; final bool showQueue; final List? extraActions; - PlayerActions({ + + const PlayerActions({ this.mainAxisAlignment = MainAxisAlignment.center, this.floatingQueue = true, this.showQueue = true, this.extraActions, super.key, }); - final logger = getLogger(PlayerActions); @override Widget build(BuildContext context, ref) { diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index 1b9d9f86f..c88f62582 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -10,7 +10,6 @@ import 'package:spotube/collections/intents.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; -import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -19,14 +18,12 @@ class PlayerControls extends HookConsumerWidget { final PaletteGenerator? palette; final bool compact; - PlayerControls({ + const PlayerControls({ this.palette, this.compact = false, super.key, }); - final logger = getLogger(PlayerControls); - static FocusNode focusNode = FocusNode(); @override diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index e7dbacd22..23904aef4 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -16,7 +16,6 @@ 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/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -27,9 +26,8 @@ import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; class BottomPlayer extends HookConsumerWidget { - BottomPlayer({super.key}); + const BottomPlayer({super.key}); - final logger = getLogger(BottomPlayer); @override Widget build(BuildContext context, ref) { final auth = ref.watch(authenticationProvider); diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 04389ffc0..70ad72de5 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -7,7 +7,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/artist/artist_album_list.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/logger.dart'; + import 'package:spotube/pages/artist/section/footer.dart'; import 'package:spotube/pages/artist/section/header.dart'; import 'package:spotube/pages/artist/section/related_artists.dart'; @@ -18,8 +18,7 @@ class ArtistPage extends HookConsumerWidget { static const name = "artist"; final String artistId; - final logger = getLogger(ArtistPage); - ArtistPage(this.artistId, {super.key}); + const ArtistPage(this.artistId, {super.key}); @override Widget build(BuildContext context, ref) { diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index 65e4c82ed..a49050ad5 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -8,7 +8,7 @@ import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/logger.dart'; +import 'package:spotube/services/logger/logger.dart'; class LogsPage extends HookWidget { static const name = "logs"; @@ -61,7 +61,7 @@ class LogsPage extends HookWidget { useEffect(() { final timer = Timer.periodic(const Duration(seconds: 5), (t) async { - path.value ??= await getLogsPath(); + path.value ??= await AppLogger.getLogsPath(); final raw = await path.value!.readAsString(); final hasChanged = rawLogs.value != raw; rawLogs.value = raw; diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index 28eb131bc..000a28af0 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -7,7 +7,7 @@ import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/connect/connect.dart'; -import 'package:spotube/models/logger.dart'; + import 'package:spotube/provider/connect/clients.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -46,8 +46,6 @@ final volumeProvider = StateProvider( (ref) => 1.0, ); -final logger = getLogger('ConnectNotifier'); - class ConnectNotifier extends AsyncNotifier { @override build() async { @@ -58,7 +56,7 @@ class ConnectNotifier extends AsyncNotifier { final service = connectClients.asData!.value.resolvedService!; - logger.t( + AppLogger.log.t( '♾️ Connecting to ${service.name}: ws://${service.host}:${service.port}/ws', ); @@ -68,7 +66,7 @@ class ConnectNotifier extends AsyncNotifier { await channel.ready; - logger.t( + AppLogger.log.t( '✅ Connected to ${service.name}: ws://${service.host}:${service.port}/ws', ); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart index 8e75a87ec..0d35b4734 100644 --- a/lib/provider/server/routes/connect.dart +++ b/lib/provider/server/routes/connect.dart @@ -7,7 +7,7 @@ import 'package:shelf/shelf.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/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/volume_provider.dart'; @@ -25,11 +25,9 @@ class ServerConnectRoutes { final Ref ref; final StreamController _connectClientStreamController; final List subscriptions; - final SpotubeLogger logger; ServerConnectRoutes(this.ref) : _connectClientStreamController = StreamController.broadcast(), - subscriptions = [], - logger = getLogger('ConnectServer') { + subscriptions = [] { ref.onDispose(() { _connectClientStreamController.close(); for (final subscription in subscriptions) { @@ -193,7 +191,7 @@ class ServerConnectRoutes { } }, onDone: () { - logger.i('Connection closed'); + AppLogger.log.i('Connection closed'); }, ), ]); diff --git a/lib/services/cli/cli.dart b/lib/services/cli/cli.dart index 720216c70..985c0e723 100644 --- a/lib/services/cli/cli.dart +++ b/lib/services/cli/cli.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:spotube/models/logger.dart'; Future startCLI(List args) async { final parser = ArgParser(); @@ -15,13 +14,6 @@ Future startCLI(List args) async { abbr: 'v', help: 'Verbose mode', defaultsTo: !kReleaseMode, - callback: (verbose) { - if (verbose) { - logEnv['VERBOSE'] = 'true'; - logEnv['DEBUG'] = 'true'; - logEnv['ERROR'] = 'true'; - } - }, ); parser.addFlag( "version", diff --git a/lib/services/download_manager/chunked_download.dart b/lib/services/download_manager/chunked_download.dart index 9e5e0a989..80a3e78fb 100644 --- a/lib/services/download_manager/chunked_download.dart +++ b/lib/services/download_manager/chunked_download.dart @@ -2,9 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:spotube/models/logger.dart'; - -final logger = getLogger("ChunkedDownload"); /// Downloading by spiting as file in chunks extension ChunkDownload on Dio { @@ -69,11 +66,7 @@ extension ChunkDownload on Dio { } await raf.close(); - logger.d("Downloaded file path: ${headFile.path}"); - headFile = await headFile.rename(savePath); - - logger.d("Renamed file path: ${headFile.path}"); } final firstResponse = await downloadChunk( diff --git a/lib/services/download_manager/download_manager.dart b/lib/services/download_manager/download_manager.dart index afbee88cb..d2072bd7e 100644 --- a/lib/services/download_manager/download_manager.dart +++ b/lib/services/download_manager/download_manager.dart @@ -1,18 +1,18 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; -import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:spotube/models/logger.dart'; + import 'package:spotube/services/download_manager/chunked_download.dart'; import 'package:spotube/services/download_manager/download_request.dart'; import 'package:spotube/services/download_manager/download_status.dart'; import 'package:spotube/services/download_manager/download_task.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/utils/primitive_utils.dart'; export './download_request.dart'; @@ -25,7 +25,6 @@ typedef DownloadStatusEvent = ({ }); class DownloadManager { - final logger = getLogger("DownloadManager"); final Map _cache = {}; final Queue _queue = Queue(); var dio = Dio(); @@ -77,7 +76,6 @@ class DownloadManager { } setStatus(task, DownloadStatus.downloading); - logger.d("[DownloadManager] $url"); final file = File(savePath.toString()); await Directory(path.dirname(savePath)).create(recursive: true); @@ -99,11 +97,8 @@ class DownloadManager { final partialFileExist = await partialFile.exists(); if (fileExist) { - logger.d("[DownloadManager] File Exists"); setStatus(task, DownloadStatus.completed); } else if (partialFileExist) { - logger.d("[DownloadManager] Partial File Exists"); - final partialFileLength = await partialFile.length(); final response = await dio.download( @@ -225,7 +220,6 @@ class DownloadManager { } Future pauseDownload(String url) async { - logger.d("[DownloadManager] Pause Download"); var task = getDownload(url)!; setStatus(task, DownloadStatus.paused); task.request.cancelToken.cancel(); @@ -234,7 +228,6 @@ class DownloadManager { } Future cancelDownload(String url) async { - logger.d("[DownloadManager] Cancel Download"); var task = getDownload(url)!; setStatus(task, DownloadStatus.canceled); _queue.remove(task.request); @@ -242,7 +235,6 @@ class DownloadManager { } Future resumeDownload(String url) async { - logger.d("[DownloadManager] Resume Download"); var task = getDownload(url)!; setStatus(task, DownloadStatus.downloading); task.request.cancelToken = CancelToken(); @@ -405,7 +397,6 @@ class DownloadManager { while (_queue.isNotEmpty && runningTasks < maxConcurrentTasks) { runningTasks++; - logger.d('Concurrent workers: $runningTasks'); var currentRequest = _queue.removeFirst(); await download( diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 5950bc8ce..c00f07ab9 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -4,7 +4,7 @@ import 'package:html/dom.dart' hide Text; import 'package:spotify/spotify.dart'; import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/modules/root/update_dialog.dart'; -import 'package:spotube/models/logger.dart'; + import 'package:spotube/models/lyrics.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/dio/dio.dart'; @@ -24,8 +24,6 @@ import 'package:spotube/collections/env.dart'; import 'package:version/version.dart'; abstract class ServiceUtils { - static final logger = getLogger("ServiceUtils"); - static final _englishMatcherRegex = RegExp( "^[a-zA-Z0-9\\s!\"#\$%&\\'()*+,-.\\/:;<=>?@\\[\\]^_`{|}~]*\$", ); @@ -194,8 +192,6 @@ abstract class ServiceUtils { artists: artistNames, ); - logger.v("[Searching Subtitle] $query"); - final searchUri = Uri.parse("$baseUri/subtitles4songs.aspx").replace( queryParameters: {"q": query}, ); @@ -227,7 +223,6 @@ abstract class ServiceUtils { // not result was found at all if (rateSortedResults.first["points"] == 0) { - logger.e("[Subtitle not found] ${track.name}"); return Future.error("Subtitle lookup failed", StackTrace.current); } @@ -235,8 +230,6 @@ abstract class ServiceUtils { final subtitleUri = Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc"); - logger.v("[Selected subtitle] ${topResult.text} | $subtitleUri"); - final lrcDocument = parser.parse((await globalDio.getUri( subtitleUri, options: Options(responseType: ResponseType.plain), From d359d130de628bb1b67bd43d14a18d6d6e6d0ff6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 6 Jul 2024 22:08:49 +0600 Subject: [PATCH 54/92] chore: resize currently downloading --- lib/modules/library/user_downloads.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/library/user_downloads.dart b/lib/modules/library/user_downloads.dart index a5f3883a9..7fe9800cf 100644 --- a/lib/modules/library/user_downloads.dart +++ b/lib/modules/library/user_downloads.dart @@ -31,7 +31,7 @@ class UserDownloads extends HookConsumerWidget { context.l10n .currently_downloading(downloadManager.$downloadCount), maxLines: 1, - style: Theme.of(context).textTheme.headlineMedium, + style: Theme.of(context).textTheme.titleMedium, ), ), const SizedBox(width: 10), From 67ae2e81596dfac850d036f5a1a98f8d4c16ad74 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Jul 2024 21:05:28 +0600 Subject: [PATCH 55/92] chore: fix remote playback not working --- lib/provider/audio_player/state.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 387c2e30a..0e3004f51 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -40,6 +40,7 @@ class AudioPlayerState { httpHeaders: media['httpHeaders'], )), ) + .cast() .toList(), index: json['playlist']['index'], ), From c7d8ed567a9bd9b23098ecab7ed923e30f66f72f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Jul 2024 21:17:49 +0600 Subject: [PATCH 56/92] fix: lyrics page doesn't scroll to top after song ends #885 --- lib/pages/lyrics/synced_lyrics.dart | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 217967255..c2bf7b81f 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; @@ -56,7 +58,11 @@ class SyncedLyrics extends HookConsumerWidget { ref.listen( audioPlayerProvider.select((s) => s.activeTrack), (previous, next) { - controller.scrollToIndex(0); + controller.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); ref.read(syncedLyricsDelayProvider.notifier).state = 0; }, ); @@ -69,6 +75,23 @@ class SyncedLyrics extends HookConsumerWidget { final bodyTextTheme = textTheme.bodyLarge?.copyWith( color: palette.bodyTextColor, ); + + useEffect(() { + StreamSubscription? subscription; + WidgetsBinding.instance.addPostFrameCallback((_) { + subscription = audioPlayer.positionStream.listen((event) { + if (event > Duration.zero) return; + controller.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }); + }); + + return subscription?.cancel; + }, [controller]); + return Stack( children: [ CustomScrollView( From 44861a9f5c80625e0ff9a04dc9752dc186a0fa73 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Jul 2024 22:13:05 +0600 Subject: [PATCH 57/92] fix: unescape html escape values #1300 --- lib/components/playbutton_card.dart | 17 ++++--------- .../sections/header/flexible_header.dart | 10 ++++---- lib/extensions/string.dart | 2 +- .../playlist/playlist_create_dialog.dart | 3 ++- lib/modules/stats/common/playlist_item.dart | 4 ++-- lib/pages/playlist/playlist.dart | 21 +++++++++++++--- lib/provider/spotify/playlist/favorite.dart | 14 +++++++++++ lib/provider/spotify/playlist/playlist.dart | 24 +++++++++++++++---- 8 files changed, 65 insertions(+), 30 deletions(-) diff --git a/lib/components/playbutton_card.dart b/lib/components/playbutton_card.dart index ffd91cd2e..a0b96ab8f 100644 --- a/lib/components/playbutton_card.dart +++ b/lib/components/playbutton_card.dart @@ -8,18 +8,10 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/hover_builder.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/string.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; -final htmlTagRegexp = RegExp(r"<[^>]*>", caseSensitive: true); - -String? useDescription(String? description) { - return useMemoized(() { - if (description == null) return null; - return description.replaceAll(htmlTagRegexp, ''); - }, [description]); -} - class PlaybuttonCard extends HookWidget { final void Function()? onTap; final void Function()? onPlaybuttonPressed; @@ -66,8 +58,7 @@ class PlaybuttonCard extends HookWidget { others: 15, ); - final cleanDescription = useDescription(description); - + var unescapeHtml = description?.unescapeHtml(); return Container( constraints: BoxConstraints(maxWidth: size), margin: margin, @@ -205,11 +196,11 @@ class PlaybuttonCard extends HookWidget { overflow: TextOverflow.ellipsis, ), ), - if (cleanDescription != null) + if (description != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: AutoSizeText( - cleanDescription, + unescapeHtml!, maxLines: 2, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(.5), diff --git a/lib/components/tracks_view/sections/header/flexible_header.dart b/lib/components/tracks_view/sections/header/flexible_header.dart index 6e8fc2d11..6845cc3e8 100644 --- a/lib/components/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/tracks_view/sections/header/flexible_header.dart @@ -5,12 +5,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/playbutton_card.dart'; import 'package:spotube/components/tracks_view/sections/header/header_actions.dart'; import 'package:spotube/components/tracks_view/sections/header/header_buttons.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:gap/gap.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/string.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/utils/platform.dart'; @@ -24,8 +24,6 @@ class TrackViewFlexHeader extends HookConsumerWidget { final defaultTextStyle = DefaultTextStyle.of(context); final mediaQuery = MediaQuery.of(context); - final description = useDescription(props.description); - final palette = usePaletteColor(props.image, ref); return IconTheme( @@ -127,10 +125,10 @@ class TrackViewFlexHeader extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 10), - if (description != null && - description.isNotEmpty) + if (props.description != null && + props.description!.isNotEmpty) Text( - description, + props.description!.unescapeHtml(), style: defaultTextStyle.style.copyWith( color: palette.bodyTextColor, diff --git a/lib/extensions/string.dart b/lib/extensions/string.dart index 0aa41dc67..d3706f3f2 100644 --- a/lib/extensions/string.dart +++ b/lib/extensions/string.dart @@ -7,7 +7,7 @@ extension UnescapeHtml on String { } extension NullableUnescapeHtml on String? { - String? unescapeHtml() => this == null ? null : htmlEscape.convert(this!); + String? unescapeHtml() => this?.unescapeHtml(); } extension StringExtension on String { diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index 6c333986e..b9e4be8fc 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -15,6 +15,7 @@ import 'package:spotube/components/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/extensions/string.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; @@ -54,7 +55,7 @@ class PlaylistCreateDialog extends HookConsumerWidget { text: updatingPlaylist?.name, ); final description = useTextEditingController( - text: updatingPlaylist?.description, + text: updatingPlaylist?.description?.unescapeHtml(), ); final public = useState( updatingPlaylist?.public ?? false, diff --git a/lib/modules/stats/common/playlist_item.dart b/lib/modules/stats/common/playlist_item.dart index 79e40d717..515c97b37 100644 --- a/lib/modules/stats/common/playlist_item.dart +++ b/lib/modules/stats/common/playlist_item.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/playbutton_card.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/extensions/string.dart'; import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -28,7 +28,7 @@ class StatsPlaylistItem extends StatelessWidget { ), title: Text(playlist.name!), subtitle: Text( - playlist.description!.replaceAll(htmlTagRegexp, ''), + playlist.description?.unescapeHtml() ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index f7c5a431e..31426f20e 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -12,19 +13,33 @@ import 'package:spotube/provider/spotify/spotify.dart'; class PlaylistPage extends HookConsumerWidget { static const name = "playlist"; - final PlaylistSimple playlist; + final PlaylistSimple _playlist; const PlaylistPage({ super.key, - required this.playlist, - }); + required PlaylistSimple playlist, + }) : _playlist = playlist; @override Widget build(BuildContext context, ref) { + final playlist = ref + .watch( + favoritePlaylistsProvider.select( + (value) => value.whenData( + (value) => + value.items.firstWhereOrNull((s) => s.id == _playlist.id), + ), + ), + ) + .asData + ?.value ?? + _playlist; + final tracks = ref.watch(playlistTracksProvider(playlist.id!)); final tracksNotifier = ref.watch(playlistTracksProvider(playlist.id!).notifier); final isFavoritePlaylist = ref.watch(isFavoritePlaylistProvider(playlist.id!)); + final favoritePlaylistsNotifier = ref.watch(favoritePlaylistsProvider.notifier); diff --git a/lib/provider/spotify/playlist/favorite.dart b/lib/provider/spotify/playlist/favorite.dart index a0e051aa6..000001ade 100644 --- a/lib/provider/spotify/playlist/favorite.dart +++ b/lib/provider/spotify/playlist/favorite.dart @@ -51,6 +51,20 @@ class FavoritePlaylistsNotifier ); } + void updatePlaylist(PlaylistSimple playlist) { + if (state.value == null) return; + + if (state.value!.items.none((e) => e.id == playlist.id)) return; + + state = AsyncData( + state.value!.copyWith( + items: state.value!.items + .map((element) => element.id == playlist.id ? playlist : element) + .toList(), + ), + ); + } + Future addFavorite(PlaylistSimple playlist) async { await update((state) async { await spotify.playlists.followPlaylist(playlist.id!); diff --git a/lib/provider/spotify/playlist/playlist.dart b/lib/provider/spotify/playlist/playlist.dart index fd420cd93..0eec3a87d 100644 --- a/lib/provider/spotify/playlist/playlist.dart +++ b/lib/provider/spotify/playlist/playlist.dart @@ -71,16 +71,32 @@ class PlaylistNotifier extends FamilyAsyncNotifier { state.id!, input.base64Image!, ); + + final playlist = await spotify.playlists.get(state.id!); + + ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist); + return playlist; } - return spotify.playlists.get(state.id!); - } catch (e) { + final playlist = Playlist.fromJson( + { + ...state.toJson(), + 'name': input.playlistName, + 'collaborative': input.collaborative, + 'description': input.description, + 'public': input.public, + }, + ); + + ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist); + + return playlist; + } catch (e, stack) { onError?.call(e); + AppLogger.reportError(e, stack); rethrow; } }); - - ref.invalidate(favoritePlaylistsProvider); } } From 7a31119e3ce755e9ed054f48fb508eec3a3bb703 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Jul 2024 22:34:16 +0600 Subject: [PATCH 58/92] fix(mini-player): macos titlebar over logo #1244 --- lib/pages/lyrics/mini_lyrics.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index d92220591..0da512bb5 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -81,12 +81,13 @@ class MiniLyricsPage extends HookConsumerWidget { firstChild: DragToMoveArea( child: Row( children: [ - const SizedBox(width: 10), - SizedBox( - height: 30, - width: 30, - child: Sidebar.brandLogo(), - ), + const Gap(10), + if (!kIsMacOS) + SizedBox( + height: 30, + width: 30, + child: Sidebar.brandLogo(), + ), const Spacer(), if (showLyrics.value) SizedBox( From 00f1b3422fa78316381eb954cf344659bda97313 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Jul 2024 22:37:54 +0600 Subject: [PATCH 59/92] fix: playlist share button does not work #1639 --- lib/pages/playlist/playlist.dart | 3 ++- lib/provider/audio_player/audio_player.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 31426f20e..e1b33e982 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -66,7 +66,8 @@ class PlaylistPage extends HookConsumerWidget { tracks: tracks.asData?.value.items ?? [], routePath: '/playlist/${playlist.id}', isLiked: isFavoritePlaylist.asData?.value ?? false, - shareUrl: playlist.externalUrls?.spotify ?? "", + shareUrl: playlist.externalUrls?.spotify ?? + "https://open.spotify.com/playlist/${playlist.id}", onHeart: isFavoritePlaylist.asData?.value == null ? null : () async { diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 5323f3c0c..437b666e4 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -287,7 +287,7 @@ class AudioPlayerNotifier extends Notifier { await ref.read(sourcedTrackProvider(intendedActiveTrack).future); } - if(medias.isEmpty) return; + if (medias.isEmpty) return; await removeCollections(state.collections); @@ -317,6 +317,7 @@ class AudioPlayerNotifier extends Notifier { Future stop() async { await audioPlayer.stop(); + await removeCollections(state.collections); ref.read(discordProvider.notifier).clear(); } } From abfdbc63cef8e7d5a7895f8e777dd64d9b73ffe5 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Jul 2024 23:08:21 +0600 Subject: [PATCH 60/92] fix: Too many artists for a track causing overflows #1470 --- .../dialogs/track_details_dialog.dart | 1 + lib/components/links/artist_link.dart | 85 ++++++++------ lib/components/track_tile/track_options.dart | 13 ++- lib/components/track_tile/track_tile.dart | 13 ++- lib/l10n/app_en.arb | 3 +- .../library/user_downloads/download_item.dart | 9 ++ lib/modules/player/player.dart | 10 ++ lib/modules/player/player_track_details.dart | 8 ++ lib/modules/stats/common/album_item.dart | 7 ++ lib/modules/stats/common/track_item.dart | 7 ++ lib/pages/connect/control/control.dart | 8 ++ lib/pages/track/track.dart | 7 +- untranslated_messages.json | 106 +++++++++++++++++- 13 files changed, 240 insertions(+), 37 deletions(-) diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart index 2495863c8..61bca7b1f 100644 --- a/lib/components/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -28,6 +28,7 @@ class TrackDetailsDialog extends HookWidget { artists: track.artists ?? [], mainAxisAlignment: WrapAlignment.start, textStyle: const TextStyle(color: Colors.blue), + hideOverflowArtist: false, ), context.l10n.album: LinkText( track.album!.name!, diff --git a/lib/components/links/artist_link.dart b/lib/components/links/artist_link.dart index 47ddecd84..d5ec24f87 100644 --- a/lib/components/links/artist_link.dart +++ b/lib/components/links/artist_link.dart @@ -1,6 +1,8 @@ +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/links/anchor_button.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -9,7 +11,9 @@ class ArtistLink extends StatelessWidget { final WrapCrossAlignment crossAxisAlignment; final WrapAlignment mainAxisAlignment; final TextStyle textStyle; + final bool hideOverflowArtist; final void Function(String route)? onRouteChange; + final VoidCallback? onOverflowArtistClick; const ArtistLink({ super.key, @@ -18,44 +22,61 @@ class ArtistLink extends StatelessWidget { this.mainAxisAlignment = WrapAlignment.center, this.textStyle = const TextStyle(), this.onRouteChange, - }); + this.hideOverflowArtist = true, + this.onOverflowArtistClick, + }) : assert(hideOverflowArtist ? onOverflowArtistClick != null : true); @override Widget build(BuildContext context) { + final ThemeData(:colorScheme) = Theme.of(context); + return Wrap( crossAxisAlignment: crossAxisAlignment, alignment: mainAxisAlignment, - children: artists - .asMap() - .entries - .map( - (artist) => Builder(builder: (context) { - if (artist.value.name == null) { - return Text("Spotify", style: textStyle); - } - return AnchorButton( - (artist.key != artists.length - 1) - ? "${artist.value.name}, " - : artist.value.name!, - onTap: () { - if (onRouteChange != null) { - onRouteChange?.call("/artist/${artist.value.id}"); - } else { - ServiceUtils.pushNamed( - context, - ArtistPage.name, - pathParameters: { - "id": artist.value.id!, - }, - ); - } - }, - overflow: TextOverflow.ellipsis, - style: textStyle, - ); - }), - ) - .toList(), + children: [ + ...(hideOverflowArtist ? artists.take(3).toList() : artists) + .asMap() + .entries + .map( + (artist) => Builder(builder: (context) { + if (artist.value.name == null) { + return Text("Spotify", style: textStyle); + } + return AnchorButton( + (artist.key != artists.length - 1) + ? "${artist.value.name}, " + : artist.value.name!, + onTap: () { + if (onRouteChange != null) { + onRouteChange?.call("/artist/${artist.value.id}"); + } else { + ServiceUtils.pushNamed( + context, + ArtistPage.name, + pathParameters: { + "id": artist.value.id!, + }, + ); + } + }, + overflow: TextOverflow.ellipsis, + style: textStyle, + ); + }), + ), + if (hideOverflowArtist && artists.length > 3) + AnchorButton( + context.l10n.and_n_more(artists.length - 3), + onTap: () { + onOverflowArtistClick?.call(); + }, + overflow: TextOverflow.ellipsis, + style: textStyle.copyWith( + color: colorScheme.secondary, + decoration: TextDecoration.underline, + ), + ), + ], ); } } diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index c6cfdd35a..a4c5661af 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -20,6 +20,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart'; @@ -27,6 +28,7 @@ import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -313,7 +315,16 @@ class TrackOptions extends HookConsumerWidget { ), subtitle: Align( alignment: Alignment.centerLeft, - child: ArtistLink(artists: track.artists!), + child: ArtistLink( + artists: track.artists!, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": track.id!, + }, + ), + ), ), ), ], diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 0e8d2cd0b..12ce063f2 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -17,9 +17,11 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; +import 'package:spotube/utils/service_utils.dart'; class TrackTile extends HookConsumerWidget { /// [index] will not be shown if null @@ -245,7 +247,16 @@ class TrackTile extends HookConsumerWidget { : ClipRect( child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 40), - child: ArtistLink(artists: track.artists ?? []), + child: ArtistLink( + artists: track.artists ?? [], + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": track.id!, + }, + ), + ), ), ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 04fc85666..ab6152256 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -325,5 +325,6 @@ "connect_client_alert": "You're being controlled by {client}", "this_device": "This Device", "remote": "Remote", - "stats": "Stats" + "stats": "Stats", + "and_n_more": "and {count} more" } \ No newline at end of file diff --git a/lib/modules/library/user_downloads/download_item.dart b/lib/modules/library/user_downloads/download_item.dart index bc9abf6ac..c4bd7bcef 100644 --- a/lib/modules/library/user_downloads/download_item.dart +++ b/lib/modules/library/user_downloads/download_item.dart @@ -7,9 +7,11 @@ import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/services/download_manager/download_status.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/utils/service_utils.dart'; class DownloadItem extends HookConsumerWidget { final Track track; @@ -62,6 +64,13 @@ class DownloadItem extends HookConsumerWidget { subtitle: ArtistLink( artists: track.artists ?? [], mainAxisAlignment: WrapAlignment.start, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": track.id!, + }, + ), ), trailing: isQueryingSourceInfo ? Text( diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index d75df7960..6db846929 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -24,11 +24,13 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -260,6 +262,14 @@ class PlayerView extends HookConsumerWidget { panelController.close(); GoRouter.of(context).push(route); }, + onOverflowArtistClick: () => + ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": currentTrack!.id!, + }, + ), ), ], ), diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index d722830e2..8d3b99fad 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -9,6 +9,7 @@ import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -81,6 +82,13 @@ class PlayerTrackDetails extends HookConsumerWidget { onRouteChange: (route) { ServiceUtils.push(context, route); }, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": track!.id!, + }, + ), ) ], ), diff --git a/lib/modules/stats/common/album_item.dart b/lib/modules/stats/common/album_item.dart index 58604c454..eec687176 100644 --- a/lib/modules/stats/common/album_item.dart +++ b/lib/modules/stats/common/album_item.dart @@ -35,6 +35,13 @@ class StatsAlbumItem extends StatelessWidget { child: ArtistLink( artists: album.artists ?? [], mainAxisAlignment: WrapAlignment.start, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + AlbumPage.name, + pathParameters: { + "id": album.id!, + }, + ), ), ), ], diff --git a/lib/modules/stats/common/track_item.dart b/lib/modules/stats/common/track_item.dart index 33991d432..44e813405 100644 --- a/lib/modules/stats/common/track_item.dart +++ b/lib/modules/stats/common/track_item.dart @@ -33,6 +33,13 @@ class StatsTrackItem extends StatelessWidget { subtitle: ArtistLink( artists: track.artists!, mainAxisAlignment: WrapAlignment.start, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": track.id!, + }, + ), ), trailing: info, onTap: () { diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index d27b7867e..cae0bd1b9 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -144,6 +144,14 @@ class ConnectControlPage extends HookConsumerWidget { artists: playlist.activeTrack?.artists ?? [], textStyle: textTheme.bodyMedium!, mainAxisAlignment: WrapAlignment.start, + onOverflowArtistClick: () => + ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": playlist.activeTrack!.id!, + }, + ), ), ), ], diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index dc4defc87..6f3af0e4b 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -148,7 +148,12 @@ class TrackPage extends HookConsumerWidget { children: [ const Icon(SpotubeIcons.artist), const Gap(5), - ArtistLink(artists: track.artists!), + Flexible( + child: ArtistLink( + artists: track.artists!, + hideOverflowArtist: false, + ), + ), ], ), const Gap(10), diff --git a/untranslated_messages.json b/untranslated_messages.json index 9e26dfeeb..5da4c3c6a 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1 +1,105 @@ -{} \ No newline at end of file +{ + "ar": [ + "and_n_more" + ], + + "bn": [ + "and_n_more" + ], + + "ca": [ + "and_n_more" + ], + + "cs": [ + "and_n_more" + ], + + "de": [ + "and_n_more" + ], + + "es": [ + "and_n_more" + ], + + "eu": [ + "and_n_more" + ], + + "fa": [ + "and_n_more" + ], + + "fi": [ + "and_n_more" + ], + + "fr": [ + "and_n_more" + ], + + "hi": [ + "and_n_more" + ], + + "id": [ + "and_n_more" + ], + + "it": [ + "and_n_more" + ], + + "ja": [ + "and_n_more" + ], + + "ka": [ + "and_n_more" + ], + + "ko": [ + "and_n_more" + ], + + "ne": [ + "and_n_more" + ], + + "nl": [ + "and_n_more" + ], + + "pl": [ + "and_n_more" + ], + + "pt": [ + "and_n_more" + ], + + "ru": [ + "and_n_more" + ], + + "th": [ + "and_n_more" + ], + + "tr": [ + "and_n_more" + ], + + "uk": [ + "and_n_more" + ], + + "vi": [ + "and_n_more" + ], + + "zh": [ + "and_n_more" + ] +} From d22bba5393f649068a937ae3d2016c6cd5fa89bd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 10 Jul 2024 00:11:40 +0600 Subject: [PATCH 61/92] fix: incorrect datatype used for MPRIS position property #1521 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 14f2a5b09..040e23ecf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct main" description: name: audio_service_mpris - sha256: a8d1583f9143d17b2facc994a99bd1ea257cec43adcb8d7349458555c62b570f + sha256: b16db3584a4b2464c0bfd575c1a21765723d257931222f8adfcb0511f940d352 url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.5" audio_service_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 58ca0ae96..06f968f30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: args: ^2.5.0 async: ^2.9.0 audio_service: ^0.18.13 - audio_service_mpris: ^0.1.3 + audio_service_mpris: ^0.1.5 audio_session: ^0.1.19 auto_size_text: ^3.0.0 buttons_tabbar: ^1.3.8 From a6e13ffc08691b264bd9fd2e60b53b8762e6f601 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 10 Jul 2024 00:23:22 +0600 Subject: [PATCH 62/92] fix(linux): OS Media control not working for Flatpak #1627 --- lib/services/audio_services/audio_services.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index 63e43c4dd..6545ab4a1 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -22,8 +22,9 @@ class AudioServices { final mobile = kIsMobile || kIsMacOS || kIsLinux ? await AudioService.init( builder: () => MobileAudioService(playback), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.krtirtho.Spotube', + config: AudioServiceConfig( + androidNotificationChannelId: + kIsLinux ? 'spotube' : 'com.krtirtho.Spotube', androidNotificationChannelName: 'Spotube', androidNotificationOngoing: true, ), From 6a500731d601c3ac7c70699fb3a374c8dff82303 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 14 Jul 2024 18:58:47 +0600 Subject: [PATCH 63/92] feat: discord rpc for macOS, windows-arm64 and linux-arm64 (#1713) * feat: add discord rpc support for macos, windows arm64 and linux arm64 * chore: discord rpc not clearing activity after close/setting rpc to false * chore: add migration script to move from files from macos sandbox to non-sandbox directories --- lib/main.dart | 13 ++- lib/pages/home/home.dart | 6 +- lib/pages/settings/sections/desktop.dart | 15 ++- .../audio_player/audio_player_streams.dart | 2 +- lib/provider/discord_provider.dart | 101 ++++++++++-------- lib/services/logger/logger.dart | 15 +++ lib/utils/migrations/sandbox.dart | 58 ++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 2 +- macos/Podfile.lock | 19 ++-- macos/Runner/DebugProfile.entitlements | 30 +++--- macos/Runner/Release.entitlements | 30 +++--- macos/Runner/RunnerDebug.entitlements | 30 +++--- pubspec.lock | 31 +++--- pubspec.yaml | 5 +- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 2 +- 17 files changed, 230 insertions(+), 136 deletions(-) create mode 100644 lib/utils/migrations/sandbox.dart diff --git a/lib/main.dart b/lib/main.dart index 7e8da0f27..a9a2e3b91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:dart_discord_rpc/dart_discord_rpc.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hive/hive.dart'; @@ -12,6 +12,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:spotube/collections/env.dart'; import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; @@ -37,6 +38,7 @@ import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/themes/theme.dart'; import 'package:spotube/utils/migrations/hive.dart'; +import 'package:spotube/utils/migrations/sandbox.dart'; import 'package:spotube/utils/platform.dart'; import 'package:system_theme/system_theme.dart'; import 'package:path_provider/path_provider.dart'; @@ -67,6 +69,8 @@ Future main(List rawArgs) async { MediaKit.ensureInitialized(); + await migrateMacOsFromSandboxToNoSandbox(); + // force High Refresh Rate on some Android devices (like One Plus) if (kIsAndroid) { await FlutterDisplayMode.setHighRefreshRate(); @@ -82,8 +86,8 @@ Future main(List rawArgs) async { MetadataGod.initialize(); } - if (kIsWindows || kIsLinux) { - DiscordRPC.initialize(); + if (kIsDesktop) { + await FlutterDiscordRPC.initialize(Env.discordAppId); } await KVStoreService.initialize(); @@ -108,6 +112,9 @@ Future main(List rawArgs) async { overrides: [ databaseProvider.overrideWith((ref) => database), ], + observers: const [ + AppLoggerProviderObserver(), + ], child: const Spotube(), ), ); diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 7afd59386..efdca4f79 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -4,6 +4,7 @@ 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/models/database/database.dart'; import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/modules/home/sections/featured.dart'; import 'package:spotube/modules/home/sections/feed.dart'; @@ -15,6 +16,7 @@ import 'package:spotube/modules/home/sections/recent.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/pages/settings/settings.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -26,6 +28,8 @@ class HomePage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final controller = useScrollController(); final mediaQuery = MediaQuery.of(context); + final layoutMode = + ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); return SafeArea( bottom: false, @@ -34,7 +38,7 @@ class HomePage extends HookConsumerWidget { body: CustomScrollView( controller: controller, slivers: [ - if (mediaQuery.smAndDown) + if (mediaQuery.smAndDown || layoutMode == LayoutMode.compact) SliverAppBar( floating: true, title: Assets.spotubeLogoPng.image(height: 45), diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 88f0ae6d4..c61f01500 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -8,8 +8,6 @@ import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/utils/platform.dart'; - class SettingsDesktopSection extends HookConsumerWidget { const SettingsDesktopSection({super.key}); @@ -54,13 +52,12 @@ class SettingsDesktopSection extends HookConsumerWidget { value: preferences.systemTitleBar, onChanged: preferencesNotifier.setSystemTitleBar, ), - if (!kIsMacOS) - SwitchListTile( - secondary: const Icon(SpotubeIcons.discord), - title: Text(context.l10n.discord_rich_presence), - value: preferences.discordPresence, - onChanged: preferencesNotifier.setDiscordPresence, - ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.discord), + title: Text(context.l10n.discord_rich_presence), + value: preferences.discordPresence, + onChanged: preferencesNotifier.setDiscordPresence, + ), ], ); } diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 368fc6d96..845f12ea8 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -43,7 +43,7 @@ class AudioPlayerStreamListeners { ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); UserPreferences get preferences => ref.read(userPreferencesProvider); - Discord get discord => ref.read(discordProvider); + DiscordNotifier get discord => ref.read(discordProvider.notifier); AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider); PlaybackHistoryActions get history => ref.read(playbackHistoryActionsProvider); diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index 29c537625..1e819af09 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -1,67 +1,76 @@ -import 'package:dart_discord_rpc/dart_discord_rpc.dart'; -import 'package:flutter/foundation.dart'; +import 'dart:async'; + +import 'package:flutter_discord_rpc/flutter_discord_rpc.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/audio_player/audio_player.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; +class DiscordNotifier extends AsyncNotifier { + @override + FutureOr build() async { + final enabled = ref.watch( + userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop)); + final playback = ref.read(audioPlayerProvider); + + final subscription = + FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { + if (connected && playback.activeTrack != null) { + await updatePresence(playback.activeTrack!); + } + }); - Discord(this.isEnabled) - : discordRPC = (kIsWindows || kIsLinux) && isEnabled - ? DiscordRPC(applicationId: Env.discordAppId) - : null { - discordRPC?.start(autoRegister: true); + ref.onDispose(() async { + subscription.cancel(); + await close(); + await FlutterDiscordRPC.instance.dispose(); + }); + + if (!enabled && FlutterDiscordRPC.instance.isConnected) { + await clear(); + await close(); + } else { + await FlutterDiscordRPC.instance.connect(autoRetry: true); + } } - void updatePresence(Track track) { - clear(); + Future updatePresence(Track track) async { + await clear(); final artistNames = track.artists?.asString() ?? ""; - discordRPC?.updatePresence( - DiscordPresence( - details: "Song: ${track.name} by $artistNames", + await FlutterDiscordRPC.instance.setActivity( + activity: RPCActivity( + details: "${track.name} by $artistNames", state: "Vibing in Music", - startTimeStamp: DateTime.now().millisecondsSinceEpoch, - largeImageKey: "spotube-logo-foreground", - largeImageText: "Spotube", - smallImageKey: "spotube-logo-foreground", - smallImageText: "Spotube", + assets: const RPCAssets( + largeImage: "spotube-logo-foreground", + largeText: "Spotube", + smallImage: "spotube-logo-foreground", + smallText: "Spotube", + ), + buttons: [ + RPCButton( + label: "Listen on Spotify", + url: track.externalUrls?.spotify ?? + "https://open.spotify.com/tracks/${track.id}", + ), + ], + timestamps: RPCTimestamps( + start: DateTime.now().millisecondsSinceEpoch, + ), ), ); } - void clear() { - discordRPC?.clearPresence(); + Future clear() async { + await FlutterDiscordRPC.instance.clearActivity(); } - void shutdown() { - discordRPC?.shutDown(); - } - - @override - void dispose() { - clear(); - shutdown(); - super.dispose(); + Future close() async { + await FlutterDiscordRPC.instance.disconnect(); } } -final discordProvider = ChangeNotifierProvider( - (ref) { - final isEnabled = - ref.watch(userPreferencesProvider.select((s) => s.discordPresence)); - final playback = ref.read(audioPlayerProvider); - final discord = Discord(isEnabled); - - if (playback.activeTrack != null) { - discord.updatePresence(playback.activeTrack!); - } - - return discord; - }, -); +final discordProvider = + AsyncNotifierProvider(() => DiscordNotifier()); diff --git a/lib/services/logger/logger.dart b/lib/services/logger/logger.dart index 6ba76ea19..38252e87e 100644 --- a/lib/services/logger/logger.dart +++ b/lib/services/logger/logger.dart @@ -4,6 +4,7 @@ import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -88,3 +89,17 @@ class AppLogger { } } } + +class AppLoggerProviderObserver extends ProviderObserver { + const AppLoggerProviderObserver(); + + @override + void providerDidFail( + ProviderBase provider, + Object error, + StackTrace stackTrace, + ProviderContainer container, + ) { + AppLogger.reportError(error, stackTrace); + } +} diff --git a/lib/utils/migrations/sandbox.dart b/lib/utils/migrations/sandbox.dart new file mode 100644 index 000000000..1ed5090ac --- /dev/null +++ b/lib/utils/migrations/sandbox.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/utils/platform.dart'; + +/// Migrates sandbox files on macOS to non-sandbox directories +Future migrateMacOsFromSandboxToNoSandbox() async { + if (!kIsMacOS) return; + + try { + final sandboxApplicationSupportDir = Directory( + "/Users/${Platform.environment["USER"]}/Library/Containers/oss.krtirtho.spotube/Data/Library/Application Support/oss.krtirtho.spotube", + ); + + if (!await sandboxApplicationSupportDir.exists()) { + stdout.writeln("🔵 Sandbox directory not found, skipping migration"); + return; + } + + const fileExts = [".db", ".lock", ".hive"]; + + final supportDir = await getApplicationSupportDirectory() + ..create(recursive: true); + + final supportFiles = await supportDir.list().toList(); + final oldSupportFiles = await sandboxApplicationSupportDir.list().toList(); + + if (oldSupportFiles.isEmpty) { + stdout.writeln( + "🔵 No files found in sandboxed directory, skipping migration", + ); + return; + } else if (supportFiles.any( + (file) => file is File && fileExts.contains(extension(file.path)))) { + stdout.writeln( + "🔵 Non-sandbox directory is not empty, skipping migration", + ); + return; + } + + for (final oldSupportFile in oldSupportFiles) { + if (oldSupportFile is File && + fileExts.contains(extension(oldSupportFile.path))) { + final newPath = join(supportDir.path, basename(oldSupportFile.path)); + await oldSupportFile.copy(newPath); + } + } + + stdout.writeln("✅ Migrated sandboxed files to non-sandboxed directory"); + } catch (e, stack) { + stdout.writeln( + "❌ Error migrating sandboxed files to non-sandboxed directory", + ); + AppLogger.reportError(e, stack); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2218d1108..5550ed227 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,7 +6,6 @@ #include "generated_plugin_registrant.h" -#include #include #include #include @@ -22,9 +21,6 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) dart_discord_rpc_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DartDiscordRpcPlugin"); - dart_discord_rpc_plugin_register_with_registrar(dart_discord_rpc_registrar); g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bb0776b51..93bf6bc0e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - dart_discord_rpc desktop_webview_window file_selector_linux flutter_secure_storage_linux @@ -20,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_discord_rpc media_kit_native_event_loop metadata_god ) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 9ab2ee38b..b3bed710b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -14,6 +14,7 @@ PODS: - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS + - flutter_discord_rpc (0.0.1) - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - OrderedSet (~> 5.0) @@ -41,14 +42,14 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS - - sqlite3 (3.46.0): - - sqlite3/common (= 3.46.0) - - sqlite3/common (3.46.0) - - sqlite3/fts5 (3.46.0): + - "sqlite3 (3.46.0+1)": + - "sqlite3/common (= 3.46.0+1)" + - "sqlite3/common (3.46.0+1)" + - "sqlite3/fts5 (3.46.0+1)": - sqlite3/common - - sqlite3/perf-threadsafe (3.46.0): + - "sqlite3/perf-threadsafe (3.46.0+1)": - sqlite3/common - - sqlite3/rtree (3.46.0): + - "sqlite3/rtree (3.46.0+1)": - sqlite3/common - sqlite3_flutter_libs (0.0.1): - FlutterMacOS @@ -75,6 +76,7 @@ DEPENDENCIES: - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - flutter_discord_rpc (from `Flutter/ephemeral/.symlinks/plugins/flutter_discord_rpc/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) @@ -114,6 +116,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + flutter_discord_rpc: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_discord_rpc/macos flutter_inappwebview_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_secure_storage_macos: @@ -159,6 +163,7 @@ SPEC CHECKSUMS: desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + flutter_discord_rpc: 53b006f68ef620a99fe1b3ba7e83513f3ae95b4c flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 @@ -172,7 +177,7 @@ SPEC CHECKSUMS: screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d + sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index f05277de8..6e73fa3ca 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -1,18 +1,18 @@ - - com.apple.security.app-sandbox - - com.apple.security.assets.music.read-write - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - - + + com.apple.security.app-sandbox + + com.apple.security.assets.music.read-write + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + + \ No newline at end of file diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index f05277de8..6e73fa3ca 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -1,18 +1,18 @@ - - com.apple.security.app-sandbox - - com.apple.security.assets.music.read-write - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - - + + com.apple.security.app-sandbox + + com.apple.security.assets.music.read-write + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + + \ No newline at end of file diff --git a/macos/Runner/RunnerDebug.entitlements b/macos/Runner/RunnerDebug.entitlements index f05277de8..6e73fa3ca 100644 --- a/macos/Runner/RunnerDebug.entitlements +++ b/macos/Runner/RunnerDebug.entitlements @@ -1,18 +1,18 @@ - - com.apple.security.app-sandbox - - com.apple.security.assets.music.read-write - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - - + + com.apple.security.app-sandbox + + com.apple.security.assets.music.read-write + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + + \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 040e23ecf..28b682a88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -221,10 +221,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -433,15 +433,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - dart_discord_rpc: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "4d05017838ebeadcdb832e1893fabad1506fddba" - url: "https://github.com/Tommypop2/dart_discord_rpc.git" - source: git - version: "0.0.3" dart_mappable: dependency: transitive description: @@ -721,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + flutter_discord_rpc: + dependency: "direct main" + description: + name: flutter_discord_rpc + sha256: "290c0d91c8ef24c3acb84cb6fcc5c5fed652fe4871b59896c8d180d5eae5d647" + url: "https://pub.dev" + source: hosted + version: "0.1.0+1" flutter_displaymode: dependency: "direct main" description: @@ -1791,6 +1790,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + retry: + dependency: "direct main" + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" riverpod: dependency: "direct main" description: @@ -2488,5 +2495,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index 06f968f30..58c8c55f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,9 +100,7 @@ dependencies: very_good_infinite_list: ^0.7.1 gap: ^3.0.1 sliver_tools: ^0.2.12 - dart_discord_rpc: - git: - url: https://github.com/Tommypop2/dart_discord_rpc.git + flutter_discord_rpc: ^0.1.0+1 html_unescape: ^2.0.0 wikipedia_api: ^0.1.0 skeletonizer: ^1.1.1 @@ -130,6 +128,7 @@ dependencies: sqlite3_flutter_libs: ^0.5.23 sqlite3: ^2.4.3 encrypt: ^5.0.3 + retry: ^3.1.2 dev_dependencies: build_runner: ^2.4.9 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4fcf30199..217a7cb4d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -28,8 +27,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); BonsoirWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi")); - DartDiscordRpcPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); DesktopWebviewWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d0dd6751a..cbbd2acc2 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links bonsoir_windows - dart_discord_rpc desktop_webview_window file_selector_windows flutter_secure_storage_windows @@ -22,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_discord_rpc media_kit_native_event_loop metadata_god smtc_windows From e6fee03c2066a2e1b8f6f5b97c688ac40439a003 Mon Sep 17 00:00:00 2001 From: arenekosreal <17194552+arenekosreal@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:38:28 +0800 Subject: [PATCH 64/92] feat(linux): Use XDG_STATE_HOME to storage logs (#1675) * feat(linux): Use XDG_STATE_HOME to storage logs * fix: Clean LSP suggestions. * fix: Use Platform.environment instead String.fromEnvironment The latter seems return an empty string. See: https://github.com/flutter/flutter/issues/55870#issuecomment-936612420 --- lib/services/logger/logger.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/services/logger/logger.dart b/lib/services/logger/logger.dart index 38252e87e..1df7b5aa0 100644 --- a/lib/services/logger/logger.dart +++ b/lib/services/logger/logger.dart @@ -65,6 +65,11 @@ class AppLogger { if (kIsMacOS) { dir = join((await getLibraryDirectory()).path, "Logs"); } + + if (kIsLinux) { + dir = join(_getXdgStateHome(), "spotube"); + } + final file = File(join(dir, ".spotube_logs")); if (!await file.exists()) { await file.create(recursive: true); @@ -88,6 +93,20 @@ class AppLogger { ); } } + + static String _getXdgStateHome() { + // path_provider seems does not support XDG_STATE_HOME, + // which is the specification to store application logs on Linux. + // See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + // TODO: Use path_provider once it supports XDG_STATE_HOME + if (const bool.hasEnvironment("XDG_STATE_HOME")) { + String xdgStateHomeRaw = Platform.environment["XDG_STATE_HOME"] ?? ""; + if (xdgStateHomeRaw.isNotEmpty) { + return xdgStateHomeRaw; + } + } + return join(Platform.environment["HOME"] ?? "", ".local", "state"); + } } class AppLoggerProviderObserver extends ProviderObserver { From a2ba46ea45471e42ce28ed544f825ab3f0cf4b0a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 14 Jul 2024 21:24:35 +0600 Subject: [PATCH 65/92] fix(android): app getting killed from background --- android/app/build.gradle | 2 +- lib/provider/history/recent.dart | 12 ++++---- .../audio_services/audio_services.dart | 28 +++++++++++++++---- pubspec.lock | 28 +++++++++---------- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7bcd9b6ad..e175f356b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,7 +31,7 @@ if (keystorePropertiesFile.exists()) { android { compileSdkVersion 34 - ndkVersion "21.4.7075529" + ndkVersion "25.1.8937393" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/lib/provider/history/recent.dart b/lib/provider/history/recent.dart index 8894b7138..ef393a17a 100644 --- a/lib/provider/history/recent.dart +++ b/lib/provider/history/recent.dart @@ -9,13 +9,15 @@ class RecentlyPlayedItemNotifier extends AsyncNotifier> { build() async { final database = ref.watch(databaseProvider); - final uniqueItemIds = await (database.selectOnly(database.historyTable, - distinct: true) + final uniqueItemIds = await (database.selectOnly( + database.historyTable, + distinct: true, + ) ..addColumns([database.historyTable.itemId, database.historyTable.id]) ..where( - database.historyTable.type.isIn([ - HistoryEntryType.playlist.name, - HistoryEntryType.album.name, + database.historyTable.type.isInValues([ + HistoryEntryType.playlist, + HistoryEntryType.album, ]), ) ..limit(10) diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index 6545ab4a1..dbddba8b3 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -1,4 +1,5 @@ import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; @@ -9,11 +10,13 @@ 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 { +class AudioServices with WidgetsBindingObserver { final MobileAudioService? mobile; final WindowsAudioService? smtc; - AudioServices(this.mobile, this.smtc); + AudioServices(this.mobile, this.smtc) { + WidgetsBinding.instance.addObserver(this); + } static Future create( Ref ref, @@ -27,15 +30,15 @@ class AudioServices { kIsLinux ? 'spotube' : 'com.krtirtho.Spotube', androidNotificationChannelName: 'Spotube', androidNotificationOngoing: true, + androidNotificationIcon: "drawable/ic_launcher_monochrome", + androidStopForegroundOnPause: false, + androidNotificationChannelDescription: "Spotube Media Controls", ), ) : null; final smtc = kIsWindows ? WindowsAudioService(ref, playback) : null; - return AudioServices( - mobile, - smtc, - ); + return AudioServices(mobile, smtc); } Future addTrack(Track track) async { @@ -65,7 +68,20 @@ class AudioServices { mobile?.session?.setActive(false); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.detached: + deactivateSession(); + mobile?.stop(); + break; + default: + break; + } + } + void dispose() { smtc?.dispose(); + WidgetsBinding.instance.removeObserver(this); } } diff --git a/pubspec.lock b/pubspec.lock index 28b682a88..716f5d228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1266,10 +1266,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: @@ -1322,26 +1322,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: @@ -1474,10 +1474,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: @@ -2186,10 +2186,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: @@ -2394,10 +2394,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 1441736627bb826fb2d0e130f909037d1588e8a3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 26 Jul 2024 15:31:58 +0600 Subject: [PATCH 66/92] fix(windows): window stretching #1553 --- .../use_fix_window_stretching.dart | 21 +++++++++++++++++++ lib/main.dart | 2 ++ windows/runner/main.cpp | 5 ++--- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 lib/hooks/configurators/use_fix_window_stretching.dart diff --git a/lib/hooks/configurators/use_fix_window_stretching.dart b/lib/hooks/configurators/use_fix_window_stretching.dart new file mode 100644 index 000000000..a6603d59e --- /dev/null +++ b/lib/hooks/configurators/use_fix_window_stretching.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; + +void useFixWindowStretching() { + useEffect(() { + if (!kIsWindows) return; + WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) async { + await Future.delayed(const Duration(milliseconds: 100), () { + windowManager.getSize().then((Size value) { + windowManager.setSize( + Size(value.width + 1, value.height + 1), + ); + }); + }); + }); + + return null; + }, []); +} diff --git a/lib/main.dart b/lib/main.dart index 45f4462d7..f4292bd65 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:spotube/collections/intents.dart'; 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_fix_window_stretching.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player_streams.dart'; @@ -134,6 +135,7 @@ class Spotube extends HookConsumerWidget { ref.listen(serverProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); + useFixWindowStretching(); useDisableBatteryOptimizations(); useDeepLinking(ref); useCloseBehavior(ref); diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index b938ff495..d86a2421c 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -19,14 +19,13 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, flutter::DartProject project(L"data"); - std::vector command_line_arguments = - GetCommandLineArguments(); + std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); + Win32Window::Size size(1200, 800); if (!window.CreateAndShow(L"spotube", origin, size)) { return EXIT_FAILURE; } From 9b05b8adf1641e45e838d01fcbc2c74659d225a0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 26 Jul 2024 15:32:49 +0600 Subject: [PATCH 67/92] chore: migrate to flutter 3.22.3 --- .fvm/fvm_config.json | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index df8efa0eb..160b5b293 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.22.1", + "flutterSdkVersion": "3.22.3", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index e99aebab9..a67e9c135 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.6 + FLUTTER_VERSION: 3.22.3 permissions: contents: write From bd511584e712d03b2dd862d9657ef95f1357bf55 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 22 Jul 2024 09:46:04 +0600 Subject: [PATCH 68/92] fix: local track metadata timeout --- lib/provider/local_tracks/local_tracks_provider.dart | 8 +++++--- lib/services/audio_services/audio_services.dart | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart index 6d2da59c7..c739722bd 100644 --- a/lib/provider/local_tracks/local_tracks_provider.dart +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:spotube/services/logger/logger.dart'; @@ -49,7 +50,7 @@ final localTracksProvider = userPreferencesProvider.select((s) => s.localLibraryLocation), ); - for (var location in [downloadLocation, ...localLibraryLocations]) { + for (final location in [downloadLocation, ...localLibraryLocations]) { if (location.isEmpty) continue; final entities = []; if (await Directory(location).exists()) { @@ -67,7 +68,8 @@ final localTracksProvider = }).map( (file) async { try { - final metadata = await MetadataGod.readMetadata(file: file.path); + final metadata = await MetadataGod.readMetadata(file: file.path) + .timeout(const Duration(seconds: 10)); final imageFile = File(join( (await getTemporaryDirectory()).path, @@ -89,7 +91,7 @@ final localTracksProvider = "art": imageFile.path }; } catch (e, stack) { - if (e is FfiException) { + if (e case FfiException() || TimeoutException()) { return {"file": file}; } AppLogger.reportError(e, stack); diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index dbddba8b3..d1820a007 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -29,7 +29,7 @@ class AudioServices with WidgetsBindingObserver { androidNotificationChannelId: kIsLinux ? 'spotube' : 'com.krtirtho.Spotube', androidNotificationChannelName: 'Spotube', - androidNotificationOngoing: true, + androidNotificationOngoing: false, androidNotificationIcon: "drawable/ic_launcher_monochrome", androidStopForegroundOnPause: false, androidNotificationChannelDescription: "Spotube Media Controls", From b211813213b11c2c95df454b383886994636fbc8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 22 Jul 2024 09:54:59 +0600 Subject: [PATCH 69/92] fix: go to track album shows up for local tracks --- lib/components/track_tile/track_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index a4c5661af..84b0f41f6 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -335,7 +335,7 @@ class TrackOptions extends HookConsumerWidget { leading: const Icon(SpotubeIcons.trash), title: Text(context.l10n.delete), ), - if (mediaQuery.smAndDown) + if (mediaQuery.smAndDown && !isLocalTrack) PopSheetEntry( value: TrackOptionValue.album, leading: const Icon(SpotubeIcons.album), From 0eb78d14ca4da02d29e180d13569b950462ab088 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 1 Aug 2024 14:15:40 +0600 Subject: [PATCH 70/92] chore: use frb based plugins from git --- .github/workflows/spotube-release-binary.yml | 4 + lib/main.dart | 5 + lib/provider/download_manager_provider.dart | 2 +- .../local_tracks/local_tracks_provider.dart | 96 +++--- .../audio_services/smtc_windows_web.dart | 276 ------------------ .../audio_services/windows_audio_service.dart | 10 +- macos/Podfile.lock | 10 +- pubspec.lock | 92 ++---- pubspec.yaml | 23 +- 9 files changed, 118 insertions(+), 400 deletions(-) delete mode 100644 lib/services/audio_services/smtc_windows_web.dart diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index a67e9c135..8fcf228ab 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -82,6 +82,10 @@ jobs: - name: Set up Docker Buildx if: ${{matrix.platform == 'linux_arm'}} uses: docker/setup-buildx-action@v3 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable - name: Install ${{matrix.platform}} dependencies run: | diff --git a/lib/main.dart b/lib/main.dart index d1f19d7a6..64710f47c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,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:smtc_windows/smtc_windows.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/routes.dart'; @@ -91,6 +92,10 @@ Future main(List rawArgs) async { await FlutterDiscordRPC.initialize(Env.discordAppId); } + if(kIsWindows){ + await SMTCWindows.initialize(); + } + await KVStoreService.initialize(); await EncryptedKvStoreService.initialize(); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 0e80d729b..ec6ffc184 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -70,7 +70,7 @@ class DownloadManagerProvider extends ChangeNotifier { trackNumber: track.trackNumber, discNumber: track.discNumber, durationMs: track.durationMs?.toDouble() ?? 0.0, - fileSize: await file.length(), + fileSize: BigInt.from(await file.length()), trackTotal: track.album?.tracks?.length ?? 0, picture: imageBytes != null ? Picture( diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart index c739722bd..ca22d8419 100644 --- a/lib/provider/local_tracks/local_tracks_provider.dart +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -14,7 +14,7 @@ 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; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FrbException; const supportedAudioTypes = [ "audio/webm", @@ -37,7 +37,7 @@ final localTracksProvider = FutureProvider>>((ref) async { try { if (kIsWeb) return {}; - final Map> tracks = {}; + final Map> libraryToTracks = {}; final downloadLocation = ref.watch( userPreferencesProvider.select((s) => s.downloadLocation), @@ -52,59 +52,61 @@ final localTracksProvider = for (final location in [downloadLocation, ...localLibraryLocations]) { if (location.isEmpty) continue; - final entities = []; + final entities = []; if (await Directory(location).exists()) { try { - entities.addAll(Directory(location).listSync(recursive: true)); + final dirEntities = + await Directory(location).list(recursive: true).toList(); + + entities.addAll( + dirEntities + .where( + (e) => + e is File && + supportedAudioTypes.contains(lookupMimeType(e.path)), + ) + .cast(), + ); } catch (e, stack) { AppLogger.reportError(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) - .timeout(const Duration(seconds: 10)); + final List> filesWithMetadata = []; - 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 (final file in entities) { + try { + final metadata = await MetadataGod.readMetadata(file: file.path); - return { - "metadata": metadata, - "file": file, - "art": imageFile.path - }; - } catch (e, stack) { - if (e case FfiException() || TimeoutException()) { - return {"file": file}; - } - AppLogger.reportError(e, stack); - return {}; - } - }, - ), - )) - .where((e) => e.isNotEmpty) - .toList(); + await Future.delayed(const Duration(milliseconds: 50)); + + 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, + ); + } + + filesWithMetadata.add( + {"metadata": metadata, "file": file, "art": imageFile.path}, + ); + } catch (e, stack) { + if (e case FrbException() || TimeoutException()) { + filesWithMetadata.add({"file": file}); + } + AppLogger.reportError(e, stack); + continue; + } + } - // ignore: no_leading_underscores_for_local_identifiers - final _tracks = filesWithMetadata + final tracksFromMetadata = filesWithMetadata .map( (fileWithMetadata) => LocalTrack.fromTrack( track: Track().fromFile( @@ -117,9 +119,9 @@ final localTracksProvider = ) .toList(); - tracks[location] = _tracks; + libraryToTracks[location] = tracksFromMetadata; } - return tracks; + return libraryToTracks; } catch (e, stack) { AppLogger.reportError(e, stack); return {}; diff --git a/lib/services/audio_services/smtc_windows_web.dart b/lib/services/audio_services/smtc_windows_web.dart deleted file mode 100644 index 055d43be1..000000000 --- a/lib/services/audio_services/smtc_windows_web.dart +++ /dev/null @@ -1,276 +0,0 @@ -// ignore_for_file: constant_identifier_names - -class MusicMetadata { - final String? title; - final String? artist; - final String? album; - final String? albumArtist; - final String? thumbnail; - - const MusicMetadata({ - this.title, - this.artist, - this.album, - this.albumArtist, - this.thumbnail, - }); -} - -enum PlaybackStatus { - Closed, - Changing, - Stopped, - Playing, - Paused, -} - -enum PressedButton { - play, - pause, - next, - previous, - fastForward, - rewind, - stop, - record, - channelUp, - channelDown; - - static PressedButton fromString(String button) { - switch (button) { - case 'play': - return PressedButton.play; - case 'pause': - return PressedButton.pause; - case 'next': - return PressedButton.next; - case 'previous': - return PressedButton.previous; - case 'fast_forward': - return PressedButton.fastForward; - case 'rewind': - return PressedButton.rewind; - case 'stop': - return PressedButton.stop; - case 'record': - return PressedButton.record; - case 'channel_up': - return PressedButton.channelUp; - case 'channel_down': - return PressedButton.channelDown; - default: - throw Exception('Unknown button: $button'); - } - } -} - -class SMTCConfig { - final bool playEnabled; - final bool pauseEnabled; - final bool stopEnabled; - final bool nextEnabled; - final bool prevEnabled; - final bool fastForwardEnabled; - final bool rewindEnabled; - - const SMTCConfig({ - required this.playEnabled, - required this.pauseEnabled, - required this.stopEnabled, - required this.nextEnabled, - required this.prevEnabled, - required this.fastForwardEnabled, - required this.rewindEnabled, - }); -} - -enum RepeatMode { - none, - track, - list; - - static RepeatMode fromString(String value) { - switch (value) { - case 'none': - return none; - case 'track': - return track; - case 'list': - return list; - default: - throw Exception('Unknown repeat mode: $value'); - } - } - - String get asString => toString().split('.').last; -} - -class PlaybackTimeline { - final int startTimeMs; - final int endTimeMs; - final int positionMs; - final int? minSeekTimeMs; - final int? maxSeekTimeMs; - - const PlaybackTimeline({ - required this.startTimeMs, - required this.endTimeMs, - required this.positionMs, - this.minSeekTimeMs, - this.maxSeekTimeMs, - }); -} - -class SMTCWindows { - SMTCWindows({ - SMTCConfig? config, - PlaybackTimeline? timeline, - MusicMetadata? metadata, - PlaybackStatus? status, - bool? shuffleEnabled, - RepeatMode? repeatMode, - bool? enabled, - }); - - SMTCConfig get config => throw UnimplementedError(); - PlaybackTimeline get timeline => throw UnimplementedError(); - MusicMetadata get metadata => throw UnimplementedError(); - PlaybackStatus? get status => throw UnimplementedError(); - Stream get buttonPressStream => throw UnimplementedError(); - Stream get shuffleChangeStream => throw UnimplementedError(); - Stream get repeatModeChangeStream => throw UnimplementedError(); - - bool get isPlayEnabled => config.playEnabled; - bool get isPauseEnabled => config.pauseEnabled; - bool get isStopEnabled => config.stopEnabled; - bool get isNextEnabled => config.nextEnabled; - bool get isPrevEnabled => config.prevEnabled; - bool get isFastForwardEnabled => config.fastForwardEnabled; - bool get isRewindEnabled => config.rewindEnabled; - - bool get isShuffleEnabled => throw UnimplementedError(); - RepeatMode get repeatMode => throw UnimplementedError(); - bool get enabled => throw UnimplementedError(); - - Duration? get startTime => Duration(milliseconds: timeline.startTimeMs); - Duration? get endTime => Duration(milliseconds: timeline.endTimeMs); - Duration? get position => Duration(milliseconds: timeline.positionMs); - Duration? get minSeekTime => timeline.minSeekTimeMs == null - ? null - : Duration(milliseconds: timeline.minSeekTimeMs!); - Duration? get maxSeekTime => timeline.maxSeekTimeMs == null - ? null - : Duration(milliseconds: timeline.maxSeekTimeMs!); - - Future updateConfig(SMTCConfig config) { - throw UnimplementedError(); - } - - Future updateTimeline(PlaybackTimeline timeline) { - throw UnimplementedError(); - } - - Future updateMetadata(MusicMetadata metadata) { - throw UnimplementedError(); - } - - Future clearMetadata() { - throw UnimplementedError(); - } - - Future dispose() async { - throw UnimplementedError(); - } - - Future disableSmtc() { - throw UnimplementedError(); - } - - Future enableSmtc() { - throw UnimplementedError(); - } - - Future setPlaybackStatus(PlaybackStatus status) async { - throw UnimplementedError(); - } - - Future setIsPlayEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setIsPauseEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setIsStopEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setIsNextEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setIsPrevEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setIsFastForwardEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setIsRewindEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setTimeline(PlaybackTimeline timeline) { - return updateTimeline(timeline); - } - - Future setTitle(String title) { - throw UnimplementedError(); - } - - Future setArtist(String artist) { - throw UnimplementedError(); - } - - Future setAlbum(String album) { - throw UnimplementedError(); - } - - Future setAlbumArtist(String albumArtist) { - throw UnimplementedError(); - } - - Future setThumbnail(String thumbnail) { - throw UnimplementedError(); - } - - Future setPosition(Duration position) { - throw UnimplementedError(); - } - - Future setStartTime(Duration startTime) { - throw UnimplementedError(); - } - - Future setEndTime(Duration endTime) { - throw UnimplementedError(); - } - - Future setMaxSeekTime(Duration maxSeekTime) { - throw UnimplementedError(); - } - - Future setMinSeekTime(Duration minSeekTime) { - throw UnimplementedError(); - } - - Future setShuffleEnabled(bool enabled) { - throw UnimplementedError(); - } - - Future setRepeatMode(RepeatMode repeatMode) { - throw UnimplementedError(); - } -} diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index 0b3113fc3..8edc50696 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -18,7 +18,7 @@ class WindowsAudioService { WindowsAudioService(this.ref, this.audioPlayerNotifier) : smtc = SMTCWindows(enabled: false) { - smtc.setPlaybackStatus(PlaybackStatus.Stopped); + smtc.setPlaybackStatus(PlaybackStatus.stopped); final buttonStream = smtc.buttonPressStream.listen((event) { switch (event) { case PressedButton.play: @@ -45,16 +45,16 @@ class WindowsAudioService { audioPlayer.playerStateStream.listen((state) async { switch (state) { case AudioPlaybackState.playing: - await smtc.setPlaybackStatus(PlaybackStatus.Playing); + await smtc.setPlaybackStatus(PlaybackStatus.playing); break; case AudioPlaybackState.paused: - await smtc.setPlaybackStatus(PlaybackStatus.Paused); + await smtc.setPlaybackStatus(PlaybackStatus.paused); break; case AudioPlaybackState.stopped: - await smtc.setPlaybackStatus(PlaybackStatus.Stopped); + await smtc.setPlaybackStatus(PlaybackStatus.stopped); break; case AudioPlaybackState.completed: - await smtc.setPlaybackStatus(PlaybackStatus.Changing); + await smtc.setPlaybackStatus(PlaybackStatus.changing); break; default: break; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index b3bed710b..9b122cbeb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -14,7 +14,8 @@ PODS: - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS - - flutter_discord_rpc (0.0.1) + - flutter_discord_rpc (0.0.1): + - FlutterMacOS - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - OrderedSet (~> 5.0) @@ -27,7 +28,8 @@ PODS: - FlutterMacOS - media_kit_native_event_loop (1.0.0): - FlutterMacOS - - metadata_god (0.0.1) + - metadata_god (0.0.1): + - FlutterMacOS - OrderedSet (5.0.0) - package_info_plus (0.0.1): - FlutterMacOS @@ -163,14 +165,14 @@ SPEC CHECKSUMS: desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 - flutter_discord_rpc: 53b006f68ef620a99fe1b3ba7e83513f3ae95b4c + flutter_discord_rpc: 67a7c10ea24d9d3bf35d01af643f48fbcfa7c24f flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff media_kit_libs_macos_audio: 3871782a4f3f84c77f04d7666c87800a781c24da media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5 - metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 + metadata_god: 829f61208b44ac1173e7cd32ab740d8776be5435 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c diff --git a/pubspec.lock b/pubspec.lock index 716f5d228..b21c0091f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -715,10 +715,9 @@ packages: flutter_discord_rpc: dependency: "direct main" description: - name: flutter_discord_rpc - sha256: "290c0d91c8ef24c3acb84cb6fcc5c5fed652fe4871b59896c8d180d5eae5d647" - url: "https://pub.dev" - source: hosted + path: "../frb_plugins/packages/flutter_discord_rpc" + relative: true + source: path version: "0.1.0+1" flutter_displaymode: dependency: "direct main" @@ -918,10 +917,10 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: "02720226035257ad0b571c1256f43df3e1556a499f6bcb004849a0faaa0e87f0" + sha256: fac14d2dd67eeba29a20e5d99fac0d4d9fcd552cdf6bf4f8945f7679c6b07b1d url: "https://pub.dev" source: hosted - version: "1.82.6" + version: "2.1.0" flutter_secure_storage: dependency: "direct main" description: @@ -1266,10 +1265,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: @@ -1322,26 +1321,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,18 +1473,17 @@ 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: - name: metadata_god - sha256: cf13931c39eba0b9443d16e8940afdabee125bf08945f18d4c0d02bcae2a3317 - url: "https://pub.dev" - source: hosted - version: "0.5.2+1" + path: "../frb_plugins/packages/metadata_god" + relative: true + source: path + version: "0.5.3" mime: dependency: "direct main" description: @@ -1766,14 +1764,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" - puppeteer: - dependency: transitive - description: - name: puppeteer - sha256: "6833edca01b1e9dcdd9a6e41bad84b706dfba4366d095c4edff64b00c02ac472" - url: "https://pub.dev" - source: hosted - version: "3.8.0" quiver: dependency: transitive description: @@ -1935,14 +1925,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.4" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" shelf_web_socket: dependency: "direct main" description: @@ -1999,10 +1981,9 @@ packages: smtc_windows: dependency: "direct main" description: - name: smtc_windows - sha256: "0fd64d0c6a0c8ea4ea7908d31195eadc8f6d45d5245159fc67259e9e8704100f" - url: "https://pub.dev" - source: hosted + path: "../frb_plugins/packages/smtc_windows" + relative: true + source: path version: "0.1.3" source_gen: dependency: transitive @@ -2031,12 +2012,11 @@ packages: spotify: dependency: "direct main" description: - path: "." - ref: "fix/explicit-to-json" - resolved-ref: c4b37c599413ac7bfd78993e416a56105c62b634 - url: "https://github.com/KRTirtho/spotify-dart.git" - source: git - version: "0.13.6" + name: spotify + sha256: "705f09a457a893973451c15f4072670ac4783d67e42c35c080c55a48dee3a01f" + url: "https://pub.dev" + source: hosted + version: "0.13.7" sprintf: dependency: transitive description: @@ -2186,10 +2166,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: @@ -2230,14 +2210,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" type_plus: dependency: transitive description: @@ -2394,10 +2366,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: @@ -2495,5 +2467,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index 58c8c55f7..983d4001e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,11 @@ dependencies: logger: ^2.0.2 media_kit: ^1.1.10+1 media_kit_libs_audio: ^1.0.4 - metadata_god: ^0.5.2+1 + metadata_god: + git: + url: https://github.com/KRTirtho/frb_plugins.git + path: packages/metadata_god + ref: cargokit mime: ^1.0.2 package_info_plus: ^6.0.0 palette_generator: ^0.3.3 @@ -81,7 +85,11 @@ dependencies: scroll_to_index: ^3.0.1 sidebarx: ^0.17.1 shared_preferences: ^2.2.3 - smtc_windows: ^0.1.3 + smtc_windows: + git: + url: https://github.com/KRTirtho/frb_plugins.git + path: packages/smtc_windows + ref: cargokit stroke_text: ^0.0.2 system_theme: ^2.1.0 titlebar_buttons: ^1.0.0 @@ -100,7 +108,11 @@ dependencies: very_good_infinite_list: ^0.7.1 gap: ^3.0.1 sliver_tools: ^0.2.12 - flutter_discord_rpc: ^0.1.0+1 + flutter_discord_rpc: + git: + url: https://github.com/KRTirtho/frb_plugins.git + path: packages/flutter_discord_rpc + ref: cargokit html_unescape: ^2.0.0 wikipedia_api: ^0.1.0 skeletonizer: ^1.1.1 @@ -109,10 +121,7 @@ dependencies: flutter_sharing_intent: ^1.1.0 flutter_broadcasts: ^0.4.0 freezed_annotation: ^2.4.1 - spotify: - git: - url: https://github.com/KRTirtho/spotify-dart.git - ref: fix/explicit-to-json + spotify: ^0.13.7 bonsoir: ^5.1.9 shelf: ^1.4.1 shelf_router: ^1.1.4 From d515f3d3be9c6a50faadfbe6d3b6b12064d070f1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 1 Aug 2024 16:46:26 +0600 Subject: [PATCH 71/92] cd: fix rustup target failing for ios --- .github/Dockerfile | 2 +- .github/Dockerfile.flutter_distributor | 23 ----------------------- cli/commands/install-dependencies.dart | 5 +++++ 3 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 .github/Dockerfile.flutter_distributor diff --git a/.github/Dockerfile b/.github/Dockerfile index 2e3934499..4d0645d1b 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -1,6 +1,6 @@ ARG FLUTTER_VERSION -FROM --platform=linux/arm64 krtirtho/flutter_distributor_arm64:${FLUTTER_VERSION} +FROM --platform=linux/arm64 krtirtho/flutter_distributor:${FLUTTER_VERSION} ARG BUILD_VERSION diff --git a/.github/Dockerfile.flutter_distributor b/.github/Dockerfile.flutter_distributor deleted file mode 100644 index d842e533f..000000000 --- a/.github/Dockerfile.flutter_distributor +++ /dev/null @@ -1,23 +0,0 @@ -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 libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev && \ - 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/cli/commands/install-dependencies.dart b/cli/commands/install-dependencies.dart index 6875e35fb..dc519cc61 100644 --- a/cli/commands/install-dependencies.dart +++ b/cli/commands/install-dependencies.dart @@ -58,6 +58,11 @@ class InstallDependenciesCommand extends Command { ); break; case "ios": + await shell.run( + """ + rustup target add aarch64-apple-ios + """, + ); break; case "android": await shell.run( From 0d537abab3dbadee3e9f3c1c6d91cedad500651a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 1 Aug 2024 17:19:17 +0600 Subject: [PATCH 72/92] cd: disable arm64 --- .github/workflows/spotube-release-binary.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 8fcf228ab..72f060414 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -36,11 +36,11 @@ jobs: 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: linux_arm + # files: | + # dist/Spotube-linux-aarch64.deb + # dist/spotube-linux-*-aarch64.tar.xz - os: ubuntu-latest platform: android files: | From 4b65319879127e70a155af91217ac795e685e4f1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 4 Aug 2024 10:27:05 +0600 Subject: [PATCH 73/92] chore: pubspec update --- pubspec.lock | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b21c0091f..bd0ab415c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -715,9 +715,11 @@ packages: flutter_discord_rpc: dependency: "direct main" description: - path: "../frb_plugins/packages/flutter_discord_rpc" - relative: true - source: path + path: "packages/flutter_discord_rpc" + ref: cargokit + resolved-ref: "7ca0bbb786d8ce0e4bf8341b673b6c709ba69146" + url: "https://github.com/KRTirtho/frb_plugins.git" + source: git version: "0.1.0+1" flutter_displaymode: dependency: "direct main" @@ -1480,9 +1482,11 @@ packages: metadata_god: dependency: "direct main" description: - path: "../frb_plugins/packages/metadata_god" - relative: true - source: path + path: "packages/metadata_god" + ref: cargokit + resolved-ref: "7ca0bbb786d8ce0e4bf8341b673b6c709ba69146" + url: "https://github.com/KRTirtho/frb_plugins.git" + source: git version: "0.5.3" mime: dependency: "direct main" @@ -1981,9 +1985,11 @@ packages: smtc_windows: dependency: "direct main" description: - path: "../frb_plugins/packages/smtc_windows" - relative: true - source: path + path: "packages/smtc_windows" + ref: cargokit + resolved-ref: "7ca0bbb786d8ce0e4bf8341b673b6c709ba69146" + url: "https://github.com/KRTirtho/frb_plugins.git" + source: git version: "0.1.3" source_gen: dependency: transitive From 04650422640730537dfcb7347f31d697b85bfa65 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 4 Aug 2024 11:32:50 +0600 Subject: [PATCH 74/92] cd: re-enable arm --- .github/workflows/spotube-release-binary.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 72f060414..3d8fee5a9 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -36,11 +36,11 @@ jobs: 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: linux_arm + files: | + dist/Spotube-linux-aarch64.deb + dist/spotube-linux-*-aarch64.tar.xz - os: ubuntu-latest platform: android files: | @@ -83,6 +83,7 @@ jobs: if: ${{matrix.platform == 'linux_arm'}} uses: docker/setup-buildx-action@v3 - name: Setup Rust toolchain + if: ${{matrix.platform != 'linux_arm'}} uses: dtolnay/rust-toolchain@stable with: toolchain: stable From b5f3894983b5a3579f53dddbd6859963c6ebc5a0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 4 Aug 2024 12:03:48 +0600 Subject: [PATCH 75/92] cd: add aarch64-unknown-linux-gnu manually in dockerfile for linux arm --- .github/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/Dockerfile b/.github/Dockerfile index 4d0645d1b..f6a9f538a 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -10,6 +10,8 @@ COPY . . RUN chown -R $(whoami) /app +RUN rustup target add aarch64-unknown-linux-gnu + RUN flutter pub get RUN alias dpkg-deb="dpkg-deb --Zxz" &&\ From 1c66646798ecc6835df9e48d3b3923ee8690352a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 7 Aug 2024 21:54:47 +0600 Subject: [PATCH 76/92] fix(windows): local tracks plays but disabled playback controls --- lib/pages/library/local_folder.dart | 3 +- lib/provider/audio_player/audio_player.dart | 6 ++- lib/services/audio_player/audio_player.dart | 37 +++++++++++-------- .../audio_players_streams_mixin.dart | 5 ++- linux/flutter/generated_plugin_registrant.cc | 4 -- linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 15 ++------ pubspec.yaml | 11 ------ .../flutter/generated_plugin_registrant.cc | 3 -- windows/flutter/generated_plugins.cmake | 1 - 11 files changed, 35 insertions(+), 53 deletions(-) diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 16891bc18..ad1d5d827 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -37,9 +37,10 @@ class LocalLibraryPage extends HookConsumerWidget { currentTrack ??= tracks.first; final isPlaylistPlaying = playlist.containsTracks(tracks); if (!isPlaylistPlaying) { + var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id); await playback.load( tracks, - initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id), + initialIndex: indexWhere, autoPlay: true, ); } else if (isPlaylistPlaying && diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 437b666e4..3bc719424 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart' hide Track; @@ -11,6 +12,7 @@ import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/discord_provider.dart'; +import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -283,7 +285,7 @@ class AudioPlayerNotifier extends Notifier { // Giving the initial track a boost so MediaKit won't skip // because of timeout final intendedActiveTrack = medias.elementAt(initialIndex); - if (intendedActiveTrack is! LocalTrack) { + if (intendedActiveTrack.track is! LocalTrack) { await ref.read(sourcedTrackProvider(intendedActiveTrack).future); } @@ -292,7 +294,7 @@ class AudioPlayerNotifier extends Notifier { await removeCollections(state.collections); await audioPlayer.openPlaylist( - medias, + medias.map((s) => s as Media).toList(), initialIndex: initialIndex, autoPlay: autoPlay, ); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 7915dc3bd..4febecdf1 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -41,9 +41,16 @@ class SpotubeMedia extends mk.Media { ); @override - String get uri => track is LocalTrack - ? (track as LocalTrack).path - : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; + String get uri { + return switch (track) { + /// [super.uri] must be used instead of [track.path] to prevent wrong + /// path format exceptions in Windows causing [extras] to be null + LocalTrack() => super.uri, + _ => + "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:" + "$serverPort/stream/${track.id}", + }; + } factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http") @@ -56,20 +63,20 @@ class SpotubeMedia extends mk.Media { ); } - @override - operator ==(Object other) { - if (other is! SpotubeMedia) return false; + // @override + // operator ==(Object other) { + // if (other is! SpotubeMedia) return false; - final isLocal = track is LocalTrack && other.track is LocalTrack; - return isLocal - ? (other.track as LocalTrack).path == (track as LocalTrack).path - : other.track.id == track.id; - } + // final isLocal = track is LocalTrack && other.track is LocalTrack; + // return isLocal + // ? (other.track as LocalTrack).path == (track as LocalTrack).path + // : other.track.id == track.id; + // } - @override - int get hashCode => track is LocalTrack - ? (track as LocalTrack).path.hashCode - : track.id.hashCode; + // @override + // int get hashCode => track is LocalTrack + // ? (track as LocalTrack).path.hashCode + // : track.id.hashCode; } abstract class AudioPlayerInterface { diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index 03ce0d5d5..6b9616fa7 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -149,5 +149,8 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { Stream get errorStream => _mkPlayer.stream.error; - Stream get playlistStream => _mkPlayer.stream.playlist; + Stream get playlistStream => _mkPlayer.stream.playlist.map((s){ + print("[Stream Playlist]: $s"); + return s; + }); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 5550ed227..0f93d754d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -48,9 +47,6 @@ 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 93bf6bc0e..ff6426968 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,7 +12,6 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever sqlite3_flutter_libs system_theme - system_tray tray_manager url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8a65bb53c..ea94bf6dd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -23,7 +23,6 @@ import shared_preferences_foundation import sqflite import sqlite3_flutter_libs import system_theme -import system_tray import tray_manager import url_launcher_macos import window_manager @@ -47,7 +46,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) 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 bd0ab415c..d1d9c7809 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -717,7 +717,7 @@ packages: description: path: "packages/flutter_discord_rpc" ref: cargokit - resolved-ref: "7ca0bbb786d8ce0e4bf8341b673b6c709ba69146" + resolved-ref: ed8d0d67fae7a23acc9b58c84f01d02abd8d8515 url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.1.0+1" @@ -1484,7 +1484,7 @@ packages: description: path: "packages/metadata_god" ref: cargokit - resolved-ref: "7ca0bbb786d8ce0e4bf8341b673b6c709ba69146" + resolved-ref: ed8d0d67fae7a23acc9b58c84f01d02abd8d8515 url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.5.3" @@ -1987,7 +1987,7 @@ packages: description: path: "packages/smtc_windows" ref: cargokit - resolved-ref: "7ca0bbb786d8ce0e4bf8341b673b6c709ba69146" + resolved-ref: ed8d0d67fae7a23acc9b58c84f01d02abd8d8515 url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.1.3" @@ -2151,15 +2151,6 @@ 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 983d4001e..e4b72df46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -164,17 +164,6 @@ 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/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 217a7cb4d..f2d60e211 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -45,8 +44,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); 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 cbbd2acc2..bea4d8017 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -14,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever sqlite3_flutter_libs system_theme - system_tray tray_manager url_launcher_windows window_manager From ce19ef1efd7af2506460cbbaf4dc32d96f350b73 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 7 Aug 2024 22:12:57 +0600 Subject: [PATCH 77/92] chore: upgrade plugin versions --- pubspec.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index d1d9c7809..5fce257fe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -717,7 +717,7 @@ packages: description: path: "packages/flutter_discord_rpc" ref: cargokit - resolved-ref: ed8d0d67fae7a23acc9b58c84f01d02abd8d8515 + resolved-ref: "1b839bf02afd5dfa56b0dc25f60af04aa9bfc7c3" url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.1.0+1" @@ -1484,7 +1484,7 @@ packages: description: path: "packages/metadata_god" ref: cargokit - resolved-ref: ed8d0d67fae7a23acc9b58c84f01d02abd8d8515 + resolved-ref: "1b839bf02afd5dfa56b0dc25f60af04aa9bfc7c3" url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.5.3" @@ -1987,7 +1987,7 @@ packages: description: path: "packages/smtc_windows" ref: cargokit - resolved-ref: ed8d0d67fae7a23acc9b58c84f01d02abd8d8515 + resolved-ref: "1b839bf02afd5dfa56b0dc25f60af04aa9bfc7c3" url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.1.3" From ebaf5615ad511867c46b567e6293d96360842b22 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 9 Aug 2024 13:13:11 +0600 Subject: [PATCH 78/92] cd: free up space for linux arm --- .github/workflows/spotube-release-binary.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 3d8fee5a9..b103ea2e0 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -99,6 +99,11 @@ jobs: echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties + - name: Unessary hosted tools + if: ${{matrix.platform == 'linux_arm'}} + run: | + sudo rm -rf /usr/share/dotnet + - name: Build ${{matrix.platform}} binaries run: dart cli/cli.dart build ${{matrix.platform}} env: From 1cc7882177203d345bb732714b742655eb8c494a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 9 Aug 2024 19:55:56 +0600 Subject: [PATCH 79/92] fix(windows): app crashes when no internet --- pubspec.lock | 24 ++++++++++++------------ pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 5fce257fe..cd96e007e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,50 +125,50 @@ packages: dependency: "direct main" description: name: bonsoir - sha256: "9703ca3ce201c7ab6cd278ae5a530a125959687f59c2b97822f88a8db5bef106" + sha256: b7697a954c772a6ddc68d52b3e4768947cc98613127f7720a05b14ed1e59d68b url: "https://pub.dev" source: hosted - version: "5.1.9" + version: "5.1.10" bonsoir_android: dependency: transitive description: name: bonsoir_android - sha256: "19583ae34a5e5743fa2c16619e4ec699b35ae5e6cece59b99b1cf21c1b4ed618" + sha256: a72d83a78780c1f238e3178d0585e5604fbd9f2503206293737cdfab899ce8d0 url: "https://pub.dev" source: hosted - version: "5.1.4" + version: "5.1.5" bonsoir_darwin: dependency: transitive description: name: bonsoir_darwin - sha256: "985c4c38b4cbfa57ed5870e724a7e17aa080ee7f49d03b43e6d08781511505c6" + sha256: "2d25c70f0d09260be1c2ab583b80dd89cbbfd59997579dadf789c5af00c7b2e4" url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "5.1.3" bonsoir_linux: dependency: transitive description: name: bonsoir_linux - sha256: "65554b20bc169c68c311eb31fab46ccdd8ee3d3dd89a2d57c338f4cbf6ceb00d" + sha256: f2639aded6e15943a9822de98a663a1056f37cbfd0a74d72c9eaa941965945c2 url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "5.1.3" bonsoir_platform_interface: dependency: transitive description: name: bonsoir_platform_interface - sha256: "4ee898bec0b5a63f04f82b06da9896ae8475f32a33b6fa395bea56399daeb9f0" + sha256: "08bb8b35d0198168b3bce87dbc718e4e510336cff1d97e43762e030c01636d45" url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "5.1.3" bonsoir_windows: dependency: transitive description: name: bonsoir_windows - sha256: abbc90b73ac39e823b0c127da43b91d8906dcc530fc0cec4e169cf0d8c4404b1 + sha256: d4a0ca479d4f3679487a61f3174fb9fe1651e323c778b02dfa630490366be65d url: "https://pub.dev" source: hosted - version: "5.1.4" + version: "5.1.5" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e4b72df46..7f6f0f294 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -122,7 +122,7 @@ dependencies: flutter_broadcasts: ^0.4.0 freezed_annotation: ^2.4.1 spotify: ^0.13.7 - bonsoir: ^5.1.9 + bonsoir: ^5.1.10 shelf: ^1.4.1 shelf_router: ^1.1.4 shelf_web_socket: ^1.0.4 From 39ea7a701c3aaf3ba74d99d7778d97ecc92d7db5 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 9 Aug 2024 20:07:24 +0600 Subject: [PATCH 80/92] chore: remove unnecessary print statements --- lib/services/audio_player/audio_players_streams_mixin.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index 6b9616fa7..3995acf77 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -149,8 +149,7 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { Stream get errorStream => _mkPlayer.stream.error; - Stream get playlistStream => _mkPlayer.stream.playlist.map((s){ - print("[Stream Playlist]: $s"); + Stream get playlistStream => _mkPlayer.stream.playlist.map((s) { return s; - }); + }); } From 123eb168a3d548e4474cb70466e67bf6dc879408 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 9 Aug 2024 22:41:29 +0600 Subject: [PATCH 81/92] fix(linux): tray icon wrong name for flatpak --- lib/provider/tray_manager/tray_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/provider/tray_manager/tray_manager.dart b/lib/provider/tray_manager/tray_manager.dart index 2145cbefc..9cc4becc6 100644 --- a/lib/provider/tray_manager/tray_manager.dart +++ b/lib/provider/tray_manager/tray_manager.dart @@ -24,7 +24,7 @@ class SystemTrayManager with TrayListener { kIsWindows ? 'assets/spotube-logo.ico' : kIsFlatpak - ? 'com.github.KRTirtho.Spotube.png' + ? 'com.github.KRTirtho.Spotube' : 'assets/spotube-logo.png', ); trayManager.addListener(this); From 6456b43d10726660166dd6a0b66df2f94b7f1c43 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 10 Aug 2024 20:53:17 +0600 Subject: [PATCH 82/92] refactor: logs page show full log --- lib/pages/settings/logs.dart | 128 +++++---------------------- lib/provider/logs/logs_provider.dart | 12 +++ macos/Podfile.lock | 6 -- 3 files changed, 35 insertions(+), 111 deletions(-) create mode 100644 lib/provider/logs/logs_provider.dart diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index a49050ad5..91087b7e6 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -1,77 +1,23 @@ -import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/provider/logs/logs_provider.dart'; -class LogsPage extends HookWidget { +class LogsPage extends HookConsumerWidget { static const name = "logs"; const LogsPage({super.key}); - List<({DateTime? date, String body})> parseLogs(String raw) { - return raw - .split( - "======================================================================", - ) - .map( - (line) { - DateTime? date; - line = line - .replaceAll( - "============================== CATCHER LOG ==============================", - "", - ) - .split("\n") - .map((l) { - if (l.startsWith("Crash occurred on")) { - date = DateTime.parse( - l.split("Crash occurred on")[1].trim(), - ); - return ""; - } - return l; - }) - .where((l) => l.replaceAll("\n", "").trim().isNotEmpty) - .join("\n"); - - return ( - date: date, - body: line, - ); - }, - ) - .where((e) => e.date != null && e.body.isNotEmpty) - .toList() - ..sort((a, b) => b.date!.compareTo(a.date!)); - } - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { final controller = useScrollController(); - final logs = useState>([]); - final rawLogs = useRef(""); - final path = useRef(null); - - useEffect(() { - final timer = Timer.periodic(const Duration(seconds: 5), (t) async { - path.value ??= await AppLogger.getLogsPath(); - final raw = await path.value!.readAsString(); - final hasChanged = rawLogs.value != raw; - rawLogs.value = raw; - if (hasChanged) logs.value = parseLogs(rawLogs.value); - }); - return () { - timer.cancel(); - }; - }, []); + final logsQuery = ref.watch(logsProvider); return Scaffold( appBar: PageWindowTitleBar( @@ -82,7 +28,9 @@ class LogsPage extends HookWidget { icon: const Icon(SpotubeIcons.clipboard), iconSize: 16, onPressed: () async { - await Clipboard.setData(ClipboardData(text: rawLogs.value)); + final logsSnapshot = await ref.read(logsProvider.future); + + await Clipboard.setData(ClipboardData(text: logsSnapshot)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -95,52 +43,22 @@ class LogsPage extends HookWidget { ], ), body: SafeArea( - child: InterScrollbar( - controller: controller, - child: ListView.builder( - controller: controller, - itemCount: logs.value.length, - itemBuilder: (context, index) { - final log = logs.value[index]; - return Stack( - children: [ - SectionCardWithHeading( - heading: log.date.toString(), - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: SelectableText(log.body), - ), - ], - ), - Positioned( - right: 10, - top: 0, - child: IconButton( - icon: const Icon(SpotubeIcons.clipboard), - onPressed: () async { - await Clipboard.setData( - ClipboardData(text: log.body), - ); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.copied_to_clipboard( - log.date.toString(), - ), - ), - ), - ); - } - }, - ), + child: switch (logsQuery) { + AsyncData(:final value) => Card( + child: InterScrollbar( + controller: controller, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SingleChildScrollView( + controller: controller, + child: Text(value), ), - ], - ); - }, - ), - ), + ), + ), + ), + AsyncError(:final error) => Center(child: Text(error.toString())), + _ => const Center(child: CircularProgressIndicator()), + }, ), ); } diff --git a/lib/provider/logs/logs_provider.dart b/lib/provider/logs/logs_provider.dart new file mode 100644 index 000000000..b0e95caed --- /dev/null +++ b/lib/provider/logs/logs_provider.dart @@ -0,0 +1,12 @@ +import 'dart:convert'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/services/logger/logger.dart'; + +final logsProvider = StreamProvider.autoDispose((ref) async* { + final file = await AppLogger.getLogsPath(); + final stream = file.openRead().transform(utf8.decoder); + await for (final line in stream) { + yield line; + } +}); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 9b122cbeb..b3092d8cd 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -61,8 +61,6 @@ PODS: - sqlite3/rtree - system_theme (0.0.1): - FlutterMacOS - - system_tray (0.0.1): - - FlutterMacOS - tray_manager (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -93,7 +91,6 @@ DEPENDENCIES: - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/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`) @@ -148,8 +145,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/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: @@ -182,7 +177,6 @@ SPEC CHECKSUMS: sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc - system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 From 64d25509b4aa603a451532cbf8369b71e11f3659 Mon Sep 17 00:00:00 2001 From: nexpid <60316309+nexpid@users.noreply.github.com> Date: Sat, 10 Aug 2024 17:30:11 +0200 Subject: [PATCH 83/92] feat(discord): album art, playing time and play pause support (#1765) --- lib/provider/discord_provider.dart | 60 +++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index 1e819af09..23be0bc34 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -6,6 +6,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; class DiscordNotifier extends AsyncNotifier { @@ -13,17 +14,39 @@ class DiscordNotifier extends AsyncNotifier { FutureOr build() async { final enabled = ref.watch( userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop)); - final playback = ref.read(audioPlayerProvider); - final subscription = - FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { - if (connected && playback.activeTrack != null) { - await updatePresence(playback.activeTrack!); - } - }); + var lastPosition = audioPlayer.position; + + final subscriptions = + [ + FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { + final playback = ref.read(audioPlayerProvider); + if (connected && playback.activeTrack != null) { + await updatePresence(playback.activeTrack!); + } + }), + audioPlayer.playerStateStream.listen((state) async { + final playback = ref.read(audioPlayerProvider); + if (playback.activeTrack == null) return; + + await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + }), + audioPlayer.positionStream.listen((position) async { + final playback = ref.read(audioPlayerProvider); + if (playback.activeTrack != null) { + final diff = position.inMilliseconds - lastPosition.inMilliseconds; + if (diff > 500 || diff < -500) { + await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + } + } + lastPosition = position; + }) + ]; ref.onDispose(() async { - subscription.cancel(); + for (final subscription in subscriptions) { + subscription.cancel(); + } await close(); await FlutterDiscordRPC.instance.dispose(); }); @@ -37,15 +60,18 @@ class DiscordNotifier extends AsyncNotifier { } Future updatePresence(Track track) async { - await clear(); - final artistNames = track.artists?.asString() ?? ""; + final artistNames = track.artists?.asString(); + final isPlaying = audioPlayer.isPlaying; + final position = audioPlayer.position; + await FlutterDiscordRPC.instance.setActivity( activity: RPCActivity( - details: "${track.name} by $artistNames", - state: "Vibing in Music", - assets: const RPCAssets( - largeImage: "spotube-logo-foreground", - largeText: "Spotube", + details: track.name, + state: artistNames != null ? "by $artistNames" : null, + assets: RPCAssets( + largeImage: + track.album?.images?.first.url ?? "spotube-logo-foreground", + largeText: track.album?.name ?? "Unknown album", smallImage: "spotube-logo-foreground", smallText: "Spotube", ), @@ -57,7 +83,7 @@ class DiscordNotifier extends AsyncNotifier { ), ], timestamps: RPCTimestamps( - start: DateTime.now().millisecondsSinceEpoch, + start: isPlaying ? DateTime.now().millisecondsSinceEpoch - position.inMilliseconds : null, ), ), ); @@ -73,4 +99,4 @@ class DiscordNotifier extends AsyncNotifier { } final discordProvider = - AsyncNotifierProvider(() => DiscordNotifier()); + AsyncNotifierProvider(() => DiscordNotifier()); \ No newline at end of file From 84f47df6c16c0ba1c35f2bde0b1c26fbb0f07168 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 10 Aug 2024 21:35:38 +0600 Subject: [PATCH 84/92] feat(discord): add listening activity type --- lib/provider/discord_provider.dart | 52 ++++++++++++++++-------------- pubspec.lock | 6 ++-- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index 23be0bc34..8f8cb375a 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -17,31 +17,30 @@ class DiscordNotifier extends AsyncNotifier { var lastPosition = audioPlayer.position; - final subscriptions = - [ - FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { - final playback = ref.read(audioPlayerProvider); - if (connected && playback.activeTrack != null) { - await updatePresence(playback.activeTrack!); - } - }), - audioPlayer.playerStateStream.listen((state) async { - final playback = ref.read(audioPlayerProvider); - if (playback.activeTrack == null) return; + final subscriptions = [ + FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { + final playback = ref.read(audioPlayerProvider); + if (connected && playback.activeTrack != null) { + await updatePresence(playback.activeTrack!); + } + }), + audioPlayer.playerStateStream.listen((state) async { + final playback = ref.read(audioPlayerProvider); + if (playback.activeTrack == null) return; + await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + }), + audioPlayer.positionStream.listen((position) async { + final playback = ref.read(audioPlayerProvider); + if (playback.activeTrack != null) { + final diff = position.inMilliseconds - lastPosition.inMilliseconds; + if (diff > 500 || diff < -500) { await updatePresence(ref.read(audioPlayerProvider).activeTrack!); - }), - audioPlayer.positionStream.listen((position) async { - final playback = ref.read(audioPlayerProvider); - if (playback.activeTrack != null) { - final diff = position.inMilliseconds - lastPosition.inMilliseconds; - if (diff > 500 || diff < -500) { - await updatePresence(ref.read(audioPlayerProvider).activeTrack!); - } - } - lastPosition = position; - }) - ]; + } + } + lastPosition = position; + }) + ]; ref.onDispose(() async { for (final subscription in subscriptions) { @@ -83,8 +82,11 @@ class DiscordNotifier extends AsyncNotifier { ), ], timestamps: RPCTimestamps( - start: isPlaying ? DateTime.now().millisecondsSinceEpoch - position.inMilliseconds : null, + start: isPlaying + ? DateTime.now().millisecondsSinceEpoch - position.inMilliseconds + : null, ), + activityType: ActivityType.listening, ), ); } @@ -99,4 +101,4 @@ class DiscordNotifier extends AsyncNotifier { } final discordProvider = - AsyncNotifierProvider(() => DiscordNotifier()); \ No newline at end of file + AsyncNotifierProvider(() => DiscordNotifier()); diff --git a/pubspec.lock b/pubspec.lock index cd96e007e..0bc24dff4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -717,7 +717,7 @@ packages: description: path: "packages/flutter_discord_rpc" ref: cargokit - resolved-ref: "1b839bf02afd5dfa56b0dc25f60af04aa9bfc7c3" + resolved-ref: "331636d8e378e3ac9ad30a4b0d3eed17d5a85fe9" url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.1.0+1" @@ -1484,7 +1484,7 @@ packages: description: path: "packages/metadata_god" ref: cargokit - resolved-ref: "1b839bf02afd5dfa56b0dc25f60af04aa9bfc7c3" + resolved-ref: "331636d8e378e3ac9ad30a4b0d3eed17d5a85fe9" url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.5.3" @@ -1987,7 +1987,7 @@ packages: description: path: "packages/smtc_windows" ref: cargokit - resolved-ref: "1b839bf02afd5dfa56b0dc25f60af04aa9bfc7c3" + resolved-ref: "331636d8e378e3ac9ad30a4b0d3eed17d5a85fe9" url: "https://github.com/KRTirtho/frb_plugins.git" source: git version: "0.1.3" From 388e2d0289cfb11d21c096cc20d463f34a9b803e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 10 Aug 2024 21:50:20 +0600 Subject: [PATCH 85/92] fix(ios): permission exception --- ios/Podfile.lock | 48 ++++++++++++------- .../configurators/use_get_storage_perms.dart | 40 ++++++++++------ 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f8533902b..7e5f24b53 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -49,6 +49,8 @@ PODS: - Flutter (1.0.0) - flutter_broadcasts (0.0.1): - Flutter + - flutter_discord_rpc (0.0.1): + - Flutter - flutter_inappwebview_ios (0.0.1): - Flutter - flutter_inappwebview_ios/Core (= 0.0.1) @@ -58,17 +60,12 @@ PODS: - OrderedSet (~> 5.0) - flutter_keyboard_visibility (0.0.1): - Flutter - - flutter_mailer (0.0.1): - - Flutter - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter - flutter_sharing_intent (0.0.1): - Flutter - - fluttertoast (0.0.2): - - Flutter - - Toast - image_picker_ios (0.0.1): - Flutter - integration_test (0.0.1): @@ -77,7 +74,8 @@ PODS: - Flutter - media_kit_native_event_loop (1.0.0): - Flutter - - metadata_god (0.0.1) + - metadata_god (0.0.1): + - Flutter - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -95,8 +93,22 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS + - "sqlite3 (3.46.0+1)": + - "sqlite3/common (= 3.46.0+1)" + - "sqlite3/common (3.46.0+1)" + - "sqlite3/fts5 (3.46.0+1)": + - sqlite3/common + - "sqlite3/perf-threadsafe (3.46.0+1)": + - sqlite3/common + - "sqlite3/rtree (3.46.0+1)": + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - sqlite3 (~> 3.46.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - SwiftyGif (5.4.4) - - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter @@ -110,13 +122,12 @@ DEPENDENCIES: - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - Flutter (from `Flutter`) - flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`) + - flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) - - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`) @@ -127,6 +138,7 @@ DEPENDENCIES: - 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/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -135,8 +147,8 @@ SPEC REPOS: - DKPhotoGallery - OrderedSet - SDWebImage + - sqlite3 - SwiftyGif - - Toast EXTERNAL SOURCES: app_links: @@ -157,20 +169,18 @@ EXTERNAL SOURCES: :path: Flutter flutter_broadcasts: :path: ".symlinks/plugins/flutter_broadcasts/ios" + flutter_discord_rpc: + :path: ".symlinks/plugins/flutter_discord_rpc/ios" flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_keyboard_visibility: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" - flutter_mailer: - :path: ".symlinks/plugins/flutter_mailer/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_sharing_intent: :path: ".symlinks/plugins/flutter_sharing_intent/ios" - fluttertoast: - :path: ".symlinks/plugins/fluttertoast/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: @@ -191,6 +201,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" @@ -206,18 +218,17 @@ SPEC CHECKSUMS: file_selector_ios: 78baf21d03f1e37a7df97bb2494f9cd86de8fa5d Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882 + flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 - flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 - fluttertoast: 9f2f8e81bb5ce18facb9748d7855bf5a756fe3db image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3 media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837 - metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 + metadata_god: 4bbd8523cdb5d42c5e59d2fabad01ff8f4bc53f9 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c @@ -225,8 +236,9 @@ SPEC CHECKSUMS: SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 + sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e diff --git a/lib/hooks/configurators/use_get_storage_perms.dart b/lib/hooks/configurators/use_get_storage_perms.dart index 9cccbfe06..f860aaa71 100644 --- a/lib/hooks/configurators/use_get_storage_perms.dart +++ b/lib/hooks/configurators/use_get_storage_perms.dart @@ -12,25 +12,35 @@ void useGetStoragePermissions(WidgetRef ref) { useAsyncEffect( () async { - if (!kIsMobile) return; + if (kIsAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; - final androidInfo = await DeviceInfoPlugin().androidInfo; + final hasNoStoragePerm = androidInfo.version.sdkInt < 33 && + !await Permission.storage.isGranted && + !await Permission.storage.isLimited; - final hasNoStoragePerm = androidInfo.version.sdkInt < 33 && - !await Permission.storage.isGranted && - !await Permission.storage.isLimited; + final hasNoAudioPerm = androidInfo.version.sdkInt >= 33 && + !await Permission.audio.isGranted && + !await Permission.audio.isLimited; - final hasNoAudioPerm = androidInfo.version.sdkInt >= 33 && - !await Permission.audio.isGranted && - !await Permission.audio.isLimited; - - if (hasNoStoragePerm) { - await Permission.storage.request(); - if (context.mounted) ref.invalidate(localTracksProvider); + if (hasNoStoragePerm) { + await Permission.storage.request(); + if (context.mounted) ref.invalidate(localTracksProvider); + } + if (hasNoAudioPerm) { + await Permission.audio.request(); + if (context.mounted) ref.invalidate(localTracksProvider); + } } - if (hasNoAudioPerm) { - await Permission.audio.request(); - if (context.mounted) ref.invalidate(localTracksProvider); + + if (kIsIOS) { + final hasStoragePerm = await Permission.storage.isGranted || + await Permission.storage.isLimited; + + if (!hasStoragePerm) { + await Permission.storage.request(); + if (context.mounted) ref.invalidate(localTracksProvider); + } } }, null, From 95b68687d57b27f6a6d75c51f03e79cd6a266ac1 Mon Sep 17 00:00:00 2001 From: Marat Budkevich <93652988+marat2509@users.noreply.github.com> Date: Sat, 10 Aug 2024 19:03:42 +0300 Subject: [PATCH 86/92] fix(translations): fix Russian translations (#1696) --- lib/l10n/app_ru.arb | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0a1c1c22d..2e0aa77bc 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -8,16 +8,16 @@ "genre_categories_filter": "Фильтр по категориям или жанрам...", "genre": "Жанр", "personalized": "Персонализированный", - "featured": "Будующий", - "new_releases": "Новые", - "songs": "Песни", + "featured": "Популярное", + "new_releases": "Новое", + "songs": "Треки", "playing_track": "Играет {track}", "queue_clear_alert": "Это удалит текущую очередь. {track_length} треков будет удалено. Вы хотите продолжить?", "load_more": "Загрузить больше", "playlists": "Плейлисты", "artists": "Исполнители", "albums": "Альбомы", - "tracks": "Трек", + "tracks": "Треки", "downloads": "Загрузки", "filter_playlists": "Применить фильтры к вашим плейлистам...", "liked_tracks": "Понравившиеся треки", @@ -25,20 +25,22 @@ "create_playlist": "Создание плейлиста", "create_a_playlist": "Создать плейлист", "create": "Создать", - "cancel": "Отменить", + "cancel": "Отмена", + "update": "Обновить", "playlist_name": "Назвать плейлист", "name_of_playlist": "Название плейлиста", "description": "Описание", - "public": "Публичные", + "public": "Публичный", "collaborative": "Совместный", "search_local_tracks": "Поиск песен на вашем устройстве...", "play": "Играть", "delete": "Удалить", - "none": "Никто", + "none": "Пусто", "sort_a_z": "Сортировка по алфавиту", "sort_z_a": "Сортировка по алфавиту в обратную сторону", "sort_artist": "Сортировать по исполнителю", "sort_album": "Сортировать по альбомам", + "sort_duration": "Сортировать по длительности", "sort_tracks": "Сортировать треки", "currently_downloading": "Загружается ({tracks_length})", "cancel_all": "Отменить все", @@ -104,6 +106,9 @@ "always_on_top": "Всегда сверху", "exit_mini_player": "Выйти из мини-плеера", "download_location": "Место загрузки", + "local_library": "Локальная библиотека", + "add_library_location": "Добавить в библиотеку", + "remove_library_location": "Удалить из библиотеки", "account": "Аккаунт", "login_with_spotify": "Войдите с помощью своей учетной записи Spotify", "connect_with_spotify": "Подключитесь к Spotify", @@ -141,7 +146,7 @@ "close": "Закрыть", "minimize_to_tray": "Свернуть", "show_tray_icon": "Показать значок на панели задач", - "about": "О", + "about": "О нас", "u_love_spotube": "Мы знаем что вам нравится Spotube", "check_for_updates": "Проверьте наличие обновлений", "about_spotube": "О Spotube", @@ -175,9 +180,11 @@ "step_2": "Шаг 2", "step_2_steps": "1. После входа в систему нажмите F12 или щелкните правой кнопкой мыши > «Проверить», чтобы открыть инструменты разработчика браузера.\n2. Затем перейдите на вкладку \"Application\" (Chrome, Edge, Brave и т.д..) or \"Storage\" (Firefox, Palemoon и т.д..)\n3. Перейдите в раздел \"Cookies\", а затем в подраздел \"https://accounts.spotify.com\"", "step_3": "Шаг 3", - "success_emoji": "Успешно 🥳", + "step_3_steps": "Скопируйте значение Cookie \"sp_dc\"", + "success_emoji": "Успешно🥳", "success_message": "Теперь вы успешно вошли в свою учетную запись Spotify. Отличная работа, приятель!", "step_4": "Шаг 4", + "step_4_steps": "Вставьте скопированное значение \"sp_dc\", "something_went_wrong": "Что-то пошло не так", "piped_instance": "Экземпляр сервера Piped", "piped_description": "Серверный экземпляр Piped для сопоставления треков", @@ -205,7 +212,7 @@ "popularity": "Популярность", "key": "Ключ", "duration": "Продолжительность (с)", - "tempo": "Время (BPM)", + "tempo": "Темп (BPM)", "mode": "Режим", "time_signature": "Тактовый размер", "short": "Короткий", @@ -257,8 +264,6 @@ "you_are_offline": "Нет доступа к сети", "connection_restored": "Ваше интернет-соединение восстановлено", "use_system_title_bar": "Использовать системную панель заголовка", - "update_playlist": "Обновить плейлист", - "update": "Обновить", "crunching_results": "Обработка результатов...", "search_to_get_results": "Поиск для получения результатов", "use_amoled_mode": "Режим AMOLED", @@ -283,11 +288,8 @@ "browse_all": "Просмотреть все", "genres": "Жанры", "explore_genres": "Исследовать жанры", - "step_3_steps": "Скопируйте значение файла cookie \"sp_dc\"", - "step_4_steps": "Вставьте скопированное значение \"sp_dc\"", "friends": "Друзья", "no_lyrics_available": "Извините, не удается найти текст для этого трека", - "sort_duration": "Сортировка по Длительности", "start_a_radio": "Запустить радио", "how_to_start_radio": "Как вы хотите запустить радио?", "replace_queue_question": "Хотите заменить текущую очередь или добавить к ней?", @@ -295,6 +297,7 @@ "delete_playlist": "Удалить плейлист", "delete_playlist_confirmation": "Вы уверены, что хотите удалить этот плейлист?", "local_tracks": "Локальные треки", + "local_tab": "Локальное", "song_link": "Ссылка на песню", "skip_this_nonsense": "Пропустить этот бред", "freedom_of_music": "“Свобода музыки”", @@ -321,9 +324,5 @@ "connect_client_alert": "Вас контролирует {client}", "this_device": "Это устройство", "remote": "Дистанционное управление", - "local_library": "Местная библиотека", - "add_library_location": "Добавить в библиотеку", - "remove_library_location": "Удалить из библиотеки", - "local_tab": "Местный", "stats": "Статистика" } \ No newline at end of file From 7408a8786019064abab302d349fabc7c5266f262 Mon Sep 17 00:00:00 2001 From: Josu Igoa Date: Sat, 10 Aug 2024 18:06:15 +0200 Subject: [PATCH 87/92] feat(translations): make state page's hard coded strings translatable (#1719) --- lib/collections/language_codes.dart | 6 +- .../dialogs/select_device_dialog.dart | 9 +-- lib/components/fallbacks/not_found.dart | 5 +- lib/components/playbutton_card.dart | 4 +- .../sections/header/header_actions.dart | 5 +- lib/l10n/app_en.arb | 61 ++++++++++++++- lib/l10n/app_eu.arb | 76 +++++++++++++++++-- lib/modules/home/sections/feed.dart | 5 +- lib/modules/home/sections/friends.dart | 3 +- lib/modules/home/sections/recent.dart | 3 +- .../playlist_generate/multi_select_field.dart | 2 +- lib/modules/player/player.dart | 3 +- lib/modules/playlist/playlist_card.dart | 5 +- .../playlist/playlist_create_dialog.dart | 2 +- lib/modules/root/update_dialog.dart | 13 ++-- .../settings/color_scheme_picker_dialog.dart | 7 +- lib/modules/stats/summary/summary.dart | 23 +++--- lib/modules/stats/top/albums.dart | 4 +- lib/modules/stats/top/artists.dart | 6 +- lib/modules/stats/top/top.dart | 29 +++---- lib/modules/stats/top/tracks.dart | 4 +- lib/pages/lyrics/lyrics.dart | 2 +- lib/pages/lyrics/synced_lyrics.dart | 2 +- lib/pages/profile/profile.dart | 22 +++--- lib/pages/settings/sections/accounts.dart | 2 +- lib/pages/stats/albums/albums.dart | 10 +-- lib/pages/stats/artists/artists.dart | 9 ++- lib/pages/stats/fees/fees.dart | 11 +-- lib/pages/stats/minutes/minutes.dart | 8 +- lib/pages/stats/playlists/playlists.dart | 9 ++- lib/pages/stats/streams/streams.dart | 8 +- 31 files changed, 251 insertions(+), 107 deletions(-) diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index f46e0efe9..44da6ee61 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -83,7 +83,7 @@ abstract class LanguageLocals { // ), "eu": const ISOLanguageName( name: "Basque", - nativeName: "euskara", + nativeName: "Euskara", ), // "be": const ISOLanguageName( // name: "Belarusian", @@ -354,8 +354,8 @@ abstract class LanguageLocals { // nativeName: "KiKongo", // ), "ko": const ISOLanguageName( - name: "Korean", - nativeName: "한국어 (韓國語), 조선말 (朝鮮語)", + name: "Korean", + nativeName: "한국어 (韓國語), 조선말 (朝鮮語)", ), // "ku": const ISOLanguageName( // name: "Kurdish", diff --git a/lib/components/dialogs/select_device_dialog.dart b/lib/components/dialogs/select_device_dialog.dart index cd8dedb7c..3a3bde60a 100644 --- a/lib/components/dialogs/select_device_dialog.dart +++ b/lib/components/dialogs/select_device_dialog.dart @@ -15,15 +15,12 @@ class SelectDeviceDialog extends HookConsumerWidget { final remoteService = connectClients.asData!.value.resolvedService!; return AlertDialog( - title: const Text("Choose the device:"), + title: Text(context.l10n.choose_the_device), insetPadding: const EdgeInsets.all(16), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - "There are multiple device connected.\n" - "Choose the device you want this action to take place", - ), + Text(context.l10n.multiple_device_connected), RadioListTile.adaptive( title: Text(remoteService.name), value: true, @@ -33,7 +30,7 @@ class SelectDeviceDialog extends HookConsumerWidget { }, ), RadioListTile.adaptive( - title: const Text("This Device"), + title: Text(context.l10n.this_device), value: false, groupValue: isRemoteService.value, onChanged: (value) { diff --git a/lib/components/fallbacks/not_found.dart b/lib/components/fallbacks/not_found.dart index 5a74f6720..ce168f178 100644 --- a/lib/components/fallbacks/not_found.dart +++ b/lib/components/fallbacks/not_found.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/extensions/context.dart'; class NotFound extends StatelessWidget { final bool vertical; @@ -18,9 +19,9 @@ class NotFound extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("Nothing found", style: theme.textTheme.titleLarge), + Text(context.l10n.nothing_found, style: theme.textTheme.titleLarge), Text( - "The box is empty", + context.l10n.the_box_is_empty, style: theme.textTheme.titleMedium, ), ], diff --git a/lib/components/playbutton_card.dart b/lib/components/playbutton_card.dart index a0b96ab8f..a1a9bfb44 100644 --- a/lib/components/playbutton_card.dart +++ b/lib/components/playbutton_card.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:skeletonizer/skeletonizer.dart'; - import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/hover_builder.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/string.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; @@ -128,7 +128,7 @@ class PlaybuttonCard extends HookWidget { ), if (isHovered) Text( - "Owned by you", + context.l10n.owned_by_you, style: theme.textTheme.bodySmall?.copyWith( color: Colors.white, ), diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index 94f0baa2d..8e378f97c 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -32,6 +32,9 @@ class TrackViewHeaderActions extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); + final copiedText = + context.l10n.copied_shareurl_to_clipboard(props.shareUrl); + return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -48,7 +51,7 @@ class TrackViewHeaderActions extends HookConsumerWidget { width: 300, behavior: SnackBarBehavior.floating, content: Text( - "Copied ${props.shareUrl} to clipboard", + copiedText, textAlign: TextAlign.center, ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ab6152256..63c805f4d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -326,5 +326,64 @@ "this_device": "This Device", "remote": "Remote", "stats": "Stats", - "and_n_more": "and {count} more" + "and_n_more": "and {count} more", + "recently_played": "Recently Played", + "browse_more": "Browse More", + "no_title": "No Title", + "not_playing": "Not playing", + "epic_failure": "Epic failure!", + "added_num_tracks_to_queue": "Added {tracks_length} tracks to queue", + "spotube_has_an_update": "Spotube has an update", + "download_now": "Download Now", + "nightly_version": "Spotube Nightly {nightlyBuildNum} has been released", + "release_version": "Spotube v{version} has been released", + "read_the_latest": "Read the latest ", + "release_notes": "release notes", + "pick_color_scheme": "Pick color scheme", + "save": "Save", + "choose_the_device": "Choose the device:", + "multiple_device_connected": "There are multiple device connected.\nChoose the device you want this action to take place", + "nothing_found": "Nothing found", + "the_box_is_empty": "The box is empty", + "top_tracks": "Top Tracks", + "top_artists": "Top Artists", + "top_albums": "Top Albums", + "this_week": "This week", + "this_month": "This month", + "last_6_months": "Last 6 months", + "this_year": "This year", + "last_2_years": "Last 2 years", + "all_time": "All time", + "powered_by_provider": "Powered by {providerName}", + "email": "Email", + "profile_followers": "Followers", + "birthday": "Birthday", + "country": "Country", + "subscription": "Subscription", + "not_born": "Not born", + "hacker": "Hacker", + "profile": "Profile", + "no_name": "No Name", + "edit": "Edit", + "user_profile": "User Profile", + "count_plays": "{count} plays", + "streaming_fees_hypothetical": "Streaming fees (hypothetical)", + "minutes_listened": "Minutes listened", + "streamed_songs": "Streamed songs", + "count_streams": "{count} streams", + "owned_by_you": "Owned by you", + "copied_shareurl_to_clipboard": "Copied {shareUrl} to clipboard", + "spotify_hipotetical_calculation": "*This is calculated based on Spotify's per stream\npayout of $0.003 to $0.005. This is a hypothetical\ncalculation to give user insight about how much they\nwould have paid to the artists if they were to listen\ntheir song in Spotify.", + "count_mins": "{minutes} mins", + "summary_minutes": "minutes", + "summary_listened_to_music": "Listened to music", + "summary_songs": "songs", + "summary_streamed_overall": "Streamed overall", + "summary_owed_to_artists": "Owed to artists\nthis month", + "summary_artists": "artist's", + "summary_music_reached_you": "Music reached you", + "summary_full_albums": "full albums", + "summary_got_your_love": "Got your love", + "summary_playlists": "playlists", + "summary_were_on_repeat": "Were on repeat" } \ No newline at end of file diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index fb00a925f..8f041581e 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -107,6 +107,9 @@ "always_on_top": "Beti ikusgai", "exit_mini_player": "Irten mini erreproduzitzailetik", "download_location": "Deskargen kokapena", + "local_library": "Liburutegi lokala", + "add_library_location": "Gehitu liburutegira", + "remove_library_location": "Kendu liburutegitik", "account": "Kontua", "login_with_spotify": "Hasi saioa zure Spotify kontuarekin", "connect_with_spotify": "Spotify-rekin konektatu", @@ -118,8 +121,8 @@ "market_place_region": "Dendaren herrialdea", "recommendation_country": "Gomendio herrialdea", "appearance": "Itxura", - "layout_mode": "Diseinu modua", - "override_layout_settings": "Responsive diseinu moduaren ezarpenak ezeztatu", + "layout_mode": "Diseinua", + "override_layout_settings": "Responsive diseinuaren ezarpenak ezeztatu", "adaptive": "Moldagarria", "compact": "Trinkoa", "extended": "Hedatua", @@ -287,7 +290,7 @@ "genres": "Generoak", "explore_genres": "Esploratu generoak", "friends": "Lagunak", - "no_lyrics_available": "Sentitzen dut, ezin dira kanta honen hitzak aurkitu", + "no_lyrics_available": "Sentitzen dugu, 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?", @@ -295,6 +298,7 @@ "delete_playlist": "Ezabatu zerrenda", "delete_playlist_confirmation": "Ziur zaude zerrenda ezabatu nahi duzula?", "local_tracks": "Kanta lokalak", + "local_tab": "Lokalean", "song_link": "Kantaren lotura", "skip_this_nonsense": "Utzi txorakeria hau", "freedom_of_music": "“Musika Askatasuna”", @@ -321,9 +325,65 @@ "connect_client_alert": "{client} gailuak kontrolatzen zaitu", "this_device": "Gailu hau", "remote": "Urrunekoa", - "local_library": "Liburutegi lokala", - "add_library_location": "Gehitu liburutegira", - "remove_library_location": "Kendu liburutegitik", - "local_tab": "Tokiko", - "stats": "Estatistikak" + "stats": "Estatistikak", + "and_n_more": "eta {count} gehiago", + "recently_played": "Berriki entzunak", + "browse_more": "Gehiago Bilatu", + "no_title": "Titulurik ez", + "not_playing": "Erreprodukziorik ez", + "epic_failure": "Sekulako errorea!", + "added_num_tracks_to_queue": "{tracks_length} kanta gehitu dira zerrendara", + "spotube_has_an_update": "Spotube-ren eguneraketa bat dago", + "download_now": "Orain deskargatu", + "nightly_version": "Spotube {nightlyBuildNum} Nightly-a argitaratu da", + "release_version": "Spotube v{version} argitaratu da", + "read_the_latest": "Irakurri azken ", + "release_notes": "argitatratze oharrak", + "pick_color_scheme": "Aukeratu kolore eskema", + "save": "Gorde", + "choose_the_device": "Aukeratu gailua:", + "multiple_device_connected": "Hainbat gailu daude konektatuta.\nAukeratu zein gailutan aplikatu nahi duzun ekintza hau", + "nothing_found": "Ezer ez da aurkitu", + "the_box_is_empty": "Kaxa hutsik dago", + "top_tracks": "Top Kantak", + "top_artists": "Top Artistak", + "top_albums": "Top Albumak", + "this_week": "Aste honetan", + "this_month": "Hilabete honetan", + "last_6_months": "Azken 6 hilabeteetan", + "this_year": "Aurten", + "last_2_years": "Azken 2 urtetan", + "all_time": "Betidanik", + "powered_by_provider": "{providerName}-ren eskutik", + "email": "Email", + "profile_followers": "Jarraitzaileak", + "birthday": "Jaiotze-data", + "country": "Herrialdea", + "subscription": "Harpidetzak", + "not_born": "Jaio gabe", + "hacker": "Hacker", + "profile": "Profila", + "no_name": "Izenik Ez", + "edit": "Editatu", + "user_profile": "Erabiltzaile Profila", + "count_plays": "{count} erreprodukzio", + "streaming_fees_hypothetical": "Streaming ordainketa (hipotetikoa)", + "minutes_listened": "Entzundako minutuak", + "streamed_songs": "Stream-eatutako kantak", + "count_streams": "{count} stream", + "owned_by_you": "Zure jabetzakoa", + "copied_shareurl_to_clipboard": "{shareUrl} arbelera kopiatua", + "spotify_hipotetical_calculation": "*Sportify-k stream bakoitzeko duen $0.003 eta $0.005\nordainsarian oinarritua da. Kalkulu hipotetiko bat,\nkanta hauek Spotify-n entzun bazenitu,\nberaiek artistari zenbat ordaiduko lioketen jakin dezazun.", + "count_mins": "{minutes} minutu", + "summary_minutes": "minutu", + "summary_listened_to_music": "Musika entzuten", + "summary_songs": "kanta", + "summary_streamed_overall": "Stream-eatuta oro har", + "summary_owed_to_artists": "Hilabete honetan\nartistei zor zaiena", + "summary_artists": "artisten", + "summary_music_reached_you": "Musika ailegatu zaizu", + "summary_full_albums": "album osok", + "summary_got_your_love": "Izan dute zure maitasuna", + "summary_playlists": "zerrenda", + "summary_were_on_repeat": "Dituzu errepikatze moduan" } \ No newline at end of file diff --git a/lib/modules/home/sections/feed.dart b/lib/modules/home/sections/feed.dart index f66f01f24..8685fe19b 100644 --- a/lib/modules/home/sections/feed.dart +++ b/lib/modules/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/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.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'; @@ -33,14 +34,14 @@ class HomePageFeedSection extends HookConsumerWidget { else if (item.playlist != null) item.playlist!.asPlaylist ], - title: Text(section.title ?? "No Titel"), + title: Text(section.title ?? context.l10n.no_title), hasNextPage: false, isLoadingNextPage: false, onFetchMore: () {}, titleTrailing: Directionality( textDirection: TextDirection.rtl, child: TextButton.icon( - label: const Text("Browse More"), + label: Text(context.l10n.browse_more), icon: const Icon(SpotubeIcons.angleRight), onPressed: () => ServiceUtils.pushNamed( context, diff --git a/lib/modules/home/sections/friends.dart b/lib/modules/home/sections/friends.dart index d6bed6a8b..6f59c209f 100644 --- a/lib/modules/home/sections/friends.dart +++ b/lib/modules/home/sections/friends.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/home/sections/friends/friend_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/provider/authentication/authentication.dart'; @@ -73,7 +74,7 @@ class HomePageFriendsSection extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'Friends', + context.l10n.friends, style: Theme.of(context).textTheme.titleMedium, ), ), diff --git a/lib/modules/home/sections/recent.dart b/lib/modules/home/sections/recent.dart index b26c0e164..43c0459d9 100644 --- a/lib/modules/home/sections/recent.dart +++ b/lib/modules/home/sections/recent.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/history/recent.dart'; @@ -22,7 +23,7 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget { return Skeletonizer( enabled: history.isLoading, child: HorizontalPlaybuttonCardView( - title: const Text('Recently Played'), + title: Text(context.l10n.recently_played), items: [ for (final item in historyData) if (item.playlist != null) diff --git a/lib/modules/library/playlist_generate/multi_select_field.dart b/lib/modules/library/playlist_generate/multi_select_field.dart index e54fc2ba3..8cafe02fd 100644 --- a/lib/modules/library/playlist_generate/multi_select_field.dart +++ b/lib/modules/library/playlist_generate/multi_select_field.dart @@ -187,7 +187,7 @@ class _MultiSelectDialog extends HookWidget { return AlertDialog( scrollable: true, - title: dialogTitle ?? const Text('Select'), + title: dialogTitle ?? Text(context.l10n.select), contentPadding: mediaQuery.mdAndUp ? null : const EdgeInsets.all(16), insetPadding: const EdgeInsets.all(16), actions: [ diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 6db846929..538af6851 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -233,7 +233,8 @@ class PlayerView extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AutoSizeText( - currentTrack?.name ?? "Not playing", + currentTrack?.name ?? + context.l10n.not_playing, style: TextStyle( color: titleTextColor, fontSize: 22, diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index d6ea2a460..df683a801 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/playbutton_card.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/pages/playlist/playlist.dart'; @@ -133,8 +134,8 @@ class PlaylistCard extends HookConsumerWidget { historyNotifier.addPlaylists([playlist]); if (context.mounted) { final snackbar = SnackBar( - content: - Text("Added ${fetchedInitialTracks.length} tracks to queue"), + content: Text(context.l10n + .added_num_tracks_to_queue(fetchedInitialTracks.length)), action: SnackBarAction( label: "Undo", onPressed: () { diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index b9e4be8fc..78680a1c5 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -76,7 +76,7 @@ class PlaylistCreateDialog extends HookConsumerWidget { scaffold.showSnackBar( SnackBar( content: Text( - l10n.error(error.message ?? "Epic failure!"), + l10n.error(error.message ?? context.l10n.epic_failure), style: theme.textTheme.bodyMedium!.copyWith( color: theme.colorScheme.onError, ), diff --git a/lib/modules/root/update_dialog.dart b/lib/modules/root/update_dialog.dart index 4a3130960..27b857df4 100644 --- a/lib/modules/root/update_dialog.dart +++ b/lib/modules/root/update_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/components/links/anchor_button.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:version/version.dart'; class RootAppUpdateDialog extends StatelessWidget { @@ -16,10 +17,10 @@ class RootAppUpdateDialog extends StatelessWidget { const url = "https://spotube.krtirtho.dev/downloads"; const nightlyUrl = "https://spotube.krtirtho.dev/downloads/nightly"; return AlertDialog( - title: const Text("Spotube has an update"), + title: Text(context.l10n.spotube_has_an_update), actions: [ FilledButton( - child: const Text("Download Now"), + child: Text(context.l10n.download_now), onPressed: () => launchUrlString( nightlyBuildNum != null ? nightlyUrl : url, mode: LaunchMode.externalApplication, @@ -31,16 +32,16 @@ class RootAppUpdateDialog extends StatelessWidget { children: [ Text( nightlyBuildNum != null - ? "Spotube Nightly $nightlyBuildNum has been released" - : "Spotube v$version has been released", + ? context.l10n.nightly_version(nightlyBuildNum!) + : context.l10n.release_version(version!), ), if (nightlyBuildNum == null) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("Read the latest "), + Text(context.l10n.read_the_latest), AnchorButton( - "release notes", + context.l10n.release_notes, style: const TextStyle(color: Colors.blue), onTap: () => launchUrlString( url, diff --git a/lib/modules/settings/color_scheme_picker_dialog.dart b/lib/modules/settings/color_scheme_picker_dialog.dart index 8d0983752..550446bc0 100644 --- a/lib/modules/settings/color_scheme_picker_dialog.dart +++ b/lib/modules/settings/color_scheme_picker_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:system_theme/system_theme.dart'; @@ -69,17 +70,17 @@ class ColorSchemePickerDialog extends HookConsumerWidget { } return AlertDialog( - title: const Text("Pick color scheme"), + title: Text(context.l10n.pick_color_scheme), actions: [ OutlinedButton( - child: const Text("Cancel"), + child: Text(context.l10n.cancel), onPressed: () { Navigator.pop(context); }, ), FilledButton( onPressed: onOk, - child: const Text("Save"), + child: Text(context.l10n.save), ), ], content: SizedBox( diff --git a/lib/modules/stats/summary/summary.dart b/lib/modules/stats/summary/summary.dart index ef8aa1b02..46068fece 100644 --- a/lib/modules/stats/summary/summary.dart +++ b/lib/modules/stats/summary/summary.dart @@ -5,6 +5,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/summary/summary_card.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.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'; @@ -45,8 +46,8 @@ class StatsPageSummarySection extends HookConsumerWidget { delegate: SliverChildListDelegate([ SummaryCard( title: summaryData.duration.inMinutes.toDouble(), - unit: "minutes", - description: 'Listened to music', + unit: context.l10n.summary_minutes, + description: context.l10n.summary_listened_to_music, color: Colors.purple, onTap: () { ServiceUtils.pushNamed(context, StatsMinutesPage.name); @@ -54,8 +55,8 @@ class StatsPageSummarySection extends HookConsumerWidget { ), SummaryCard( title: summaryData.tracks.toDouble(), - unit: "songs", - description: 'Streamed overall', + unit: context.l10n.summary_songs, + description: context.l10n.summary_streamed_overall, color: Colors.lightBlue, onTap: () { ServiceUtils.pushNamed(context, StatsStreamsPage.name); @@ -64,7 +65,7 @@ class StatsPageSummarySection extends HookConsumerWidget { SummaryCard.unformatted( title: usdFormatter.format(summaryData.fees.toDouble()), unit: "", - description: 'Owed to artists\nthis month', + description: context.l10n.summary_owed_to_artists, color: Colors.green, onTap: () { ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); @@ -72,8 +73,8 @@ class StatsPageSummarySection extends HookConsumerWidget { ), SummaryCard( title: summaryData.artists.toDouble(), - unit: "artist's", - description: 'Music reached you', + unit: context.l10n.summary_artists, + description: context.l10n.summary_music_reached_you, color: Colors.yellow, onTap: () { ServiceUtils.pushNamed(context, StatsArtistsPage.name); @@ -81,8 +82,8 @@ class StatsPageSummarySection extends HookConsumerWidget { ), SummaryCard( title: summaryData.albums.toDouble(), - unit: "full albums", - description: 'Got your love', + unit: context.l10n.summary_full_albums, + description: context.l10n.summary_got_your_love, color: Colors.pink, onTap: () { ServiceUtils.pushNamed(context, StatsAlbumsPage.name); @@ -90,8 +91,8 @@ class StatsPageSummarySection extends HookConsumerWidget { ), SummaryCard( title: summaryData.playlists.toDouble(), - unit: "playlists", - description: 'Were on repeat', + unit: context.l10n.summary_playlists, + description: context.l10n.summary_were_on_repeat, color: Colors.teal, onTap: () { ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); diff --git a/lib/modules/stats/top/albums.dart b/lib/modules/stats/top/albums.dart index 4329b871e..e401340e6 100644 --- a/lib/modules/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/albums.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -35,7 +36,8 @@ class TopAlbums extends HookConsumerWidget { return StatsAlbumItem( album: album.album, info: Text( - "${compactNumberFormatter.format(album.count)} plays", + context.l10n + .count_plays(compactNumberFormatter.format(album.count)), ), ); }, diff --git a/lib/modules/stats/top/artists.dart b/lib/modules/stats/top/artists.dart index d5eb2d0e7..3e4e098d4 100644 --- a/lib/modules/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -38,7 +39,10 @@ class TopArtists extends HookConsumerWidget { final artist = artistsData[index]; return StatsArtistItem( artist: artist.artist, - info: Text("${compactNumberFormatter.format(artist.count)} plays"), + info: Text( + context.l10n + .count_plays(compactNumberFormatter.format(artist.count)), + ), ); }, ), diff --git a/lib/modules/stats/top/top.dart b/lib/modules/stats/top/top.dart index ea52c5173..643064aa4 100644 --- a/lib/modules/stats/top/top.dart +++ b/lib/modules/stats/top/top.dart @@ -5,6 +5,7 @@ import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/tracks.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; @@ -24,23 +25,23 @@ class StatsPageTopSection extends HookConsumerWidget { floating: true, flexibleSpace: ThemedButtonsTabBar( controller: tabController, - tabs: const [ + tabs: [ Tab( child: Padding( - padding: EdgeInsets.all(5), - child: Text("Top Tracks"), + padding: const EdgeInsets.all(5), + child: Text(context.l10n.top_tracks), ), ), Tab( child: Padding( - padding: EdgeInsets.all(5), - child: Text("Top Artists"), + padding: const EdgeInsets.all(5), + child: Text(context.l10n.top_artists), ), ), Tab( child: Padding( - padding: EdgeInsets.all(5), - child: Text("Top Albums"), + padding: const EdgeInsets.all(5), + child: Text(context.l10n.top_albums), ), ), ], @@ -61,30 +62,30 @@ class StatsPageTopSection extends HookConsumerWidget { historyDurationNotifier.update((_) => value); }, icon: const Icon(Icons.arrow_drop_down), - items: const [ + items: [ DropdownMenuItem( value: HistoryDuration.days7, - child: Text("This week"), + child: Text(context.l10n.this_week), ), DropdownMenuItem( value: HistoryDuration.days30, - child: Text("This month"), + child: Text(context.l10n.this_month), ), DropdownMenuItem( value: HistoryDuration.months6, - child: Text("Last 6 months"), + child: Text(context.l10n.last_6_months), ), DropdownMenuItem( value: HistoryDuration.year, - child: Text("This year"), + child: Text(context.l10n.this_year), ), DropdownMenuItem( value: HistoryDuration.years2, - child: Text("Last 2 years"), + child: Text(context.l10n.last_2_years), ), DropdownMenuItem( value: HistoryDuration.allTime, - child: Text("All time"), + child: Text(context.l10n.all_time), ), ], ), diff --git a/lib/modules/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart index be457b2e3..7fba220de 100644 --- a/lib/modules/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -37,7 +38,8 @@ class TopTracks extends HookConsumerWidget { return StatsTrackItem( track: track.track, info: Text( - "${compactNumberFormatter.format(track.count)} plays", + context.l10n + .count_plays(compactNumberFormatter.format(track.count)), ), ); }, diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 18ce6e28a..810c18d6c 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -73,7 +73,7 @@ class LyricsPage extends HookConsumerWidget { return Align( alignment: Alignment.bottomRight, - child: Text("Powered by $providerName"), + child: Text(context.l10n.powered_by_provider(providerName)), ); }, ), diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index c2bf7b81f..643c10640 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -103,7 +103,7 @@ class SyncedLyrics extends HookConsumerWidget { backgroundColor: Colors.transparent, centerTitle: true, title: Text( - playlist.activeTrack?.name ?? "Not Playing", + playlist.activeTrack?.name ?? context.l10n.not_playing, style: headlineTextStyle, ), bottom: PreferredSize( diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index e6546960c..67bd8f570 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -27,21 +28,22 @@ class ProfilePage extends HookConsumerWidget { final userProperties = useMemoized( () => { - "Email": meData.email ?? "N/A", - "Followers": meData.followers?.total.toString() ?? "N/A", - "Birthday": meData.birthdate ?? "Not born", - "Country": spotifyMarkets + context.l10n.email: meData.email ?? "N/A", + context.l10n.profile_followers: + meData.followers?.total.toString() ?? "N/A", + context.l10n.birthday: meData.birthdate ?? context.l10n.not_born, + context.l10n.country: spotifyMarkets .firstWhere((market) => market.$1 == meData.country) .$2, - "Subscription": meData.product ?? "Hacker", + context.l10n.subscription: meData.product ?? context.l10n.hacker, }, [meData], ); return SafeArea( child: Scaffold( - appBar: const PageWindowTitleBar( - title: Text("Profile"), + appBar: PageWindowTitleBar( + title: Text(context.l10n.profile), titleSpacing: 0, automaticallyImplyLeading: true, centerTitle: false, @@ -72,7 +74,7 @@ class ProfilePage extends HookConsumerWidget { const SliverGap(10), SliverToBoxAdapter( child: Text( - meData.displayName ?? "No Name", + meData.displayName ?? context.l10n.no_name, style: textTheme.titleLarge, textAlign: TextAlign.center, ), @@ -85,7 +87,7 @@ class ProfilePage extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( - label: const Text("Edit"), + label: Text(context.l10n.edit), icon: const Icon(SpotubeIcons.edit), onPressed: () { launchUrlString( @@ -118,7 +120,7 @@ class ProfilePage extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(6), child: Text( - key, + '$key', style: textTheme.titleSmall, ), ), diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 7e37b68be..1b5b7e398 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -92,7 +92,7 @@ class SettingsAccountSection extends HookConsumerWidget { if (auth.asData?.value != null) ListTile( leading: const Icon(SpotubeIcons.user), - title: const Text("User Profile"), + title: Text(context.l10n.user_profile), trailing: Padding( padding: const EdgeInsets.all(8.0), child: CircleAvatar( diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index db0eedf6b..e14a2f320 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -4,6 +4,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/albums.dart'; @@ -24,10 +25,10 @@ class StatsAlbumsPage extends HookConsumerWidget { final albumsData = topAlbums.asData?.value.items ?? []; return Scaffold( - appBar: const PageWindowTitleBar( + appBar: PageWindowTitleBar( automaticallyImplyLeading: true, centerTitle: false, - title: Text("Albums"), + title: Text(context.l10n.albums), ), body: Skeletonizer( enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, @@ -43,9 +44,8 @@ class StatsAlbumsPage extends HookConsumerWidget { final album = albumsData[index]; return StatsAlbumItem( album: album.album, - info: Text( - "${compactNumberFormatter.format(album.count)} plays", - ), + info: Text(context.l10n + .count_plays(compactNumberFormatter.format(album.count))), ); }, ), diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index 80ff5f235..436bbb57c 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -5,6 +5,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/tracks.dart'; @@ -27,10 +28,10 @@ class StatsArtistsPage extends HookConsumerWidget { () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); return Scaffold( - appBar: const PageWindowTitleBar( + appBar: PageWindowTitleBar( automaticallyImplyLeading: true, centerTitle: false, - title: Text("Artists"), + title: Text(context.l10n.artists), ), body: Skeletonizer( enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, @@ -46,8 +47,8 @@ class StatsArtistsPage extends HookConsumerWidget { final artist = artistsData[index]; return StatsArtistItem( artist: artist.artist, - info: - Text("${compactNumberFormatter.format(artist.count)} plays"), + info: Text(context.l10n + .count_plays(compactNumberFormatter.format(artist.count))), ); }, ), diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 33d223aed..5f9aa779f 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -6,6 +6,7 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/tracks.dart'; @@ -40,10 +41,10 @@ class StatsStreamFeesPage extends HookConsumerWidget { ); return Scaffold( - appBar: const PageWindowTitleBar( + appBar: PageWindowTitleBar( automaticallyImplyLeading: true, centerTitle: false, - title: Text("Streaming fees (hypothetical)"), + title: Text(context.l10n.streaming_fees_hypothetical), ), body: CustomScrollView( slivers: [ @@ -54,11 +55,7 @@ class StatsStreamFeesPage extends HookConsumerWidget { 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.", + context.l10n.spotify_hipotetical_calculation, style: textTheme.bodySmall?.copyWith( color: hintColor, ), diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index ea3048ef4..35bea3abd 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -5,6 +5,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/tracks.dart'; @@ -27,8 +28,8 @@ class StatsMinutesPage extends HookConsumerWidget { final tracksData = topTracks.asData?.value.items ?? []; return Scaffold( - appBar: const PageWindowTitleBar( - title: Text("Minutes listened"), + appBar: PageWindowTitleBar( + title: Text(context.l10n.minutes_listened), centerTitle: false, automaticallyImplyLeading: true, ), @@ -48,7 +49,8 @@ class StatsMinutesPage extends HookConsumerWidget { return StatsTrackItem( track: track.track, info: Text( - "${compactNumberFormatter.format(track.count)} plays", + context.l10n + .count_plays(compactNumberFormatter.format(track.count)), ), ); }, diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index a6db3e1cf..4e83b0a27 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -4,6 +4,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/playlists.dart'; @@ -25,10 +26,10 @@ class StatsPlaylistsPage extends HookConsumerWidget { final playlistsData = topPlaylists.asData?.value.items ?? []; return Scaffold( - appBar: const PageWindowTitleBar( + appBar: PageWindowTitleBar( automaticallyImplyLeading: true, centerTitle: false, - title: Text("Playlists"), + title: Text(context.l10n.playlists), ), body: Skeletonizer( enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, @@ -45,7 +46,9 @@ class StatsPlaylistsPage extends HookConsumerWidget { return StatsPlaylistItem( playlist: playlist.playlist, info: Text( - "${compactNumberFormatter.format(playlist.count)} plays"), + context.l10n + .count_plays(compactNumberFormatter.format(playlist.count)), + ), ); }, ), diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index dd5856d0a..5c90e8791 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -5,6 +5,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/history/top.dart'; import 'package:spotube/provider/history/top/tracks.dart'; @@ -27,8 +28,8 @@ class StatsStreamsPage extends HookConsumerWidget { final tracksData = topTracks.asData?.value.items ?? []; return Scaffold( - appBar: const PageWindowTitleBar( - title: Text("Streamed songs"), + appBar: PageWindowTitleBar( + title: Text(context.l10n.streamed_songs), centerTitle: false, automaticallyImplyLeading: true, ), @@ -48,7 +49,8 @@ class StatsStreamsPage extends HookConsumerWidget { return StatsTrackItem( track: track.track, info: Text( - "${compactNumberFormatter.format(track.count * track.track.duration!.inMinutes)} mins", + context.l10n.count_mins(compactNumberFormatter + .format(track.count * track.track.duration!.inMinutes)), ), ); }, From 9b7a7ef1cfe119e6c94ed4a1774a29c148544e79 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 10 Aug 2024 22:54:25 +0600 Subject: [PATCH 88/92] chore: update translations and refactor to flutter 3.22 ThemeData --- .../adaptive/adaptive_pop_sheet_list.dart | 2 +- .../fallbacks/anonymous_fallback.dart | 8 +- lib/components/links/anchor_button.dart | 2 +- lib/components/links/artist_link.dart | 1 - lib/components/playbutton_card.dart | 6 +- lib/components/themed_button_tab_bar.dart | 2 +- lib/components/titlebar/titlebar_buttons.dart | 12 +- lib/l10n/app_ar.arb | 61 +++++++++- lib/l10n/app_bn.arb | 61 +++++++++- lib/l10n/app_ca.arb | 61 +++++++++- lib/l10n/app_cs.arb | 61 +++++++++- lib/l10n/app_de.arb | 61 +++++++++- lib/l10n/app_en.arb | 5 +- lib/l10n/app_es.arb | 61 +++++++++- lib/l10n/app_eu.arb | 5 +- lib/l10n/app_fa.arb | 61 +++++++++- lib/l10n/app_fi.arb | 61 +++++++++- lib/l10n/app_fr.arb | 61 +++++++++- lib/l10n/app_hi.arb | 61 +++++++++- lib/l10n/app_id.arb | 61 +++++++++- lib/l10n/app_it.arb | 61 +++++++++- lib/l10n/app_ja.arb | 61 +++++++++- lib/l10n/app_ka.arb | 61 +++++++++- lib/l10n/app_ko.arb | 61 +++++++++- lib/l10n/app_ne.arb | 61 +++++++++- lib/l10n/app_nl.arb | 61 +++++++++- lib/l10n/app_pl.arb | 61 +++++++++- lib/l10n/app_pt.arb | 61 +++++++++- lib/l10n/app_ru.arb | 64 ++++++++++- lib/l10n/app_th.arb | 61 +++++++++- lib/l10n/app_tr.arb | 61 +++++++++- lib/l10n/app_uk.arb | 61 +++++++++- lib/l10n/app_vi.arb | 61 +++++++++- lib/l10n/app_zh.arb | 61 +++++++++- lib/modules/artist/artist_card.dart | 4 +- .../home/sections/friends/friend_item.dart | 2 +- lib/modules/home/sections/genres.dart | 2 +- .../local_folder/local_folder_item.dart | 2 +- .../playlist_generate/multi_select_field.dart | 2 +- lib/modules/player/player.dart | 2 +- lib/modules/player/player_queue.dart | 3 +- lib/modules/player/sibling_tracks_sheet.dart | 3 +- lib/modules/root/bottom_player.dart | 6 +- lib/modules/root/sidebar.dart | 2 +- lib/modules/root/spotube_navigation_bar.dart | 2 +- .../settings/color_scheme_picker_dialog.dart | 4 +- lib/pages/lyrics/lyrics.dart | 2 +- lib/pages/lyrics/mini_lyrics.dart | 14 +-- lib/pages/profile/profile.dart | 2 +- lib/pages/root/root_app.dart | 4 +- lib/pages/search/search.dart | 6 +- lib/pages/settings/sections/about.dart | 7 +- lib/pages/settings/sections/accounts.dart | 2 +- lib/pages/stats/fees/fees.dart | 16 +-- lib/provider/audio_player/audio_player.dart | 2 - .../audio_players_streams_mixin.dart | 7 +- lib/themes/theme.dart | 19 ++-- untranslated_messages.json | 106 +----------------- 58 files changed, 1579 insertions(+), 213 deletions(-) diff --git a/lib/components/adaptive/adaptive_pop_sheet_list.dart b/lib/components/adaptive/adaptive_pop_sheet_list.dart index 1686801c9..97dc61321 100644 --- a/lib/components/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/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/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart index 799297e3e..62ed8ddd6 100644 --- a/lib/components/fallbacks/anonymous_fallback.dart +++ b/lib/components/fallbacks/anonymous_fallback.dart @@ -15,9 +15,13 @@ class AnonymousFallback extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final isLoggedIn = ref.watch(authenticationProvider) != null; + final isLoggedIn = ref.watch(authenticationProvider); - if (isLoggedIn && child != null) return child!; + if (isLoggedIn.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (isLoggedIn.asData?.value != null && child != null) return child!; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/components/links/anchor_button.dart b/lib/components/links/anchor_button.dart index d78bbf962..c6f0b889b 100644 --- a/lib/components/links/anchor_button.dart +++ b/lib/components/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/links/artist_link.dart b/lib/components/links/artist_link.dart index d5ec24f87..9f06f1b37 100644 --- a/lib/components/links/artist_link.dart +++ b/lib/components/links/artist_link.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/extensions/context.dart'; diff --git a/lib/components/playbutton_card.dart b/lib/components/playbutton_card.dart index a1a9bfb44..d540d31e2 100644 --- a/lib/components/playbutton_card.dart +++ b/lib/components/playbutton_card.dart @@ -64,12 +64,12 @@ class PlaybuttonCard extends HookWidget { margin: margin, child: Material( color: Color.lerp( - theme.colorScheme.surfaceVariant, + theme.colorScheme.surfaceContainerHighest, theme.colorScheme.surface, useBrightnessValue(.9, .7), ), borderRadius: radius, - shadowColor: theme.colorScheme.background, + shadowColor: theme.colorScheme.surface, elevation: 3, child: InkWell( mouseCursor: SystemMouseCursors.click, @@ -149,7 +149,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/themed_button_tab_bar.dart b/lib/components/themed_button_tab_bar.dart index b21ca9924..c245e5f4e 100644 --- a/lib/components/themed_button_tab_bar.dart +++ b/lib/components/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/titlebar/titlebar_buttons.dart b/lib/components/titlebar/titlebar_buttons.dart index 425bf2f16..35cdf08e1 100644 --- a/lib/components/titlebar/titlebar_buttons.dart +++ b/lib/components/titlebar/titlebar_buttons.dart @@ -42,16 +42,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/l10n/app_ar.arb b/lib/l10n/app_ar.arb index b474ec7ea..a962b41ba 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -325,5 +325,64 @@ "add_library_location": "أضف إلى المكتبة", "remove_library_location": "إزالة من المكتبة", "local_tab": "محلي", - "stats": "إحصائيات" + "stats": "إحصائيات", + "and_n_more": "و {count} أكثر", + "recently_played": "تم تشغيله مؤخرًا", + "browse_more": "تصفح المزيد", + "no_title": "بدون عنوان", + "not_playing": "غير مشغل", + "epic_failure": "فشل كبير!", + "added_num_tracks_to_queue": "تمت إضافة {tracks_length} مسارات إلى قائمة الانتظار", + "spotube_has_an_update": "يوجد تحديث لسبوتيوب", + "download_now": "تحميل الآن", + "nightly_version": "تم إصدار سبوتيوب الليلي {nightlyBuildNum}", + "release_version": "تم إصدار سبوتيوب v{version}", + "read_the_latest": "اقرأ الأحدث", + "release_notes": "ملاحظات الإصدار", + "pick_color_scheme": "اختر نظام الألوان", + "save": "حفظ", + "choose_the_device": "اختر الجهاز:", + "multiple_device_connected": "تم توصيل أجهزة متعددة.\nاختر الجهاز الذي تريد إجراء هذه العملية عليه", + "nothing_found": "لم يتم العثور على شيء", + "the_box_is_empty": "الصندوق فارغ", + "top_artists": "أفضل الفنانين", + "top_albums": "أفضل الألبومات", + "this_week": "هذا الأسبوع", + "this_month": "هذا الشهر", + "last_6_months": "آخر 6 أشهر", + "this_year": "هذا العام", + "last_2_years": "آخر سنتين", + "all_time": "كل الوقت", + "powered_by_provider": "مدعوم من {providerName}", + "email": "البريد الإلكتروني", + "profile_followers": "المتابعين", + "birthday": "عيد الميلاد", + "subscription": "اشتراك", + "not_born": "لم يولد", + "hacker": "هاكر", + "profile": "الملف الشخصي", + "no_name": "بدون اسم", + "edit": "تعديل", + "user_profile": "ملف المستخدم", + "count_plays": "{count} تشغيلات", + "streaming_fees_hypothetical": "رسوم البث (افتراضية)", + "minutes_listened": "الدقائق المستمعة", + "streamed_songs": "الأغاني المذاعة", + "count_streams": "{count} بث", + "owned_by_you": "مملوك لك", + "copied_shareurl_to_clipboard": "تم نسخ {shareUrl} إلى الحافظة", + "spotify_hipotetical_calculation": "*هذا محسوب بناءً على الدفع لكل بث من سبوتيفاي\nبقيمة 0.003 إلى 0.005 دولار. هذا حساب افتراضي\nلإعطاء المستخدم فكرة عن المبلغ الذي\nكان سيدفعه للفنانين إذا كانوا قد استمعوا\nإلى أغنيتهم على سبوتيفاي.", + "count_mins": "{minutes} دقيقة", + "summary_minutes": "الدقائق", + "summary_listened_to_music": "استمعت إلى الموسيقى", + "summary_songs": "أغاني", + "summary_streamed_overall": "بث بشكل عام", + "summary_owed_to_artists": "مدين للفنانين\nهذا الشهر", + "summary_artists": "الفنانين", + "summary_music_reached_you": "وصلت إليك الموسيقى", + "summary_full_albums": "ألبومات كاملة", + "summary_got_your_love": "حصلت على حبك", + "summary_playlists": "قوائم التشغيل", + "summary_were_on_repeat": "كانت على التكرار", + "total_money": "المجموع {money}" } \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 2cf8dd43e..97872c8c9 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -325,5 +325,64 @@ "add_library_location": "লাইব্রেরিতে যোগ করুন", "remove_library_location": "লাইব্রেরি থেকে সরান", "local_tab": "স্থানীয়", - "stats": "পরিসংখ্যান" + "stats": "পরিসংখ্যান", + "and_n_more": "এবং {count} আরও", + "recently_played": "সম্প্রতি বাজানো", + "browse_more": "আরও ব্রাউজ করুন", + "no_title": "কোনো শিরোনাম নেই", + "not_playing": "চালানো হচ্ছে না", + "epic_failure": "বিরাট ব্যর্থতা!", + "added_num_tracks_to_queue": "{tracks_length} ট্র্যাক সারিতে যোগ করা হয়েছে", + "spotube_has_an_update": "স্পটিউবে একটি আপডেট আছে", + "download_now": "এখনই ডাউনলোড করুন", + "nightly_version": "স্পটিউব নাইটলি {nightlyBuildNum} প্রকাশিত হয়েছে", + "release_version": "স্পটিউব v{version} প্রকাশিত হয়েছে", + "read_the_latest": "সর্বশেষ পড়ুন", + "release_notes": "রিলিজ নোট", + "pick_color_scheme": "রঙের থিম নির্বাচন করুন", + "save": "সংরক্ষণ করুন", + "choose_the_device": "ডিভাইস নির্বাচন করুন:", + "multiple_device_connected": "একাধিক ডিভাইস সংযুক্ত রয়েছে।\nযে ডিভাইসে আপনি এই ক্রিয়াটি চালাতে চান সেটি নির্বাচন করুন", + "nothing_found": "কিছুই পাওয়া যায়নি", + "the_box_is_empty": "বাক্সটি খালি", + "top_artists": "শীর্ষ শিল্পী", + "top_albums": "শীর্ষ অ্যালবাম", + "this_week": "এই সপ্তাহ", + "this_month": "এই মাস", + "last_6_months": "গত ৬ মাস", + "this_year": "এই বছর", + "last_2_years": "গত ২ বছর", + "all_time": "সব সময়", + "powered_by_provider": "{providerName} দ্বারা চালিত", + "email": "ইমেইল", + "profile_followers": "অনুসারী", + "birthday": "জন্মদিন", + "subscription": "সাবস্ক্রিপশন", + "not_born": "জন্মগ্রহণ করেনি", + "hacker": "হ্যাকার", + "profile": "প্রোফাইল", + "no_name": "কোন নাম নেই", + "edit": "সম্পাদনা করুন", + "user_profile": "ব্যবহারকারীর প্রোফাইল", + "count_plays": "{count} বার প্লে হয়েছে", + "streaming_fees_hypothetical": "স্ট্রিমিং ফি (ধারণাগত)", + "minutes_listened": "শুনেছেন মিনিট", + "streamed_songs": "স্ট্রিম করা গান", + "count_streams": "{count} বার স্ট্রিম", + "owned_by_you": "আপনার মালিকানাধীন", + "copied_shareurl_to_clipboard": "{shareUrl} ক্লিপবোর্ডে কপি করা হয়েছে", + "spotify_hipotetical_calculation": "*এটি স্পোটিফাইয়ের প্রতি স্ট্রিম\n$0.003 থেকে $0.005 পেআউটের ভিত্তিতে গণনা করা হয়েছে। এটি একটি ধারণাগত\nগণনা ব্যবহারকারীদেরকে জানাতে দেয় যে কত টাকা\nতারা শিল্পীদের দিতো যদি তারা স্পোটিফাইতে\nতাদের গান শুনতেন।", + "count_mins": "{minutes} মিনিট", + "summary_minutes": "মিনিট", + "summary_listened_to_music": "সঙ্গীত শুনেছেন", + "summary_songs": "গান", + "summary_streamed_overall": "মোট স্ট্রিম", + "summary_owed_to_artists": "এই মাসে\nশিল্পীদেরকে ঋণী", + "summary_artists": "শিল্পীর", + "summary_music_reached_you": "আপনার কাছে পৌঁছেছে সঙ্গীত", + "summary_full_albums": "সম্পূর্ণ অ্যালবাম", + "summary_got_your_love": "আপনার ভালোবাসা পেয়েছে", + "summary_playlists": "প্লেলিস্ট", + "summary_were_on_repeat": "পুনরাবৃত্তিতে ছিল", + "total_money": "মোট {money}" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index ca4b019ae..2cda6e88c 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -325,5 +325,64 @@ "add_library_location": "Afegeix a la biblioteca", "remove_library_location": "Elimina de la biblioteca", "local_tab": "Local", - "stats": "Estadístiques" + "stats": "Estadístiques", + "and_n_more": "i {count} més", + "recently_played": "Reproduït recentment", + "browse_more": "Navega més", + "no_title": "Sense títol", + "not_playing": "No s'està reproduint", + "epic_failure": "Fracàs èpic!", + "added_num_tracks_to_queue": "Afegit {tracks_length} pistes a la cua", + "spotube_has_an_update": "Spotube té una actualització", + "download_now": "Descarregar ara", + "nightly_version": "Spotube Nightly {nightlyBuildNum} ha estat publicat", + "release_version": "Spotube v{version} ha estat publicat", + "read_the_latest": "Llegeix el més recent", + "release_notes": "notes de la versió", + "pick_color_scheme": "Tria l'esquema de colors", + "save": "Desar", + "choose_the_device": "Tria el dispositiu:", + "multiple_device_connected": "Hi ha diversos dispositius connectats.\nTria el dispositiu on vols realitzar aquesta acció", + "nothing_found": "No s'ha trobat res", + "the_box_is_empty": "La caixa està buida", + "top_artists": "Millors artistes", + "top_albums": "Millors àlbums", + "this_week": "Aquesta setmana", + "this_month": "Aquest mes", + "last_6_months": "Últims 6 mesos", + "this_year": "Aquest any", + "last_2_years": "Últims 2 anys", + "all_time": "Tots els temps", + "powered_by_provider": "Funciona amb {providerName}", + "email": "Correu electrònic", + "profile_followers": "Seguidors", + "birthday": "Aniversari", + "subscription": "Subscripció", + "not_born": "No ha nascut", + "hacker": "Hacker", + "profile": "Perfil", + "no_name": "Sense nom", + "edit": "Editar", + "user_profile": "Perfil d'usuari", + "count_plays": "{count} reproduccions", + "streaming_fees_hypothetical": "Comissions de streaming (hipotètic)", + "minutes_listened": "minuts escoltats", + "streamed_songs": "cançons reproduïdes", + "count_streams": "{count} reproduccions", + "owned_by_you": "De la teva propietat", + "copied_shareurl_to_clipboard": "S'ha copiat {shareUrl} al porta-retalls", + "spotify_hipotetical_calculation": "*Això es calcula basant-se en els\npagaments per reproducció de Spotify de $0.003 a $0.005.\nAquest és un càlcul hipotètic per\ndonar als usuaris una idea de quant\nhaurien pagat als artistes si haguessin escoltat\nla seva cançó a Spotify.", + "count_mins": "{minutes} minuts", + "summary_minutes": "minuts", + "summary_listened_to_music": "has escoltat música", + "summary_songs": "cançons", + "summary_streamed_overall": "reproduït en general", + "summary_owed_to_artists": "degut als artistes\nAquest mes", + "summary_artists": "artistes", + "summary_music_reached_you": "La música t'ha arribat", + "summary_full_albums": "Àlbums complets", + "summary_got_your_love": "ha aconseguit el teu amor", + "summary_playlists": "llistes de reproducció", + "summary_were_on_repeat": "estaven en repetició", + "total_money": "total {money}" } \ No newline at end of file diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 7191c1089..b1a22ee26 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -325,5 +325,64 @@ "add_library_location": "Přidat do knihovny", "remove_library_location": "Odebrat z knihovny", "local_tab": "Místní", - "stats": "Statistiky" + "stats": "Statistiky", + "and_n_more": "a dalších {count}", + "recently_played": "Nedávno přehráno", + "browse_more": "Procházet více", + "no_title": "Bez názvu", + "not_playing": "Nepřehrává se", + "epic_failure": "Epické selhání!", + "added_num_tracks_to_queue": "Přidáno {tracks_length} skladeb do fronty", + "spotube_has_an_update": "Spotube má aktualizaci", + "download_now": "Stáhnout nyní", + "nightly_version": "Byla vydána noční verze Spotube {nightlyBuildNum}", + "release_version": "Byla vydána verze Spotube v{version}", + "read_the_latest": "Přečtěte si nejnovější ", + "release_notes": "poznámky k vydání", + "pick_color_scheme": "Vyberte barevné schéma", + "save": "Uložit", + "choose_the_device": "Vyberte zařízení:", + "multiple_device_connected": "Je připojeno více zařízení.\nVyberte zařízení, na kterém chcete provést tuto akci", + "nothing_found": "Nic nenalezeno", + "the_box_is_empty": "Krabice je prázdná", + "top_artists": "Nejlepší umělci", + "top_albums": "Nejlepší alba", + "this_week": "Tento týden", + "this_month": "Tento měsíc", + "last_6_months": "Posledních 6 měsíců", + "this_year": "Tento rok", + "last_2_years": "Poslední 2 roky", + "all_time": "Všechny časy", + "powered_by_provider": "Pohání {providerName}", + "email": "Email", + "profile_followers": "Sledující", + "birthday": "Narozeniny", + "subscription": "Předplatné", + "not_born": "Nenarozen", + "hacker": "Hacker", + "profile": "Profil", + "no_name": "Bez jména", + "edit": "Upravit", + "user_profile": "Uživatelský profil", + "count_plays": "{count} přehrání", + "streaming_fees_hypothetical": "Poplatky za streamování (hypotetické)", + "minutes_listened": "Poslouchané minuty", + "streamed_songs": "Streamované skladby", + "count_streams": "{count} streamů", + "owned_by_you": "Vlastněno vámi", + "copied_shareurl_to_clipboard": "Zkopírováno {shareUrl} do schránky", + "spotify_hipotetical_calculation": "*Toto je vypočítáno na základě výplaty\nza stream Spotify od $0.003 do $0.005.\nToto je hypotetický výpočet,\nabyste měli představu o tom, kolik\nbyste zaplatili umělcům,\npokud byste poslouchali jejich píseň na Spotify.", + "count_mins": "{minutes} minut", + "summary_minutes": "minuty", + "summary_listened_to_music": "Poslouchal(a) hudbu", + "summary_songs": "písně", + "summary_streamed_overall": "Streamováno celkově", + "summary_owed_to_artists": "Dluženo umělcům\nTento měsíc", + "summary_artists": "umělců", + "summary_music_reached_you": "Hudba vás oslovila", + "summary_full_albums": "plná alba", + "summary_got_your_love": "Získal vaši lásku", + "summary_playlists": "playlisty", + "summary_were_on_repeat": "Byly na opakování", + "total_money": "Celkem {money}" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c455e08a6..4b9495aa8 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -325,5 +325,64 @@ "add_library_location": "Zur Bibliothek hinzufügen", "remove_library_location": "Aus der Bibliothek entfernen", "local_tab": "Lokal", - "stats": "Statistiken" + "stats": "Statistiken", + "and_n_more": "und {count} mehr", + "recently_played": "Zuletzt gespielt", + "browse_more": "Mehr durchsuchen", + "no_title": "Kein Titel", + "not_playing": "Wird nicht abgespielt", + "epic_failure": "Episches Versagen!", + "added_num_tracks_to_queue": "{tracks_length} Titel zur Warteschlange hinzugefügt", + "spotube_has_an_update": "Spotube hat ein Update", + "download_now": "Jetzt herunterladen", + "nightly_version": "Spotube Nightly {nightlyBuildNum} wurde veröffentlicht", + "release_version": "Spotube v{version} wurde veröffentlicht", + "read_the_latest": "Lese die neuesten ", + "release_notes": "Versionshinweise", + "pick_color_scheme": "Farbschema wählen", + "save": "Speichern", + "choose_the_device": "Wähle das Gerät:", + "multiple_device_connected": "Es sind mehrere Geräte verbunden.\nWähle das Gerät, auf dem diese Aktion ausgeführt werden soll", + "nothing_found": "Nichts gefunden", + "the_box_is_empty": "Die Box ist leer", + "top_artists": "Top-Künstler", + "top_albums": "Top-Alben", + "this_week": "Diese Woche", + "this_month": "Diesen Monat", + "last_6_months": "Letzte 6 Monate", + "this_year": "Dieses Jahr", + "last_2_years": "Letzte 2 Jahre", + "all_time": "Alle Zeiten", + "powered_by_provider": "Bereitgestellt von {providerName}", + "email": "Email", + "profile_followers": "Follower", + "birthday": "Geburtstag", + "subscription": "Abonnement", + "not_born": "Nicht geboren", + "hacker": "Hacker", + "profile": "Profil", + "no_name": "Kein Name", + "edit": "Bearbeiten", + "user_profile": "Benutzerprofil", + "count_plays": "{count} Wiedergaben", + "streaming_fees_hypothetical": "Streaming-Gebühren (hypothetisch)", + "minutes_listened": "Gehörte Minuten", + "streamed_songs": "Gestreamte Lieder", + "count_streams": "{count} Streams", + "owned_by_you": "In Ihrem Besitz", + "copied_shareurl_to_clipboard": "{shareUrl} in die Zwischenablage kopiert", + "spotify_hipotetical_calculation": "*Dies ist basierend auf Spotifys\npro Stream Auszahlung von $0,003 bis $0,005\nberechnet. Dies ist eine hypothetische Berechnung,\num dem Benutzer Einblick zu geben,\nwieviel sie den Künstlern gezahlt hätten,\nwenn sie ihren Song auf Spotify gehört hätten.", + "count_mins": "{minutes} Minuten", + "summary_minutes": "Minuten", + "summary_listened_to_music": "Hat Musik gehört", + "summary_songs": "Lieder", + "summary_streamed_overall": "Insgesamt gestreamt", + "summary_owed_to_artists": "Den Künstlern geschuldet\nDiesen Monat", + "summary_artists": "Künstler", + "summary_music_reached_you": "Musik hat Sie erreicht", + "summary_full_albums": "volle Alben", + "summary_got_your_love": "Hat Ihre Liebe gewonnen", + "summary_playlists": "Wiedergabelisten", + "summary_were_on_repeat": "Wurden wiederholt", + "total_money": "Gesamt {money}" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 63c805f4d..06a90d793 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -345,7 +345,6 @@ "multiple_device_connected": "There are multiple device connected.\nChoose the device you want this action to take place", "nothing_found": "Nothing found", "the_box_is_empty": "The box is empty", - "top_tracks": "Top Tracks", "top_artists": "Top Artists", "top_albums": "Top Albums", "this_week": "This week", @@ -358,7 +357,6 @@ "email": "Email", "profile_followers": "Followers", "birthday": "Birthday", - "country": "Country", "subscription": "Subscription", "not_born": "Not born", "hacker": "Hacker", @@ -385,5 +383,6 @@ "summary_full_albums": "full albums", "summary_got_your_love": "Got your love", "summary_playlists": "playlists", - "summary_were_on_repeat": "Were on repeat" + "summary_were_on_repeat": "Were on repeat", + "total_money": "Total {money}" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 6558c743c..6834d845b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -325,5 +325,64 @@ "add_library_location": "Añadir a la biblioteca", "remove_library_location": "Eliminar de la biblioteca", "local_tab": "Local", - "stats": "Estadísticas" + "stats": "Estadísticas", + "and_n_more": "y {count} más", + "recently_played": "Recién reproducido", + "browse_more": "Explorar más", + "no_title": "Sin título", + "not_playing": "No reproduciendo", + "epic_failure": "¡Fallo épico!", + "added_num_tracks_to_queue": "Se añadieron {tracks_length} canciones a la cola", + "spotube_has_an_update": "Spotube tiene una actualización", + "download_now": "Descargar ahora", + "nightly_version": "Spotube Nightly {nightlyBuildNum} ha sido lanzado", + "release_version": "Spotube v{version} ha sido lanzado", + "read_the_latest": "Lee las últimas ", + "release_notes": "notas de la versión", + "pick_color_scheme": "Elige esquema de color", + "save": "Guardar", + "choose_the_device": "Elige el dispositivo:", + "multiple_device_connected": "Hay múltiples dispositivos conectados.\nElige el dispositivo en el que deseas realizar esta acción", + "nothing_found": "Nada encontrado", + "the_box_is_empty": "La caja está vacía", + "top_artists": "Artistas principales", + "top_albums": "Álbumes principales", + "this_week": "Esta semana", + "this_month": "Este mes", + "last_6_months": "Últimos 6 meses", + "this_year": "Este año", + "last_2_years": "Últimos 2 años", + "all_time": "Todos los tiempos", + "powered_by_provider": "Impulsado por {providerName}", + "email": "Correo electrónico", + "profile_followers": "Seguidores", + "birthday": "Cumpleaños", + "subscription": "Suscripción", + "not_born": "No nacido", + "hacker": "Hacker", + "profile": "Perfil", + "no_name": "Sin nombre", + "edit": "Editar", + "user_profile": "Perfil de usuario", + "count_plays": "{count} reproducciones", + "streaming_fees_hypothetical": "Tarifas de streaming (hipotéticas)", + "minutes_listened": "Minutos escuchados", + "streamed_songs": "Canciones reproducidas", + "count_streams": "{count} streams", + "owned_by_you": "En tu posesión", + "copied_shareurl_to_clipboard": "Copiado {shareUrl} al portapapeles", + "spotify_hipotetical_calculation": "*Esto se calcula en base al\npago por stream de Spotify de $0.003 a $0.005.\nEs un cálculo hipotético para dar\nuna idea de cuánto habría\npagado a los artistas si hubieras escuchado\nsu canción en Spotify.", + "count_mins": "{minutes} minutos", + "summary_minutes": "minutos", + "summary_listened_to_music": "Escuchó música", + "summary_songs": "canciones", + "summary_streamed_overall": "Transmitido en general", + "summary_owed_to_artists": "Debido a los artistas\nEste mes", + "summary_artists": "artistas", + "summary_music_reached_you": "La música te alcanzó", + "summary_full_albums": "álbumes completos", + "summary_got_your_love": "Obtuvo tu amor", + "summary_playlists": "listas de reproducción", + "summary_were_on_repeat": "Estaban en repetición", + "total_money": "Total {money}" } \ No newline at end of file diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index 8f041581e..6cc416201 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -345,7 +345,6 @@ "multiple_device_connected": "Hainbat gailu daude konektatuta.\nAukeratu zein gailutan aplikatu nahi duzun ekintza hau", "nothing_found": "Ezer ez da aurkitu", "the_box_is_empty": "Kaxa hutsik dago", - "top_tracks": "Top Kantak", "top_artists": "Top Artistak", "top_albums": "Top Albumak", "this_week": "Aste honetan", @@ -358,7 +357,6 @@ "email": "Email", "profile_followers": "Jarraitzaileak", "birthday": "Jaiotze-data", - "country": "Herrialdea", "subscription": "Harpidetzak", "not_born": "Jaio gabe", "hacker": "Hacker", @@ -385,5 +383,6 @@ "summary_full_albums": "album osok", "summary_got_your_love": "Izan dute zure maitasuna", "summary_playlists": "zerrenda", - "summary_were_on_repeat": "Dituzu errepikatze moduan" + "summary_were_on_repeat": "Dituzu errepikatze moduan", + "total_money": "Guztira {money}" } \ No newline at end of file diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index b939de59d..5611e0cc4 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -325,5 +325,64 @@ "add_library_location": "اضافه کردن به کتابخانه", "remove_library_location": "حذف از کتابخانه", "local_tab": "محلی", - "stats": "آمار" + "stats": "آمار", + "and_n_more": "و {count} بیشتر", + "recently_played": "اخیراً پخش شده", + "browse_more": "بیشتر مرور کنید", + "no_title": "بدون عنوان", + "not_playing": "در حال پخش نیست", + "epic_failure": "شکست حماسی!", + "added_num_tracks_to_queue": "{tracks_length} ترک به صف اضافه شد", + "spotube_has_an_update": "Spotube یک بروزرسانی دارد", + "download_now": "اکنون دانلود کنید", + "nightly_version": "نسخه شبانه Spotube {nightlyBuildNum} منتشر شد", + "release_version": "نسخه Spotube v{version} منتشر شد", + "read_the_latest": "آخرین‌ها را بخوانید", + "release_notes": "یادداشت‌های انتشار", + "pick_color_scheme": "طرح رنگ را انتخاب کنید", + "save": "ذخیره", + "choose_the_device": "دستگاه را انتخاب کنید:", + "multiple_device_connected": "چندین دستگاه متصل هستند.\nدستگاهی را انتخاب کنید که می‌خواهید این عملیات بر روی آن انجام شود", + "nothing_found": "چیزی پیدا نشد", + "the_box_is_empty": "جعبه خالی است", + "top_artists": "بهترین هنرمندان", + "top_albums": "بهترین آلبوم‌ها", + "this_week": "این هفته", + "this_month": "این ماه", + "last_6_months": "۶ ماه گذشته", + "this_year": "امسال", + "last_2_years": "۲ سال گذشته", + "all_time": "همیشه", + "powered_by_provider": "توسط {providerName} پشتیبانی شده است", + "email": "ایمیل", + "profile_followers": "دنبال‌کنندگان", + "birthday": "تولد", + "subscription": "اشتراک", + "not_born": "متولد نشده", + "hacker": "هکر", + "profile": "پروفایل", + "no_name": "بدون نام", + "edit": "ویرایش", + "user_profile": "پروفایل کاربر", + "count_plays": "{count} پخش", + "streaming_fees_hypothetical": "هزینه‌های پخش (فرضی)", + "minutes_listened": "دقایق گوش داده شده", + "streamed_songs": "ترانه‌های پخش شده", + "count_streams": "{count} پخش", + "owned_by_you": "توسط شما مالکیت شده", + "copied_shareurl_to_clipboard": "{shareUrl} به کلیپ‌بورد کپی شد", + "spotify_hipotetical_calculation": "*این بر اساس پرداخت هر پخش اسپاتیفای\nبه مبلغ 0.003 تا 0.005 دلار محاسبه شده است.\nاین یک محاسبه فرضی است که به کاربران نشان دهد چقدر ممکن است\nبه هنرمندان پرداخت می‌کردند اگر ترانه آنها را در اسپاتیفای گوش می‌دادند.", + "count_mins": "{minutes} دقیقه", + "summary_minutes": "دقیقه‌ها", + "summary_listened_to_music": "به موسیقی گوش داده شده", + "summary_songs": "ترانه‌ها", + "summary_streamed_overall": "پخش شده به طور کلی", + "summary_owed_to_artists": "به هنرمندان بدهکار است\nاین ماه", + "summary_artists": "هنرمندان", + "summary_music_reached_you": "موسیقی به شما رسیده است", + "summary_full_albums": "آلبوم‌های کامل", + "summary_got_your_love": "عشق شما را به دست آورد", + "summary_playlists": "لیست‌های پخش", + "summary_were_on_repeat": "در تکرار بودند", + "total_money": "مجموع {money}" } \ No newline at end of file diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index d0767e95e..57f209aba 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -325,5 +325,64 @@ "add_library_location": "Lisää kirjastoon", "remove_library_location": "Poista kirjastosta", "local_tab": "Paikallinen", - "stats": "Tilastot" + "stats": "Tilastot", + "and_n_more": "ja {count} lisää", + "recently_played": "Äskettäin soitetut", + "browse_more": "Selaa lisää", + "no_title": "Ei otsikkoa", + "not_playing": "Ei soi", + "epic_failure": "Epäonnistuminen!", + "added_num_tracks_to_queue": "Lisätty {tracks_length} kappaletta jonoon", + "spotube_has_an_update": "Spotubella on päivitys", + "download_now": "Lataa nyt", + "nightly_version": "Spotube Nightly {nightlyBuildNum} on julkaistu", + "release_version": "Spotube v{version} on julkaistu", + "read_the_latest": "Lue viimeisimmät", + "release_notes": "julkaisumuistiinpanot", + "pick_color_scheme": "Valitse värimaailma", + "save": "Tallenna", + "choose_the_device": "Valitse laite:", + "multiple_device_connected": "Useita laitteita on kytketty.\nValitse laite, jossa haluat toiminnon suorittaa", + "nothing_found": "Ei tuloksia", + "the_box_is_empty": "Laatikko on tyhjä", + "top_artists": "Suosituimmat artistit", + "top_albums": "Suosituimmat albumit", + "this_week": "Tällä viikolla", + "this_month": "Tässä kuussa", + "last_6_months": "Viimeiset 6 kuukautta", + "this_year": "Tänä vuonna", + "last_2_years": "Viimeiset 2 vuotta", + "all_time": "Kaikki ajat", + "powered_by_provider": "Tuottanut {providerName}", + "email": "Sähköposti", + "profile_followers": "Seuraajat", + "birthday": "Syntymäpäivä", + "subscription": "Tilaus", + "not_born": "Ei syntynyt", + "hacker": "Hakkeri", + "profile": "Profiili", + "no_name": "Ei nimeä", + "edit": "Muokkaa", + "user_profile": "Käyttäjäprofiili", + "count_plays": "{count} toistoa", + "streaming_fees_hypothetical": "Suoratoiston maksut (hypoteettinen)", + "minutes_listened": "Kuunneltuja minuutteja", + "streamed_songs": "Suoratoistettuja kappaleita", + "count_streams": "{count} suoratoistoa", + "owned_by_you": "Sinun omistama", + "copied_shareurl_to_clipboard": "{shareUrl} kopioitu leikepöydälle", + "spotify_hipotetical_calculation": "*Tämä on laskettu Spotifyn suoratoiston\nmaksun perusteella, joka on 0,003–0,005 dollaria.\nTämä on hypoteettinen laskelma, joka antaa käyttäjälle käsityksen\nsiitä, kuinka paljon he olisivat maksaneet artisteille,\njollei heidän kappaleensa olisi kuunneltu Spotifyssa.", + "count_mins": "{minutes} min", + "summary_minutes": "minuuttia", + "summary_listened_to_music": "Kuunneltu musiikkia", + "summary_songs": "kappaletta", + "summary_streamed_overall": "Suoratoistettu yhteensä", + "summary_owed_to_artists": "Maksettava artisteille\nTässä kuussa", + "summary_artists": "artisti", + "summary_music_reached_you": "Musiikki saavutti sinut", + "summary_full_albums": "täydet albumit", + "summary_got_your_love": "Sai rakkautesi", + "summary_playlists": "soittolistat", + "summary_were_on_repeat": "Olivat toistossa", + "total_money": "Yhteensä {money}" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 6bd2d0f84..4a41dec98 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -325,5 +325,64 @@ "add_library_location": "Ajouter à la bibliothèque", "remove_library_location": "Retirer de la bibliothèque", "local_tab": "Local", - "stats": "Statistiques" + "stats": "Statistiques", + "and_n_more": "et {count} de plus", + "recently_played": "Récemment joué", + "browse_more": "Parcourir plus", + "no_title": "Sans titre", + "not_playing": "Non joué", + "epic_failure": "Échec épique!", + "added_num_tracks_to_queue": "{tracks_length} morceaux ajoutés à la file d'attente", + "spotube_has_an_update": "Spotube a une mise à jour", + "download_now": "Télécharger maintenant", + "nightly_version": "Spotube Nightly {nightlyBuildNum} a été publié", + "release_version": "Spotube v{version} a été publié", + "read_the_latest": "Lisez les dernières ", + "release_notes": "notes de version", + "pick_color_scheme": "Choisissez le schéma de couleurs", + "save": "Sauvegarder", + "choose_the_device": "Choisissez l'appareil:", + "multiple_device_connected": "Plusieurs appareils sont connectés.\nChoisissez l'appareil sur lequel vous souhaitez effectuer cette action", + "nothing_found": "Rien trouvé", + "the_box_is_empty": "La boîte est vide", + "top_artists": "Meilleurs artistes", + "top_albums": "Meilleurs albums", + "this_week": "Cette semaine", + "this_month": "Ce mois-ci", + "last_6_months": "Les 6 derniers mois", + "this_year": "Cette année", + "last_2_years": "Les 2 dernières années", + "all_time": "De tous les temps", + "powered_by_provider": "Propulsé par {providerName}", + "email": "Email", + "profile_followers": "Abonnés", + "birthday": "Anniversaire", + "subscription": "Abonnement", + "not_born": "Non né", + "hacker": "Hacker", + "profile": "Profil", + "no_name": "Sans nom", + "edit": "Modifier", + "user_profile": "Profil utilisateur", + "count_plays": "{count} lectures", + "streaming_fees_hypothetical": "Frais de streaming (hypothétiques)", + "minutes_listened": "Minutes écoutées", + "streamed_songs": "Morceaux diffusés", + "count_streams": "{count} streams", + "owned_by_you": "Possédé par vous", + "copied_shareurl_to_clipboard": "{shareUrl} copié dans le presse-papier", + "spotify_hipotetical_calculation": "*Cela est calculé en fonction du\npaiement par stream de Spotify de 0,003 $ à 0,005 $.\nIl s'agit d'un calcul hypothétique pour donner\nune idée de combien vous auriez\npayé aux artistes si vous aviez\nécouté leur chanson sur Spotify.", + "count_mins": "{minutes} minutes", + "summary_minutes": "minutes", + "summary_listened_to_music": "A écouté de la musique", + "summary_songs": "morceaux", + "summary_streamed_overall": "Diffusé en général", + "summary_owed_to_artists": "Dû aux artistes\nCe mois-ci", + "summary_artists": "artistes", + "summary_music_reached_you": "La musique vous a atteint", + "summary_full_albums": "albums complets", + "summary_got_your_love": "A obtenu votre amour", + "summary_playlists": "playlists", + "summary_were_on_repeat": "Était en répétition", + "total_money": "Total {money}" } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 7dc809c70..a65e3f756 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -325,5 +325,64 @@ "add_library_location": "पुस्तकालय में जोड़ें", "remove_library_location": "पुस्तकालय से हटाएं", "local_tab": "स्थानीय", - "stats": "आंकड़े" + "stats": "आंकड़े", + "and_n_more": "और {count} और", + "recently_played": "हाल ही में खेले गए", + "browse_more": "अधिक ब्राउज़ करें", + "no_title": "कोई शीर्षक नहीं", + "not_playing": "नहीं चल रहा", + "epic_failure": "महान असफलता!", + "added_num_tracks_to_queue": "{tracks_length} ट्रैक्स कतार में जोड़े गए", + "spotube_has_an_update": "Spotube में एक अपडेट है", + "download_now": "अभी डाउनलोड करें", + "nightly_version": "Spotube Nightly {nightlyBuildNum} जारी किया गया है", + "release_version": "Spotube v{version} जारी किया गया है", + "read_the_latest": "नवीनतम पढ़ें", + "release_notes": "रिलीज़ नोट्स", + "pick_color_scheme": "रंग योजना चुनें", + "save": "सहेजें", + "choose_the_device": "उपकरण चुनें:", + "multiple_device_connected": "कई उपकरण जुड़े हुए हैं।\nउस उपकरण को चुनें जिस पर आप यह क्रिया करना चाहते हैं", + "nothing_found": "कुछ भी नहीं मिला", + "the_box_is_empty": "बॉक्स खाली है", + "top_artists": "शीर्ष कलाकार", + "top_albums": "शीर्ष एल्बम", + "this_week": "इस हफ्ते", + "this_month": "इस महीने", + "last_6_months": "पिछले 6 महीने", + "this_year": "इस साल", + "last_2_years": "पिछले 2 साल", + "all_time": "सभी समय", + "powered_by_provider": "{providerName} द्वारा संचालित", + "email": "ईमेल", + "profile_followers": "अनुयायी", + "birthday": "जन्मदिन", + "subscription": "सदस्यता", + "not_born": "अभी पैदा नहीं हुआ", + "hacker": "हैकर", + "profile": "प्रोफ़ाइल", + "no_name": "कोई नाम नहीं", + "edit": "संपादित करें", + "user_profile": "उपयोगकर्ता प्रोफ़ाइल", + "count_plays": "{count} प्ले", + "streaming_fees_hypothetical": "*Spotify की प्रति स्ट्रीम भुगतान के आधार पर\n$0.003 से $0.005 तक गणना की गई है। यह एक काल्पनिक\nगणना है जो उपयोगकर्ता को यह जानकारी देती है कि वे कितना भुगतान\nकरते यदि वे Spotify पर गाने सुनते।", + "count_mins": "{minutes} मिनट", + "summary_minutes": "मिनट", + "summary_listened_to_music": "सुनी गई संगीत", + "summary_songs": "गाने", + "summary_streamed_overall": "कुल स्ट्रीम", + "summary_owed_to_artists": "कलाकारों को देनदार\nइस महीने", + "summary_artists": "कलाकार", + "summary_music_reached_you": "संगीत आपके पास पहुंच गया", + "summary_full_albums": "पूरा एल्बम", + "summary_got_your_love": "आपका प्यार मिला", + "summary_playlists": "प्लेलिस्ट", + "summary_were_on_repeat": "दोहराया गया", + "total_money": "कुल {money}", + "minutes_listened": "सुनिएका मिनेटहरू", + "streamed_songs": "स्ट्रीम गरिएका गीतहरू", + "count_streams": "{count} स्ट्रिम", + "owned_by_you": "तपाईंले स्वामित्व गरेको", + "copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो", + "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।" } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 669f5e2a1..0a417c404 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -325,5 +325,64 @@ "add_library_location": "Tambahkan ke perpustakaan", "remove_library_location": "Hapus dari perpustakaan", "local_tab": "Lokal", - "stats": "Statistik" + "stats": "Statistik", + "and_n_more": "dan {count} lainnya", + "recently_played": "Baru saja diputar", + "browse_more": "Telusuri lebih banyak", + "no_title": "Tanpa judul", + "not_playing": "Tidak diputar", + "epic_failure": "Kegagalan epik!", + "added_num_tracks_to_queue": "Menambahkan {tracks_length} trek ke antrean", + "spotube_has_an_update": "Spotube memiliki pembaruan", + "download_now": "Unduh sekarang", + "nightly_version": "Spotube Nightly {nightlyBuildNum} telah dirilis", + "release_version": "Spotube v{version} telah dirilis", + "read_the_latest": "Baca yang terbaru ", + "release_notes": "catatan rilis", + "pick_color_scheme": "Pilih skema warna", + "save": "Simpan", + "choose_the_device": "Pilih perangkat:", + "multiple_device_connected": "Beberapa perangkat terhubung.\nPilih perangkat tempat Anda ingin melakukan tindakan ini", + "nothing_found": "Tidak ditemukan apa pun", + "the_box_is_empty": "Kotak kosong", + "top_artists": "Artis Teratas", + "top_albums": "Album Teratas", + "this_week": "Minggu ini", + "this_month": "Bulan ini", + "last_6_months": "6 bulan terakhir", + "this_year": "Tahun ini", + "last_2_years": "2 tahun terakhir", + "all_time": "Sepanjang waktu", + "powered_by_provider": "Didukung oleh {providerName}", + "email": "Email", + "profile_followers": "Pengikut", + "birthday": "Ulang Tahun", + "subscription": "Langganan", + "not_born": "Belum lahir", + "hacker": "Hacker", + "profile": "Profil", + "no_name": "Tanpa nama", + "edit": "Edit", + "user_profile": "Profil pengguna", + "count_plays": "{count} pemutaran", + "streaming_fees_hypothetical": "Biaya streaming (hipotetis)", + "minutes_listened": "Menit didengarkan", + "streamed_songs": "Lagu yang disiarkan", + "count_streams": "{count} streams", + "owned_by_you": "Dimiliki oleh Anda", + "copied_shareurl_to_clipboard": "{shareUrl} disalin ke clipboard", + "spotify_hipotetical_calculation": "*Ini dihitung berdasarkan pembayaran\nper stream Spotify dari $0,003 hingga $0,005.\nIni adalah perhitungan hipotetis untuk memberi\npengguna gambaran tentang berapa banyak\nmereka akan membayar kepada artis jika\nmereka mendengarkan lagu mereka di Spotify.", + "count_mins": "{minutes} menit", + "summary_minutes": "menit", + "summary_listened_to_music": "Mendengarkan musik", + "summary_songs": "lagu", + "summary_streamed_overall": "Disiarkan secara keseluruhan", + "summary_owed_to_artists": "Terhutang kepada artis\nBulan ini", + "summary_artists": "artis", + "summary_music_reached_you": "Musik mencapai Anda", + "summary_full_albums": "album lengkap", + "summary_got_your_love": "Mendapatkan cinta Anda", + "summary_playlists": "daftar putar", + "summary_were_on_repeat": "Sedang diulang", + "total_money": "Total {money}" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 9ba30acc8..6cbcbb6a5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -326,5 +326,64 @@ "add_library_location": "Aggiungi alla biblioteca", "remove_library_location": "Rimuovi dalla biblioteca", "local_tab": "Locale", - "stats": "Statistiche" + "stats": "Statistiche", + "and_n_more": "e {count} in più", + "recently_played": "Riprodotti di recente", + "browse_more": "Esplora di più", + "no_title": "Nessun titolo", + "not_playing": "Non in riproduzione", + "epic_failure": "Fallimento epico!", + "added_num_tracks_to_queue": "Aggiunti {tracks_length} brani alla coda", + "spotube_has_an_update": "Spotube ha un aggiornamento", + "download_now": "Scarica ora", + "nightly_version": "Spotube Nightly {nightlyBuildNum} è stato rilasciato", + "release_version": "Spotube v{version} è stato rilasciato", + "read_the_latest": "Leggi l'ultimo ", + "release_notes": "note di rilascio", + "pick_color_scheme": "Scegli uno schema di colori", + "save": "Salva", + "choose_the_device": "Scegli il dispositivo:", + "multiple_device_connected": "Sono collegati più dispositivi.\nScegli il dispositivo su cui vuoi che venga eseguita questa azione", + "nothing_found": "Nessun risultato", + "the_box_is_empty": "La scatola è vuota", + "top_artists": "Artisti Top", + "top_albums": "Album Top", + "this_week": "Questa settimana", + "this_month": "Questo mese", + "last_6_months": "Ultimi 6 mesi", + "this_year": "Quest'anno", + "last_2_years": "Ultimi 2 anni", + "all_time": "Di tutti i tempi", + "powered_by_provider": "Sostenuto da {providerName}", + "email": "Email", + "profile_followers": "Follower", + "birthday": "Compleanno", + "subscription": "Abbonamento", + "not_born": "Non nato", + "hacker": "Hacker", + "profile": "Profilo", + "no_name": "Nessun nome", + "edit": "Modifica", + "user_profile": "Profilo utente", + "count_plays": "{count} riproduzioni", + "streaming_fees_hypothetical": "Spese di streaming (ipotetico)", + "minutes_listened": "Minuti ascoltati", + "streamed_songs": "Brani in streaming", + "count_streams": "{count} streaming", + "owned_by_you": "Di tua proprietà", + "copied_shareurl_to_clipboard": "Copiato {shareUrl} negli appunti", + "spotify_hipotetical_calculation": "*Questo è calcolato in base al pagamento per streaming di Spotify\nche va da $0.003 a $0.005. Questo è un calcolo ipotetico\nper dare all'utente un'idea di quanto avrebbe pagato agli artisti se avesse ascoltato\ne loro canzoni su Spotify.", + "count_mins": "{minutes} min", + "summary_minutes": "minuti", + "summary_listened_to_music": "Musica ascoltata", + "summary_songs": "brani", + "summary_streamed_overall": "Streaming complessivo", + "summary_owed_to_artists": "Dovuto agli artisti\nquesto mese", + "summary_artists": "dell'artista", + "summary_music_reached_you": "La musica ti ha raggiunto", + "summary_full_albums": "album completi", + "summary_got_your_love": "Ha ricevuto il tuo amore", + "summary_playlists": "playlist", + "summary_were_on_repeat": "Erano in ripetizione", + "total_money": "Totale {money}" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 35e76b69b..a26c8ba0d 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -325,5 +325,64 @@ "add_library_location": "ライブラリに追加", "remove_library_location": "ライブラリから削除", "local_tab": "ローカル", - "stats": "統計" + "stats": "統計", + "and_n_more": "そして {count} つのアイテム", + "recently_played": "最近再生された", + "browse_more": "もっと見る", + "no_title": "タイトルなし", + "not_playing": "再生中ではありません", + "epic_failure": "壮大な失敗!", + "added_num_tracks_to_queue": "{tracks_length} 曲をキューに追加しました", + "spotube_has_an_update": "Spotube にアップデートがあります", + "download_now": "今すぐダウンロード", + "nightly_version": "Spotube Nightly {nightlyBuildNum} がリリースされました", + "release_version": "Spotube v{version} がリリースされました", + "read_the_latest": "最新の ", + "release_notes": "リリースノート", + "pick_color_scheme": "カラースキームを選択", + "save": "保存", + "choose_the_device": "デバイスを選択:", + "multiple_device_connected": "複数のデバイスが接続されています。\nこのアクションを実行するデバイスを選択してください", + "nothing_found": "何も見つかりませんでした", + "the_box_is_empty": "ボックスは空です", + "top_artists": "トップアーティスト", + "top_albums": "トップアルバム", + "this_week": "今週", + "this_month": "今月", + "last_6_months": "過去6か月", + "this_year": "今年", + "last_2_years": "過去2年間", + "all_time": "全期間", + "powered_by_provider": "{providerName} 提供", + "email": "メール", + "profile_followers": "フォロワー", + "birthday": "誕生日", + "subscription": "サブスクリプション", + "not_born": "未出生", + "hacker": "ハッカー", + "profile": "プロフィール", + "no_name": "名前なし", + "edit": "編集", + "user_profile": "ユーザープロフィール", + "count_plays": "{count} 回再生", + "streaming_fees_hypothetical": "*これは Spotify のストリームあたりの支払い\nが $0.003 から $0.005 であると仮定して計算されています。\nこれは、Spotify でその曲を聴いた場合にアーティストにいくら支払ったかの\n洞察を得るための仮定の計算です。", + "count_mins": "{minutes} 分", + "summary_minutes": "分", + "summary_listened_to_music": "音楽を聴いた", + "summary_songs": "曲", + "summary_streamed_overall": "全体のストリーミング", + "summary_owed_to_artists": "今月アーティストに支払うべき額", + "summary_artists": "アーティストの", + "summary_music_reached_you": "音楽があなたに届いた", + "summary_full_albums": "フルアルバム", + "summary_got_your_love": "あなたの愛を受け取った", + "summary_playlists": "プレイリスト", + "summary_were_on_repeat": "リピートしていた", + "total_money": "合計 {money}", + "minutes_listened": "リスニング時間", + "streamed_songs": "ストリーミングされた曲", + "count_streams": "{count} 回のストリーム", + "owned_by_you": "あなたが所有", + "copied_shareurl_to_clipboard": "{shareUrl} をクリップボードにコピーしました", + "spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。" } \ No newline at end of file diff --git a/lib/l10n/app_ka.arb b/lib/l10n/app_ka.arb index 28fcc26a3..66d7f8886 100644 --- a/lib/l10n/app_ka.arb +++ b/lib/l10n/app_ka.arb @@ -325,5 +325,64 @@ "add_library_location": "ბიბლიოთეკაში დამატება", "remove_library_location": "ბიბლიოთეკიდან წაშლა", "local_tab": "ადგილობრივი", - "stats": "სტატისტიკა" + "stats": "სტატისტიკა", + "and_n_more": "და {count} მეტი", + "recently_played": "მიუწვდელი", + "browse_more": "დაიცალეთ მეტი", + "no_title": "არ აქვს სათაური", + "not_playing": "არ ერთვის", + "epic_failure": "ეპიკური მარცხი!", + "added_num_tracks_to_queue": "დამატებული {tracks_length} ტრეკი რიგში", + "spotube_has_an_update": "Spotube-ს აქვს განახლება", + "download_now": "ჩამოტვირთეთ ახლავე", + "nightly_version": "Spotube Nightly {nightlyBuildNum} გამოშვებულია", + "release_version": "Spotube v{version} გამოშვებულია", + "read_the_latest": "წაიკითხეთ უახლესი ", + "release_notes": "გამოშვების შენიშვნები", + "pick_color_scheme": "აირჩიეთ ფერის სქემა", + "save": "შეინახეთ", + "choose_the_device": "აირჩიეთ მოწყობილობა:", + "multiple_device_connected": "დაკავშირებულია რამდენიმე მოწყობილობა.\nაირჩიეთ მოწყობილობა, რომელზეც უნდა განხორციელდეს ეს მოქმედება", + "nothing_found": "არაფერი მოიძებნა", + "the_box_is_empty": "კვადრატია ცარიელი", + "top_artists": "ტოპ არტისტები", + "top_albums": "ტოპ ალბომები", + "this_week": "ამ კვირას", + "this_month": "ამ თვეში", + "last_6_months": "ბოლო 6 თვე", + "this_year": "ამ წელს", + "last_2_years": "ბოლო 2 წელი", + "all_time": "ყველა დრო", + "powered_by_provider": "{providerName}-ით გაწვდილი", + "email": "ელ. ფოსტა", + "profile_followers": "გამყვანები", + "birthday": "დაბადების დღე", + "subscription": "გამოწერა", + "not_born": "არ დაბადებულა", + "hacker": "ჰაკერი", + "profile": "პროფილი", + "no_name": "არ არის სახელი", + "edit": "რედაქტირება", + "user_profile": "მომხმარებლის პროფილი", + "count_plays": "{count} გაწვდვა", + "streaming_fees_hypothetical": "*ეს рассчитывается на основе выплат за поток от Spotify\nот $0.003 до $0.005. ეს ჰიპოთეტური გამოთვლა იძლევა მომხმარებელს წარმოდგენას იმაზე, რამდენად\nგადახდილი იქნებოდა არტისტებისთვის, თუ მათ მოუსმინოს Spotify-ს ტრეკებს.", + "count_mins": "{minutes} წუთი", + "summary_minutes": "წუთები", + "summary_listened_to_music": "მუსიკა გაწვდილი", + "summary_songs": "მელოდია", + "summary_streamed_overall": "გაწვდილი საერთო", + "summary_owed_to_artists": "გადასახადი არტისტებს\nამ თვეში", + "summary_artists": "არტისტების", + "summary_music_reached_you": "მუსიკა ჩაგივარდა", + "summary_full_albums": "სრული ალბომები", + "summary_got_your_love": "მოსულა თქვენი სიყვარული", + "summary_playlists": "პლეილისტები", + "summary_were_on_repeat": "გადაწვდილი იყო", + "total_money": "მთლიანი {money}", + "minutes_listened": "წუთები მოუსმინეს", + "streamed_songs": "სტრიმირებული სიმღერები", + "count_streams": "{count} სტრიმი", + "owned_by_you": "შენ მიერ საკუთრებული", + "copied_shareurl_to_clipboard": "{shareUrl} აიღო კლიპბორდზე", + "spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე." } \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index cb6e09992..10036ba5b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -326,5 +326,64 @@ "add_library_location": "도서관에 추가", "remove_library_location": "도서관에서 제거", "local_tab": "로컬", - "stats": "통계" + "stats": "통계", + "and_n_more": "그리고 {count}개 더", + "recently_played": "최근 재생", + "browse_more": "더 보기", + "no_title": "제목 없음", + "not_playing": "재생 중이 아님", + "epic_failure": "서사적 실패!", + "added_num_tracks_to_queue": "{tracks_length} 곡을 대기열에 추가했습니다", + "spotube_has_an_update": "Spotube에 업데이트가 있습니다", + "download_now": "지금 다운로드", + "nightly_version": "Spotube Nightly {nightlyBuildNum}이 출시되었습니다", + "release_version": "Spotube v{version}이 출시되었습니다", + "read_the_latest": "최신 ", + "release_notes": "릴리스 노트", + "pick_color_scheme": "색상 테마 선택", + "save": "저장", + "choose_the_device": "디바이스 선택:", + "multiple_device_connected": "여러 디바이스가 연결되어 있습니다.\n이 작업을 실행할 디바이스를 선택하세요", + "nothing_found": "찾을 수 없음", + "the_box_is_empty": "상자가 비어 있습니다", + "top_artists": "톱 아티스트", + "top_albums": "톱 앨범", + "this_week": "이번 주", + "this_month": "이번 달", + "last_6_months": "지난 6개월", + "this_year": "올해", + "last_2_years": "지난 2년", + "all_time": "모든 시간", + "powered_by_provider": "{providerName} 제공", + "email": "이메일", + "profile_followers": "팔로워", + "birthday": "생일", + "subscription": "구독", + "not_born": "태어나지 않음", + "hacker": "해커", + "profile": "프로필", + "no_name": "이름 없음", + "edit": "편집", + "user_profile": "사용자 프로필", + "count_plays": "{count} 재생", + "streaming_fees_hypothetical": "*이것은 Spotify의 스트림당 지급액\n$0.003에서 $0.005를 기준으로 계산된 것입니다.\n이것은 사용자가 Spotify에서 곡을 들었을 때\n아티스트에게 지불했을 금액에 대한 통찰을 제공하기 위한\n가상의 계산입니다.", + "count_mins": "{minutes} 분", + "summary_minutes": "분", + "summary_listened_to_music": "듣는 음악", + "summary_songs": "곡", + "summary_streamed_overall": "전체 스트리밍", + "summary_owed_to_artists": "이번 달 아티스트에게 지급해야 할 금액", + "summary_artists": "아티스트의", + "summary_music_reached_you": "음악이 도달함", + "summary_full_albums": "전체 앨범", + "summary_got_your_love": "당신의 사랑을 받음", + "summary_playlists": "플레이리스트", + "summary_were_on_repeat": "반복 재생됨", + "total_money": "총 {money}", + "minutes_listened": "청취한 시간", + "streamed_songs": "스트리밍된 곡", + "count_streams": "{count} 스트림", + "owned_by_you": "당신이 소유", + "copied_shareurl_to_clipboard": "{shareUrl}를 클립보드에 복사했습니다", + "spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다." } \ No newline at end of file diff --git a/lib/l10n/app_ne.arb b/lib/l10n/app_ne.arb index f8e8d46a7..ce2a1e4b9 100644 --- a/lib/l10n/app_ne.arb +++ b/lib/l10n/app_ne.arb @@ -325,5 +325,64 @@ "add_library_location": "पुस्तकालयमा थप्नुहोस्", "remove_library_location": "पुस्तकालयबाट हटाउनुहोस्", "local_tab": "स्थानिय", - "stats": "तथ्याङ्क" + "stats": "तथ्याङ्क", + "and_n_more": "राम्रो {count} थप", + "recently_played": "हालै खेलेको", + "browse_more": "थप हेर्नुहोस्", + "no_title": "शीर्षक छैन", + "not_playing": "खेलिरहेको छैन", + "epic_failure": "महाकवि असफलता!", + "added_num_tracks_to_queue": "{tracks_length} ट्र्याकहरू तालिकामा थपिएका छन्", + "spotube_has_an_update": "Spotube मा अपडेट छ", + "download_now": "अहिले डाउनलोड गर्नुहोस्", + "nightly_version": "Spotube Nightly {nightlyBuildNum} रिलिज गरिएको छ", + "release_version": "Spotube v{version} रिलिज गरिएको छ", + "read_the_latest": "अर्को ", + "release_notes": "रिलिज नोटहरू", + "pick_color_scheme": "रंग योजना चयन गर्नुहोस्", + "save": "सुरक्षित गर्नुहोस्", + "choose_the_device": "उपकरण चयन गर्नुहोस्:", + "multiple_device_connected": "धेरै उपकरण जडान गरिएको छ।\nयो क्रियाकलाप गर्ने उपकरण चयन गर्नुहोस्", + "nothing_found": "केही फेला परेन", + "the_box_is_empty": "बक्स खाली छ", + "top_artists": "शीर्ष कलाकारहरू", + "top_albums": "शीर्ष एल्बमहरू", + "this_week": "यो हप्ता", + "this_month": "यो महिना", + "last_6_months": "पछिल्लो ६ महिना", + "this_year": "यो वर्ष", + "last_2_years": "पछिल्लो २ वर्ष", + "all_time": "सबै समय", + "powered_by_provider": "{providerName} द्वारा शक्ति प्राप्त", + "email": "ईमेल", + "profile_followers": "अनुयायीहरू", + "birthday": "जन्मदिन", + "subscription": "सदस्यता", + "not_born": "जन्मिएको छैन", + "hacker": "ह्याकर", + "profile": "प्रोफाइल", + "no_name": "नाम छैन", + "edit": "सम्पादन गर्नुहोस्", + "user_profile": "प्रयोगकर्ता प्रोफाइल", + "count_plays": "{count} खेलाइन्छ", + "streaming_fees_hypothetical": "*यो Spotify को प्रति स्ट्रिमको आधारमा गणना गरिएको छ\n$0.003 देखि $0.005 बीचको भुक्तानी। यो एक काल्पनिक गणना हो\nउपयोगकर्तालाई यो थाहा दिनको लागि कि उनीहरूले अर्टिस्टहरूलाई\nSpotify मा गीत सुनेको भए कति भुक्तानी गर्ने थिए।", + "count_mins": "{minutes} मिनेट", + "summary_minutes": "मिनेट", + "summary_listened_to_music": "सङ्गीत सुन्नु", + "summary_songs": "गीतहरू", + "summary_streamed_overall": "सामान्य रूपले स्ट्रीम गरिएको", + "summary_owed_to_artists": "यस महिना कलाकारहरूलाई देन", + "summary_artists": "कलाकारको", + "summary_music_reached_you": "सङ्गीत तपाईंलाई पुग्यो", + "summary_full_albums": "पूर्ण एल्बमहरू", + "summary_got_your_love": "तपाईंको माया प्राप्त गरियो", + "summary_playlists": "प्लेइस्ट", + "summary_were_on_repeat": "पुनरावृत्ति गरियो", + "total_money": "कुल {money}", + "minutes_listened": "सुनिएका मिनेटहरू", + "streamed_songs": "स्ट्रीम गरिएका गीतहरू", + "count_streams": "{count} स्ट्रिम", + "owned_by_you": "तपाईंले स्वामित्व गरेको", + "copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो", + "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।" } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index aa5c846d4..5e22446d3 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -326,5 +326,64 @@ "add_library_location": "Toevoegen aan bibliotheek", "remove_library_location": "Verwijderen uit bibliotheek", "local_tab": "Lokaal", - "stats": "Statistieken" + "stats": "Statistieken", + "and_n_more": "en {count} meer", + "recently_played": "Onlangs afgespeeld", + "browse_more": "Meer bekijken", + "no_title": "Geen titel", + "not_playing": "Niet aan het afspelen", + "epic_failure": "Epische mislukking!", + "added_num_tracks_to_queue": "{tracks_length} nummers aan de wachtrij toegevoegd", + "spotube_has_an_update": "Spotube heeft een update", + "download_now": "Nu downloaden", + "nightly_version": "Spotube Nightly {nightlyBuildNum} is uitgebracht", + "release_version": "Spotube v{version} is uitgebracht", + "read_the_latest": "Lees de nieuwste ", + "release_notes": "release-opmerkingen", + "pick_color_scheme": "Kies kleurenschema", + "save": "Opslaan", + "choose_the_device": "Kies het apparaat:", + "multiple_device_connected": "Er zijn meerdere apparaten verbonden.\nKies het apparaat waarop je deze actie wilt uitvoeren", + "nothing_found": "Niets gevonden", + "the_box_is_empty": "De doos is leeg", + "top_artists": "Topartiesten", + "top_albums": "Topalbums", + "this_week": "Deze week", + "this_month": "Deze maand", + "last_6_months": "Laatste 6 maanden", + "this_year": "Dit jaar", + "last_2_years": "Laatste 2 jaar", + "all_time": "All time", + "powered_by_provider": "Aangedreven door {providerName}", + "email": "E-mail", + "profile_followers": "Volgers", + "birthday": "Verjaardag", + "subscription": "Abonnement", + "not_born": "Niet geboren", + "hacker": "Hacker", + "profile": "Profiel", + "no_name": "Geen naam", + "edit": "Bewerken", + "user_profile": "Gebruikersprofiel", + "count_plays": "{count} afspeelbeurten", + "streaming_fees_hypothetical": "*Dit is berekend op basis van Spotify's uitbetaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om gebruikers inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun lied op Spotify zouden hebben beluisterd.", + "count_mins": "{minutes} min", + "summary_minutes": "minuten", + "summary_listened_to_music": "Beluisterde muziek", + "summary_songs": "nummers", + "summary_streamed_overall": "Totaal gestreamd", + "summary_owed_to_artists": "Te betalen aan artiesten\ndeze maand", + "summary_artists": "van de artiest", + "summary_music_reached_you": "Muziek heeft je bereikt", + "summary_full_albums": "volledige albums", + "summary_got_your_love": "Kreeg je liefde", + "summary_playlists": "afspeellijsten", + "summary_were_on_repeat": "Was op herhaling", + "total_money": "Totaal {money}", + "minutes_listened": "Luistertijd", + "streamed_songs": "Gestreamde nummers", + "count_streams": "{count} streams", + "owned_by_you": "Bezit door jou", + "copied_shareurl_to_clipboard": "{shareUrl} gekopieerd naar klembord", + "spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren." } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2c4e83698..06449ad95 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -325,5 +325,64 @@ "add_library_location": "Dodaj do biblioteki", "remove_library_location": "Usuń z biblioteki", "local_tab": "Lokalny", - "stats": "Statystyki" + "stats": "Statystyki", + "and_n_more": "i {count} więcej", + "recently_played": "Ostatnio odtwarzane", + "browse_more": "Zobacz więcej", + "no_title": "Brak tytułu", + "not_playing": "Nie odtwarzane", + "epic_failure": "Epicka porażka!", + "added_num_tracks_to_queue": "Dodano {tracks_length} utworów do kolejki", + "spotube_has_an_update": "Spotube ma aktualizację", + "download_now": "Pobierz teraz", + "nightly_version": "Spotube Nightly {nightlyBuildNum} został wydany", + "release_version": "Spotube v{version} został wydany", + "read_the_latest": "Przeczytaj najnowsze ", + "release_notes": "notatki o wersji", + "pick_color_scheme": "Wybierz schemat kolorów", + "save": "Zapisz", + "choose_the_device": "Wybierz urządzenie:", + "multiple_device_connected": "Jest wiele urządzeń podłączonych.\nWybierz urządzenie, na którym chcesz wykonać tę akcję", + "nothing_found": "Nic nie znaleziono", + "the_box_is_empty": "Pudełko jest puste", + "top_artists": "Najlepsi artyści", + "top_albums": "Najlepsze albumy", + "this_week": "W tym tygodniu", + "this_month": "W tym miesiącu", + "last_6_months": "Ostatnie 6 miesięcy", + "this_year": "W tym roku", + "last_2_years": "Ostatnie 2 lata", + "all_time": "Wszystkie czasy", + "powered_by_provider": "Napędzane przez {providerName}", + "email": "E-mail", + "profile_followers": "Obserwujący", + "birthday": "Data urodzenia", + "subscription": "Subskrypcja", + "not_born": "Nie urodzony", + "hacker": "Haker", + "profile": "Profil", + "no_name": "Brak nazwy", + "edit": "Edytuj", + "user_profile": "Profil użytkownika", + "count_plays": "{count} odtworzeń", + "streaming_fees_hypothetical": "*Obliczone na podstawie wypłaty Spotify za stream\nod $0.003 do $0.005. Jest to hipotetyczne\nobliczenie, które ma na celu pokazanie, ile\nużytkownik zapłaciłby artystom, gdyby odsłuchał\ntych utworów na Spotify.", + "count_mins": "{minutes} min", + "summary_minutes": "minuty", + "summary_listened_to_music": "Słuchana muzyka", + "summary_songs": "utwory", + "summary_streamed_overall": "Ogółem streamowane", + "summary_owed_to_artists": "Do zapłaty artystom\nw tym miesiącu", + "summary_artists": "artystów", + "summary_music_reached_you": "Muzyka dotarła do Ciebie", + "summary_full_albums": "pełne albumy", + "summary_got_your_love": "Otrzymał Twoją miłość", + "summary_playlists": "playlisty", + "summary_were_on_repeat": "Były na powtarzaniu", + "total_money": "Łącznie {money}", + "minutes_listened": "Minuty odsłuchane", + "streamed_songs": "Strumieniowane utwory", + "count_streams": "{count} strumieni", + "owned_by_you": "Własność Twoja", + "copied_shareurl_to_clipboard": "{shareUrl} skopiowano do schowka", + "spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify." } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 88cf5cb38..7231d15a2 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -325,5 +325,64 @@ "add_library_location": "Adicionar à biblioteca", "remove_library_location": "Remover da biblioteca", "local_tab": "Local", - "stats": "Estatísticas" + "stats": "Estatísticas", + "and_n_more": "e {count} mais", + "recently_played": "Reproduzido Recentemente", + "browse_more": "Ver Mais", + "no_title": "Sem Título", + "not_playing": "Não está a reproduzir", + "epic_failure": "Fracasso épico!", + "added_num_tracks_to_queue": "Adicionados {tracks_length} faixas à fila", + "spotube_has_an_update": "Spotube tem uma atualização", + "download_now": "Baixar Agora", + "nightly_version": "Spotube Nightly {nightlyBuildNum} foi lançado", + "release_version": "Spotube v{version} foi lançado", + "read_the_latest": "Leia o mais recente ", + "release_notes": "notas de versão", + "pick_color_scheme": "Escolha o esquema de cores", + "save": "Salvar", + "choose_the_device": "Escolha o dispositivo:", + "multiple_device_connected": "Há vários dispositivos conectados.\nEscolha o dispositivo no qual deseja executar esta ação", + "nothing_found": "Nada encontrado", + "the_box_is_empty": "A caixa está vazia", + "top_artists": "Principais Artistas", + "top_albums": "Principais Álbuns", + "this_week": "Esta semana", + "this_month": "Este mês", + "last_6_months": "Últimos 6 meses", + "this_year": "Este ano", + "last_2_years": "Últimos 2 anos", + "all_time": "De todos os tempos", + "powered_by_provider": "Desenvolvido por {providerName}", + "email": "E-mail", + "profile_followers": "Seguidores", + "birthday": "Aniversário", + "subscription": "Assinatura", + "not_born": "Não nascido", + "hacker": "Hacker", + "profile": "Perfil", + "no_name": "Sem Nome", + "edit": "Editar", + "user_profile": "Perfil do Usuário", + "count_plays": "{count} reproduzidos", + "streaming_fees_hypothetical": "*Calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Isso é um cálculo hipotético\npara fornecer uma visão ao usuário sobre quanto eles\nteriam pago aos artistas se estivessem ouvindo\no seu som no Spotify.", + "count_mins": "{minutes} min", + "summary_minutes": "minutos", + "summary_listened_to_music": "Música ouvida", + "summary_songs": "faixas", + "summary_streamed_overall": "Total de streams", + "summary_owed_to_artists": "Devido aos artistas\neste mês", + "summary_artists": "artista", + "summary_music_reached_you": "A música chegou até você", + "summary_full_albums": "álbuns completos", + "summary_got_your_love": "Recebeu seu amor", + "summary_playlists": "playlists", + "summary_were_on_repeat": "Estavam em repetição", + "total_money": "Total {money}", + "minutes_listened": "Minutos ouvidos", + "streamed_songs": "Músicas transmitidas", + "count_streams": "{count} streams", + "owned_by_you": "De sua propriedade", + "copied_shareurl_to_clipboard": "{shareUrl} copiado para a área de transferência", + "spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify." } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2e0aa77bc..7cffb42a6 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -184,7 +184,7 @@ "success_emoji": "Успешно🥳", "success_message": "Теперь вы успешно вошли в свою учетную запись Spotify. Отличная работа, приятель!", "step_4": "Шаг 4", - "step_4_steps": "Вставьте скопированное значение \"sp_dc\", + "step_4_steps": "Вставьте скопированное значение \"sp_dc\"", "something_went_wrong": "Что-то пошло не так", "piped_instance": "Экземпляр сервера Piped", "piped_description": "Серверный экземпляр Piped для сопоставления треков", @@ -324,5 +324,65 @@ "connect_client_alert": "Вас контролирует {client}", "this_device": "Это устройство", "remote": "Дистанционное управление", - "stats": "Статистика" + "stats": "Статистика", + "update_playlist": "Обновить плейлист", + "and_n_more": "и {count} еще", + "recently_played": "Недавно воспроизведено", + "browse_more": "Посмотреть больше", + "no_title": "Без названия", + "not_playing": "Не воспроизводится", + "epic_failure": "Эпическое фиаско!", + "added_num_tracks_to_queue": "Добавлено {tracks_length} треков в очередь", + "spotube_has_an_update": "В Spotube доступно обновление", + "download_now": "Скачать сейчас", + "nightly_version": "Spotube Nightly {nightlyBuildNum} выпущен", + "release_version": "Spotube v{version} выпущен", + "read_the_latest": "Читать последние ", + "release_notes": "заметки о версии", + "pick_color_scheme": "Выберите цветовую схему", + "save": "Сохранить", + "choose_the_device": "Выберите устройство:", + "multiple_device_connected": "Подключено несколько устройств.\nВыберите устройство, на котором вы хотите выполнить это действие", + "nothing_found": "Ничего не найдено", + "the_box_is_empty": "Коробка пуста", + "top_artists": "Лучшие артисты", + "top_albums": "Лучшие альбомы", + "this_week": "На этой неделе", + "this_month": "В этом месяце", + "last_6_months": "Последние 6 месяцев", + "this_year": "В этом году", + "last_2_years": "Последние 2 года", + "all_time": "Все время", + "powered_by_provider": "При поддержке {providerName}", + "email": "Электронная почта", + "profile_followers": "Подписчики", + "birthday": "День рождения", + "subscription": "Подписка", + "not_born": "Не рожден", + "hacker": "Хакер", + "profile": "Профиль", + "no_name": "Без имени", + "edit": "Редактировать", + "user_profile": "Профиль пользователя", + "count_plays": "{count} воспроизведений", + "streaming_fees_hypothetical": "*Рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический\nрасчет, чтобы показать пользователю, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify.", + "count_mins": "{minutes} мин", + "summary_minutes": "минуты", + "summary_listened_to_music": "Слушанная музыка", + "summary_songs": "песни", + "summary_streamed_overall": "Всего стримов", + "summary_owed_to_artists": "К выплате артистам\nв этом месяце", + "summary_artists": "артиста", + "summary_music_reached_you": "Музыка дошла до вас", + "summary_full_albums": "полные альбомы", + "summary_got_your_love": "Получил вашу любовь", + "summary_playlists": "плейлисты", + "summary_were_on_repeat": "Были на повторе", + "total_money": "Всего {money}", + "minutes_listened": "Минут прослушивания", + "streamed_songs": "Стримленные песни", + "count_streams": "{count} стримов", + "owned_by_you": "Ваша собственность", + "copied_shareurl_to_clipboard": "{shareUrl} скопировано в буфер обмена", + "spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify." } \ No newline at end of file diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 60ced74bf..3cac73f7f 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -326,5 +326,64 @@ "add_library_location": "เพิ่มในห้องสมุด", "remove_library_location": "ลบออกจากห้องสมุด", "local_tab": "ท้องถิ่น", - "stats": "สถิติ" + "stats": "สถิติ", + "and_n_more": "และ {count} อีก", + "recently_played": "เพลงที่เพิ่งเล่น", + "browse_more": "ดูเพิ่มเติม", + "no_title": "ไม่มีชื่อ", + "not_playing": "ไม่เล่น", + "epic_failure": "ล้มเหลวอย่างยิ่ง!", + "added_num_tracks_to_queue": "เพิ่ม {tracks_length} เพลงในคิว", + "spotube_has_an_update": "Spotube มีการอัปเดต", + "download_now": "ดาวน์โหลดตอนนี้", + "nightly_version": "Spotube Nightly {nightlyBuildNum} ได้รับการปล่อยออกมา", + "release_version": "Spotube v{version} ได้รับการปล่อยออกมา", + "read_the_latest": "อ่านข่าวสารล่าสุด ", + "release_notes": "บันทึกการปล่อย", + "pick_color_scheme": "เลือกธีมสี", + "save": "บันทึก", + "choose_the_device": "เลือกอุปกรณ์:", + "multiple_device_connected": "มีอุปกรณ์เชื่อมต่อหลายเครื่อง\nเลือกอุปกรณ์ที่คุณต้องการให้การดำเนินการนี้เกิดขึ้น", + "nothing_found": "ไม่พบข้อมูล", + "the_box_is_empty": "กล่องว่างเปล่า", + "top_artists": "ศิลปินยอดนิยม", + "top_albums": "อัลบั้มยอดนิยม", + "this_week": "สัปดาห์นี้", + "this_month": "เดือนนี้", + "last_6_months": "6 เดือนที่ผ่านมา", + "this_year": "ปีนี้", + "last_2_years": "2 ปีที่ผ่านมา", + "all_time": "ตลอดกาล", + "powered_by_provider": "ขับเคลื่อนโดย {providerName}", + "email": "อีเมล", + "profile_followers": "ผู้ติดตาม", + "birthday": "วันเกิด", + "subscription": "การสมัครสมาชิก", + "not_born": "ยังไม่เกิด", + "hacker": "แฮ็กเกอร์", + "profile": "โปรไฟล์", + "no_name": "ไม่มีชื่อ", + "edit": "แก้ไข", + "user_profile": "โปรไฟล์ผู้ใช้", + "count_plays": "{count} การเล่น", + "streaming_fees_hypothetical": "*คำนวณจากการจ่ายเงินต่อการสตรีมของ Spotify\nระหว่าง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ข้อมูลแก่ผู้ใช้เกี่ยวกับจำนวนเงินที่พวกเขา\nอาจจะจ่ายให้กับศิลปินหากพวกเขาฟังเพลงของพวกเขาใน Spotify", + "count_mins": "{minutes} นาที", + "summary_minutes": "นาที", + "summary_listened_to_music": "ฟังเพลง", + "summary_songs": "เพลง", + "summary_streamed_overall": "สตรีมทั้งหมด", + "summary_owed_to_artists": "ค้างชำระให้ศิลปิน\nในเดือนนี้", + "summary_artists": "ศิลปิน", + "summary_music_reached_you": "เพลงมาถึงคุณ", + "summary_full_albums": "อัลบั้มเต็ม", + "summary_got_your_love": "ได้รับความรักของคุณ", + "summary_playlists": "เพลย์ลิสต์", + "summary_were_on_repeat": "อยู่ในโหมดซ้ำ", + "total_money": "รวม {money}", + "minutes_listened": "เวลาที่ฟัง", + "streamed_songs": "เพลงที่สตรีม", + "count_streams": "{count} สตรีม", + "owned_by_you": "เป็นเจ้าของโดยคุณ", + "copied_shareurl_to_clipboard": "{shareUrl} คัดลอกไปที่คลิปบอร์ดแล้ว", + "spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify." } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index b329cfa7d..b5a0ec1ee 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -325,5 +325,64 @@ "add_library_location": "Kütüphaneye ekle", "remove_library_location": "Kütüphaneden çıkar", "local_tab": "Yerel", - "stats": "İstatistikler" + "stats": "İstatistikler", + "and_n_more": "ve {count} daha", + "recently_played": "Son Çalınanlar", + "browse_more": "Daha Fazla Göz At", + "no_title": "Başlık Yok", + "not_playing": "Çalmıyor", + "epic_failure": "Efsanevi başarısızlık!", + "added_num_tracks_to_queue": "{tracks_length} şarkı sıraya eklendi", + "spotube_has_an_update": "Spotube bir güncelleme aldı", + "download_now": "Şimdi İndir", + "nightly_version": "Spotube Nightly {nightlyBuildNum} yayımlandı", + "release_version": "Spotube v{version} yayımlandı", + "read_the_latest": "Son haberleri oku", + "release_notes": "sürüm notları", + "pick_color_scheme": "Renk şeması seç", + "save": "Kaydet", + "choose_the_device": "Cihazı seçin:", + "multiple_device_connected": "Birden fazla cihaz bağlı.\nBu işlemi gerçekleştirmek istediğiniz cihazı seçin", + "nothing_found": "Hiçbir şey bulunamadı", + "the_box_is_empty": "Kutu boş", + "top_artists": "En İyi Sanatçılar", + "top_albums": "En İyi Albümler", + "this_week": "Bu hafta", + "this_month": "Bu ay", + "last_6_months": "Son 6 ay", + "this_year": "Bu yıl", + "last_2_years": "Son 2 yıl", + "all_time": "Tüm zamanlar", + "powered_by_provider": "{providerName} tarafından desteklenmektedir", + "email": "E-posta", + "profile_followers": "Takipçiler", + "birthday": "Doğum Günü", + "subscription": "Abonelik", + "not_born": "Henüz doğmadı", + "hacker": "Hacker", + "profile": "Profil", + "no_name": "İsim Yok", + "edit": "Düzenle", + "user_profile": "Kullanıcı Profili", + "count_plays": "{count} çalma", + "streaming_fees_hypothetical": "*Spotify'ın akış başına ödeme miktarına\n$0.003 ile $0.005 arasında hesaplanmıştır. Bu, kullanıcıya\nSpotify'da şarkılarını dinlerse sanatçılara ne kadar ödeme\nyapmış olabileceğini göstermek için hipotetik bir hesaplamadır.", + "count_mins": "{minutes} dk", + "summary_minutes": "dakika", + "summary_listened_to_music": "Dinlenen müzik", + "summary_songs": "şarkılar", + "summary_streamed_overall": "Genel olarak akış", + "summary_owed_to_artists": "Sanatçılara borç\nbu ay", + "summary_artists": "sanatçının", + "summary_music_reached_you": "Müzik sana ulaştı", + "summary_full_albums": "tam albümler", + "summary_got_your_love": "Sevgini aldı", + "summary_playlists": "çalma listeleri", + "summary_were_on_repeat": "Tekrarda vardı", + "total_money": "Toplam {money}", + "minutes_listened": "Dinlenilen Dakikalar", + "streamed_songs": "Yayınlanan Şarkılar", + "count_streams": "{count} yayın", + "owned_by_you": "Sahip olduğunuz", + "copied_shareurl_to_clipboard": "{shareUrl} panoya kopyalandı", + "spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir." } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index d056524ea..013a64b75 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -325,5 +325,64 @@ "add_library_location": "Додати до бібліотеки", "remove_library_location": "Видалити з бібліотеки", "local_tab": "Місцевий", - "stats": "Статистика" + "stats": "Статистика", + "and_n_more": "і {count} більше", + "recently_played": "Нещодавно Відтворене", + "browse_more": "Переглянути Більше", + "no_title": "Без Назви", + "not_playing": "Не Відтворюється", + "epic_failure": "Епічний провал!", + "added_num_tracks_to_queue": "Додано {tracks_length} треків до черги", + "spotube_has_an_update": "Spotube має оновлення", + "download_now": "Завантажити Зараз", + "nightly_version": "Spotube Nightly {nightlyBuildNum} було випущено", + "release_version": "Spotube v{version} було випущено", + "read_the_latest": "Читати останні новини", + "release_notes": "ноти про випуск", + "pick_color_scheme": "Оберіть кольорову схему", + "save": "Зберегти", + "choose_the_device": "Виберіть пристрій:", + "multiple_device_connected": "Підключено кілька пристроїв.\nВиберіть пристрій, на якому ви хочете виконати цю дію", + "nothing_found": "Нічого не знайдено", + "the_box_is_empty": "Коробка порожня", + "top_artists": "Топ Артисти", + "top_albums": "Топ Альбоми", + "this_week": "Цього тижня", + "this_month": "Цього місяця", + "last_6_months": "Останні 6 місяців", + "this_year": "Цього року", + "last_2_years": "Останні 2 роки", + "all_time": "Усі часи", + "powered_by_provider": "Забезпечено {providerName}", + "email": "Електронна пошта", + "profile_followers": "Підписники", + "birthday": "День народження", + "subscription": "Підписка", + "not_born": "Ще не народжений", + "hacker": "Хакер", + "profile": "Профіль", + "no_name": "Без імені", + "edit": "Редагувати", + "user_profile": "Профіль користувача", + "count_plays": "{count} відтворень", + "streaming_fees_hypothetical": "*Розраховано на основі виплат Spotify за стримінг\nвід $0.003 до $0.005. Це гіпотетичний\nрозрахунок, щоб дати уявлення користувачу про те, скільки б він\nзаплатив артистам, якби слухав їхні пісні на Spotify.", + "count_mins": "{minutes} хв", + "summary_minutes": "хвилини", + "summary_listened_to_music": "Прослухана музика", + "summary_songs": "пісні", + "summary_streamed_overall": "Загалом стримів", + "summary_owed_to_artists": "Заборгованість артистам\nцього місяця", + "summary_artists": "артистів", + "summary_music_reached_you": "Музика досягла вас", + "summary_full_albums": "повні альбоми", + "summary_got_your_love": "Отримав вашу любов", + "summary_playlists": "плейлисти", + "summary_were_on_repeat": "Були на повторі", + "total_money": "Загалом {money}", + "minutes_listened": "Хвилини прослуховування", + "streamed_songs": "Стримлені пісні", + "count_streams": "{count} стримів", + "owned_by_you": "Ваша власність", + "copied_shareurl_to_clipboard": "{shareUrl} скопійовано в буфер обміну", + "spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify." } \ No newline at end of file diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 6bbd6cb66..5791793e0 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -325,5 +325,64 @@ "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ê" + "stats": "Thống kê", + "and_n_more": "và {count} cái khác", + "recently_played": "Gần đây đã phát", + "browse_more": "Xem thêm", + "no_title": "Không có tiêu đề", + "not_playing": "Không phát", + "epic_failure": "Thất bại hoàn toàn!", + "added_num_tracks_to_queue": "Đã thêm {tracks_length} bài hát vào danh sách phát", + "spotube_has_an_update": "Spotube có bản cập nhật", + "download_now": "Tải về ngay", + "nightly_version": "Spotube Nightly {nightlyBuildNum} đã được phát hành", + "release_version": "Spotube v{version} đã được phát hành", + "read_the_latest": "Đọc tin mới nhất", + "release_notes": "ghi chú phát hành", + "pick_color_scheme": "Chọn chủ đề màu sắc", + "save": "Lưu", + "choose_the_device": "Chọn thiết bị:", + "multiple_device_connected": "Có nhiều thiết bị kết nối.\nChọn thiết bị mà bạn muốn thực hiện hành động này", + "nothing_found": "Không tìm thấy gì", + "the_box_is_empty": "Hộp trống", + "top_artists": "Những Nghệ Sĩ Hàng Đầu", + "top_albums": "Những Album Hàng Đầu", + "this_week": "Tuần này", + "this_month": "Tháng này", + "last_6_months": "6 tháng qua", + "this_year": "Năm nay", + "last_2_years": "2 năm qua", + "all_time": "Mọi thời đại", + "powered_by_provider": "Cung cấp bởi {providerName}", + "email": "Email", + "profile_followers": "Người theo dõi", + "birthday": "Ngày sinh", + "subscription": "Gói cước", + "not_born": "Chưa sinh", + "hacker": "Tin tặc", + "profile": "Hồ sơ", + "no_name": "Không có tên", + "edit": "Chỉnh sửa", + "user_profile": "Hồ sơ người dùng", + "count_plays": "{count} lần phát", + "streaming_fees_hypothetical": "*Tính toán dựa trên thanh toán của Spotify cho mỗi lần phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ngive người dùng cái nhìn về số tiền họ sẽ chi trả cho các nghệ sĩ nếu họ nghe\nbài hát của họ trên Spotify.", + "count_mins": "{minutes} phút", + "summary_minutes": "phút", + "summary_listened_to_music": "Đã nghe nhạc", + "summary_songs": "bài hát", + "summary_streamed_overall": "Stream tổng cộng", + "summary_owed_to_artists": "Nợ nghệ sĩ\ntrong tháng này", + "summary_artists": "nghệ sĩ", + "summary_music_reached_you": "Âm nhạc đã đến với bạn", + "summary_full_albums": "album đầy đủ", + "summary_got_your_love": "Nhận được tình yêu của bạn", + "summary_playlists": "danh sách phát", + "summary_were_on_repeat": "Đã được phát lại", + "total_money": "Tổng cộng {money}", + "minutes_listened": "Thời gian nghe", + "streamed_songs": "Bài hát đã phát", + "count_streams": "{count} lượt phát", + "owned_by_you": "Thuộc sở hữu của bạn", + "copied_shareurl_to_clipboard": "{shareUrl} đã sao chép vào bảng tạm", + "spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify." } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b145f97bc..914472138 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -325,5 +325,64 @@ "add_library_location": "添加到图书馆", "remove_library_location": "从图书馆中删除", "local_tab": "本地", - "stats": "统计" + "stats": "统计", + "and_n_more": "和 {count} 更多", + "recently_played": "最近播放", + "browse_more": "浏览更多", + "no_title": "没有标题", + "not_playing": "未播放", + "epic_failure": "史诗级失败!", + "added_num_tracks_to_queue": "已将 {tracks_length} 首曲目添加到队列", + "spotube_has_an_update": "Spotube 有更新", + "download_now": "立即下载", + "nightly_version": "Spotube Nightly {nightlyBuildNum} 已发布", + "release_version": "Spotube v{version} 已发布", + "read_the_latest": "阅读最新", + "release_notes": "版本说明", + "pick_color_scheme": "选择配色方案", + "save": "保存", + "choose_the_device": "选择设备:", + "multiple_device_connected": "已连接多个设备。\n选择您希望执行此操作的设备", + "nothing_found": "未找到任何内容", + "the_box_is_empty": "箱子为空", + "top_artists": "热门艺术家", + "top_albums": "热门专辑", + "this_week": "本周", + "this_month": "本月", + "last_6_months": "过去6个月", + "this_year": "今年", + "last_2_years": "过去2年", + "all_time": "所有时间", + "powered_by_provider": "由 {providerName} 提供支持", + "email": "电子邮件", + "profile_followers": "关注者", + "birthday": "生日", + "subscription": "订阅", + "not_born": "尚未出生", + "hacker": "黑客", + "profile": "个人资料", + "no_name": "无名", + "edit": "编辑", + "user_profile": "用户资料", + "count_plays": "{count} 次播放", + "streaming_fees_hypothetical": "*基于 Spotify 每次播放的支付金额\n从 $0.003 到 $0.005 计算。这是一个假设性的\n计算,旨在让用户了解如果他们在 Spotify 上收听\n这些歌曲,可能会付给艺术家的金额。", + "count_mins": "{minutes} 分钟", + "summary_minutes": "分钟", + "summary_listened_to_music": "听音乐", + "summary_songs": "歌曲", + "summary_streamed_overall": "总体流媒体", + "summary_owed_to_artists": "本月欠艺术家的", + "summary_artists": "艺术家的", + "summary_music_reached_you": "音乐触及了你", + "summary_full_albums": "完整专辑", + "summary_got_your_love": "获得了你的爱", + "summary_playlists": "播放列表", + "summary_were_on_repeat": "已重复播放", + "total_money": "总计 {money}", + "minutes_listened": "听的分钟数", + "streamed_songs": "已流媒体歌曲", + "count_streams": "{count} 次流媒体", + "owned_by_you": "由您拥有", + "copied_shareurl_to_clipboard": "{shareUrl} 已复制到剪贴板", + "spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算,用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。" } \ No newline at end of file diff --git a/lib/modules/artist/artist_card.dart b/lib/modules/artist/artist_card.dart index 896271f24..add2608d9 100644 --- a/lib/modules/artist/artist_card.dart +++ b/lib/modules/artist/artist_card.dart @@ -46,9 +46,9 @@ class ArtistCard extends HookConsumerWidget { width: size, margin: const EdgeInsets.symmetric(vertical: 5), child: Material( - shadowColor: theme.colorScheme.background, + shadowColor: theme.colorScheme.surface, color: Color.lerp( - theme.colorScheme.surfaceVariant, + theme.colorScheme.surfaceContainerHighest, theme.colorScheme.surface, useBrightnessValue(.9, .7), ), diff --git a/lib/modules/home/sections/friends/friend_item.dart b/lib/modules/home/sections/friends/friend_item.dart index bb97af04a..773a4a8c1 100644 --- a/lib/modules/home/sections/friends/friend_item.dart +++ b/lib/modules/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.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(15), ), constraints: const BoxConstraints( diff --git a/lib/modules/home/sections/genres.dart b/lib/modules/home/sections/genres.dart index 3e12e5e9c..5f2dfa5ed 100644 --- a/lib/modules/home/sections/genres.dart +++ b/lib/modules/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/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index a5831fc21..02e47a534 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/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/modules/library/playlist_generate/multi_select_field.dart b/lib/modules/library/playlist_generate/multi_select_field.dart index 8cafe02fd..7118d57df 100644 --- a/lib/modules/library/playlist_generate/multi_select_field.dart +++ b/lib/modules/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/modules/player/player.dart b/lib/modules/player/player.dart index 538af6851..3202eedad 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -278,7 +278,7 @@ class PlayerView extends HookConsumerWidget { const SizedBox(height: 10), PlayerControls(palette: palette), const SizedBox(height: 25), - PlayerActions( + const PlayerActions( mainAxisAlignment: MainAxisAlignment.spaceEvenly, showQueue: false, ), diff --git a/lib/modules/player/player_queue.dart b/lib/modules/player/player_queue.dart index 2431d82ed..369b95d25 100644 --- a/lib/modules/player/player_queue.dart +++ b/lib/modules/player/player_queue.dart @@ -121,7 +121,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/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 092d631f8..b58a58946 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -211,7 +211,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/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index 23904aef4..7f37c4720 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -48,7 +48,7 @@ class BottomPlayer extends HookConsumerWidget { ); final theme = Theme.of(context); - final bg = theme.colorScheme.surfaceVariant; + final bg = theme.colorScheme.surfaceContainerHighest; final bgColor = useBrightnessValue( Color.lerp(bg, Colors.white, 0.7), @@ -77,10 +77,10 @@ class BottomPlayer extends HookConsumerWidget { child: PlayerTrackDetails(track: playlist.activeTrack), ), // controls - Flexible( + const Flexible( flex: 3, child: Padding( - padding: const EdgeInsets.only(top: 5), + padding: EdgeInsets.only(top: 5), child: PlayerControls(), ), ), diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index ef7357985..8f7f495c2 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -72,7 +72,7 @@ class Sidebar extends HookConsumerWidget { ); final theme = Theme.of(context); - final bg = theme.colorScheme.surfaceVariant; + final bg = theme.colorScheme.surfaceContainerHighest; final bgColor = useBrightnessValue( Color.lerp(bg, Colors.white, 0.7), diff --git a/lib/modules/root/spotube_navigation_bar.dart b/lib/modules/root/spotube_navigation_bar.dart index c624a40c2..978891b8a 100644 --- a/lib/modules/root/spotube_navigation_bar.dart +++ b/lib/modules/root/spotube_navigation_bar.dart @@ -69,7 +69,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/modules/settings/color_scheme_picker_dialog.dart b/lib/modules/settings/color_scheme_picker_dialog.dart index 550446bc0..f29335055 100644 --- a/lib/modules/settings/color_scheme_picker_dialog.dart +++ b/lib/modules/settings/color_scheme_picker_dialog.dart @@ -180,9 +180,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/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index 810c18d6c..a81e3ba6c 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 0da512bb5..dbff563df 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -108,8 +108,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 { @@ -133,8 +132,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 { @@ -155,7 +153,7 @@ class MiniLyricsPage extends HookConsumerWidget { ), style: ButtonStyle( foregroundColor: snapshot.data == true - ? MaterialStateProperty.all( + ? WidgetStateProperty.all( theme.colorScheme.primary) : null, ), @@ -187,12 +185,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, ), @@ -245,7 +243,7 @@ class MiniLyricsPage extends HookConsumerWidget { } : null, ), - Flexible(child: PlayerControls(compact: true)), + const Flexible(child: PlayerControls(compact: true)), IconButton( tooltip: context.l10n.exit_mini_player, icon: const Icon(SpotubeIcons.maximize), diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index 67bd8f570..9e51793d0 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -120,7 +120,7 @@ class ProfilePage extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(6), child: Text( - '$key', + key, style: textTheme.titleSmall, ), ), diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 402a7cf05..f7aedf63e 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -205,11 +205,11 @@ class RootApp extends HookConsumerWidget { ), ) : null, - bottomNavigationBar: Column( + bottomNavigationBar: const Column( mainAxisSize: MainAxisSize.min, children: [ BottomPlayer(), - const SpotubeNavigationBar(), + SpotubeNavigationBar(), ], ), ), diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index e28a5eff8..d5de12f0e 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -211,7 +211,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), @@ -219,7 +219,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), ), ), @@ -245,7 +245,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/sections/about.dart b/lib/pages/settings/sections/about.dart index c16930799..dfb5272b5 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 1b5b7e398..c670e96dd 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -130,7 +130,7 @@ class SettingsAccountSection extends HookConsumerWidget { : FilledButton( onPressed: onLogin, style: ButtonStyle( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(25.0), ), diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 5f9aa779f..da62fb307 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -70,7 +70,7 @@ class StatsStreamFeesPage extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Total ${usdFormatter.format(total)}", + context.l10n.total_money(usdFormatter.format(total)), style: textTheme.titleLarge, ), DropdownButton( @@ -79,30 +79,30 @@ class StatsStreamFeesPage extends HookConsumerWidget { if (value == null) return; duration.value = value; }, - items: const [ + items: [ DropdownMenuItem( value: HistoryDuration.days7, - child: Text("This week"), + child: Text(context.l10n.this_week), ), DropdownMenuItem( value: HistoryDuration.days30, - child: Text("This month"), + child: Text(context.l10n.this_month), ), DropdownMenuItem( value: HistoryDuration.months6, - child: Text("Last 6 months"), + child: Text(context.l10n.last_6_months), ), DropdownMenuItem( value: HistoryDuration.year, - child: Text("This year"), + child: Text(context.l10n.this_year), ), DropdownMenuItem( value: HistoryDuration.years2, - child: Text("Last 2 years"), + child: Text(context.l10n.last_2_years), ), DropdownMenuItem( value: HistoryDuration.allTime, - child: Text("All time"), + child: Text(context.l10n.all_time), ), ], ), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 3bc719424..c40f683d1 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart' hide Track; @@ -12,7 +11,6 @@ import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/discord_provider.dart'; -import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index 3995acf77..324059107 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -45,12 +45,9 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { Stream percentCompletedStream(double percent) { return positionStream .asyncMap( - (position) async => (await duration)?.inSeconds == 0 + (position) async => duration == Duration.zero ? 0 - : (position.inSeconds / - ((await duration)?.inSeconds ?? 100) * - 100) - .toInt(), + : (position.inSeconds / duration.inSeconds * 100).toInt(), ) .where((event) => event >= percent); } diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 1129b7915..485e5af7b 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -4,7 +4,6 @@ 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, brightness: brightness, ); @@ -30,7 +29,7 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { navigationBarTheme: const NavigationBarThemeData( labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, height: 50, - iconTheme: MaterialStatePropertyAll( + iconTheme: WidgetStatePropertyAll( IconThemeData(size: 18), ), ), @@ -48,7 +47,7 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), color: scheme.surface, elevation: 4, - labelTextStyle: MaterialStatePropertyAll( + labelTextStyle: WidgetStatePropertyAll( TextStyle(color: scheme.onSurface), ), ), @@ -60,25 +59,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)), diff --git a/untranslated_messages.json b/untranslated_messages.json index 5da4c3c6a..9e26dfeeb 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,105 +1 @@ -{ - "ar": [ - "and_n_more" - ], - - "bn": [ - "and_n_more" - ], - - "ca": [ - "and_n_more" - ], - - "cs": [ - "and_n_more" - ], - - "de": [ - "and_n_more" - ], - - "es": [ - "and_n_more" - ], - - "eu": [ - "and_n_more" - ], - - "fa": [ - "and_n_more" - ], - - "fi": [ - "and_n_more" - ], - - "fr": [ - "and_n_more" - ], - - "hi": [ - "and_n_more" - ], - - "id": [ - "and_n_more" - ], - - "it": [ - "and_n_more" - ], - - "ja": [ - "and_n_more" - ], - - "ka": [ - "and_n_more" - ], - - "ko": [ - "and_n_more" - ], - - "ne": [ - "and_n_more" - ], - - "nl": [ - "and_n_more" - ], - - "pl": [ - "and_n_more" - ], - - "pt": [ - "and_n_more" - ], - - "ru": [ - "and_n_more" - ], - - "th": [ - "and_n_more" - ], - - "tr": [ - "and_n_more" - ], - - "uk": [ - "and_n_more" - ], - - "vi": [ - "and_n_more" - ], - - "zh": [ - "and_n_more" - ] -} +{} \ No newline at end of file From b32ec9ccf92f8e2f07e1832ce56cb55a7062ecc6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 10 Aug 2024 23:21:48 +0600 Subject: [PATCH 89/92] chore: bump version and generate changelog --- .github/workflows/spotube-publish-binary.yml | 2 +- CHANGELOG.md | 37 +++++++++++++++++++- cli/commands/build/windows.dart | 19 ++++++++++ lib/modules/root/sidebar.dart | 2 +- pubspec.lock | 4 +-- pubspec.yaml | 6 ++-- windows/runner/Runner.rc | 4 +-- 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 7f85173fc..9d3ce4700 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.1 + default: 3.8.0 required: true dry_run: description: Dry run diff --git a/CHANGELOG.md b/CHANGELOG.md index 22919a32f..7e4345740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,42 @@ 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) +## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06) + +### Features + +- translations: make state page's hard coded strings translatable (#1719) +- discord: add listening activity type +- discord: album art, playing time and play pause support (#1765) +- linux: Use XDG_STATE_HOME to storage logs (#1675) +- discord rpc for macOS, windows-arm64 and linux-arm64 (#1713) +- desktop: implement webview based login +- stats: add lazy loading support + +### Bug Fixes + +- translations: fix Russian translations (#1696) +- ios: permission exception +- linux: tray icon wrong name for flatpak +- windows: app crashes when no internet +- windows: local tracks plays but disabled playback controls +- go to track album shows up for local tracks +- local track metadata timeout +- windows: window stretching #1553 +- android: app getting killed from background +- linux: OS Media control not working for Flatpak #1627 +- incorrect datatype used for MPRIS position property #1521 +- Too many artists for a track causing overflows +- playlist share button does not work #1639 +- unescape html escape values #1300 +- lyrics page doesn't scroll to top after song ends #885 +- changed source doesn't get saved and uses the wrong once again +- null exception in album page navigated from /home +- popup menu item opacity +- linux: change app id in flatpak environment + + +## [3.7.1](https://github.com/krtirtho/spotube/compare/v3.7.0...v3.7.1) (2024-06-06) ### Bug Fixes diff --git a/cli/commands/build/windows.dart b/cli/commands/build/windows.dart index 15e0bf170..c44ed52f5 100644 --- a/cli/commands/build/windows.dart +++ b/cli/commands/build/windows.dart @@ -41,6 +41,25 @@ class WindowsBuildCommand extends Command with BuildCommandCommonSteps { await bootstrap(); await innoDependInstall(); + final runnerRCFile = File( + join(cwd.path, "windows", "runner", "Runner.rc"), + ); + + runnerRCFile.writeAsStringSync( + runnerRCFile + .readAsStringSync() + .replaceAll("%{{SPOTUBE_VERSION}}%", versionWithoutBuildNumber) + .replaceAll( + "%{{SPOTUBE_VERSION_AS_NUMBER}}%", + [ + pubspec.version!.major, + pubspec.version!.minor, + pubspec.version!.patch, + 0 + ].join(","), + ), + ); + await shell.run( "flutter_distributor package --platform=windows --targets=exe --skip-clean", ); diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index 8f7f495c2..f29644fba 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -75,7 +75,7 @@ class Sidebar extends HookConsumerWidget { final bg = theme.colorScheme.surfaceContainerHighest; final bgColor = useBrightnessValue( - Color.lerp(bg, Colors.white, 0.7), + Color.lerp(bg, Colors.white, 0.6), Color.lerp(bg, Colors.black, 0.45)!, ); diff --git a/pubspec.lock b/pubspec.lock index 0bc24dff4..6f0f3e73f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1764,10 +1764,10 @@ packages: dependency: "direct dev" description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" quiver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7f6f0f294..77aa3f5ba 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.1+32 +version: 3.8.0+33 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube @@ -156,8 +156,8 @@ dev_dependencies: custom_lint: ^0.6.4 riverpod_lint: ^2.3.10 process_run: ^0.14.2 - pubspec_parse: ^1.2.2 - pub_api_client: ^2.4.0 + pubspec_parse: ^1.3.0 + pub_api_client: ^2.7.0 xml: ^6.5.0 io: ^1.0.4 drift_dev: ^2.18.0 diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 62e150f8a..c77ce0c64 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -63,13 +63,13 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" #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,0 +#define VERSION_AS_NUMBER %{{SPOTUBE_VERSION_AS_NUMBER}}% #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else -#define VERSION_AS_STRING "1.0.0" +#define VERSION_AS_STRING "%{{SPOTUBE_VERSION}}%" #endif VS_VERSION_INFO VERSIONINFO From b0a07b58d554375a7a66a76c8636dbe7064956ff Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 11 Aug 2024 13:45:03 +0600 Subject: [PATCH 90/92] cd: add playstore publish support --- .github/workflows/spotube-publish-binary.yml | 34 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 9d3ce4700..10ad810da 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -12,10 +12,10 @@ on: type: boolean default: true jobs: - description: Jobs to run (flathub,aur,winget,chocolatey) + description: Jobs to run (flathub,aur,winget,chocolatey,playstore) required: true type: string - default: "flathub,aur,winget,chocolatey" + default: "flathub,aur,winget,chocolatey,playstore" jobs: flathub: @@ -104,3 +104,33 @@ jobs: - name: Publish to Chocolatey Repository if: ${{ !inputs.dry_run }} run: choco push Spotube-windows-x86_64.nupkg --source https://push.chocolatey.org/ + + playstore: + runs-on: ubuntu-latest + if: contains(inputs.jobs, 'playstore') + steps: + - name: Tagname (workflow dispatch) + run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV + + - uses: robinraju/release-downloader@main + with: + tag: ${{ env.TAG_NAME }} + tarBall: false + zipBall: false + out-file-path: dist + fileName: "Spotube-playstore-all-arch.aab" + + - name: Create service-account.json + run: | + echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json + + - name: Upload Android Release to Play Store + if: ${{!inputs.dry_run}} + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJson: ./service-account.json + releaseFiles: ./dist/Spotube-playstore-all-arch.aab + packageName: oss.krtirtho.spotube + track: production + status: draft + releaseName: ${{ env.TAG_NAME }} \ No newline at end of file From 1b024b41fec457a86482066c4c9f33a8b5432937 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 11 Aug 2024 13:49:00 +0600 Subject: [PATCH 91/92] cd: fix playstore publish download faiils --- .github/workflows/spotube-publish-binary.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 10ad810da..bed5085f9 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -114,6 +114,7 @@ jobs: - uses: robinraju/release-downloader@main with: + repository: KRTirtho/spotube tag: ${{ env.TAG_NAME }} tarBall: false zipBall: false From c681401b6e7a32e2e49310254695f0b342bb1980 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 11 Aug 2024 13:50:39 +0600 Subject: [PATCH 92/92] cd: fix playstore publish download faiils --- .github/workflows/spotube-publish-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index bed5085f9..089dd185e 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -115,7 +115,7 @@ jobs: - uses: robinraju/release-downloader@main with: repository: KRTirtho/spotube - tag: ${{ env.TAG_NAME }} + tag: v${{ env.TAG_NAME }} tarBall: false zipBall: false out-file-path: dist