diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index 68ea2d67..5d918a03 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -284,7 +284,7 @@ jobs:
macos:
- runs-on: macos-12
+ runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.12.0
@@ -327,7 +327,7 @@ jobs:
- name: Package Macos App
run: |
- python3 -m pip install setuptools
+ brew install python-setuptools
npm install -g appdmg
mkdir -p build/${{ env.BUILD_VERSION }}
appdmg appdmg.json build/Spotube-macos-universal.dmg
@@ -349,7 +349,7 @@ jobs:
limit-access-to-actor: true
iOS:
- runs-on: macos-latest
+ runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.10.0
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0e6a4294..462d33ef 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,11 +2,15 @@
"cmake.configureOnOpen": false,
"cSpell.words": [
"acousticness",
+ "Amoled",
+ "Buildless",
"danceability",
+ "fuzzywuzzy",
"instrumentalness",
"Mpris",
"riverpod",
"Scrobblenaut",
+ "skeletonizer",
"speechiness",
"Spotube",
"winget"
diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets
new file mode 100644
index 00000000..9a18929b
--- /dev/null
+++ b/.vscode/snippets.code-snippets
@@ -0,0 +1,170 @@
+{
+ "PaginatedState": {
+ "scope": "dart",
+ "prefix": "paginatedState",
+ "description": "Generate a PaginatedState",
+ "body": [
+ "class ${1:Model}State extends PaginatedState<${2:Model}> {",
+ " ${1:Model}State({",
+ " required super.items,",
+ " required super.offset,",
+ " required super.limit,",
+ " required super.hasMore,",
+ " });",
+ " ",
+ " @override",
+ " ${1:Model}State copyWith({",
+ " List<${2:Model}>? items,",
+ " int? offset,",
+ " int? limit,",
+ " bool? hasMore,",
+ " }) {",
+ " return ${1:Model}State(",
+ " items: items ?? this.items,",
+ " offset: offset ?? this.offset,",
+ " limit: limit ?? this.limit,",
+ " hasMore: hasMore ?? this.hasMore,",
+ " );",
+ " }",
+ "}"
+ ]
+ },
+ "PaginatedAsyncNotifier": {
+ "scope": "dart",
+ "prefix": "paginatedAsyncNotifier",
+ "description": "Generate a PaginatedAsyncNotifier",
+ "body": [
+ "class ${1:NotifierName}Notifier extends PaginatedAsyncNotifier<${3:Item}, ${2:Model}State> {",
+ " ${1:NotifierName}Notifier() : super();",
+ " ",
+ " @override",
+ " fetch(int offset, int limit) async {",
+ " throw UnimplementedError();",
+ " }",
+ " ",
+ " @override",
+ " build() async {",
+ " throw UnimplementedError();",
+ " }",
+ "}"
+ ]
+ },
+ "PaginaitedNotifierWithState": {
+ "scope": "dart",
+ "prefix": "paginatedNotifierWithState",
+ "description": "Generate a PaginatedNotifier with PaginatedState",
+ "body": [
+ "class $1State extends PaginatedState<$2> {",
+ " $1State({",
+ " required super.items,",
+ " required super.offset,",
+ " required super.limit,",
+ " required super.hasMore,",
+ " });",
+ " ",
+ " @override",
+ " $1State copyWith({",
+ " List<$2>? items,",
+ " int? offset,",
+ " int? limit,",
+ " bool? hasMore,",
+ " }) {",
+ " return $1State(",
+ " items: items ?? this.items,",
+ " offset: offset ?? this.offset,",
+ " limit: limit ?? this.limit,",
+ " hasMore: hasMore ?? this.hasMore,",
+ " );",
+ " }",
+ "}",
+ " ",
+ "class $1Notifier",
+ " extends PaginatedAsyncNotifier<$2, $1State> {",
+ " $1Notifier() : super();",
+ " ",
+ " @override",
+ " fetch(int offset, int limit) async {",
+ " throw UnimplementedError();",
+ " }",
+ " ",
+ " @override",
+ " build() async {",
+ " throw UnimplementedError();",
+ " }",
+ "}",
+ " ",
+ "final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProvider<$1Notifier, $1State>(",
+ " ()=> $1Notifier(),",
+ ");"
+ ]
+ },
+ "FamilyPaginatedAsyncNotifier": {
+ "scope": "dart",
+ "prefix": "familyPaginatedAsyncNotifier",
+ "description": "Generate a FamilyPaginatedAsyncNotifier",
+ "body": [
+ "class ${1:NotifierName}Notifier extends FamilyPaginatedAsyncNotifier<${3:Item}, ${2:Model}State, {$4:Arg}> {",
+ " ${1:NotifierName}Notifier() : super();",
+ " ",
+ " @override",
+ " fetch(arg, offset, limit) async {",
+ " throw UnimplementedError();",
+ " }",
+ " ",
+ " @override",
+ " build(arg) async {",
+ " throw UnimplementedError();",
+ " }",
+ "}"
+ ]
+ },
+ "FamilyPaginaitedNotifierWithState": {
+ "scope": "dart",
+ "prefix": "familyPaginatedNotifierWithState",
+ "description": "Generate a FamilyPaginatedAsyncNotifier with PaginatedState",
+ "body": [
+ "class $1State extends PaginatedState<$2> {",
+ " $1State({",
+ " required super.items,",
+ " required super.offset,",
+ " required super.limit,",
+ " required super.hasMore,",
+ " });",
+ " ",
+ " @override",
+ " $1State copyWith({",
+ " List<$2>? items,",
+ " int? offset,",
+ " int? limit,",
+ " bool? hasMore,",
+ " }) {",
+ " return $1State(",
+ " items: items ?? this.items,",
+ " offset: offset ?? this.offset,",
+ " limit: limit ?? this.limit,",
+ " hasMore: hasMore ?? this.hasMore,",
+ " );",
+ " }",
+ "}",
+ " ",
+ "class $1Notifier",
+ " extends FamilyPaginatedAsyncNotifier<$2, $1State, $3> {",
+ " $1Notifier() : super();",
+ " ",
+ " @override",
+ " fetch(arg, offset, limit) async {",
+ " throw UnimplementedError();",
+ " }",
+ " ",
+ " @override",
+ " build(arg) async {",
+ " throw UnimplementedError();",
+ " }",
+ "}",
+ " ",
+ "final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProviderFamily<$1Notifier, $1State, $3>(",
+ " ()=> $1Notifier(),",
+ ");"
+ ]
+ },
+}
\ No newline at end of file
diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
index 13996cea..e859f9e6 100644
--- a/CONTRIBUTION.md
+++ b/CONTRIBUTION.md
@@ -25,7 +25,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
- [Before Submitting an Enhancement](#before-submitting-an-enhancement)
- [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
- [Your First Code Contribution](#your-first-code-contribution)
- - [Submit translations](#submit-translations)
+ - [Submit Translations](#submit-translations)
## Code of Conduct
@@ -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
+ $ 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
```
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
- Arch/Manjaro
```bash
- yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify
+ yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan
```
- Fedora
```bash
- dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel
+ dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns
```
- Clone the Repo
- Create a `.env` in root of the project following the `.env.example` template
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 5f2cbbe1..4ba476e0 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -25,6 +25,7 @@ linter:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
+ avoid_renaming_method_parameters: false
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -34,3 +35,5 @@ analyzer:
- patterns
errors:
invalid_annotation_target: ignore
+ plugins:
+ - custom_lint
diff --git a/ios/Podfile b/ios/Podfile
index bc3dcaa6..7235f482 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '12.0'
+platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 0b75217f..1d048cc9 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -5,6 +5,9 @@ PODS:
- Flutter
- audio_session (0.0.1):
- Flutter
+ - bonsoir_darwin (0.0.1):
+ - Flutter
+ - FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.4):
@@ -44,11 +47,13 @@ PODS:
- file_selector_ios (0.0.1):
- Flutter
- Flutter (1.0.0)
- - flutter_inappwebview (0.0.1):
+ - flutter_broadcasts (0.0.1):
- Flutter
- - flutter_inappwebview/Core (= 0.0.1)
+ - flutter_inappwebview_ios (0.0.1):
+ - Flutter
+ - flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- - flutter_inappwebview/Core (0.0.1):
+ - flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_keyboard_visibility (0.0.1):
@@ -102,11 +107,13 @@ DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
+ - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
+ - flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/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`)
@@ -142,6 +149,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_service/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
+ bonsoir_darwin:
+ :path: ".symlinks/plugins/bonsoir_darwin/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
@@ -150,8 +159,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_selector_ios/ios"
Flutter:
:path: Flutter
- flutter_inappwebview:
- :path: ".symlinks/plugins/flutter_inappwebview/ios"
+ flutter_broadcasts:
+ :path: ".symlinks/plugins/flutter_broadcasts/ios"
+ flutter_inappwebview_ios:
+ :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_mailer:
@@ -191,13 +202,15 @@ SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
- device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
+ bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
+ device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
+ flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
+ flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
@@ -221,6 +234,6 @@ SPEC CHECKSUMS:
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
-PODFILE CHECKSUM: 5129d2e80ab0dfc533f262cedf032011b1dfe4fd
+PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
COCOAPODS: 1.15.2
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 8e103cfa..ffd511a4 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -66,5 +66,11 @@
UIViewControllerBasedStatusBarAppearance
+ NSLocalNetworkUsageDescription
+ To allow other devices on the network control playback of Spotube securely.
+ NSBonjourServices
+
+ _spotube._tcp
+
\ No newline at end of file
diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart
index 8f5f9e8b..c5379ec6 100644
--- a/lib/collections/fake.dart
+++ b/lib/collections/fake.dart
@@ -6,7 +6,7 @@ abstract class FakeData {
static final Image image = Image()
..height = 1
..width = 1
- ..url = "url";
+ ..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg";
static final Followers followers = Followers()
..href = "text"
diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart
index 43d0cf2e..80067405 100644
--- a/lib/collections/routes.dart
+++ b/lib/collections/routes.dart
@@ -4,7 +4,10 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Search;
+import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/pages/album/album.dart';
+import 'package:spotube/pages/connect/connect.dart';
+import 'package:spotube/pages/connect/control/control.dart';
import 'package:spotube/pages/getting_started/getting_started.dart';
import 'package:spotube/pages/home/genres/genre_playlists.dart';
import 'package:spotube/pages/home/genres/genres.dart';
@@ -96,8 +99,7 @@ final routerProvider = Provider((ref) {
path: "result",
pageBuilder: (context, state) => SpotubePage(
child: PlaylistGenerateResultPage(
- state:
- state.extra as PlaylistGenerateResultRouteState,
+ state: state.extra as GeneratePlaylistProviderInput,
),
),
),
@@ -173,6 +175,21 @@ final routerProvider = Provider((ref) {
);
},
),
+ GoRoute(
+ path: "/connect",
+ pageBuilder: (context, state) => const SpotubePage(
+ child: ConnectPage(),
+ ),
+ routes: [
+ GoRoute(
+ path: "control",
+ pageBuilder: (context, state) {
+ return const SpotubePage(
+ child: ConnectControlPage(),
+ );
+ },
+ )
+ ])
],
),
GoRoute(
diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart
index 6cf92085..6de21284 100644
--- a/lib/collections/spotube_icons.dart
+++ b/lib/collections/spotube_icons.dart
@@ -115,4 +115,10 @@ abstract class SpotubeIcons {
static const github = SimpleIcons.github;
static const openCollective = SimpleIcons.opencollective;
static const anonymous = FeatherIcons.user;
+ static const history = FeatherIcons.clock;
+ static const connect = FeatherIcons.link;
+ static const speaker = FeatherIcons.speaker;
+ static const monitor = FeatherIcons.monitor;
+ static const power = FeatherIcons.power;
+ static const bluetooth = FeatherIcons.bluetooth;
}
diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart
index 4d2e12d6..678bfd06 100644
--- a/lib/components/album/album_card.dart
+++ b/lib/components/album/album_card.dart
@@ -1,17 +1,19 @@
-import 'package:fl_query_hooks/fl_query_hooks.dart';
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/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/extensions/infinite_query.dart';
+import 'package:spotube/extensions/image.dart';
+import 'package:spotube/extensions/track.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/spotify_provider.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
-import 'package:spotube/services/queries/album.dart';
import 'package:spotube/utils/service_utils.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
extension FormattedAlbumType on AlbumType {
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
@@ -31,47 +33,25 @@ class AlbumCard extends HookConsumerWidget {
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
- final queryClient = useQueryClient();
-
bool isPlaylistPlaying = useMemoized(
() => playlist.containsCollection(album.id!),
[playlist, album.id],
);
final updating = useState(false);
- final spotify = ref.watch(spotifyProvider);
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
Future> fetchAllTrack() async {
if (album.tracks != null && album.tracks!.isNotEmpty) {
- return album.tracks!
- .map((track) =>
- TypeConversionUtils.simpleTrack_X_Track(track, album))
- .toList();
+ return album.tracks!.map((track) => track.asTrack(album)).toList();
}
- final job = AlbumQueries.tracksOfJob(album.id!);
-
- final query = queryClient.createInfiniteQuery(
- job.queryKey,
- (page) => job.task(page, (spotify: spotify, album: album)),
- initialPage: 0,
- nextPage: job.nextPage,
- );
-
- return await query.fetchAllTracks(
- getAllTracks: () async {
- final res = await spotify.albums.tracks(album.id!).all();
- return res
- .map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
- .toList();
- },
- );
+ await ref.read(albumTracksProvider(album).future);
+ return ref.read(albumTracksProvider(album).notifier).fetchAll();
}
return PlaybuttonCard(
- imageUrl: TypeConversionUtils.image_X_UrlString(
- album.images,
+ imageUrl: album.images.asUrlString(
placeholder: ImagePlaceholder.collection,
),
margin: const EdgeInsets.symmetric(horizontal: 10),
@@ -80,7 +60,7 @@ class AlbumCard extends HookConsumerWidget {
updating.value,
title: album.name!,
description:
- "${album.albumType?.formatted} • ${TypeConversionUtils.artists_X_String(album.artists ?? [])}",
+ "${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
onTap: () {
ServiceUtils.push(context, "/album/${album.id}", extra: album);
},
@@ -95,8 +75,19 @@ class AlbumCard extends HookConsumerWidget {
if (fetchedTracks.isEmpty) return;
- await playlistNotifier.load(fetchedTracks, autoPlay: true);
- playlistNotifier.addCollection(album.id!);
+ final isRemoteDevice = await showSelectDeviceDialog(context, ref);
+ if (isRemoteDevice) {
+ final remotePlayback = ref.read(connectProvider.notifier);
+ await remotePlayback.load(
+ WebSocketLoadEventData(
+ tracks: fetchedTracks,
+ collectionId: album.id!,
+ ),
+ );
+ } else {
+ await playlistNotifier.load(fetchedTracks, autoPlay: true);
+ playlistNotifier.addCollection(album.id!);
+ }
} finally {
updating.value = false;
}
diff --git a/lib/components/artist/artist_album_list.dart b/lib/components/artist/artist_album_list.dart
index 5114170c..a91327ce 100644
--- a/lib/components/artist/artist_album_list.dart
+++ b/lib/components/artist/artist_album_list.dart
@@ -1,38 +1,35 @@
import 'package:flutter/material.dart' hide Page;
-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/extensions/context.dart';
import 'package:spotube/models/logger.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class ArtistAlbumList extends HookConsumerWidget {
final String artistId;
ArtistAlbumList(
this.artistId, {
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
final logger = getLogger(ArtistAlbumList);
@override
Widget build(BuildContext context, ref) {
- final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
+ final albumsQuery = ref.watch(artistAlbumsProvider(artistId));
+ final albumsQueryNotifier =
+ ref.watch(artistAlbumsProvider(artistId).notifier);
- final albums = useMemoized(() {
- return albumsQuery.pages
- .expand((page) => page.items ?? const Iterable.empty())
- .toList();
- }, [albumsQuery.pages]);
+ final albums = albumsQuery.asData?.value.items ?? [];
final theme = Theme.of(context);
return HorizontalPlaybuttonCardView(
isLoadingNextPage: albumsQuery.isLoadingNextPage,
- hasNextPage: albumsQuery.hasNextPage,
+ hasNextPage: albumsQuery.asData?.value.hasMore ?? false,
items: albums,
- onFetchMore: albumsQuery.fetchNext,
+ onFetchMore: albumsQueryNotifier.fetchMore,
title: Text(
context.l10n.albums,
style: theme.textTheme.headlineSmall,
diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart
index 3526e88f..ebe18e72 100644
--- a/lib/components/artist/artist_card.dart
+++ b/lib/components/artist/artist_card.dart
@@ -6,22 +6,21 @@ import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/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';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/service_utils.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistCard extends HookConsumerWidget {
final Artist artist;
- const ArtistCard(this.artist, {Key? key}) : super(key: key);
+ const ArtistCard(this.artist, {super.key});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final backgroundImage = UniversalImage.imageProvider(
- TypeConversionUtils.image_X_UrlString(
- artist.images,
+ artist.images.asUrlString(
placeholder: ImagePlaceholder.artist,
),
);
diff --git a/lib/components/connect/connect_device.dart b/lib/components/connect/connect_device.dart
new file mode 100644
index 00000000..8ece074f
--- /dev/null
+++ b/lib/components/connect/connect_device.dart
@@ -0,0 +1,85 @@
+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/extensions/context.dart';
+import 'package:spotube/provider/connect/clients.dart';
+import 'package:spotube/utils/service_utils.dart';
+
+class ConnectDeviceButton extends HookConsumerWidget {
+ const ConnectDeviceButton({super.key});
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final ThemeData(:colorScheme) = Theme.of(context);
+ final pixelRatio = MediaQuery.of(context).devicePixelRatio;
+ final connectClients = ref.watch(connectClientsProvider);
+
+ return SizedBox(
+ height: 40 * pixelRatio,
+ child: Stack(
+ alignment: Alignment.centerRight,
+ fit: StackFit.loose,
+ children: [
+ Center(
+ child: InkWell(
+ onTap: () {
+ ServiceUtils.push(context, "/connect");
+ },
+ borderRadius: BorderRadius.circular(50),
+ child: Ink(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(50),
+ color: colorScheme.primaryContainer,
+ ),
+ padding:
+ const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (connectClients.asData?.value.resolvedService !=
+ null) ...[
+ Container(
+ width: 7,
+ height: 7,
+ decoration: BoxDecoration(
+ color: Colors.greenAccent,
+ borderRadius: BorderRadius.circular(50),
+ ),
+ ),
+ const Gap(5),
+ ],
+ Text(context.l10n.devices),
+ if (connectClients.asData?.value.services.isNotEmpty ==
+ true)
+ Text(
+ " (${connectClients.asData?.value.services.length})",
+ style: TextStyle(
+ color:
+ colorScheme.onPrimaryContainer.withOpacity(0.5),
+ ),
+ ),
+ const Gap(35),
+ ],
+ ),
+ ),
+ ),
+ ),
+ Positioned(
+ right: 0,
+ child: IconButton.filled(
+ icon: const Icon(SpotubeIcons.speaker),
+ style: IconButton.styleFrom(
+ visualDensity: VisualDensity.standard,
+ foregroundColor: colorScheme.onPrimary,
+ ),
+ onPressed: () {
+ ServiceUtils.push(context, "/connect");
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/connect/local_devices.dart b/lib/components/connect/local_devices.dart
new file mode 100644
index 00000000..dd7db971
--- /dev/null
+++ b/lib/components/connect/local_devices.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:gap/gap.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/services/audio_player/audio_player.dart';
+
+class ConnectPageLocalDevices extends HookWidget {
+ const ConnectPageLocalDevices({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData(:textTheme) = Theme.of(context);
+ final devicesFuture = useFuture(audioPlayer.devices);
+ final devicesStream = useStream(audioPlayer.devicesStream);
+ final selectedDeviceFuture = useFuture(audioPlayer.selectedDevice);
+ final selectedDeviceStream = useStream(audioPlayer.selectedDeviceStream);
+
+ final devices = devicesStream.data ?? devicesFuture.data;
+ final selectedDevice =
+ selectedDeviceStream.data ?? selectedDeviceFuture.data;
+
+ if (devices == null) {
+ return const SliverToBoxAdapter(child: SizedBox.shrink());
+ }
+
+ return SliverMainAxisGroup(
+ slivers: [
+ const SliverGap(10),
+ SliverPadding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ sliver: SliverToBoxAdapter(
+ child: Text(
+ context.l10n.this_device,
+ style: textTheme.titleMedium,
+ ),
+ ),
+ ),
+ const SliverGap(10),
+ SliverList.separated(
+ itemCount: devices.length,
+ separatorBuilder: (context, index) => const Gap(10),
+ itemBuilder: (context, index) {
+ final device = devices[index];
+
+ return Card(
+ child: ListTile(
+ leading: const Icon(SpotubeIcons.speaker),
+ title: Text(device.description),
+ subtitle: Text(device.name),
+ selected: selectedDevice == device,
+ onTap: () => audioPlayer.setAudioDevice(device),
+ ),
+ );
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart
index 5abb9524..a3deb54a 100644
--- a/lib/components/desktop_login/login_form.dart
+++ b/lib/components/desktop_login/login_form.dart
@@ -8,9 +8,9 @@ import 'package:spotube/provider/authentication_provider.dart';
class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone;
const TokenLoginForm({
- Key? key,
+ super.key,
this.onDone,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/home/sections/featured.dart b/lib/components/home/sections/featured.dart
index 8a7c2c95..0db5a1e8 100644
--- a/lib/components/home/sections/featured.dart
+++ b/lib/components/home/sections/featured.dart
@@ -1,35 +1,28 @@
import 'package:flutter/material.dart' hide Page;
-import 'package:flutter_hooks/flutter_hooks.dart';
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/extensions/context.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class HomeFeaturedSection extends HookConsumerWidget {
- const HomeFeaturedSection({Key? key}) : super(key: key);
+ const HomeFeaturedSection({super.key});
@override
Widget build(BuildContext context, ref) {
- final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
- final playlists = useMemoized(
- () => featuredPlaylistsQuery.pages
- .whereType>()
- .expand((page) => page.items ?? const []),
- [featuredPlaylistsQuery.pages],
- );
- final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
- !featuredPlaylistsQuery.isLoadingNextPage;
+ final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
+ final featuredPlaylistsNotifier =
+ ref.watch(featuredPlaylistsProvider.notifier);
return Skeletonizer(
- enabled: isLoadingFeaturedPlaylists,
+ enabled: featuredPlaylists.isLoading,
child: HorizontalPlaybuttonCardView(
- items: playlists.toList(),
+ items: featuredPlaylists.asData?.value.items ?? [],
title: Text(context.l10n.featured),
- isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage,
- hasNextPage: featuredPlaylistsQuery.hasNextPage,
- onFetchMore: featuredPlaylistsQuery.fetchNext,
+ isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
+ hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
+ onFetchMore: featuredPlaylistsNotifier.fetchMore,
),
);
}
diff --git a/lib/components/home/sections/friends.dart b/lib/components/home/sections/friends.dart
index 6382f6fd..35ec09b0 100644
--- a/lib/components/home/sections/friends.dart
+++ b/lib/components/home/sections/friends.dart
@@ -1,4 +1,3 @@
-import 'dart:ffi';
import 'dart:ui';
import 'package:flutter/material.dart';
@@ -8,15 +7,16 @@ import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/home/sections/friends/friend_item.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/spotify_friends.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class HomePageFriendsSection extends HookConsumerWidget {
- const HomePageFriendsSection({Key? key}) : super(key: key);
+ const HomePageFriendsSection({super.key});
@override
Widget build(BuildContext context, ref) {
- final friendsQuery = useQueries.user.friendActivity(ref);
- final friends = friendsQuery.data?.friends ?? FakeData.friends.friends;
+ final friendsQuery = ref.watch(friendsProvider);
+ final friends =
+ friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
final groupCount = useBreakpointValue(
sm: 3,
@@ -51,8 +51,8 @@ class HomePageFriendsSection extends HookConsumerWidget {
},
);
- if (!friendsQuery.isLoading &&
- (!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) {
+ if (friendsQuery.isLoading ||
+ friendsQuery.asData?.value.friends.isEmpty == true) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart
index fcdadab7..b883e2cc 100644
--- a/lib/components/home/sections/friends/friend_item.dart
+++ b/lib/components/home/sections/friends/friend_item.dart
@@ -1,10 +1,8 @@
-import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/models/spotify_friends.dart';
@@ -13,9 +11,9 @@ import 'package:spotube/provider/spotify_provider.dart';
class FriendItem extends HookConsumerWidget {
final SpotifyFriendActivity friend;
const FriendItem({
- Key? key,
+ super.key,
required this.friend,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
@@ -24,7 +22,6 @@ class FriendItem extends HookConsumerWidget {
colorScheme: colorScheme,
) = Theme.of(context);
- final queryClient = useQueryClient();
final spotify = ref.watch(spotifyProvider);
return Container(
@@ -86,15 +83,11 @@ class FriendItem extends HookConsumerWidget {
..onTap = () async {
context.push(
"/${friend.track.context.path}",
- extra: !friend.track.context.path
- .startsWith("album")
- ? null
- : await queryClient.fetchQuery(
- "album/${friend.track.album.id}",
- () => spotify.albums.get(
- friend.track.album.id,
- ),
- ),
+ extra:
+ !friend.track.context.path.startsWith("album")
+ ? null
+ : await spotify.albums
+ .get(friend.track.context.id),
);
},
),
@@ -110,12 +103,7 @@ class FriendItem extends HookConsumerWidget {
recognizer: TapGestureRecognizer()
..onTap = () async {
final album =
- await queryClient.fetchQuery(
- "album/${friend.track.album.id}",
- () => spotify.albums.get(
- friend.track.album.id,
- ),
- );
+ await spotify.albums.get(friend.track.album.id);
if (context.mounted) {
context.push(
"/album/${friend.track.album.id}",
diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart
index 41ba235c..ac2644f0 100644
--- a/lib/components/home/sections/genres.dart
+++ b/lib/components/home/sections/genres.dart
@@ -13,28 +13,26 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class HomeGenresSection extends HookConsumerWidget {
- const HomeGenresSection({Key? key}) : super(key: key);
+ const HomeGenresSection({super.key});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
- final recommendationMarket = ref.watch(
- userPreferencesProvider.select((s) => s.recommendationMarket),
+ final categoriesQuery = ref.watch(categoriesProvider);
+ final categories = useMemoized(
+ () =>
+ categoriesQuery.asData?.value
+ .where((c) => (c.icons?.length ?? 0) > 0)
+ .take(mediaQuery.mdAndDown ? 6 : 10)
+ .toList() ??
+ [],
+ [mediaQuery.mdAndDown, categoriesQuery.asData?.value],
);
- final categoriesQuery =
- useQueries.category.listAll(ref, recommendationMarket);
-
- final categories = categoriesQuery.data
- ?.where((c) => (c.icons?.length ?? 0) > 0)
- .take(mediaQuery.mdAndDown ? 6 : 10)
- .toList() ??
- [];
return SliverMainAxisGroup(
slivers: [
diff --git a/lib/components/home/sections/made_for_user.dart b/lib/components/home/sections/made_for_user.dart
index a3f96899..d1d269f6 100644
--- a/lib/components/home/sections/made_for_user.dart
+++ b/lib/components/home/sections/made_for_user.dart
@@ -2,19 +2,19 @@ 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/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class HomeMadeForUserSection extends HookConsumerWidget {
- const HomeMadeForUserSection({Key? key}) : super(key: key);
+ const HomeMadeForUserSection({super.key});
@override
Widget build(BuildContext context, ref) {
- final madeForUser = useQueries.views.get(ref, "made-for-x-hub");
+ final madeForUser = ref.watch(viewProvider("made-for-x-hub"));
return SliverList.builder(
- itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0,
+ itemCount: madeForUser.asData?.value["content"]?["items"]?.length ?? 0,
itemBuilder: (context, index) {
- final item = madeForUser.data?["content"]?["items"]?[index];
+ final item = madeForUser.asData?.value["content"]?["items"]?[index];
final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
diff --git a/lib/components/home/sections/new_releases.dart b/lib/components/home/sections/new_releases.dart
index 0f4a046a..57af12fd 100644
--- a/lib/components/home/sections/new_releases.dart
+++ b/lib/components/home/sections/new_releases.dart
@@ -1,56 +1,35 @@
import 'package:flutter/material.dart' hide Page;
-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/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/services/queries/queries.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class HomeNewReleasesSection extends HookConsumerWidget {
- const HomeNewReleasesSection({Key? key}) : super(key: key);
+ const HomeNewReleasesSection({super.key});
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider);
- final newReleases = useQueries.album.newReleases(ref);
- final userArtistsQuery = useQueries.artist.followedByMeAll(ref);
- final userArtists =
- userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
+ final newReleases = ref.watch(albumReleasesProvider);
+ final newReleasesNotifier = ref.read(albumReleasesProvider.notifier);
- final albums = useMemoized(
- () {
- final allReleases = newReleases.pages
- .whereType>()
- .expand((page) => page.items ?? const [])
- .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album));
+ final albums = ref.watch(userArtistAlbumReleasesProvider);
- final userArtistReleases = allReleases.where((album) {
- return album.artists
- ?.any((artist) => userArtists.contains(artist.id!)) ==
- true;
- }).toList();
-
- if (userArtistReleases.isEmpty) return allReleases.toList();
- return userArtistReleases;
- },
- [newReleases.pages],
- );
-
- final hasNewReleases = newReleases.hasPageData &&
- userArtistsQuery.hasData &&
- !newReleases.isLoadingNextPage;
-
- if (auth == null || !hasNewReleases) return const SizedBox.shrink();
+ if (auth == null ||
+ newReleases.isLoading ||
+ newReleases.asData?.value.items.isEmpty == true) {
+ return const SizedBox.shrink();
+ }
return HorizontalPlaybuttonCardView(
items: albums,
title: Text(context.l10n.new_releases),
isLoadingNextPage: newReleases.isLoadingNextPage,
- hasNextPage: newReleases.hasNextPage,
- onFetchMore: newReleases.fetchNext,
+ hasNextPage: newReleases.asData?.value.hasMore ?? false,
+ onFetchMore: newReleasesNotifier.fetchMore,
);
}
}
diff --git a/lib/components/library/playlist_generate/multi_select_field.dart b/lib/components/library/playlist_generate/multi_select_field.dart
index ed5eb38f..e54fc2ba 100644
--- a/lib/components/library/playlist_generate/multi_select_field.dart
+++ b/lib/components/library/playlist_generate/multi_select_field.dart
@@ -25,7 +25,7 @@ class MultiSelectField extends HookWidget {
final bool enabled;
const MultiSelectField({
- Key? key,
+ super.key,
required this.options,
required this.selectedOptions,
required this.getValueForOption,
@@ -36,7 +36,7 @@ class MultiSelectField extends HookWidget {
this.dialogTitle,
this.helperText,
this.enabled = true,
- }) : super(key: key);
+ });
Widget defaultSelectedOptionBuilder(T option) {
return Chip(
@@ -134,14 +134,14 @@ class _MultiSelectDialog extends HookWidget {
final String? helperText;
const _MultiSelectDialog({
- Key? key,
+ super.key,
required this.dialogTitle,
required this.options,
required this.getValueForOption,
this.optionBuilder,
this.initialSelection = const [],
this.helperText,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/library/playlist_generate/recommendation_attribute_dials.dart b/lib/components/library/playlist_generate/recommendation_attribute_dials.dart
index 87f7cb1b..d7f51ffb 100644
--- a/lib/components/library/playlist_generate/recommendation_attribute_dials.dart
+++ b/lib/components/library/playlist_generate/recommendation_attribute_dials.dart
@@ -20,12 +20,12 @@ class RecommendationAttributeDials extends HookWidget {
final double base;
const RecommendationAttributeDials({
- Key? key,
+ super.key,
required this.values,
required this.onChanged,
required this.title,
this.base = 1,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/library/playlist_generate/recommendation_attribute_fields.dart b/lib/components/library/playlist_generate/recommendation_attribute_fields.dart
index de169147..75437360 100644
--- a/lib/components/library/playlist_generate/recommendation_attribute_fields.dart
+++ b/lib/components/library/playlist_generate/recommendation_attribute_fields.dart
@@ -12,12 +12,12 @@ class RecommendationAttributeFields extends HookWidget {
final Map? presets;
const RecommendationAttributeFields({
- Key? key,
+ super.key,
required this.values,
required this.onChanged,
required this.title,
this.presets,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart b/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart
index b1665d32..73c58deb 100644
--- a/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart
+++ b/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart
@@ -26,7 +26,7 @@ class SeedsMultiAutocomplete extends HookWidget {
final SelectedItemDisplayType selectedItemDisplayType;
const SeedsMultiAutocomplete({
- Key? key,
+ super.key,
required this.seeds,
required this.fetchSeeds,
required this.autocompleteOptionBuilder,
@@ -35,7 +35,7 @@ class SeedsMultiAutocomplete extends HookWidget {
this.inputDecoration,
this.enabled = true,
this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/library/playlist_generate/simple_track_tile.dart b/lib/components/library/playlist_generate/simple_track_tile.dart
index 86800d06..cf4ddb1a 100644
--- a/lib/components/library/playlist_generate/simple_track_tile.dart
+++ b/lib/components/library/playlist_generate/simple_track_tile.dart
@@ -4,16 +4,16 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
+import 'package:spotube/extensions/image.dart';
class SimpleTrackTile extends HookWidget {
final Track track;
final VoidCallback? onDelete;
const SimpleTrackTile({
- Key? key,
+ super.key,
required this.track,
this.onDelete,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
@@ -21,8 +21,7 @@ class SimpleTrackTile extends HookWidget {
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: UniversalImage(
- path: TypeConversionUtils.image_X_UrlString(
- track.album?.images,
+ path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.artist,
),
height: 40,
diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart
index 200d1c59..f58d6693 100644
--- a/lib/components/library/user_albums.dart
+++ b/lib/components/library/user_albums.dart
@@ -4,7 +4,6 @@ import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
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/collections/spotube_icons.dart';
@@ -13,44 +12,39 @@ import 'package:spotube/components/shared/fallbacks/not_found.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/extensions/album_simple.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/services/queries/queries.dart';
-
-import 'package:spotube/utils/type_conversion_utils.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class UserAlbums extends HookConsumerWidget {
- const UserAlbums({Key? key}) : super(key: key);
+ const UserAlbums({super.key});
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider);
- final albumsQuery = useQueries.album.ofMine(ref);
+ final albumsQuery = ref.watch(favoriteAlbumsProvider);
+ final albumsQueryNotifier = ref.watch(favoriteAlbumsProvider.notifier);
final controller = useScrollController();
final searchText = useState('');
- final allAlbums = useMemoized(
- () => albumsQuery.pages
- .expand((element) => element.items ?? []),
- [albumsQuery.pages],
- );
-
final albums = useMemoized(() {
if (searchText.value.isEmpty) {
- return allAlbums;
+ return albumsQuery.asData?.value.items ?? [];
}
- return allAlbums
- .map((e) => (
- weightedRatio(e.name!, searchText.value),
- e,
- ))
- .sorted((a, b) => b.$1.compareTo(a.$1))
- .where((e) => e.$1 > 50)
- .map((e) => e.$2)
- .toList();
- }, [allAlbums, searchText.value]);
+ return albumsQuery.asData?.value.items
+ .map((e) => (
+ weightedRatio(e.name!, searchText.value),
+ e,
+ ))
+ .sorted((a, b) => b.$1.compareTo(a.$1))
+ .where((e) => e.$1 > 50)
+ .map((e) => e.$2)
+ .toList() ??
+ [];
+ }, [albumsQuery.asData?.value, searchText.value]);
if (auth == null) {
return const AnonymousFallback();
@@ -60,7 +54,7 @@ class UserAlbums extends HookConsumerWidget {
return RefreshIndicator(
onRefresh: () async {
- await albumsQuery.refresh();
+ ref.invalidate(favoriteAlbumsProvider);
},
child: SafeArea(
child: Scaffold(
@@ -85,7 +79,7 @@ class UserAlbums extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0),
controller: controller,
child: Skeletonizer(
- enabled: albumsQuery.pages.isEmpty,
+ enabled: albumsQuery.isLoading,
child: Center(
child: Wrap(
runSpacing: 20,
@@ -93,7 +87,8 @@ class UserAlbums extends HookConsumerWidget {
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
- if (albumsQuery.pages.isEmpty)
+ if (albumsQuery.asData?.value == null ||
+ albumsQuery.asData!.value.items.isEmpty)
...List.generate(
10,
(index) => AlbumCard(FakeData.album),
@@ -103,16 +98,17 @@ class UserAlbums extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [NotFound()],
),
- for (final album in albums)
- AlbumCard(
- TypeConversionUtils.simpleAlbum_X_Album(album),
- ),
- if (albums.isNotEmpty && albumsQuery.hasNextPage)
- Waypoint(
- controller: controller,
- isGrid: true,
- onTouchEdge: albumsQuery.fetchNext,
- child: AlbumCard(FakeData.album),
+ for (final album in albums) AlbumCard(album.toAlbum()),
+ if (albums.isNotEmpty &&
+ albumsQuery.asData?.value.hasMore == true)
+ Skeletonizer(
+ enabled: true,
+ child: Waypoint(
+ controller: controller,
+ isGrid: true,
+ onTouchEdge: albumsQueryNotifier.fetchMore,
+ child: AlbumCard(FakeData.album),
+ ),
)
],
),
diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart
index 36b8528e..de6830c8 100644
--- a/lib/components/library/user_artists.dart
+++ b/lib/components/library/user_artists.dart
@@ -13,22 +13,22 @@ import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class UserArtists extends HookConsumerWidget {
- const UserArtists({Key? key}) : super(key: key);
+ const UserArtists({super.key});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final auth = ref.watch(AuthenticationNotifier.provider);
- final artistQuery = useQueries.artist.followedByMeAll(ref);
+ final artistQuery = ref.watch(followedArtistsProvider);
final searchText = useState('');
final filteredArtists = useMemoized(() {
- final artists = artistQuery.data ?? [];
+ final artists = artistQuery.asData?.value.items ?? [];
if (searchText.value.isEmpty) {
return artists.toList();
@@ -42,7 +42,7 @@ class UserArtists extends HookConsumerWidget {
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList();
- }, [artistQuery.data, searchText.value]);
+ }, [artistQuery.asData?.value.items, searchText.value]);
final controller = useScrollController();
@@ -66,7 +66,7 @@ class UserArtists extends HookConsumerWidget {
),
),
backgroundColor: theme.scaffoldBackgroundColor,
- body: artistQuery.data?.isEmpty == true
+ body: artistQuery.asData?.value.items.isEmpty == true
? Padding(
padding: const EdgeInsets.all(20),
child: Row(
@@ -80,7 +80,7 @@ class UserArtists extends HookConsumerWidget {
)
: RefreshIndicator(
onRefresh: () async {
- await artistQuery.refresh();
+ ref.invalidate(followedArtistsProvider);
},
child: InterScrollbar(
controller: controller,
@@ -109,8 +109,9 @@ class UserArtists extends HookConsumerWidget {
)
]
: filteredArtists
- .mapIndexed((index, artist) =>
- ArtistCard(artist))
+ .mapIndexed(
+ (index, artist) => ArtistCard(artist),
+ )
.toList(),
),
),
diff --git a/lib/components/library/user_downloads.dart b/lib/components/library/user_downloads.dart
index c8ceee66..3a1162e6 100644
--- a/lib/components/library/user_downloads.dart
+++ b/lib/components/library/user_downloads.dart
@@ -7,7 +7,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart';
class UserDownloads extends HookConsumerWidget {
- const UserDownloads({Key? key}) : super(key: key);
+ const UserDownloads({super.key});
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/library/user_downloads/download_item.dart b/lib/components/library/user_downloads/download_item.dart
index 10dec410..a145fdad 100644
--- a/lib/components/library/user_downloads/download_item.dart
+++ b/lib/components/library/user_downloads/download_item.dart
@@ -4,18 +4,19 @@ 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/extensions/context.dart';
+import 'package:spotube/extensions/image.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/type_conversion_utils.dart';
class DownloadItem extends HookConsumerWidget {
final Track track;
const DownloadItem({
- Key? key,
+ super.key,
required this.track,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
@@ -51,16 +52,15 @@ class DownloadItem extends HookConsumerWidget {
child: UniversalImage(
height: 40,
width: 40,
- path: TypeConversionUtils.image_X_UrlString(
- track.album?.images,
+ path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
),
),
),
title: Text(track.name ?? ''),
- subtitle: TypeConversionUtils.artists_X_ClickableArtists(
- track.artists ?? [],
+ subtitle: ArtistLink(
+ artists: track.artists ?? [],
mainAxisAlignment: WrapAlignment.start,
),
trailing: isQueryingSourceInfo
diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart
index 095e6e97..778558f6 100644
--- a/lib/components/library/user_local_tracks.dart
+++ b/lib/components/library/user_local_tracks.dart
@@ -21,12 +21,13 @@ import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
+import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
const supportedAudioTypes = [
@@ -111,7 +112,7 @@ final localTracksProvider = FutureProvider>((ref) async {
final tracks = filesWithMetadata
.map(
(fileWithMetadata) => LocalTrack.fromTrack(
- track: TypeConversionUtils.localTrack_X_Track(
+ track: Track().fromFile(
fileWithMetadata["file"],
metadata: fileWithMetadata["metadata"],
art: fileWithMetadata["art"],
@@ -129,7 +130,7 @@ final localTracksProvider = FutureProvider>((ref) async {
});
class UserLocalTracks extends HookConsumerWidget {
- const UserLocalTracks({Key? key}) : super(key: key);
+ const UserLocalTracks({super.key});
Future playLocalTracks(
WidgetRef ref,
@@ -159,7 +160,7 @@ class UserLocalTracks extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying =
- playlist.containsTracks(trackSnapshot.value ?? []);
+ playlist.containsTracks(trackSnapshot.asData?.value ?? []);
final searchController = useTextEditingController();
useValueListenable(searchController);
@@ -176,13 +177,13 @@ class UserLocalTracks extends HookConsumerWidget {
children: [
const SizedBox(width: 10),
FilledButton(
- onPressed: trackSnapshot.value != null
+ onPressed: trackSnapshot.asData?.value != null
? () async {
- if (trackSnapshot.value?.isNotEmpty == true) {
+ if (trackSnapshot.asData?.value.isNotEmpty == true) {
if (!isPlaylistPlaying) {
await playLocalTracks(
ref,
- trackSnapshot.value!,
+ trackSnapshot.asData!.value,
);
} else {
// TODO: Remove stop capability
@@ -217,7 +218,7 @@ class UserLocalTracks extends HookConsumerWidget {
FilledButton(
child: const Icon(SpotubeIcons.refresh),
onPressed: () {
- ref.refresh(localTracksProvider);
+ ref.invalidate(localTracksProvider);
},
)
],
@@ -242,7 +243,7 @@ class UserLocalTracks extends HookConsumerWidget {
return sortedTracks
.map((e) => (
weightedRatio(
- "${e.name} - ${TypeConversionUtils.artists_X_String(e.artists ?? [])}",
+ "${e.name} - ${e.artists?.asString() ?? ""}",
searchController.text,
),
e,
@@ -269,7 +270,7 @@ class UserLocalTracks extends HookConsumerWidget {
return Expanded(
child: RefreshIndicator(
onRefresh: () async {
- ref.refresh(localTracksProvider);
+ ref.invalidate(localTracksProvider);
},
child: InterScrollbar(
controller: controller,
@@ -282,12 +283,17 @@ class UserLocalTracks extends HookConsumerWidget {
trackSnapshot.isLoading ? 5 : filteredTracks.length,
itemBuilder: (context, index) {
if (trackSnapshot.isLoading) {
- return TrackTile(track: FakeData.track, index: index);
+ return TrackTile(
+ playlist: playlist,
+ track: FakeData.track,
+ index: index,
+ );
}
final track = filteredTracks[index];
return TrackTile(
index: index,
+ playlist: playlist,
track: track,
userPlaylist: false,
onTap: () async {
@@ -310,8 +316,11 @@ class UserLocalTracks extends HookConsumerWidget {
enabled: true,
child: ListView.builder(
itemCount: 5,
- itemBuilder: (context, index) =>
- TrackTile(track: FakeData.track, index: index),
+ itemBuilder: (context, index) => TrackTile(
+ track: FakeData.track,
+ index: index,
+ playlist: playlist,
+ ),
),
),
),
diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart
index 32e91ed6..3ff028b6 100644
--- a/lib/components/library/user_playlists.dart
+++ b/lib/components/library/user_playlists.dart
@@ -17,10 +17,10 @@ import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class UserPlaylists extends HookConsumerWidget {
- const UserPlaylists({Key? key}) : super(key: key);
+ const UserPlaylists({super.key});
@override
Widget build(BuildContext context, ref) {
@@ -28,13 +28,9 @@ class UserPlaylists extends HookConsumerWidget {
final auth = ref.watch(AuthenticationNotifier.provider);
- final playlistsQuery = useQueries.playlist.ofMine(ref);
-
- final pagePlaylists = useMemoized(
- () => playlistsQuery.pages
- .expand((page) => page.items?.toList() ?? []),
- [playlistsQuery.pages],
- );
+ final playlistsQuery = ref.watch(favoritePlaylistsProvider);
+ final playlistsQueryNotifier =
+ ref.watch(favoritePlaylistsProvider.notifier);
final likedTracksPlaylist = useMemoized(
() => PlaylistSimple()
@@ -58,12 +54,12 @@ class UserPlaylists extends HookConsumerWidget {
if (searchText.value.isEmpty) {
return [
likedTracksPlaylist,
- ...pagePlaylists,
+ ...?playlistsQuery.asData?.value.items,
];
}
return [
likedTracksPlaylist,
- ...pagePlaylists,
+ ...?playlistsQuery.asData?.value.items,
]
.map((e) => (weightedRatio(e.name!, searchText.value), e))
.sorted((a, b) => b.$1.compareTo(a.$1))
@@ -71,7 +67,7 @@ class UserPlaylists extends HookConsumerWidget {
.map((e) => e.$2)
.toList();
},
- [pagePlaylists, searchText.value],
+ [playlistsQuery, searchText.value],
);
final controller = useScrollController();
@@ -81,7 +77,9 @@ class UserPlaylists extends HookConsumerWidget {
}
return RefreshIndicator(
- onRefresh: playlistsQuery.refresh,
+ onRefresh: () async {
+ ref.invalidate(favoritePlaylistsProvider);
+ },
child: SafeArea(
child: InterScrollbar(
controller: controller,
@@ -132,14 +130,14 @@ class UserPlaylists extends HookConsumerWidget {
),
itemBuilder: (context, index) {
if (playlists.isNotEmpty && index == playlists.length) {
- if (!playlistsQuery.hasNextPage) {
+ if (playlistsQuery.asData?.value.hasMore != true) {
return const SizedBox.shrink();
}
return Waypoint(
controller: controller,
isGrid: true,
- onTouchEdge: playlistsQuery.fetchNext,
+ onTouchEdge: playlistsQueryNotifier.fetchMore,
child: Skeletonizer(
enabled: true,
child: PlaylistCard(FakeData.playlistSimple),
diff --git a/lib/components/lyrics/zoom_controls.dart b/lib/components/lyrics/zoom_controls.dart
index f50ea71d..73beb4ae 100644
--- a/lib/components/lyrics/zoom_controls.dart
+++ b/lib/components/lyrics/zoom_controls.dart
@@ -17,7 +17,7 @@ class ZoomControls extends HookWidget {
final String unit;
const ZoomControls({
- Key? key,
+ super.key,
required this.value,
required this.onChanged,
this.min,
@@ -27,7 +27,7 @@ class ZoomControls extends HookWidget {
this.decreaseIcon = const Icon(SpotubeIcons.zoomOut),
this.direction = Axis.horizontal,
this.unit = "%",
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart
index 458676e3..6dbd9b11 100644
--- a/lib/components/player/player.dart
+++ b/lib/components/player/player.dart
@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_actions.dart';
@@ -13,29 +12,33 @@ import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/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/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_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/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
+
import 'package:url_launcher/url_launcher_string.dart';
class PlayerView extends HookConsumerWidget {
final PanelController panelController;
final ScrollController scrollController;
const PlayerView({
- Key? key,
+ super.key,
required this.panelController,
required this.scrollController,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
@@ -44,9 +47,7 @@ class PlayerView extends HookConsumerWidget {
final currentTrack = ref.watch(ProxyPlaylistNotifier.provider.select(
(value) => value.activeTrack,
));
- final isLocalTrack = ref.watch(ProxyPlaylistNotifier.provider.select(
- (value) => value.activeTrack is LocalTrack,
- ));
+ final isLocalTrack = currentTrack is LocalTrack;
final mediaQuery = MediaQuery.of(context);
useEffect(() {
@@ -59,8 +60,7 @@ class PlayerView extends HookConsumerWidget {
}, [mediaQuery.lgAndUp]);
String albumArt = useMemoized(
- () => TypeConversionUtils.image_X_UrlString(
- currentTrack?.album?.images,
+ () => (currentTrack?.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
[currentTrack?.album?.images],
@@ -239,19 +239,15 @@ class PlayerView extends HookConsumerWidget {
),
if (isLocalTrack)
Text(
- TypeConversionUtils.artists_X_String<
- Artist>(
- currentTrack?.artists ?? [],
- ),
+ currentTrack.artists?.asString() ?? "",
style: theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color: bodyTextColor,
),
)
else
- TypeConversionUtils
- .artists_X_ClickableArtists(
- currentTrack?.artists ?? [],
+ ArtistLink(
+ artists: currentTrack?.artists ?? [],
textStyle:
theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
@@ -307,10 +303,25 @@ class PlayerView extends HookConsumerWidget {
.height *
.7,
),
- builder: (context) {
- return const PlayerQueue(
- floating: false);
- },
+ builder: (context) => Consumer(
+ builder: (context, ref, _) {
+ final playlist = ref.watch(
+ ProxyPlaylistNotifier
+ .provider,
+ );
+ final playlistNotifier =
+ ref.read(
+ ProxyPlaylistNotifier
+ .notifier,
+ );
+ return PlayerQueue
+ .fromProxyPlaylistNotifier(
+ floating: false,
+ playlist: playlist,
+ notifier: playlistNotifier,
+ );
+ },
+ ),
);
}
: null),
@@ -368,11 +379,21 @@ class PlayerView extends HookConsumerWidget {
enabledThumbRadius: 8,
),
),
- child: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: VolumeSlider(
- fullWidth: true,
- ),
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 16),
+ child: Consumer(builder: (context, ref, _) {
+ final volume = ref.watch(volumeProvider);
+ return VolumeSlider(
+ fullWidth: true,
+ value: volume,
+ onChanged: (value) {
+ ref
+ .read(volumeProvider.notifier)
+ .setVolume(value);
+ },
+ );
+ }),
),
),
],
diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart
index 7a248aa5..4102e2ba 100644
--- a/lib/components/player/player_actions.dart
+++ b/lib/components/player/player_actions.dart
@@ -3,12 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/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/constrains.dart';
+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';
@@ -17,7 +16,6 @@ import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/sleep_timer_provider.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerActions extends HookConsumerWidget {
final MainAxisAlignment mainAxisAlignment;
@@ -29,13 +27,12 @@ class PlayerActions extends HookConsumerWidget {
this.floatingQueue = true,
this.showQueue = true,
this.extraActions,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
final logger = getLogger(PlayerActions);
@override
Widget build(BuildContext context, ref) {
- final mediaQuery = MediaQuery.of(context);
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider);
@@ -58,10 +55,8 @@ class PlayerActions extends HookConsumerWidget {
(element) =>
element.name == playlist.activeTrack?.name &&
element.album?.name == playlist.activeTrack?.album?.name &&
- TypeConversionUtils.artists_X_String(
- element.artists ?? []) ==
- TypeConversionUtils.artists_X_String(
- playlist.activeTrack?.artists ?? []),
+ element.artists?.asString() ==
+ playlist.activeTrack?.artists?.asString(),
) ==
true;
}, [localTracks, playlist.activeTrack]);
diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart
index 1000af18..0190e2e6 100644
--- a/lib/components/player/player_controls.dart
+++ b/lib/components/player/player_controls.dart
@@ -21,8 +21,8 @@ class PlayerControls extends HookConsumerWidget {
PlayerControls({
this.palette,
this.compact = false,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
final logger = getLogger(PlayerControls);
@@ -256,20 +256,16 @@ class PlayerControls extends HookConsumerWidget {
onPressed: playlist.isFetching == true
? null
: () async {
- switch (await audioPlayer.loopMode) {
- case PlaybackLoopMode.all:
- audioPlayer
- .setLoopMode(PlaybackLoopMode.one);
- break;
- case PlaybackLoopMode.one:
- audioPlayer
- .setLoopMode(PlaybackLoopMode.none);
- break;
- case PlaybackLoopMode.none:
- audioPlayer
- .setLoopMode(PlaybackLoopMode.all);
- break;
- }
+ audioPlayer.setLoopMode(
+ switch (loopMode) {
+ PlaybackLoopMode.all =>
+ PlaybackLoopMode.one,
+ PlaybackLoopMode.one =>
+ PlaybackLoopMode.none,
+ PlaybackLoopMode.none =>
+ PlaybackLoopMode.all,
+ },
+ );
},
);
}),
diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart
index 2d63811e..e2ca9674 100644
--- a/lib/components/player/player_overlay.dart
+++ b/lib/components/player/player_overlay.dart
@@ -19,8 +19,8 @@ class PlayerOverlay extends HookConsumerWidget {
const PlayerOverlay({
required this.albumArt,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context, ref) {
@@ -115,7 +115,7 @@ class PlayerOverlay extends HookConsumerWidget {
width: double.infinity,
color: Colors.transparent,
child: PlayerTrackDetails(
- albumArt: albumArt,
+ track: playlist.activeTrack,
color: textColor,
),
),
diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart
index 2784fb5f..0bf61da4 100644
--- a/lib/components/player/player_queue.dart
+++ b/lib/components/player/player_queue.dart
@@ -5,30 +5,56 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
+import 'package:gap/gap.dart';
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/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/utils/type_conversion_utils.dart';
class PlayerQueue extends HookConsumerWidget {
final bool floating;
+ final ProxyPlaylist playlist;
+
+ final Future Function(Track track) onJump;
+ final Future Function(String trackId) onRemove;
+ final Future Function(int oldIndex, int newIndex) onReorder;
+ final Future Function() onStop;
+
const PlayerQueue({
this.floating = true,
- Key? key,
- }) : super(key: key);
+ required this.playlist,
+ required this.onJump,
+ required this.onRemove,
+ required this.onReorder,
+ required this.onStop,
+ super.key,
+ });
+
+ PlayerQueue.fromProxyPlaylistNotifier({
+ this.floating = true,
+ required this.playlist,
+ required ProxyPlaylistNotifier notifier,
+ super.key,
+ }) : onJump = notifier.jumpToTrack,
+ onRemove = notifier.removeTrack,
+ onReorder = notifier.moveTrack,
+ onStop = notifier.stop;
@override
Widget build(BuildContext context, ref) {
- final playlist = ref.watch(ProxyPlaylistNotifier.provider);
+ final mediaQuery = MediaQuery.of(context);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
+ final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final controller = useAutoScrollController();
final searchText = useState('');
@@ -44,7 +70,6 @@ class PlayerQueue extends HookConsumerWidget {
topRight: Radius.circular(10),
);
final theme = Theme.of(context);
- final mediaQuery = MediaQuery.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color;
final filteredTracks = useMemoized(
@@ -55,7 +80,7 @@ class PlayerQueue extends HookConsumerWidget {
return tracks
.map((e) => (
weightedRatio(
- '${e.name!} - ${TypeConversionUtils.artists_X_String(e.artists!)}',
+ '${e.name!} - ${e.artists?.asString() ?? ""}',
searchText.value,
),
e
@@ -83,201 +108,204 @@ class PlayerQueue extends HookConsumerWidget {
return const NotFound(vertical: true);
}
- return ClipRRect(
- borderRadius: borderRadius,
- clipBehavior: Clip.hardEdge,
- child: BackdropFilter(
- filter: ImageFilter.blur(
- sigmaX: 15,
- sigmaY: 15,
- ),
- child: Container(
- padding: const EdgeInsets.only(
- top: 5.0,
- ),
- decoration: BoxDecoration(
- color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
- borderRadius: borderRadius,
- ),
- child: CallbackShortcuts(
- bindings: {
- LogicalKeySet(LogicalKeyboardKey.escape): () {
- if (!isSearching.value) {
- Navigator.of(context).pop();
- }
- isSearching.value = false;
- searchText.value = '';
- }
- },
- child: Column(
- children: [
- if (!floating)
- Container(
- height: 5,
- width: 100,
- margin: const EdgeInsets.only(bottom: 5, top: 2),
- decoration: BoxDecoration(
- color: headlineColor,
- borderRadius: BorderRadius.circular(20),
- ),
- ),
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- if (mediaQuery.mdAndUp || !isSearching.value) ...[
- const SizedBox(width: 10),
- Text(
- context.l10n.tracks_in_queue(tracks.length),
- style: TextStyle(
- color: headlineColor,
- fontWeight: FontWeight.bold,
- fontSize: 18,
- ),
- ),
- const Spacer(),
- ],
- if (mediaQuery.mdAndUp || isSearching.value)
- TextField(
- onChanged: (value) {
- searchText.value = value;
- },
- decoration: InputDecoration(
- hintText: context.l10n.search,
- isDense: true,
- prefixIcon: mediaQuery.smAndDown
- ? IconButton(
- icon: const Icon(
- Icons.arrow_back_ios_new_outlined,
- ),
- onPressed: () {
- isSearching.value = false;
- searchText.value = '';
- },
- style: IconButton.styleFrom(
- padding: EdgeInsets.zero,
- minimumSize: const Size.square(20),
- ),
- )
- : const Icon(SpotubeIcons.filter),
- constraints: BoxConstraints(
- maxHeight: 40,
- maxWidth: mediaQuery.smAndDown
- ? mediaQuery.size.width - 40
- : 300,
- ),
- ),
- )
- else
- IconButton.filledTonal(
- icon: const Icon(SpotubeIcons.filter),
- onPressed: () {
- isSearching.value = !isSearching.value;
- },
- ),
- if (mediaQuery.mdAndUp || !isSearching.value) ...[
- const SizedBox(width: 10),
- FilledButton(
- style: FilledButton.styleFrom(
- backgroundColor:
- theme.scaffoldBackgroundColor.withOpacity(0.5),
- foregroundColor: theme.textTheme.headlineSmall?.color,
- ),
- child: Row(
- children: [
- const Icon(SpotubeIcons.playlistRemove),
- const SizedBox(width: 5),
- Text(context.l10n.clear_all),
- ],
- ),
- onPressed: () {
- playlistNotifier.stop();
- Navigator.of(context).pop();
- },
- ),
- const SizedBox(width: 10),
- ],
- ],
- ),
- const SizedBox(height: 10),
- if (!isSearching.value && searchText.value.isEmpty)
- Flexible(
- child: ReorderableListView.builder(
- onReorder: (oldIndex, newIndex) {
- playlistNotifier.moveTrack(oldIndex, newIndex);
- },
- scrollController: controller,
- itemCount: tracks.length,
- shrinkWrap: true,
- buildDefaultDragHandles: false,
- onReorderStart: (index) {
- HapticFeedback.selectionClick();
- },
- onReorderEnd: (index) {
- HapticFeedback.selectionClick();
- },
- itemBuilder: (context, i) {
- final track = tracks.elementAt(i);
- return AutoScrollTag(
- key: ValueKey(i),
- controller: controller,
- index: i,
- child: Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 8.0),
- child: TrackTile(
- index: i,
- track: track,
- onTap: () async {
- if (playlist.activeTrack?.id == track.id) {
- return;
- }
- await playlistNotifier.jumpToTrack(track);
- },
- leadingActions: [
- ReorderableDragStartListener(
- index: i,
- child: const Icon(SpotubeIcons.dragHandle),
- ),
- ],
+ return LayoutBuilder(
+ builder: (context, constrains) {
+ return ClipRRect(
+ borderRadius: borderRadius,
+ clipBehavior: Clip.hardEdge,
+ child: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 15,
+ sigmaY: 15,
+ ),
+ child: Container(
+ padding: const EdgeInsets.only(
+ top: 5.0,
+ ),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
+ borderRadius: borderRadius,
+ ),
+ child: CallbackShortcuts(
+ bindings: {
+ LogicalKeySet(LogicalKeyboardKey.escape): () {
+ if (!isSearching.value) {
+ Navigator.of(context).pop();
+ }
+ isSearching.value = false;
+ searchText.value = '';
+ }
+ },
+ child: InterScrollbar(
+ controller: controller,
+ child: CustomScrollView(
+ controller: controller,
+ slivers: [
+ if (!floating)
+ SliverToBoxAdapter(
+ child: Center(
+ child: Container(
+ height: 5,
+ width: 100,
+ margin: const EdgeInsets.only(bottom: 5, top: 2),
+ decoration: BoxDecoration(
+ color: headlineColor,
+ borderRadius: BorderRadius.circular(20),
+ ),
),
),
- );
- },
- ),
- )
- else
- Flexible(
- child: InterScrollbar(
- controller: controller,
- child: ListView.builder(
- controller: controller,
+ ),
+ SliverAppBar(
+ floating: true,
+ pinned: false,
+ snap: false,
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+ automaticallyImplyLeading: !isSearching.value,
+ title: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 10,
+ sigmaY: 10,
+ ),
+ child: SizedBox(
+ height: kToolbarHeight,
+ child: mediaQuery.mdAndUp || !isSearching.value
+ ? Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ context.l10n
+ .tracks_in_queue(tracks.length),
+ style: TextStyle(
+ color: headlineColor,
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ ),
+ ),
+ )
+ : null,
+ ),
+ ),
+ actions: [
+ if (mediaQuery.mdAndUp || isSearching.value)
+ TextField(
+ onChanged: (value) {
+ searchText.value = value;
+ },
+ decoration: InputDecoration(
+ hintText: context.l10n.search,
+ isDense: true,
+ prefixIcon: mediaQuery.smAndDown
+ ? IconButton(
+ icon: const Icon(
+ Icons.arrow_back_ios_new_outlined,
+ ),
+ onPressed: () {
+ isSearching.value = false;
+ searchText.value = '';
+ },
+ style: IconButton.styleFrom(
+ padding: EdgeInsets.zero,
+ minimumSize: const Size.square(20),
+ ),
+ )
+ : const Icon(SpotubeIcons.filter),
+ constraints: BoxConstraints(
+ maxHeight: 40,
+ maxWidth: mediaQuery.smAndDown
+ ? mediaQuery.size.width - 40
+ : 300,
+ ),
+ ),
+ )
+ else
+ IconButton.filledTonal(
+ icon: const Icon(SpotubeIcons.filter),
+ onPressed: () {
+ isSearching.value = !isSearching.value;
+ },
+ ),
+ if (mediaQuery.mdAndUp || !isSearching.value) ...[
+ const SizedBox(width: 10),
+ FilledButton(
+ style: FilledButton.styleFrom(
+ backgroundColor: theme.scaffoldBackgroundColor
+ .withOpacity(0.5),
+ foregroundColor:
+ theme.textTheme.headlineSmall?.color,
+ ),
+ child: Row(
+ children: [
+ const Icon(SpotubeIcons.playlistRemove),
+ const SizedBox(width: 5),
+ Text(context.l10n.clear_all),
+ ],
+ ),
+ onPressed: () {
+ playlistNotifier.stop();
+ Navigator.of(context).pop();
+ },
+ ),
+ const SizedBox(width: 10),
+ ],
+ ],
+ ),
+ const SliverGap(10),
+ SliverReorderableList(
+ onReorder: (oldIndex, newIndex) {
+ playlistNotifier.moveTrack(oldIndex, newIndex);
+ },
itemCount: filteredTracks.length,
+ onReorderStart: (index) {
+ HapticFeedback.selectionClick();
+ },
+ onReorderEnd: (index) {
+ HapticFeedback.selectionClick();
+ },
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
- return Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 8.0),
- child: TrackTile(
- index: i,
- track: track,
- onTap: () async {
- if (playlist.activeTrack?.id == track.id) {
- return;
- }
- await playlistNotifier.jumpToTrack(track);
- },
+ return AutoScrollTag(
+ key: ValueKey(i),
+ controller: controller,
+ index: i,
+ child: Material(
+ color: Colors.transparent,
+ child: TrackTile(
+ playlist: playlist,
+ index: i,
+ track: track,
+ onTap: () async {
+ if (playlist.activeTrack?.id == track.id) {
+ return;
+ }
+ await playlistNotifier.jumpToTrack(track);
+ },
+ leadingActions: [
+ if (!isSearching.value &&
+ searchText.value.isEmpty)
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: ReorderableDragStartListener(
+ index: i,
+ child: const Icon(
+ SpotubeIcons.dragHandle,
+ ),
+ ),
+ ),
+ ],
+ ),
),
);
},
),
- ),
+ const SliverGap(100),
+ ],
),
- ],
+ ),
+ ),
),
),
- ),
- ),
+ );
+ },
);
}
}
diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart
index 66cb9ef5..65e40fe6 100644
--- a/lib/components/player/player_track_details.dart
+++ b/lib/components/player/player_track_details.dart
@@ -4,17 +4,18 @@ 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/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/utils/service_utils.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerTrackDetails extends HookConsumerWidget {
- final String? albumArt;
final Color? color;
- const PlayerTrackDetails({Key? key, this.albumArt, this.color})
- : super(key: key);
+ final Track? track;
+ const PlayerTrackDetails({super.key, this.color, this.track});
@override
Widget build(BuildContext context, ref) {
@@ -34,7 +35,8 @@ class PlayerTrackDetails extends HookConsumerWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
- path: albumArt ?? "",
+ path: (track?.album?.images)
+ .asUrlString(placeholder: ImagePlaceholder.albumArt),
placeholder: Assets.albumPlaceholder.path,
),
),
@@ -55,9 +57,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
),
),
Text(
- TypeConversionUtils.artists_X_String(
- playback.activeTrack?.artists ?? [],
- ),
+ playback.activeTrack?.artists?.asString() ?? "",
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall!.copyWith(color: color),
)
@@ -76,8 +76,8 @@ class PlayerTrackDetails extends HookConsumerWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color),
),
- TypeConversionUtils.artists_X_ClickableArtists(
- playback.activeTrack?.artists ?? [],
+ ArtistLink(
+ artists: playback.activeTrack?.artists ?? [],
onRouteChange: (route) {
ServiceUtils.push(context, route);
},
diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart
index 58b1ca8c..99ab223f 100644
--- a/lib/components/player/sibling_tracks_sheet.dart
+++ b/lib/components/player/sibling_tracks_sheet.dart
@@ -10,6 +10,7 @@ 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/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
@@ -24,7 +25,6 @@ import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
final sourceInfoToIconMap = {
YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
@@ -45,9 +45,9 @@ final sourceInfoToIconMap = {
class SiblingTracksSheet extends HookConsumerWidget {
final bool floating;
const SiblingTracksSheet({
- Key? key,
+ super.key,
this.floating = true,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
@@ -67,7 +67,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
).trim();
final defaultSearchTerm =
- "$title - ${TypeConversionUtils.artists_X_String(playlist.activeTrack?.artists ?? [])}";
+ "$title - ${playlist.activeTrack?.artists?.asString() ?? ""}";
final searchController = useTextEditingController(
text: defaultSearchTerm,
);
diff --git a/lib/components/player/volume_slider.dart b/lib/components/player/volume_slider.dart
index 75445125..102bbef6 100644
--- a/lib/components/player/volume_slider.dart
+++ b/lib/components/player/volume_slider.dart
@@ -3,37 +3,39 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/provider/volume_provider.dart';
class VolumeSlider extends HookConsumerWidget {
final bool fullWidth;
+
+ final double value;
+ final ValueChanged onChanged;
+
const VolumeSlider({
- Key? key,
+ super.key,
this.fullWidth = false,
- }) : super(key: key);
+ required this.value,
+ required this.onChanged,
+ });
@override
Widget build(BuildContext context, ref) {
- final volume = ref.watch(volumeProvider);
- final volumeNotifier = ref.watch(volumeProvider.notifier);
-
var slider = Listener(
onPointerSignal: (event) async {
if (event is PointerScrollEvent) {
if (event.scrollDelta.dy > 0) {
- final value = volume - .2;
- volumeNotifier.setVolume(value < 0 ? 0 : value);
+ final newValue = value - .2;
+ onChanged(newValue < 0 ? 0 : newValue);
} else {
- final value = volume + .2;
- volumeNotifier.setVolume(value > 1 ? 1 : value);
+ final newValue = value + .2;
+ onChanged(newValue > 1 ? 1 : newValue);
}
}
},
child: Slider(
min: 0,
max: 1,
- value: volume,
- onChanged: volumeNotifier.setVolume,
+ value: value,
+ onChanged: onChanged,
),
);
return Row(
@@ -42,20 +44,20 @@ class VolumeSlider extends HookConsumerWidget {
children: [
IconButton(
icon: Icon(
- volume == 0
+ value == 0
? SpotubeIcons.volumeMute
- : volume <= 0.2
+ : value <= 0.2
? SpotubeIcons.volumeLow
- : volume <= 0.6
+ : value <= 0.6
? SpotubeIcons.volumeMedium
: SpotubeIcons.volumeHigh,
size: 16,
),
onPressed: () {
- if (volume == 0) {
- volumeNotifier.setVolume(1);
+ if (value == 0) {
+ onChanged(1);
} else {
- volumeNotifier.setVolume(0);
+ onChanged(0);
}
},
),
diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart
index f429a0ab..e5b87d6d 100644
--- a/lib/components/playlist/playlist_card.dart
+++ b/lib/components/playlist/playlist_card.dart
@@ -1,77 +1,59 @@
-import 'package:fl_query/fl_query.dart';
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/extensions/infinite_query.dart';
+import 'package:spotube/extensions/image.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/spotify_provider.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
-import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/service_utils.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class PlaylistCard extends HookConsumerWidget {
final PlaylistSimple playlist;
const PlaylistCard(
this.playlist, {
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context, ref) {
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
- final queryClient = QueryClient.of(context);
- final tracks = useState?>(null);
bool isPlaylistPlaying = useMemoized(
() => playlistQueue.containsCollection(playlist.id!),
[playlistQueue, playlist.id],
);
final updating = useState(false);
- final spotify = ref.watch(spotifyProvider);
- final me = useQueries.user.me(ref);
+ final me = ref.watch(meProvider);
Future> fetchAllTracks() async {
if (playlist.id == 'user-liked-tracks') {
- return await queryClient.fetchQuery(
- "user-liked-tracks",
- () => useQueries.playlist.likedTracks(spotify),
- ) ??
- [];
+ return await ref.read(likedTracksProvider.future);
}
- final query = queryClient.createInfiniteQuery, dynamic, int>(
- "playlist-tracks/${playlist.id}",
- (page) => useQueries.playlist.tracksOf(page, spotify, playlist.id!),
- initialPage: 0,
- nextPage: useQueries.playlist.tracksOfQueryNextPage,
- );
+ await ref.read(playlistTracksProvider(playlist.id!).future);
- return await query.fetchAllTracks(
- getAllTracks: () async {
- final res =
- await spotify.playlists.getTracksByPlaylistId(playlist.id!).all();
- return res.toList();
- },
- );
+ return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
}
return PlaybuttonCard(
margin: const EdgeInsets.symmetric(horizontal: 10),
title: playlist.name!,
description: playlist.description,
- imageUrl: TypeConversionUtils.image_X_UrlString(
- playlist.images,
+ imageUrl: playlist.images.asUrlString(
placeholder: ImagePlaceholder.collection,
),
isPlaying: isPlaylistPlaying,
isLoading:
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
- isOwner: playlist.owner?.id == me.data?.id && me.data?.id != null,
+ isOwner: playlist.owner?.id == me.asData?.value.id &&
+ me.asData?.value.id != null,
onTap: () {
ServiceUtils.push(
context,
@@ -92,9 +74,19 @@ class PlaylistCard extends HookConsumerWidget {
if (fetchedTracks.isEmpty) return;
- await playlistNotifier.load(fetchedTracks, autoPlay: true);
- playlistNotifier.addCollection(playlist.id!);
- tracks.value = fetchedTracks;
+ final isRemoteDevice = await showSelectDeviceDialog(context, ref);
+ if (isRemoteDevice) {
+ final remotePlayback = ref.read(connectProvider.notifier);
+ await remotePlayback.load(
+ WebSocketLoadEventData(
+ tracks: fetchedTracks,
+ collectionId: playlist.id!,
+ ),
+ );
+ } else {
+ await playlistNotifier.load(fetchedTracks, autoPlay: true);
+ playlistNotifier.addCollection(playlist.id!);
+ }
} finally {
if (context.mounted) {
updating.value = false;
@@ -112,10 +104,9 @@ class PlaylistCard extends HookConsumerWidget {
playlistNotifier.addTracks(fetchedTracks);
playlistNotifier.addCollection(playlist.id!);
- tracks.value = fetchedTracks;
if (context.mounted) {
final snackbar = SnackBar(
- content: Text("Added ${tracks.value?.length} tracks to queue"),
+ content: Text("Added ${fetchedTracks.length} tracks to queue"),
action: SnackBarAction(
label: "Undo",
onPressed: () {
diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/components/playlist/playlist_create_dialog.dart
index 2e11a209..bac98b64 100644
--- a/lib/components/playlist/playlist_create_dialog.dart
+++ b/lib/components/playlist/playlist_create_dialog.dart
@@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:form_validator/form_validator.dart';
+import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:spotify/spotify.dart';
@@ -13,21 +14,19 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/image.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
-import 'package:spotube/services/mutations/mutations.dart';
-import 'package:spotube/services/mutations/playlist.dart';
-import 'package:spotube/services/queries/queries.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class PlaylistCreateDialog extends HookConsumerWidget {
/// Track ids to add to the playlist
final List trackIds;
final String? playlistId;
PlaylistCreateDialog({
- Key? key,
+ super.key,
this.trackIds = const [],
this.playlistId,
- }) : super(key: key);
+ });
final formKey = GlobalKey();
@@ -37,13 +36,16 @@ class PlaylistCreateDialog extends HookConsumerWidget {
child: Scaffold(
backgroundColor: Colors.transparent,
body: HookBuilder(builder: (context) {
- final userPlaylists = useQueries.playlist.ofMine(ref);
+ final userPlaylists = ref.watch(favoritePlaylistsProvider);
+ final playlist = ref.watch(playlistProvider(playlistId ?? ""));
+ final playlistNotifier =
+ ref.watch(playlistProvider(playlistId ?? "").notifier);
+
final updatingPlaylist = useMemoized(
- () => userPlaylists.pages
- .expand((p) => p.items ?? [])
+ () => userPlaylists.asData?.value.items
.firstWhereOrNull((playlist) => playlist.id == playlistId),
[
- userPlaylists.pages,
+ userPlaylists.asData?.value.items,
playlistId,
],
);
@@ -84,28 +86,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
}
}, [scaffold, l10n, theme]);
- final playlistCreateMutation = useMutations.playlist.create(
- ref,
- trackIds: trackIds,
- onData: (value) {
- Navigator.pop(context);
- },
- onError: onError,
- );
-
- final playlistUpdateMutation = useMutations.playlist.update(
- ref,
- playlistId: playlistId,
- onData: (value) {
- Navigator.pop(context);
- },
- onError: onError,
- );
-
Future onCreate() async {
if (!formKey.currentState!.validate()) return;
- final PlaylistCRUDVariables payload = (
+ final PlaylistInput payload = (
playlistName: playlistName.text,
collaborative: collaborative.value,
public: public.value,
@@ -118,9 +102,14 @@ class PlaylistCreateDialog extends HookConsumerWidget {
);
if (isUpdatingPlaylist) {
- await playlistUpdateMutation.mutate(payload);
+ await playlistNotifier.modify(payload, onError);
} else {
- await playlistCreateMutation.mutate(payload);
+ await playlistNotifier.create(payload, onError);
+ }
+
+ if (context.mounted &&
+ !ref.read(playlistProvider(playlistId ?? "")).hasError) {
+ context.pop();
}
}
@@ -138,7 +127,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
},
),
FilledButton(
- onPressed: onCreate,
+ onPressed: playlist.isLoading ? null : onCreate,
child: Text(
isUpdatingPlaylist
? context.l10n.update
@@ -174,8 +163,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
children: [
UniversalImage(
path: field.value?.path ??
- TypeConversionUtils.image_X_UrlString(
- updatingPlaylist?.images,
+ (updatingPlaylist?.images).asUrlString(
placeholder: ImagePlaceholder.collection,
),
height: 200,
@@ -275,7 +263,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
}
class PlaylistCreateDialogButton extends HookConsumerWidget {
- const PlaylistCreateDialogButton({Key? key}) : super(key: key);
+ const PlaylistCreateDialogButton({super.key});
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
showDialog(
diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart
index 617e760b..19fa7c93 100644
--- a/lib/components/root/bottom_player.dart
+++ b/lib/components/root/bottom_player.dart
@@ -14,18 +14,20 @@ import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/components/player/volume_slider.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication_provider.dart';
+import 'package:spotube/provider/connect/connect.dart' hide volumeProvider;
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:spotube/utils/type_conversion_utils.dart';
class BottomPlayer extends HookConsumerWidget {
- BottomPlayer({Key? key}) : super(key: key);
+ BottomPlayer({super.key});
final logger = getLogger(BottomPlayer);
@override
@@ -34,13 +36,13 @@ class BottomPlayer extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
+ final remoteControl = ref.watch(connectProvider);
final mediaQuery = MediaQuery.of(context);
String albumArt = useMemoized(
() => playlist.activeTrack?.album?.images?.isNotEmpty == true
- ? TypeConversionUtils.image_X_UrlString(
- playlist.activeTrack?.album?.images,
+ ? (playlist.activeTrack?.album?.images).asUrlString(
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
)
@@ -74,7 +76,9 @@ class BottomPlayer extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Expanded(child: PlayerTrackDetails(albumArt: albumArt)),
+ Expanded(
+ child: PlayerTrackDetails(track: playlist.activeTrack),
+ ),
// controls
Flexible(
flex: 3,
@@ -122,10 +126,20 @@ class BottomPlayer extends HookConsumerWidget {
Container(
height: 40,
constraints: const BoxConstraints(maxWidth: 250),
- child: const VolumeSlider(),
+ padding: const EdgeInsets.only(right: 10),
+ child: Consumer(builder: (context, ref, _) {
+ final volume = ref.watch(volumeProvider);
+ return VolumeSlider(
+ fullWidth: true,
+ value: volume,
+ onChanged: (value) {
+ ref.read(volumeProvider.notifier).setVolume(value);
+ },
+ );
+ }),
)
],
- )
+ ),
],
),
),
diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart
index a55ef947..903e812e 100644
--- a/lib/components/root/sidebar.dart
+++ b/lib/components/root/sidebar.dart
@@ -11,16 +11,16 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
import 'package:spotube/provider/download_manager_provider.dart';
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/services/queries/queries.dart';
import 'package:spotube/utils/platform.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class Sidebar extends HookConsumerWidget {
final int? selectedIndex;
@@ -31,8 +31,8 @@ class Sidebar extends HookConsumerWidget {
required this.selectedIndex,
required this.onSelectedIndexChanged,
required this.child,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
static Widget brandLogo() {
return Container(
@@ -195,7 +195,7 @@ class Sidebar extends HookConsumerWidget {
}
class SidebarHeader extends HookWidget {
- const SidebarHeader({Key? key}) : super(key: key);
+ const SidebarHeader({super.key});
@override
Widget build(BuildContext context) {
@@ -234,18 +234,17 @@ class SidebarHeader extends HookWidget {
class SidebarFooter extends HookConsumerWidget {
const SidebarFooter({
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
- final me = useQueries.user.me(ref);
- final data = me.data;
+ final me = ref.watch(meProvider);
+ final data = me.asData?.value;
- final avatarImg = TypeConversionUtils.image_X_UrlString(
- data?.images,
+ final avatarImg = (data?.images).asUrlString(
index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart
index 0853c60c..489399e5 100644
--- a/lib/components/root/spotube_navigation_bar.dart
+++ b/lib/components/root/spotube_navigation_bar.dart
@@ -23,8 +23,8 @@ class SpotubeNavigationBar extends HookConsumerWidget {
const SpotubeNavigationBar({
required this.selectedIndex,
required this.onSelectedIndexChanged,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/settings/color_scheme_picker_dialog.dart b/lib/components/settings/color_scheme_picker_dialog.dart
index e0c3d618..8d098375 100644
--- a/lib/components/settings/color_scheme_picker_dialog.dart
+++ b/lib/components/settings/color_scheme_picker_dialog.dart
@@ -8,9 +8,9 @@ import 'package:system_theme/system_theme.dart';
class SpotubeColor extends Color {
final String name;
- const SpotubeColor(int color, {required this.name}) : super(color);
+ const SpotubeColor(super.color, {required this.name});
- const SpotubeColor.from(int value, {required this.name}) : super(value);
+ const SpotubeColor.from(super.value, {required this.name});
factory SpotubeColor.fromString(String string) {
final slices = string.split(":");
@@ -44,7 +44,7 @@ final Set colorsMap = {
};
class ColorSchemePickerDialog extends HookConsumerWidget {
- const ColorSchemePickerDialog({Key? key}) : super(key: key);
+ const ColorSchemePickerDialog({super.key});
@override
Widget build(BuildContext context, ref) {
@@ -119,8 +119,8 @@ class ColorTile extends StatelessWidget {
this.onPressed,
this.tooltip = "",
this.isCompact = false,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
factory ColorTile.compact({
required Color color,
diff --git a/lib/components/shared/adaptive/adaptive_popup_menu_button.dart b/lib/components/shared/adaptive/adaptive_popup_menu_button.dart
index 45f22825..02fced52 100644
--- a/lib/components/shared/adaptive/adaptive_popup_menu_button.dart
+++ b/lib/components/shared/adaptive/adaptive_popup_menu_button.dart
@@ -12,13 +12,13 @@ class Action extends StatelessWidget {
final bool isExpanded;
final Color? backgroundColor;
const Action({
- Key? key,
+ super.key,
required this.icon,
required this.text,
required this.onPressed,
this.isExpanded = true,
this.backgroundColor,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/adaptive/adaptive_select_tile.dart b/lib/components/shared/adaptive/adaptive_select_tile.dart
index 58666e46..3f6d2700 100644
--- a/lib/components/shared/adaptive/adaptive_select_tile.dart
+++ b/lib/components/shared/adaptive/adaptive_select_tile.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
class AdaptiveSelectTile extends HookWidget {
@@ -38,11 +39,22 @@ class AdaptiveSelectTile extends HookWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
- final rawControl = DropdownButton(
- items: options,
- value: value,
- onChanged: onChanged,
- menuMaxHeight: mediaQuery.size.height * 0.6,
+ final rawControl = DecoratedBox(
+ decoration: BoxDecoration(
+ color: theme.colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: DropdownButton(
+ items: options,
+ value: value,
+ onChanged: onChanged,
+ menuMaxHeight: mediaQuery.size.height * 0.6,
+ underline: const SizedBox.shrink(),
+ padding: const EdgeInsets.symmetric(horizontal: 10),
+ borderRadius: BorderRadius.circular(10),
+ icon: const Icon(SpotubeIcons.angleDown),
+ dropdownColor: theme.colorScheme.secondaryContainer,
+ ),
);
final controlPlaceholder = useMemoized(
() => options
diff --git a/lib/components/shared/animated_gradient.dart b/lib/components/shared/animated_gradient.dart
index b6485f6b..aaba2ff9 100644
--- a/lib/components/shared/animated_gradient.dart
+++ b/lib/components/shared/animated_gradient.dart
@@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
class AnimateGradient extends HookWidget {
const AnimateGradient({
- Key? key,
+ super.key,
required this.primaryColors,
required this.secondaryColors,
this.child,
@@ -17,8 +17,7 @@ class AnimateGradient extends HookWidget {
this.reverse = true,
}) : assert(primaryColors.length >= 2),
assert(primaryColors.length == secondaryColors.length),
- _controller = controller,
- super(key: key);
+ _controller = controller;
/// [controller]: pass this to have a fine control over the [Animation]
final AnimationController? _controller;
diff --git a/lib/components/shared/compact_search.dart b/lib/components/shared/compact_search.dart
index 70815291..d37cb673 100644
--- a/lib/components/shared/compact_search.dart
+++ b/lib/components/shared/compact_search.dart
@@ -11,12 +11,12 @@ class CompactSearch extends HookWidget {
final Color? iconColor;
const CompactSearch({
- Key? key,
+ super.key,
this.onChanged,
this.placeholder = "Search...",
this.icon = SpotubeIcons.search,
this.iconColor,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/dialogs/confirm_download_dialog.dart b/lib/components/shared/dialogs/confirm_download_dialog.dart
index c371e803..486310a7 100644
--- a/lib/components/shared/dialogs/confirm_download_dialog.dart
+++ b/lib/components/shared/dialogs/confirm_download_dialog.dart
@@ -5,7 +5,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
class ConfirmDownloadDialog extends StatelessWidget {
- const ConfirmDownloadDialog({Key? key}) : super(key: key);
+ const ConfirmDownloadDialog({super.key});
@override
Widget build(BuildContext context) {
@@ -82,7 +82,7 @@ class ConfirmDownloadDialog extends StatelessWidget {
class BulletPoint extends StatelessWidget {
final String text;
- const BulletPoint(this.text, {Key? key}) : super(key: key);
+ const BulletPoint(this.text, {super.key});
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/dialogs/piped_down_dialog.dart b/lib/components/shared/dialogs/piped_down_dialog.dart
index 6220adeb..b1717a2a 100644
--- a/lib/components/shared/dialogs/piped_down_dialog.dart
+++ b/lib/components/shared/dialogs/piped_down_dialog.dart
@@ -5,7 +5,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class PipedDownDialog extends HookConsumerWidget {
- const PipedDownDialog({Key? key}) : super(key: key);
+ const PipedDownDialog({super.key});
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart
index 51b77c76..5d493a68 100644
--- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart
+++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart
@@ -1,4 +1,3 @@
-import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
@@ -8,9 +7,8 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/provider/spotify_provider.dart';
-import 'package:spotube/services/queries/queries.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
+import 'package:spotube/extensions/image.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class PlaylistAddTrackDialog extends HookConsumerWidget {
/// The id of the playlist this dialog was opened from
@@ -19,33 +17,40 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
const PlaylistAddTrackDialog({
required this.tracks,
required this.openFromPlaylist,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context);
- final spotify = ref.watch(spotifyProvider);
- final userPlaylists = useQueries.playlist.ofMineAll(ref);
+ final userPlaylists = ref.watch(favoritePlaylistsProvider);
+ final favoritePlaylistsNotifier =
+ ref.watch(favoritePlaylistsProvider.notifier);
- final me = useQueries.user.me(ref);
+ final me = ref.watch(meProvider);
final filteredPlaylists = useMemoized(
() =>
- userPlaylists.data
- ?.where(
+ userPlaylists.asData?.value.items
+ .where(
(playlist) =>
playlist.owner?.id != null &&
- playlist.owner!.id == me.data?.id &&
+ playlist.owner!.id == me.asData?.value.id &&
playlist.id != openFromPlaylist,
)
.toList() ??
[],
- [userPlaylists.data, me.data?.id, openFromPlaylist],
+ [userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist],
);
final playlistsCheck = useState({});
- final queryClient = useQueryClient();
+
+ useEffect(() {
+ if (userPlaylists.asData?.value != null) {
+ favoritePlaylistsNotifier.fetchAll();
+ }
+ return null;
+ }, [userPlaylists.asData?.value]);
Future onAdd() async {
final selectedPlaylists = playlistsCheck.value.entries
@@ -54,21 +59,12 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
await Future.wait(
selectedPlaylists.map(
- (playlistId) => spotify.playlists.addTracks(
- tracks
- .map(
- (track) => track.uri!,
- )
- .toList(),
- playlistId),
+ (playlistId) => favoritePlaylistsNotifier.addTracks(
+ playlistId,
+ tracks.map((e) => e.id!).toList(),
+ ),
),
).then((_) => Navigator.pop(context, true));
-
- await queryClient.refreshQueries(
- selectedPlaylists
- .map((playlistId) => "playlist-tracks/$playlistId")
- .toList(),
- );
}
return AlertDialog(
@@ -109,8 +105,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
return CheckboxListTile(
secondary: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
- TypeConversionUtils.image_X_UrlString(
- playlist.images,
+ playlist.images.asUrlString(
placeholder: ImagePlaceholder.collection,
),
),
diff --git a/lib/components/shared/dialogs/replace_downloaded_dialog.dart b/lib/components/shared/dialogs/replace_downloaded_dialog.dart
index 77721041..00461d34 100644
--- a/lib/components/shared/dialogs/replace_downloaded_dialog.dart
+++ b/lib/components/shared/dialogs/replace_downloaded_dialog.dart
@@ -8,8 +8,7 @@ final replaceDownloadedFileState = StateProvider((ref) => null);
class ReplaceDownloadedDialog extends ConsumerWidget {
final Track track;
- const ReplaceDownloadedDialog({required this.track, Key? key})
- : super(key: key);
+ const ReplaceDownloadedDialog({required this.track, super.key});
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/dialogs/select_device_dialog.dart b/lib/components/shared/dialogs/select_device_dialog.dart
new file mode 100644
index 00000000..cd8dedb7
--- /dev/null
+++ b/lib/components/shared/dialogs/select_device_dialog.dart
@@ -0,0 +1,70 @@
+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/connect/clients.dart';
+
+class SelectDeviceDialog extends HookConsumerWidget {
+ const SelectDeviceDialog({super.key});
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final isRemoteService = useState(false);
+
+ final connectClients = ref.watch(connectClientsProvider);
+ final remoteService = connectClients.asData!.value.resolvedService!;
+
+ return AlertDialog(
+ title: const Text("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",
+ ),
+ RadioListTile.adaptive(
+ title: Text(remoteService.name),
+ value: true,
+ groupValue: isRemoteService.value,
+ onChanged: (value) {
+ isRemoteService.value = value!;
+ },
+ ),
+ RadioListTile.adaptive(
+ title: const Text("This Device"),
+ value: false,
+ groupValue: isRemoteService.value,
+ onChanged: (value) {
+ isRemoteService.value = !value!;
+ },
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop(isRemoteService.value);
+ },
+ child: Text(context.l10n.select),
+ ),
+ ],
+ );
+ }
+}
+
+Future showSelectDeviceDialog(BuildContext context, WidgetRef ref) async {
+ final connectClients = ref.read(connectClientsProvider);
+
+ if (connectClients.asData?.value.resolvedService == null) {
+ return false;
+ }
+
+ final isRemote = await showDialog(
+ context: context,
+ builder: (context) => const SelectDeviceDialog(),
+ );
+
+ return isRemote ?? false;
+}
diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/shared/dialogs/track_details_dialog.dart
index 8634776f..da2a140b 100644
--- a/lib/components/shared/dialogs/track_details_dialog.dart
+++ b/lib/components/shared/dialogs/track_details_dialog.dart
@@ -2,20 +2,20 @@ 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/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:spotube/extensions/duration.dart';
class TrackDetailsDialog extends HookWidget {
final Track track;
const TrackDetailsDialog({
- Key? key,
+ super.key,
required this.track,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
@@ -24,8 +24,8 @@ class TrackDetailsDialog extends HookWidget {
final detailsMap = {
context.l10n.title: track.name!,
- context.l10n.artist: TypeConversionUtils.artists_X_ClickableArtists(
- track.artists ?? [],
+ context.l10n.artist: ArtistLink(
+ artists: track.artists ?? [],
mainAxisAlignment: WrapAlignment.start,
textStyle: const TextStyle(color: Colors.blue),
),
diff --git a/lib/components/shared/expandable_search/expandable_search.dart b/lib/components/shared/expandable_search/expandable_search.dart
index 75ac6841..157e180f 100644
--- a/lib/components/shared/expandable_search/expandable_search.dart
+++ b/lib/components/shared/expandable_search/expandable_search.dart
@@ -10,12 +10,12 @@ class ExpandableSearchField extends StatelessWidget {
final FocusNode searchFocus;
const ExpandableSearchField({
- Key? key,
+ super.key,
required this.isFiltering,
required this.onChangeFiltering,
required this.searchController,
required this.searchFocus,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
@@ -60,12 +60,12 @@ class ExpandableSearchButton extends StatelessWidget {
final ValueChanged? onPressed;
const ExpandableSearchButton({
- Key? key,
+ super.key,
required this.isFiltering,
required this.searchFocus,
this.icon = const Icon(SpotubeIcons.filter),
this.onPressed,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/shared/fallbacks/anonymous_fallback.dart
index aea7bf38..ace7ec64 100644
--- a/lib/components/shared/fallbacks/anonymous_fallback.dart
+++ b/lib/components/shared/fallbacks/anonymous_fallback.dart
@@ -8,9 +8,9 @@ import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget {
final Widget? child;
const AnonymousFallback({
- Key? key,
+ super.key,
this.child,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/fallbacks/not_found.dart b/lib/components/shared/fallbacks/not_found.dart
index f45573ad..5a74f672 100644
--- a/lib/components/shared/fallbacks/not_found.dart
+++ b/lib/components/shared/fallbacks/not_found.dart
@@ -3,7 +3,7 @@ import 'package:spotube/collections/assets.gen.dart';
class NotFound extends StatelessWidget {
final bool vertical;
- const NotFound({Key? key, this.vertical = false}) : super(key: key);
+ const NotFound({super.key, this.vertical = false});
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart
index 81ccffdb..9475f9e3 100644
--- a/lib/components/shared/heart_button.dart
+++ b/lib/components/shared/heart_button.dart
@@ -1,5 +1,3 @@
-import 'package:fl_query/fl_query.dart';
-import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,8 +6,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/scrobbler_provider.dart';
-import 'package:spotube/services/mutations/mutations.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
class HeartButton extends HookConsumerWidget {
final bool isLiked;
@@ -23,8 +20,8 @@ class HeartButton extends HookConsumerWidget {
this.color,
this.tooltip,
this.icon,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context, ref) {
@@ -60,90 +57,50 @@ class HeartButton extends HookConsumerWidget {
typedef UseTrackToggleLike = ({
bool isLiked,
- Mutation toggleTrackLike,
- Query me,
+ Future Function(Track track) toggleTrackLike,
});
UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
- final me = useQueries.user.me(ref);
-
- final savedTracks = useQueries.playlist.likedTracksQuery(ref);
+ final savedTracks = ref.watch(likedTracksProvider);
+ final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
final isLiked = useMemoized(
- () => savedTracks.data?.any((element) => element.id == track.id) ?? false,
- [savedTracks.data, track.id],
+ () =>
+ savedTracks.asData?.value.any((element) => element.id == track.id) ??
+ false,
+ [savedTracks.asData?.value, track.id],
);
- final mounted = useIsMounted();
-
final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
- final toggleTrackLike = useMutations.track.toggleFavorite(
- ref,
- track.id!,
- onMutate: (isLiked) {
- if (isLiked) {
- savedTracks.setData(
- savedTracks.data
- ?.where((element) => element.id != track.id)
- .toList() ??
- [],
- );
- } else {
- savedTracks.setData(
- [
- ...?savedTracks.data,
- track,
- ],
- );
- }
- return isLiked;
- },
- onData: (isLiked, recoveryData) async {
- await savedTracks.refresh();
- if (isLiked) {
+ return (
+ isLiked: isLiked,
+ toggleTrackLike: (track) async {
+ await savedTracksNotifier.toggleFavorite(track);
+
+ if (!isLiked) {
await scrobblerNotifier.love(track);
} else {
await scrobblerNotifier.unlove(track);
}
},
- onError: (payload, isLiked) {
- if (!mounted()) return;
-
- if (isLiked != true) {
- savedTracks.setData(
- savedTracks.data
- ?.where((element) => element.id != track.id)
- .toList() ??
- [],
- );
- } else {
- savedTracks.setData(
- [
- ...?savedTracks.data,
- track,
- ],
- );
- }
- },
);
-
- return (isLiked: isLiked, toggleTrackLike: toggleTrackLike, me: me);
}
class TrackHeartButton extends HookConsumerWidget {
final Track track;
const TrackHeartButton({
- Key? key,
+ super.key,
required this.track,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
- final savedTracks = useQueries.playlist.likedTracksQuery(ref);
- final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
+ final savedTracks = ref.watch(likedTracksProvider);
+ final me = ref.watch(meProvider);
+ final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
- if (me.isLoading || !me.hasData) {
+ if (me.isLoading) {
return const CircularProgressIndicator();
}
@@ -152,104 +109,9 @@ class TrackHeartButton extends HookConsumerWidget {
? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite,
isLiked: isLiked,
- onPressed: savedTracks.hasData
+ onPressed: savedTracks.asData?.value != null
? () {
- toggleTrackLike.mutate(isLiked);
- }
- : null,
- );
- }
-}
-
-class PlaylistHeartButton extends HookConsumerWidget {
- final PlaylistSimple playlist;
- final IconData? icon;
- final ValueChanged? onData;
-
- const PlaylistHeartButton({
- required this.playlist,
- Key? key,
- this.icon,
- this.onData,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context, ref) {
- final me = useQueries.user.me(ref);
-
- final isLikedQuery = useQueries.playlist.doesUserFollow(
- ref,
- playlist.id!,
- me.data?.id ?? '',
- );
-
- final togglePlaylistLike = useMutations.playlist.toggleFavorite(
- ref,
- playlist.id!,
- refreshQueries: [
- isLikedQuery.key,
- ],
- onData: onData,
- );
-
- if (me.isLoading || !me.hasData) {
- return const CircularProgressIndicator();
- }
-
- return HeartButton(
- isLiked: isLikedQuery.data ?? false,
- tooltip: isLikedQuery.data ?? false
- ? context.l10n.remove_from_favorites
- : context.l10n.save_as_favorite,
- color: Colors.white,
- icon: icon,
- onPressed: isLikedQuery.hasData
- ? () {
- togglePlaylistLike.mutate(isLikedQuery.data!);
- }
- : null,
- );
- }
-}
-
-class AlbumHeartButton extends HookConsumerWidget {
- final AlbumSimple album;
-
- const AlbumHeartButton({
- required this.album,
- Key? key,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context, ref) {
- final client = useQueryClient();
- final me = useQueries.user.me(ref);
-
- final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
- final isLiked = albumIsSaved.data ?? false;
-
- final toggleAlbumLike = useMutations.album.toggleFavorite(
- ref,
- album.id!,
- refreshQueries: [albumIsSaved.key],
- onData: (_, __) async {
- await client.refreshInfiniteQueryAllPages("current-user-albums");
- },
- );
-
- if (me.isLoading || !me.hasData) {
- return const CircularProgressIndicator();
- }
-
- return HeartButton(
- isLiked: isLiked,
- tooltip: isLiked
- ? context.l10n.remove_from_favorites
- : context.l10n.save_as_favorite,
- color: Colors.white,
- onPressed: albumIsSaved.hasData
- ? () {
- toggleAlbumLike.mutate(isLiked);
+ toggleTrackLike(track);
}
: null,
);
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 dc9d30da..8f0e6048 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
@@ -24,13 +24,12 @@ class HorizontalPlaybuttonCardView extends HookWidget {
required this.hasNextPage,
required this.onFetchMore,
required this.isLoadingNextPage,
- Key? key,
- }) : assert(
+ super.key,
+ }) : assert(
items is List ||
items is List ||
items is List,
- ),
- super(key: key);
+ );
@override
Widget build(BuildContext context) {
@@ -85,11 +84,11 @@ class HorizontalPlaybuttonCardView extends HookWidget {
itemBuilder: (context, index) {
final item = items[index];
- return switch (item.runtimeType) {
- PlaylistSimple =>
+ return switch (item) {
+ PlaylistSimple() =>
PlaylistCard(item as PlaylistSimple),
- Album => AlbumCard(item as Album),
- Artist => Padding(
+ Album() => AlbumCard(item as Album),
+ Artist() => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0),
child: ArtistCard(item as Artist),
diff --git a/lib/components/shared/hover_builder.dart b/lib/components/shared/hover_builder.dart
index ec60848e..7793e744 100644
--- a/lib/components/shared/hover_builder.dart
+++ b/lib/components/shared/hover_builder.dart
@@ -7,8 +7,8 @@ class HoverBuilder extends HookWidget {
const HoverBuilder({
required this.builder,
this.permanentState,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/image/universal_image.dart b/lib/components/shared/image/universal_image.dart
index 04c62478..d8902e63 100644
--- a/lib/components/shared/image/universal_image.dart
+++ b/lib/components/shared/image/universal_image.dart
@@ -20,8 +20,8 @@ class UniversalImage extends HookWidget {
this.placeholder,
this.fit,
this.scale = 1,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
static ImageProvider imageProvider(
String path, {
diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/shared/links/anchor_button.dart
index b1b1cfea..d78bbf96 100644
--- a/lib/components/shared/links/anchor_button.dart
+++ b/lib/components/shared/links/anchor_button.dart
@@ -11,13 +11,13 @@ class AnchorButton extends HookWidget {
const AnchorButton(
this.text, {
- Key? key,
+ super.key,
this.onTap,
this.textAlign,
this.overflow,
this.maxLines,
this.style = const TextStyle(),
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/links/artist_link.dart b/lib/components/shared/links/artist_link.dart
new file mode 100644
index 00000000..af8b186a
--- /dev/null
+++ b/lib/components/shared/links/artist_link.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/widgets.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/components/shared/links/anchor_button.dart';
+import 'package:spotube/utils/service_utils.dart';
+
+class ArtistLink extends StatelessWidget {
+ final List artists;
+ final WrapCrossAlignment crossAxisAlignment;
+ final WrapAlignment mainAxisAlignment;
+ final TextStyle textStyle;
+ final void Function(String route)? onRouteChange;
+
+ const ArtistLink({
+ super.key,
+ required this.artists,
+ this.crossAxisAlignment = WrapCrossAlignment.center,
+ this.mainAxisAlignment = WrapAlignment.center,
+ this.textStyle = const TextStyle(),
+ this.onRouteChange,
+ });
+
+ @override
+ Widget build(BuildContext 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.push(
+ context,
+ "/artist/${artist.value.id}",
+ );
+ }
+ },
+ overflow: TextOverflow.ellipsis,
+ style: textStyle,
+ );
+ }),
+ )
+ .toList(),
+ );
+ }
+}
diff --git a/lib/components/shared/links/hyper_link.dart b/lib/components/shared/links/hyper_link.dart
index fd31298e..f84517b4 100644
--- a/lib/components/shared/links/hyper_link.dart
+++ b/lib/components/shared/links/hyper_link.dart
@@ -13,12 +13,12 @@ class Hyperlink extends StatelessWidget {
const Hyperlink(
this.text,
this.url, {
- Key? key,
+ super.key,
this.textAlign,
this.overflow,
this.style = const TextStyle(),
this.maxLines,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/links/link_text.dart b/lib/components/shared/links/link_text.dart
index d7b00b72..db7b6358 100644
--- a/lib/components/shared/links/link_text.dart
+++ b/lib/components/shared/links/link_text.dart
@@ -15,14 +15,14 @@ class LinkText extends StatelessWidget {
const LinkText(
this.text,
this.route, {
- Key? key,
+ super.key,
this.textAlign,
this.extra,
this.overflow,
this.style = const TextStyle(),
this.maxLines,
this.push = false,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart
index 9aa2d4a8..f956fa28 100644
--- a/lib/components/shared/page_window_title_bar.dart
+++ b/lib/components/shared/page_window_title_bar.dart
@@ -26,8 +26,10 @@ class PageWindowTitleBar extends StatefulHookConsumerWidget
final double? titleWidth;
final Widget? title;
+ final bool _sliver;
+
const PageWindowTitleBar({
- Key? key,
+ super.key,
this.actions,
this.title,
this.toolbarOpacity = 1,
@@ -42,7 +44,38 @@ class PageWindowTitleBar extends StatefulHookConsumerWidget
this.titleTextStyle,
this.titleWidth,
this.toolbarTextStyle,
- }) : super(key: key);
+ }) : _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);
@@ -64,6 +97,48 @@ class _PageWindowTitleBarState extends ConsumerState {
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: DesktopTools.platform.isMacOS &&
+ 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: widget.title,
+ 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 =
@@ -107,9 +182,9 @@ class _PageWindowTitleBarState extends ConsumerState {
class WindowTitleBarButtons extends HookConsumerWidget {
final Color? foregroundColor;
const WindowTitleBarButtons({
- Key? key,
+ super.key,
this.foregroundColor,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
@@ -277,14 +352,13 @@ class WindowButton extends StatelessWidget {
final VoidCallback? onPressed;
WindowButton(
- {Key? key,
+ {super.key,
WindowButtonColors? colors,
this.builder,
@required this.iconBuilder,
this.padding,
this.onPressed,
- this.animate = false})
- : super(key: key) {
+ this.animate = false}) {
this.colors = colors ?? _defaultButtonColors;
}
@@ -350,49 +424,30 @@ class WindowButton extends StatelessWidget {
class MinimizeWindowButton extends WindowButton {
MinimizeWindowButton(
- {Key? key,
- WindowButtonColors? colors,
- VoidCallback? onPressed,
- bool? animate})
+ {super.key, super.colors, super.onPressed, bool? animate})
: super(
- key: key,
- colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MinimizeIcon(color: buttonContext.iconColor),
- onPressed: onPressed,
);
}
class MaximizeWindowButton extends WindowButton {
MaximizeWindowButton(
- {Key? key,
- WindowButtonColors? colors,
- VoidCallback? onPressed,
- bool? animate})
+ {super.key, super.colors, super.onPressed, bool? animate})
: super(
- key: key,
- colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor),
- onPressed: onPressed,
);
}
class RestoreWindowButton extends WindowButton {
- RestoreWindowButton(
- {Key? key,
- WindowButtonColors? colors,
- VoidCallback? onPressed,
- bool? animate})
+ RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate})
: super(
- key: key,
- colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
RestoreIcon(color: buttonContext.iconColor),
- onPressed: onPressed,
);
}
@@ -404,17 +459,12 @@ final _defaultCloseButtonColors = WindowButtonColors(
class CloseWindowButton extends WindowButton {
CloseWindowButton(
- {Key? key,
- WindowButtonColors? colors,
- VoidCallback? onPressed,
- bool? animate})
+ {super.key, WindowButtonColors? colors, super.onPressed, bool? animate})
: super(
- key: key,
colors: colors ?? _defaultCloseButtonColors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor),
- onPressed: onPressed,
);
}
@@ -423,7 +473,7 @@ class CloseWindowButton extends WindowButton {
/// Close
class CloseIcon extends StatelessWidget {
final Color color;
- const CloseIcon({Key? key, required this.color}) : super(key: key);
+ const CloseIcon({super.key, required this.color});
@override
Widget build(BuildContext context) => Align(
alignment: Alignment.topLeft,
@@ -444,13 +494,13 @@ class CloseIcon extends StatelessWidget {
/// Maximize
class MaximizeIcon extends StatelessWidget {
final Color color;
- const MaximizeIcon({Key? key, required this.color}) : super(key: key);
+ const MaximizeIcon({super.key, required this.color});
@override
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
}
class _MaximizePainter extends _IconPainter {
- _MaximizePainter(Color color) : super(color);
+ _MaximizePainter(super.color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
@@ -462,15 +512,15 @@ class _MaximizePainter extends _IconPainter {
class RestoreIcon extends StatelessWidget {
final Color color;
const RestoreIcon({
- Key? key,
+ super.key,
required this.color,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
}
class _RestorePainter extends _IconPainter {
- _RestorePainter(Color color) : super(color);
+ _RestorePainter(super.color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
@@ -487,13 +537,13 @@ class _RestorePainter extends _IconPainter {
/// Minimize
class MinimizeIcon extends StatelessWidget {
final Color color;
- const MinimizeIcon({Key? key, required this.color}) : super(key: key);
+ const MinimizeIcon({super.key, required this.color});
@override
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
}
class _MinimizePainter extends _IconPainter {
- _MinimizePainter(Color color) : super(color);
+ _MinimizePainter(super.color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
@@ -512,7 +562,7 @@ abstract class _IconPainter extends CustomPainter {
}
class _AlignedPaint extends StatelessWidget {
- const _AlignedPaint(this.painter, {Key? key}) : super(key: key);
+ const _AlignedPaint(this.painter);
final CustomPainter painter;
@override
@@ -547,8 +597,7 @@ T? _ambiguate(T? value) => value;
class MouseStateBuilder extends StatefulWidget {
final MouseStateBuilderCB builder;
final VoidCallback? onPressed;
- const MouseStateBuilder({Key? key, required this.builder, this.onPressed})
- : super(key: key);
+ const MouseStateBuilder({super.key, required this.builder, this.onPressed});
@override
_MouseStateBuilderState createState() => _MouseStateBuilderState();
}
diff --git a/lib/components/shared/panels/helpers.dart b/lib/components/shared/panels/helpers.dart
index 2e754bdf..7dad96d5 100644
--- a/lib/components/shared/panels/helpers.dart
+++ b/lib/components/shared/panels/helpers.dart
@@ -47,8 +47,7 @@ class ForceDraggableWidgetRenderBox extends RenderPointerListener {
/// To make [ForceDraggableWidget] work in [Scrollable] widgets
class PanelScrollPhysics extends ScrollPhysics {
final PanelController controller;
- const PanelScrollPhysics({required this.controller, ScrollPhysics? parent})
- : super(parent: parent);
+ const PanelScrollPhysics({required this.controller, super.parent});
@override
PanelScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PanelScrollPhysics(
diff --git a/lib/components/shared/panels/sliding_up_panel.dart b/lib/components/shared/panels/sliding_up_panel.dart
index 137d5eb7..e99fe261 100644
--- a/lib/components/shared/panels/sliding_up_panel.dart
+++ b/lib/components/shared/panels/sliding_up_panel.dart
@@ -146,7 +146,7 @@ class SlidingUpPanel extends StatefulWidget {
final BoxDecoration? panelDecoration;
const SlidingUpPanel(
- {Key? key,
+ {super.key,
this.body,
this.collapsed,
this.minHeight = 100.0,
@@ -176,8 +176,7 @@ class SlidingUpPanel extends StatefulWidget {
this.panelBuilder})
: assert(panelBuilder != null),
assert(0 <= backdropOpacity && backdropOpacity <= 1.0),
- assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0),
- super(key: key);
+ assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0);
@override
SlidingUpPanelState createState() => SlidingUpPanelState();
diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart
index a8a75d30..80a27eb0 100644
--- a/lib/components/shared/playbutton_card.dart
+++ b/lib/components/shared/playbutton_card.dart
@@ -43,8 +43,8 @@ class PlaybuttonCard extends HookWidget {
this.onAddToQueuePressed,
this.onTap,
this.isOwner = false,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shared/shimmers/shimmer_lyrics.dart
index b225c008..03816202 100644
--- a/lib/components/shared/shimmers/shimmer_lyrics.dart
+++ b/lib/components/shared/shimmers/shimmer_lyrics.dart
@@ -5,7 +5,7 @@ import 'package:gap/gap.dart';
import 'package:skeletonizer/skeletonizer.dart';
class ShimmerLyrics extends HookWidget {
- const ShimmerLyrics({Key? key}) : super(key: key);
+ const ShimmerLyrics({super.key});
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart
index ab35b2e3..be72d689 100644
--- a/lib/components/shared/sort_tracks_dropdown.dart
+++ b/lib/components/shared/sort_tracks_dropdown.dart
@@ -11,8 +11,8 @@ class SortTracksDropdown extends StatelessWidget {
const SortTracksDropdown({
this.onChanged,
this.value,
- Key? key,
- }) : super(key: key);
+ super.key,
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/shared/themed_button_tab_bar.dart
index d5798189..017f04aa 100644
--- a/lib/components/shared/themed_button_tab_bar.dart
+++ b/lib/components/shared/themed_button_tab_bar.dart
@@ -5,7 +5,7 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
final List tabs;
- const ThemedButtonsTabBar({Key? key, required this.tabs}) : super(key: key);
+ const ThemedButtonsTabBar({super.key, required this.tabs});
@override
Widget build(BuildContext context) {
diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart
index a094259d..29349602 100644
--- a/lib/components/shared/track_tile/track_options.dart
+++ b/lib/components/shared/track_tile/track_options.dart
@@ -1,6 +1,5 @@
import 'dart:io';
-import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -16,17 +15,18 @@ 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/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
-import 'package:spotube/services/mutations/mutations.dart';
-import 'package:spotube/services/queries/search.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
+
import 'package:url_launcher/url_launcher_string.dart';
enum TrackOptionValue {
@@ -53,13 +53,13 @@ class TrackOptions extends HookConsumerWidget {
final ObjectRef?>? showMenuCbRef;
final Widget? icon;
const TrackOptions({
- Key? key,
+ super.key,
required this.track,
this.showMenuCbRef,
this.userPlaylist = false,
this.playlistId,
this.icon,
- }) : super(key: key);
+ });
void actionShare(BuildContext context, Track track) {
final data = "https://open.spotify.com/track/${track.id}";
@@ -99,21 +99,10 @@ class TrackOptions extends HookConsumerWidget {
final playlist = ref.read(ProxyPlaylistNotifier.provider);
final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio";
- final pages = await QueryClient.of(context)
- .fetchInfiniteQueryJob, dynamic, int, SearchParams>(
- job: SearchQueries.queryJob(query),
- args: (
- spotify: spotify,
- searchType: SearchType.playlist,
- query: query,
- ),
- ) ??
- [];
+ final pages =
+ await spotify.search.get(query, types: [SearchType.playlist]).first();
- final radios = pages
- .expand((e) => e.items?.toList() ?? [])
- .toList()
- .cast();
+ final radios = pages.map((e) => e.items).toList().cast();
final artists = track.artists!.map((e) => e.name);
@@ -176,6 +165,7 @@ class TrackOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier);
final blacklist = ref.watch(BlackListNotifier.provider);
+ final me = ref.watch(meProvider);
final favorites = useTrackToggleLike(track, ref);
@@ -190,10 +180,8 @@ class TrackOptions extends HookConsumerWidget {
);
final removingTrack = useState(null);
- final removeTrack = useMutations.playlist.removeTrackOf(
- ref,
- playlistId ?? "",
- );
+ final favoritePlaylistsNotifier =
+ ref.watch(favoritePlaylistsProvider.notifier);
final isInQueue = useMemoized(() {
if (playlist.activeTrack == null) return false;
@@ -220,7 +208,7 @@ class TrackOptions extends HookConsumerWidget {
break;
case TrackOptionValue.delete:
await File((track as LocalTrack).path).delete();
- ref.refresh(localTracksProvider);
+ ref.invalidate(localTracksProvider);
break;
case TrackOptionValue.addToQueue:
await playback.addTrack(track);
@@ -257,14 +245,15 @@ class TrackOptions extends HookConsumerWidget {
);
break;
case TrackOptionValue.favorite:
- favorites.toggleTrackLike.mutate(favorites.isLiked);
+ favorites.toggleTrackLike(track);
break;
case TrackOptionValue.addToPlaylist:
actionAddToPlaylist(context, track);
break;
case TrackOptionValue.removeFromPlaylist:
removingTrack.value = track.uri;
- removeTrack.mutate(track.uri!);
+ favoritePlaylistsNotifier
+ .removeTracks(playlistId ?? "", [track.id!]);
break;
case TrackOptionValue.blacklist:
if (isBlackListed) {
@@ -307,8 +296,8 @@ class TrackOptions extends HookConsumerWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: UniversalImage(
- path: TypeConversionUtils.image_X_UrlString(track.album!.images,
- placeholder: ImagePlaceholder.albumArt),
+ path: track.album!.images
+ .asUrlString(placeholder: ImagePlaceholder.albumArt),
fit: BoxFit.cover,
),
),
@@ -321,14 +310,12 @@ class TrackOptions extends HookConsumerWidget {
),
subtitle: Align(
alignment: Alignment.centerLeft,
- child: TypeConversionUtils.artists_X_ClickableArtists(
- track.artists!,
- ),
+ child: ArtistLink(artists: track.artists!),
),
),
],
children: switch (track.runtimeType) {
- LocalTrack => [
+ LocalTrack() => [
PopSheetEntry(
value: TrackOptionValue.delete,
leading: const Icon(SpotubeIcons.trash),
@@ -361,7 +348,7 @@ class TrackOptions extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.queueRemove),
title: Text(context.l10n.remove_from_queue),
),
- if (favorites.me.hasData)
+ if (me.asData?.value != null)
PopSheetEntry(
value: TrackOptionValue.favorite,
leading: favorites.isLiked
@@ -391,10 +378,7 @@ class TrackOptions extends HookConsumerWidget {
if (userPlaylist && auth != null)
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,
- leading: (removeTrack.isMutating || !removeTrack.hasData) &&
- removingTrack.value == track.uri
- ? const CircularProgressIndicator()
- : const Icon(SpotubeIcons.removeFilled),
+ leading: const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist),
),
PopSheetEntry(
diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart
index d268c783..61061d24 100644
--- a/lib/components/shared/track_tile/track_tile.dart
+++ b/lib/components/shared/track_tile/track_tile.dart
@@ -9,14 +9,16 @@ 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/extensions/artist_simple.dart';
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/blacklist_provider.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
+import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null
@@ -28,25 +30,26 @@ class TrackTile extends HookConsumerWidget {
final VoidCallback? onLongPress;
final bool userPlaylist;
final String? playlistId;
+ final ProxyPlaylist playlist;
final List? leadingActions;
const TrackTile({
- Key? key,
+ super.key,
this.index,
required this.track,
this.selected = false,
+ required this.playlist,
this.onTap,
this.onLongPress,
this.onChanged,
this.userPlaylist = false,
this.playlistId,
this.leadingActions,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
- final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final theme = Theme.of(context);
final blacklist = ref.watch(BlackListNotifier.provider);
@@ -63,10 +66,10 @@ class TrackTile extends HookConsumerWidget {
final showOptionCbRef = useRef?>(null);
- final isPlaying = track.id == playlist.activeTrack?.id;
-
final isLoading = useState(false);
+ final isPlaying = playlist.activeTrack?.id == track.id;
+
final isSelected = isPlaying || isLoading.value;
return LayoutBuilder(builder: (context, constrains) {
@@ -135,8 +138,7 @@ class TrackTile extends HookConsumerWidget {
child: AspectRatio(
aspectRatio: 1,
child: UniversalImage(
- path: TypeConversionUtils.image_X_UrlString(
- track.album?.images,
+ path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
fit: BoxFit.cover,
@@ -230,16 +232,12 @@ class TrackTile extends HookConsumerWidget {
alignment: Alignment.centerLeft,
child: track is LocalTrack
? Text(
- TypeConversionUtils.artists_X_String(
- track.artists ?? [],
- ),
+ track.artists?.asString() ?? '',
)
: ClipRect(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40),
- child: TypeConversionUtils.artists_X_ClickableArtists(
- track.artists ?? [],
- ),
+ child: ArtistLink(artists: track.artists ?? []),
),
),
),
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart
index 33c8fa82..80368445 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart
+++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart
@@ -8,18 +8,21 @@ 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/models/connect/connect.dart';
+import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TrackViewBodySection extends HookConsumerWidget {
- const TrackViewBodySection({Key? key}) : super(key: key);
+ const TrackViewBodySection({super.key});
@override
Widget build(BuildContext context, ref) {
@@ -89,6 +92,7 @@ class TrackViewBodySection extends HookConsumerWidget {
loadingBuilder: (context) => Skeletonizer(
enabled: true,
child: TrackTile(
+ playlist: playlist,
track: FakeData.track,
index: 0,
),
@@ -98,13 +102,18 @@ class TrackViewBodySection extends HookConsumerWidget {
child: Column(
children: List.generate(
10,
- (index) => TrackTile(track: FakeData.track, index: index),
+ (index) => TrackTile(
+ track: FakeData.track,
+ index: index,
+ playlist: playlist,
+ ),
),
),
),
itemBuilder: (context, index) {
final track = tracks[index];
return TrackTile(
+ playlist: playlist,
track: track,
index: index,
selected: trackViewState.selectedTrackIds.contains(track.id!),
@@ -125,16 +134,37 @@ class TrackViewBodySection extends HookConsumerWidget {
return;
}
- if (isActive || playlist.tracks.contains(track)) {
- await playlistNotifier.jumpToTrack(track);
+ final isRemoteDevice =
+ await showSelectDeviceDialog(context, ref);
+
+ if (isRemoteDevice) {
+ final remotePlayback = ref.read(connectProvider.notifier);
+ final remoteQueue = ref.read(queueProvider);
+ if (remoteQueue.collections.contains(props.collectionId) ||
+ remoteQueue.tracks.any((s) => s.id == track.id)) {
+ await playlistNotifier.jumpToTrack(track);
+ } else {
+ final tracks = await props.pagination.onFetchAll();
+ await remotePlayback.load(
+ WebSocketLoadEventData(
+ tracks: tracks,
+ collectionId: props.collectionId,
+ initialIndex: index,
+ ),
+ );
+ }
} else {
- final tracks = await props.pagination.onFetchAll();
- await playlistNotifier.load(
- tracks,
- initialIndex: index,
- autoPlay: true,
- );
- playlistNotifier.addCollection(props.collectionId);
+ if (isActive || playlist.tracks.contains(track)) {
+ await playlistNotifier.jumpToTrack(track);
+ } else {
+ final tracks = await props.pagination.onFetchAll();
+ await playlistNotifier.load(
+ tracks,
+ initialIndex: index,
+ autoPlay: true,
+ );
+ playlistNotifier.addCollection(props.collectionId);
+ }
}
},
);
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
index 7e4522a0..3a1538a3 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
+++ b/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
@@ -13,10 +13,10 @@ class TrackViewBodyHeaders extends HookConsumerWidget {
final FocusNode searchFocus;
const TrackViewBodyHeaders({
- Key? key,
+ super.key,
required this.isFiltering,
required this.searchFocus,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/shared/tracks_view/sections/body/track_view_options.dart
index 583c9107..5560ef3f 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_options.dart
+++ b/lib/components/shared/tracks_view/sections/body/track_view_options.dart
@@ -13,7 +13,7 @@ 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({Key? key}) : super(key: key);
+ const TrackViewBodyOptions({super.key});
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart b/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart
index ca3c6706..2f87ccc8 100644
--- a/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart
+++ b/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart
@@ -1,18 +1,18 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
- final userPlaylistsQuery = useQueries.playlist.ofMineAll(ref);
- final me = useQueries.user.me(ref);
+ final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider);
+ final me = ref.watch(meProvider);
return useMemoized(
() =>
- userPlaylistsQuery.data?.any((e) =>
+ userPlaylistsQuery.asData?.value.items.any((e) =>
e.id == playlistId &&
- me.data != null &&
- e.owner?.id == me.data?.id) ??
+ me.asData?.value != null &&
+ e.owner?.id == me.asData?.value.id) ??
false,
- [userPlaylistsQuery.data, playlistId, me.data],
+ [userPlaylistsQuery.asData?.value, playlistId, me.asData?.value],
);
}
diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart
index 19241dc6..4a704302 100644
--- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart
+++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart
@@ -14,7 +14,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart';
class TrackViewFlexHeader extends HookConsumerWidget {
- const TrackViewFlexHeader({Key? key}) : super(key: key);
+ const TrackViewFlexHeader({super.key});
@override
Widget build(BuildContext context, ref) {
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 75aa3f61..a16dd750 100644
--- a/lib/components/shared/tracks_view/sections/header/header_actions.dart
+++ b/lib/components/shared/tracks_view/sections/header/header_actions.dart
@@ -12,7 +12,7 @@ import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
class TrackViewHeaderActions extends HookConsumerWidget {
- const TrackViewHeaderActions({Key? key}) : super(key: key);
+ const TrackViewHeaderActions({super.key});
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/shared/tracks_view/sections/header/header_buttons.dart
index bae47f12..f505f765 100644
--- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart
+++ b/lib/components/shared/tracks_view/sections/header/header_buttons.dart
@@ -6,8 +6,11 @@ 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/shared/dialogs/select_device_dialog.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/models/connect/connect.dart';
+import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@@ -15,10 +18,10 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
final PaletteColor color;
final bool compact;
const TrackViewHeaderButtons({
- Key? key,
+ super.key,
required this.color,
this.compact = false,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context, ref) {
@@ -43,13 +46,25 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
final allTracks = await props.pagination.onFetchAll();
- await playlistNotifier.load(
- allTracks,
- autoPlay: true,
- initialIndex: Random().nextInt(allTracks.length),
- );
- await audioPlayer.setShuffle(true);
- playlistNotifier.addCollection(props.collectionId);
+ final isRemoteDevice = await showSelectDeviceDialog(context, ref);
+ if (isRemoteDevice) {
+ final remotePlayback = ref.read(connectProvider.notifier);
+ await remotePlayback.load(
+ WebSocketLoadEventData(
+ tracks: allTracks,
+ collectionId: props.collectionId,
+ initialIndex: Random().nextInt(allTracks.length)),
+ );
+ await remotePlayback.setShuffle(true);
+ } else {
+ await playlistNotifier.load(
+ allTracks,
+ autoPlay: true,
+ initialIndex: Random().nextInt(allTracks.length),
+ );
+ await audioPlayer.setShuffle(true);
+ playlistNotifier.addCollection(props.collectionId);
+ }
} finally {
isLoading.value = false;
}
@@ -61,8 +76,19 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
final allTracks = await props.pagination.onFetchAll();
- await playlistNotifier.load(allTracks, autoPlay: true);
- playlistNotifier.addCollection(props.collectionId);
+ final isRemoteDevice = await showSelectDeviceDialog(context, ref);
+ if (isRemoteDevice) {
+ final remotePlayback = ref.read(connectProvider.notifier);
+ await remotePlayback.load(
+ WebSocketLoadEventData(
+ tracks: allTracks,
+ collectionId: props.collectionId,
+ ),
+ );
+ } else {
+ await playlistNotifier.load(allTracks, autoPlay: true);
+ playlistNotifier.addCollection(props.collectionId);
+ }
} finally {
isLoading.value = false;
}
diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/shared/tracks_view/track_view.dart
index 4103573c..eb8f6871 100644
--- a/lib/components/shared/tracks_view/track_view.dart
+++ b/lib/components/shared/tracks_view/track_view.dart
@@ -10,7 +10,7 @@ import 'package:spotube/components/shared/tracks_view/sections/body/track_view_b
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
class TrackView extends HookConsumerWidget {
- const TrackView({Key? key}) : super(key: key);
+ const TrackView({super.key});
@override
Widget build(BuildContext context, ref) {
diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/shared/tracks_view/track_view_props.dart
index 21bbaec7..a1a07f84 100644
--- a/lib/components/shared/tracks_view/track_view_props.dart
+++ b/lib/components/shared/tracks_view/track_view_props.dart
@@ -1,6 +1,5 @@
import 'dart:async';
-import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:spotify/spotify.dart';
@@ -19,19 +18,6 @@ class PaginationProps {
required this.onRefresh,
});
- factory PaginationProps.fromQuery(
- InfiniteQuery, dynamic, int> query, {
- required Future> Function() onFetchAll,
- }) {
- return PaginationProps(
- hasNextPage: query.hasNextPage,
- isLoading: query.isLoadingNextPage,
- onFetchMore: query.fetchNext,
- onFetchAll: onFetchAll,
- onRefresh: query.refreshAll,
- );
- }
-
@override
operator ==(Object other) {
return other is PaginationProps &&
diff --git a/lib/components/shared/waypoint.dart b/lib/components/shared/waypoint.dart
index abd9f98d..08e9088a 100644
--- a/lib/components/shared/waypoint.dart
+++ b/lib/components/shared/waypoint.dart
@@ -11,12 +11,12 @@ class Waypoint extends HookWidget {
final bool isGrid;
const Waypoint({
- Key? key,
+ super.key,
required this.controller,
this.isGrid = false,
this.onTouchEdge,
this.child,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
diff --git a/lib/extensions/album_simple.dart b/lib/extensions/album_simple.dart
index 00db4dca..7c8ae09e 100644
--- a/lib/extensions/album_simple.dart
+++ b/lib/extensions/album_simple.dart
@@ -1,6 +1,6 @@
import 'package:spotify/spotify.dart';
-extension AlbumJson on AlbumSimple {
+extension AlbumExtensions on AlbumSimple {
Map toJson() {
return {
"albumType": albumType?.name,
@@ -15,4 +15,22 @@ extension AlbumJson on AlbumSimple {
.toList(),
};
}
+
+ Album toAlbum() {
+ Album album = Album();
+ album.albumType = albumType;
+ album.artists = artists;
+ album.availableMarkets = availableMarkets;
+ album.externalUrls = externalUrls;
+ album.href = href;
+ album.id = id;
+ album.images = images;
+ album.name = name;
+ album.releaseDate = releaseDate;
+ album.releaseDatePrecision = releaseDatePrecision;
+ album.tracks = tracks;
+ album.type = type;
+ album.uri = uri;
+ return album;
+ }
}
diff --git a/lib/extensions/artist_simple.dart b/lib/extensions/artist_simple.dart
index caf2e510..6a80300e 100644
--- a/lib/extensions/artist_simple.dart
+++ b/lib/extensions/artist_simple.dart
@@ -11,3 +11,9 @@ extension ArtistJson on ArtistSimple {
};
}
}
+
+extension ArtistExtension on List {
+ String asString() {
+ return map((e) => e.name?.replaceAll(",", " ")).join(", ");
+ }
+}
diff --git a/lib/extensions/image.dart b/lib/extensions/image.dart
new file mode 100644
index 00000000..ee78653a
--- /dev/null
+++ b/lib/extensions/image.dart
@@ -0,0 +1,34 @@
+import 'package:spotify/spotify.dart';
+import 'package:spotube/collections/assets.gen.dart';
+import 'package:spotube/utils/primitive_utils.dart';
+import 'package:collection/collection.dart';
+
+enum ImagePlaceholder {
+ albumArt,
+ artist,
+ collection,
+ online,
+}
+
+extension SpotifyImageExtensions on List? {
+ String asUrlString({
+ int index = 1,
+ required ImagePlaceholder placeholder,
+ }) {
+ final String placeholderUrl = {
+ ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
+ ImagePlaceholder.artist: Assets.userPlaceholder.path,
+ ImagePlaceholder.collection: Assets.placeholder.path,
+ ImagePlaceholder.online:
+ "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
+ }[placeholder]!;
+
+ final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
+
+ return sortedImage != null && sortedImage.isNotEmpty
+ ? sortedImage[
+ index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
+ .url!
+ : placeholderUrl;
+ }
+}
diff --git a/lib/extensions/infinite_query.dart b/lib/extensions/infinite_query.dart
deleted file mode 100644
index 2181ab3c..00000000
--- a/lib/extensions/infinite_query.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:fl_query/fl_query.dart';
-import 'package:spotify/spotify.dart';
-
-extension FetchAllTracks on InfiniteQuery, dynamic, int> {
- Future> fetchAllTracks({
- required Future> Function() getAllTracks,
- }) async {
- if (pages.isNotEmpty && !hasNextPage) {
- return pages.expand((page) => page).toList();
- }
- final tracks = await getAllTracks();
-
- final numOfPages = (tracks.length / 20).round();
-
- final Map> pagedTracks = {};
-
- for (var i = 0; i < numOfPages; i++) {
- if (i == numOfPages - 1) {
- final pageTracks = tracks.sublist(i * 20);
- pagedTracks[i] = pageTracks;
- break;
- }
-
- final pageTracks = tracks.sublist(i * 20, (i + 1) * 20);
- pagedTracks[i] = pageTracks;
- }
-
- for (final group in pagedTracks.entries) {
- setPageData(group.key, group.value);
- }
-
- return tracks.toList();
- }
-}
diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart
index 51498b33..d8258a6d 100644
--- a/lib/extensions/track.dart
+++ b/lib/extensions/track.dart
@@ -1,10 +1,46 @@
+import 'dart:io';
+
+import 'package:metadata_god/metadata_god.dart';
+import 'package:path/path.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/album_simple.dart';
import 'package:spotube/extensions/artist_simple.dart';
-extension TrackJson on Track {
+extension TrackExtensions on Track {
+ Track fromFile(
+ File file, {
+ Metadata? metadata,
+ String? art,
+ }) {
+ album = Album()
+ ..name = metadata?.album ?? "Unknown"
+ ..images = [if (art != null) Image()..url = art]
+ ..genres = [if (metadata?.genre != null) metadata!.genre!]
+ ..artists = [
+ Artist()
+ ..name = metadata?.albumArtist ?? "Unknown"
+ ..id = metadata?.albumArtist ?? "Unknown"
+ ..type = "artist",
+ ]
+ ..id = metadata?.album
+ ..releaseDate = metadata?.year?.toString();
+ artists = [
+ Artist()
+ ..name = metadata?.artist ?? "Unknown"
+ ..id = metadata?.artist ?? "Unknown"
+ ];
+
+ id = metadata?.title ?? basenameWithoutExtension(file.path);
+ name = metadata?.title ?? basenameWithoutExtension(file.path);
+ type = "track";
+ uri = file.path;
+ durationMs = (metadata?.durationMs?.toInt() ?? 0);
+
+ return this;
+ }
+
Map toJson() {
- return TrackJson.trackToJson(this);
+ return TrackExtensions.trackToJson(this);
}
static Map trackToJson(Track track) {
@@ -30,3 +66,27 @@ extension TrackJson on Track {
};
}
}
+
+extension TrackSimpleExtensions on TrackSimple {
+ Track asTrack(AlbumSimple album) {
+ Track track = Track();
+ track.name = name;
+ track.album = album;
+ track.artists = artists;
+ track.availableMarkets = availableMarkets;
+ track.discNumber = discNumber;
+ track.durationMs = durationMs;
+ track.explicit = explicit;
+ track.externalUrls = externalUrls;
+ track.href = href;
+ track.id = id;
+ track.isPlayable = isPlayable;
+ track.linkedFrom = linkedFrom;
+ track.name = name;
+ track.previewUrl = previewUrl;
+ track.trackNumber = trackNumber;
+ track.type = type;
+ track.uri = uri;
+ return track;
+ }
+}
diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart
index f11a1cff..2650b05c 100644
--- a/lib/hooks/configurators/use_deep_linking.dart
+++ b/lib/hooks/configurators/use_deep_linking.dart
@@ -1,10 +1,8 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
-import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
@@ -17,8 +15,6 @@ final linkStream = appLinks.allStringLinkStream.asBroadcastStream();
void useDeepLinking(WidgetRef ref) {
// single instance no worries
final spotify = ref.watch(spotifyProvider);
- final queryClient = useQueryClient();
-
final router = ref.watch(routerProvider);
useEffect(() {
@@ -32,10 +28,7 @@ void useDeepLinking(WidgetRef ref) {
case "album":
router.push(
"/album/${url.pathSegments.last}",
- extra: await queryClient.fetchQuery(
- "album/${url.pathSegments.last}",
- () => spotify.albums.get(url.pathSegments.last),
- ),
+ extra: await spotify.albums.get(url.pathSegments.last),
);
break;
case "artist":
@@ -44,10 +37,7 @@ void useDeepLinking(WidgetRef ref) {
case "playlist":
router.push(
"/playlist/${url.pathSegments.last}",
- extra: await queryClient.fetchQuery(
- "playlist/${url.pathSegments.last}",
- () => spotify.playlists.get(url.pathSegments.last),
- ),
+ extra: await spotify.playlists.get(url.pathSegments.last),
);
break;
case "track":
@@ -78,10 +68,7 @@ void useDeepLinking(WidgetRef ref) {
case "spotify:album":
await router.push(
"/album/$endSegment",
- extra: await queryClient.fetchQuery(
- "album/$endSegment",
- () => spotify.albums.get(endSegment),
- ),
+ extra: await spotify.albums.get(endSegment),
);
break;
case "spotify:artist":
@@ -93,10 +80,7 @@ void useDeepLinking(WidgetRef ref) {
case "spotify:playlist":
await router.push(
"/playlist/$endSegment",
- extra: await queryClient.fetchQuery(
- "playlist/$endSegment",
- () => spotify.playlists.get(endSegment),
- ),
+ extra: await spotify.playlists.get(endSegment),
);
break;
default:
@@ -108,5 +92,5 @@ void useDeepLinking(WidgetRef ref) {
mediaStream?.cancel();
subscription.cancel();
};
- }, [spotify, queryClient]);
+ }, [spotify]);
}
diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart
index f5d11829..3cd55e40 100644
--- a/lib/hooks/configurators/use_endless_playback.dart
+++ b/lib/hooks/configurators/use_endless_playback.dart
@@ -1,5 +1,4 @@
import 'package:catcher_2/catcher_2.dart';
-import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
@@ -8,7 +7,6 @@ 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';
import 'package:spotube/services/audio_player/audio_player.dart';
-import 'package:spotube/services/queries/search.dart';
void useEndlessPlayback(WidgetRef ref) {
final auth = ref.watch(AuthenticationNotifier.provider);
@@ -18,7 +16,6 @@ void useEndlessPlayback(WidgetRef ref) {
final endlessPlayback =
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
- final queryClient = useQueryClient();
useEffect(
() {
@@ -32,16 +29,8 @@ void useEndlessPlayback(WidgetRef ref) {
final track = playlist.tracks.last;
final query = "${track.name} Radio";
- final pages = await queryClient.fetchInfiniteQueryJob,
- dynamic, int, SearchParams>(
- job: SearchQueries.queryJob(query),
- args: (
- spotify: spotify,
- searchType: SearchType.playlist,
- query: query
- ),
- ) ??
- [];
+ final pages = await spotify.search
+ .get(query, types: [SearchType.playlist]).first();
final radios = pages
.expand((e) => e.items?.toList() ?? [])
@@ -94,7 +83,6 @@ void useEndlessPlayback(WidgetRef ref) {
[
spotify,
playback,
- queryClient,
playlist.tracks,
endlessPlayback,
auth,
diff --git a/lib/hooks/configurators/use_get_storage_perms.dart b/lib/hooks/configurators/use_get_storage_perms.dart
index 3fcb369b..86b495c4 100644
--- a/lib/hooks/configurators/use_get_storage_perms.dart
+++ b/lib/hooks/configurators/use_get_storage_perms.dart
@@ -25,11 +25,11 @@ void useGetStoragePermissions(WidgetRef ref) {
if (hasNoStoragePerm) {
await Permission.storage.request();
- if (isMounted()) ref.refresh(localTracksProvider);
+ if (isMounted()) ref.invalidate(localTracksProvider);
}
if (hasNoAudioPerm) {
await Permission.audio.request();
- if (isMounted()) ref.refresh(localTracksProvider);
+ if (isMounted()) ref.invalidate(localTracksProvider);
}
},
null,
diff --git a/lib/hooks/controllers/use_auto_scroll_controller.dart b/lib/hooks/controllers/use_auto_scroll_controller.dart
index 8edfb041..0c7119e4 100644
--- a/lib/hooks/controllers/use_auto_scroll_controller.dart
+++ b/lib/hooks/controllers/use_auto_scroll_controller.dart
@@ -39,8 +39,8 @@ class _AutoScrollControllerHook extends Hook {
this.copyTagsFrom,
this.suggestedRowHeight,
this.debugLabel,
- List