mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-16 09:05:16 +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 wikipedia = SimpleIcons.wikipedia;
|
||||||
static const discord = SimpleIcons.discord;
|
static const discord = SimpleIcons.discord;
|
||||||
static const youtube = SimpleIcons.youtube;
|
static const youtube = SimpleIcons.youtube;
|
||||||
|
static const radio = FeatherIcons.radio;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
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/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.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/library/user_local_tracks.dart';
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.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/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/dialogs/track_details_dialog.dart';
|
||||||
import 'package:spotube/components/shared/heart_button.dart';
|
import 'package:spotube/components/shared/heart_button.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.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/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_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/mutations/mutations.dart';
|
||||||
|
import 'package:spotube/services/queries/search.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
enum TrackOptionValue {
|
enum TrackOptionValue {
|
||||||
@ -36,6 +40,7 @@ enum TrackOptionValue {
|
|||||||
favorite,
|
favorite,
|
||||||
details,
|
details,
|
||||||
download,
|
download,
|
||||||
|
startRadio,
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrackOptions extends HookConsumerWidget {
|
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
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
@ -207,6 +273,9 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
case TrackOptionValue.download:
|
case TrackOptionValue.download:
|
||||||
await downloadManager.addToQueue(track);
|
await downloadManager.addToQueue(track);
|
||||||
break;
|
break;
|
||||||
|
case TrackOptionValue.startRadio:
|
||||||
|
actionStartRadio(context, ref, track);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
|
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
|
||||||
@ -287,12 +356,18 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
: context.l10n.save_as_favorite,
|
: 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(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.addToPlaylist,
|
value: TrackOptionValue.addToPlaylist,
|
||||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||||
title: Text(context.l10n.add_to_playlist),
|
title: Text(context.l10n.add_to_playlist),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
if (userPlaylist && auth != null)
|
if (userPlaylist && auth != null)
|
||||||
PopSheetEntry(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.removeFromPlaylist,
|
value: TrackOptionValue.removeFromPlaylist,
|
||||||
|
@ -286,5 +286,8 @@
|
|||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
"explore_genres": "Explore Genres",
|
"explore_genres": "Explore Genres",
|
||||||
"friends": "Friends",
|
"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/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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.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 {
|
class SearchQueries {
|
||||||
const 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(
|
InfiniteQuery<List<Page>, dynamic, int> query(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
String query,
|
String queryStr,
|
||||||
SearchType searchType,
|
SearchType searchType,
|
||||||
) {
|
) {
|
||||||
return useSpotifyInfiniteQuery<List<Page>, dynamic, int>(
|
final spotify = ref.watch(spotifyProvider);
|
||||||
"search-query/${searchType.name}",
|
final query = useInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||||
(page, spotify) {
|
job: queryJob(searchType.name),
|
||||||
if (query.trim().isEmpty) return [];
|
args: (spotify: spotify, searchType: searchType, query: queryStr),
|
||||||
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;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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