mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-16 00:55:17 +00:00
feat: start radio support
This commit is contained in:
parent
5d0b5e69a5
commit
4defeefe7e
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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?"
|
||||
}
|
@ -1,36 +1,60 @@
|
||||
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();
|
||||
|
||||
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;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
enabled: false,
|
||||
);
|
||||
|
||||
InfiniteQuery<List<Page>, dynamic, int> query(
|
||||
WidgetRef ref,
|
||||
String query,
|
||||
String queryStr,
|
||||
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,
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isEmpty) return null;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user