feat: start radio support

This commit is contained in:
Kingkor Roy Tirtho 2024-01-31 16:33:50 +06:00
parent 5d0b5e69a5
commit 4defeefe7e
5 changed files with 238 additions and 27 deletions

View File

@ -111,4 +111,5 @@ abstract class SpotubeIcons {
static const wikipedia = SimpleIcons.wikipedia;
static const discord = SimpleIcons.discord;
static const youtube = SimpleIcons.youtube;
static const radio = FeatherIcons.radio;
}

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
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';
import 'package:go_router/go_router.dart';
@ -10,6 +11,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/library/user_local_tracks.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
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';
@ -20,7 +22,9 @@ 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_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/search.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
enum TrackOptionValue {
@ -36,6 +40,7 @@ enum TrackOptionValue {
favorite,
details,
download,
startRadio,
}
class TrackOptions extends HookConsumerWidget {
@ -82,6 +87,67 @@ class TrackOptions extends HookConsumerWidget {
);
}
void actionStartRadio(
BuildContext context,
WidgetRef ref,
Track track,
) async {
final playback = ref.read(ProxyPlaylistNotifier.notifier);
final playlist = ref.read(ProxyPlaylistNotifier.provider);
final spotify = ref.read(spotifyProvider);
final pages = await QueryClient.of(context)
.fetchInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
job: SearchQueries.queryJob(SearchType.playlist.name),
args: (
spotify: spotify,
searchType: SearchType.playlist,
query: "${track.name} Radio"
),
) ??
[];
final radios = pages.expand((e) => e.items ?? <PlaylistSimple>[]).toList();
final artists = track.artists!.map((e) => e.name);
final radio = radios.firstWhere(
(e) =>
e.name == "${track.name} Radio" &&
artists.where((a) => e.name!.contains(a!)).length >= 2,
orElse: () => radios.first,
);
bool replaceQueue = false;
if (context.mounted && playlist.tracks.isNotEmpty) {
replaceQueue = await showPromptDialog(
context: context,
title: context.l10n.how_to_start_radio,
message: context.l10n.replace_queue_question,
okText: context.l10n.replace,
cancelText: context.l10n.add_to_queue,
);
}
if (replaceQueue) {
await playback.stop();
await playback.load([track], autoPlay: true);
} else {
await playback.addTrack(track);
}
final tracks =
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
await playback.addTracks(
tracks.toList()
..removeWhere((e) {
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
return e.id == track.id || isDuplicate;
}),
);
}
@override
Widget build(BuildContext context, ref) {
final scaffoldMessenger = ScaffoldMessenger.of(context);
@ -207,6 +273,9 @@ class TrackOptions extends HookConsumerWidget {
case TrackOptionValue.download:
await downloadManager.addToQueue(track);
break;
case TrackOptionValue.startRadio:
actionStartRadio(context, ref, track);
break;
}
},
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
@ -287,12 +356,18 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite,
),
),
if (auth != null)
if (auth != null) ...[
PopSheetEntry(
value: TrackOptionValue.startRadio,
leading: const Icon(SpotubeIcons.radio),
title: Text(context.l10n.start_a_radio),
),
PopSheetEntry(
value: TrackOptionValue.addToPlaylist,
leading: const Icon(SpotubeIcons.playlistAdd),
title: Text(context.l10n.add_to_playlist),
),
],
if (userPlaylist && auth != null)
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,

View File

@ -286,5 +286,8 @@
"genres": "Genres",
"explore_genres": "Explore Genres",
"friends": "Friends",
"no_lyrics_available": "Sorry, unable find lyrics for this track"
"no_lyrics_available": "Sorry, unable find lyrics for this track",
"start_a_radio": "Start a Radio",
"how_to_start_radio": "How do you want to start the radio?",
"replace_queue_question": "Do you want to replace the current queue or append to it?"
}

View File

@ -1,27 +1,26 @@
import 'package:fl_query/fl_query.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/hooks/spotify/use_spotify_infinite_query.dart';
import 'package:spotube/provider/spotify_provider.dart';
typedef SearchParams = ({
SpotifyApi spotify,
SearchType searchType,
String query
});
class SearchQueries {
const SearchQueries();
InfiniteQuery<List<Page>, dynamic, int> query(
WidgetRef ref,
String query,
SearchType searchType,
) {
return useSpotifyInfiniteQuery<List<Page>, dynamic, int>(
"search-query/${searchType.name}",
(page, spotify) {
if (query.trim().isEmpty) return [];
final queryString = query;
return spotify.search.get(
queryString,
types: [searchType],
).getPage(10, page);
},
enabled: false,
ref: ref,
static final queryJob =
InfiniteQueryJob.withVariableKey<List<Page>, dynamic, int, SearchParams>(
baseQueryKey: "search-query",
task: (variableKey, page, args) => args!.spotify.search.get(
args.query,
types: [args.searchType],
).getPage(10, page),
initialPage: 0,
nextPage: (lastPage, lastPageData) {
if (lastPageData.isEmpty) return null;
@ -31,6 +30,31 @@ class SearchQueries {
}
return lastPageData.first.nextOffset;
},
enabled: false,
);
InfiniteQuery<List<Page>, dynamic, int> query(
WidgetRef ref,
String queryStr,
SearchType searchType,
) {
final spotify = ref.watch(spotifyProvider);
final query = useInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
job: queryJob(searchType.name),
args: (spotify: spotify, searchType: searchType, query: queryStr),
);
useEffect(() {
return ref.listenManual(
spotifyProvider,
(previous, next) {
if (previous != next) {
query.refreshAll();
}
},
).close;
}, [query]);
return query;
}
}

View File

@ -1 +1,109 @@
{}
{
"ar": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"bn": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"ca": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"de": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"es": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"fa": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"fr": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"hi": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"it": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"ja": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"ne": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"nl": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"pl": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"pt": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"ru": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"tr": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"uk": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
],
"zh": [
"start_a_radio",
"how_to_start_radio",
"replace_queue_question"
]
}