Skip to content

Commit 4defeef

Browse files
committed
feat: start radio support
1 parent 5d0b5e6 commit 4defeef

File tree

5 files changed

+238
-27
lines changed

5 files changed

+238
-27
lines changed

lib/collections/spotube_icons.dart

+1
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,5 @@ abstract class SpotubeIcons {
111111
static const wikipedia = SimpleIcons.wikipedia;
112112
static const discord = SimpleIcons.discord;
113113
static const youtube = SimpleIcons.youtube;
114+
static const radio = FeatherIcons.radio;
114115
}

lib/components/shared/track_tile/track_options.dart

+77-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:io';
22

3-
import 'package:flutter/material.dart';
3+
import 'package:fl_query/fl_query.dart';
4+
import 'package:flutter/material.dart' hide Page;
45
import 'package:flutter/services.dart';
56
import 'package:flutter_hooks/flutter_hooks.dart';
67
import 'package:go_router/go_router.dart';
@@ -10,6 +11,7 @@ import 'package:spotube/collections/spotube_icons.dart';
1011
import 'package:spotube/components/library/user_local_tracks.dart';
1112
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
1213
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
14+
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
1315
import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
1416
import 'package:spotube/components/shared/heart_button.dart';
1517
import 'package:spotube/components/shared/image/universal_image.dart';
@@ -20,7 +22,9 @@ import 'package:spotube/provider/authentication_provider.dart';
2022
import 'package:spotube/provider/blacklist_provider.dart';
2123
import 'package:spotube/provider/download_manager_provider.dart';
2224
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
25+
import 'package:spotube/provider/spotify_provider.dart';
2326
import 'package:spotube/services/mutations/mutations.dart';
27+
import 'package:spotube/services/queries/search.dart';
2428
import 'package:spotube/utils/type_conversion_utils.dart';
2529

2630
enum TrackOptionValue {
@@ -36,6 +40,7 @@ enum TrackOptionValue {
3640
favorite,
3741
details,
3842
download,
43+
startRadio,
3944
}
4045

4146
class TrackOptions extends HookConsumerWidget {
@@ -82,6 +87,67 @@ class TrackOptions extends HookConsumerWidget {
8287
);
8388
}
8489

90+
void actionStartRadio(
91+
BuildContext context,
92+
WidgetRef ref,
93+
Track track,
94+
) async {
95+
final playback = ref.read(ProxyPlaylistNotifier.notifier);
96+
final playlist = ref.read(ProxyPlaylistNotifier.provider);
97+
final spotify = ref.read(spotifyProvider);
98+
final pages = await QueryClient.of(context)
99+
.fetchInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
100+
job: SearchQueries.queryJob(SearchType.playlist.name),
101+
args: (
102+
spotify: spotify,
103+
searchType: SearchType.playlist,
104+
query: "${track.name} Radio"
105+
),
106+
) ??
107+
[];
108+
109+
final radios = pages.expand((e) => e.items ?? <PlaylistSimple>[]).toList();
110+
111+
final artists = track.artists!.map((e) => e.name);
112+
113+
final radio = radios.firstWhere(
114+
(e) =>
115+
e.name == "${track.name} Radio" &&
116+
artists.where((a) => e.name!.contains(a!)).length >= 2,
117+
orElse: () => radios.first,
118+
);
119+
120+
bool replaceQueue = false;
121+
122+
if (context.mounted && playlist.tracks.isNotEmpty) {
123+
replaceQueue = await showPromptDialog(
124+
context: context,
125+
title: context.l10n.how_to_start_radio,
126+
message: context.l10n.replace_queue_question,
127+
okText: context.l10n.replace,
128+
cancelText: context.l10n.add_to_queue,
129+
);
130+
}
131+
132+
if (replaceQueue) {
133+
await playback.stop();
134+
await playback.load([track], autoPlay: true);
135+
} else {
136+
await playback.addTrack(track);
137+
}
138+
139+
final tracks =
140+
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
141+
142+
await playback.addTracks(
143+
tracks.toList()
144+
..removeWhere((e) {
145+
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
146+
return e.id == track.id || isDuplicate;
147+
}),
148+
);
149+
}
150+
85151
@override
86152
Widget build(BuildContext context, ref) {
87153
final scaffoldMessenger = ScaffoldMessenger.of(context);
@@ -207,6 +273,9 @@ class TrackOptions extends HookConsumerWidget {
207273
case TrackOptionValue.download:
208274
await downloadManager.addToQueue(track);
209275
break;
276+
case TrackOptionValue.startRadio:
277+
actionStartRadio(context, ref, track);
278+
break;
210279
}
211280
},
212281
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
@@ -287,12 +356,18 @@ class TrackOptions extends HookConsumerWidget {
287356
: context.l10n.save_as_favorite,
288357
),
289358
),
290-
if (auth != null)
359+
if (auth != null) ...[
360+
PopSheetEntry(
361+
value: TrackOptionValue.startRadio,
362+
leading: const Icon(SpotubeIcons.radio),
363+
title: Text(context.l10n.start_a_radio),
364+
),
291365
PopSheetEntry(
292366
value: TrackOptionValue.addToPlaylist,
293367
leading: const Icon(SpotubeIcons.playlistAdd),
294368
title: Text(context.l10n.add_to_playlist),
295369
),
370+
],
296371
if (userPlaylist && auth != null)
297372
PopSheetEntry(
298373
value: TrackOptionValue.removeFromPlaylist,

lib/l10n/app_en.arb

+4-1
Original file line numberDiff line numberDiff line change
@@ -286,5 +286,8 @@
286286
"genres": "Genres",
287287
"explore_genres": "Explore Genres",
288288
"friends": "Friends",
289-
"no_lyrics_available": "Sorry, unable find lyrics for this track"
289+
"no_lyrics_available": "Sorry, unable find lyrics for this track",
290+
"start_a_radio": "Start a Radio",
291+
"how_to_start_radio": "How do you want to start the radio?",
292+
"replace_queue_question": "Do you want to replace the current queue or append to it?"
290293
}

lib/services/queries/search.dart

+47-23
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,60 @@
11
import 'package:fl_query/fl_query.dart';
2+
import 'package:fl_query_hooks/fl_query_hooks.dart';
3+
import 'package:flutter_hooks/flutter_hooks.dart';
24
import 'package:hooks_riverpod/hooks_riverpod.dart';
35
import 'package:spotify/spotify.dart';
4-
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
6+
import 'package:spotube/provider/spotify_provider.dart';
7+
8+
typedef SearchParams = ({
9+
SpotifyApi spotify,
10+
SearchType searchType,
11+
String query
12+
});
513

614
class SearchQueries {
715
const SearchQueries();
16+
17+
static final queryJob =
18+
InfiniteQueryJob.withVariableKey<List<Page>, dynamic, int, SearchParams>(
19+
baseQueryKey: "search-query",
20+
task: (variableKey, page, args) => args!.spotify.search.get(
21+
args.query,
22+
types: [args.searchType],
23+
).getPage(10, page),
24+
initialPage: 0,
25+
nextPage: (lastPage, lastPageData) {
26+
if (lastPageData.isEmpty) return null;
27+
if ((lastPageData.first.isLast ||
28+
(lastPageData.first.items ?? []).length < 10)) {
29+
return null;
30+
}
31+
return lastPageData.first.nextOffset;
32+
},
33+
enabled: false,
34+
);
35+
836
InfiniteQuery<List<Page>, dynamic, int> query(
937
WidgetRef ref,
10-
String query,
38+
String queryStr,
1139
SearchType searchType,
1240
) {
13-
return useSpotifyInfiniteQuery<List<Page>, dynamic, int>(
14-
"search-query/${searchType.name}",
15-
(page, spotify) {
16-
if (query.trim().isEmpty) return [];
17-
final queryString = query;
18-
return spotify.search.get(
19-
queryString,
20-
types: [searchType],
21-
).getPage(10, page);
22-
},
23-
enabled: false,
24-
ref: ref,
25-
initialPage: 0,
26-
nextPage: (lastPage, lastPageData) {
27-
if (lastPageData.isEmpty) return null;
28-
if ((lastPageData.first.isLast ||
29-
(lastPageData.first.items ?? []).length < 10)) {
30-
return null;
31-
}
32-
return lastPageData.first.nextOffset;
33-
},
41+
final spotify = ref.watch(spotifyProvider);
42+
final query = useInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
43+
job: queryJob(searchType.name),
44+
args: (spotify: spotify, searchType: searchType, query: queryStr),
3445
);
46+
47+
useEffect(() {
48+
return ref.listenManual(
49+
spotifyProvider,
50+
(previous, next) {
51+
if (previous != next) {
52+
query.refreshAll();
53+
}
54+
},
55+
).close;
56+
}, [query]);
57+
58+
return query;
3559
}
3660
}

untranslated_messages.json

+109-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,109 @@
1-
{}
1+
{
2+
"ar": [
3+
"start_a_radio",
4+
"how_to_start_radio",
5+
"replace_queue_question"
6+
],
7+
8+
"bn": [
9+
"start_a_radio",
10+
"how_to_start_radio",
11+
"replace_queue_question"
12+
],
13+
14+
"ca": [
15+
"start_a_radio",
16+
"how_to_start_radio",
17+
"replace_queue_question"
18+
],
19+
20+
"de": [
21+
"start_a_radio",
22+
"how_to_start_radio",
23+
"replace_queue_question"
24+
],
25+
26+
"es": [
27+
"start_a_radio",
28+
"how_to_start_radio",
29+
"replace_queue_question"
30+
],
31+
32+
"fa": [
33+
"start_a_radio",
34+
"how_to_start_radio",
35+
"replace_queue_question"
36+
],
37+
38+
"fr": [
39+
"start_a_radio",
40+
"how_to_start_radio",
41+
"replace_queue_question"
42+
],
43+
44+
"hi": [
45+
"start_a_radio",
46+
"how_to_start_radio",
47+
"replace_queue_question"
48+
],
49+
50+
"it": [
51+
"start_a_radio",
52+
"how_to_start_radio",
53+
"replace_queue_question"
54+
],
55+
56+
"ja": [
57+
"start_a_radio",
58+
"how_to_start_radio",
59+
"replace_queue_question"
60+
],
61+
62+
"ne": [
63+
"start_a_radio",
64+
"how_to_start_radio",
65+
"replace_queue_question"
66+
],
67+
68+
"nl": [
69+
"start_a_radio",
70+
"how_to_start_radio",
71+
"replace_queue_question"
72+
],
73+
74+
"pl": [
75+
"start_a_radio",
76+
"how_to_start_radio",
77+
"replace_queue_question"
78+
],
79+
80+
"pt": [
81+
"start_a_radio",
82+
"how_to_start_radio",
83+
"replace_queue_question"
84+
],
85+
86+
"ru": [
87+
"start_a_radio",
88+
"how_to_start_radio",
89+
"replace_queue_question"
90+
],
91+
92+
"tr": [
93+
"start_a_radio",
94+
"how_to_start_radio",
95+
"replace_queue_question"
96+
],
97+
98+
"uk": [
99+
"start_a_radio",
100+
"how_to_start_radio",
101+
"replace_queue_question"
102+
],
103+
104+
"zh": [
105+
"start_a_radio",
106+
"how_to_start_radio",
107+
"replace_queue_question"
108+
]
109+
}

0 commit comments

Comments
 (0)