diff --git a/analysis_options.yaml b/analysis_options.yaml index 4f0718e4..5f2cbbe1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -31,4 +31,6 @@ linter: analyzer: enable-experiment: - records - - patterns \ No newline at end of file + - patterns + errors: + invalid_annotation_target: ignore diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index 515d42b5..d0d081ff 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -106,14 +106,22 @@ class TrackOptions extends HookConsumerWidget { ) ?? []; - final radios = pages.expand((e) => e.items ?? []).toList(); + final radios = pages + .expand((e) => e.items?.toList() ?? []) + .toList() + .cast(); 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, + (e) { + final validPlaylists = + artists.where((a) => e.description!.contains(a!)); + return e.name == "${track.name} Radio" && + (validPlaylists.length >= 2 || + validPlaylists.length == artists.length) && + e.owner?.displayName == "Spotify"; + }, orElse: () => radios.first, ); @@ -129,9 +137,12 @@ class TrackOptions extends HookConsumerWidget { ); } - if (replaceQueue) { + if (replaceQueue || playlist.tracks.isEmpty) { await playback.stop(); await playback.load([track], autoPlay: true); + + // we don't have to add those tracks as useEndlessPlayback will do it for us + return; } else { await playback.addTrack(track); } diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart new file mode 100644 index 00000000..ffbb6991 --- /dev/null +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -0,0 +1,102 @@ +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'; +import 'package:spotube/provider/authentication_provider.dart'; +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); + final playback = ref.watch(ProxyPlaylistNotifier.notifier); + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final spotify = ref.watch(spotifyProvider); + final endlessPlayback = + ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); + + final queryClient = useQueryClient(); + + useEffect( + () { + if (!endlessPlayback || auth == null) return null; + + void listener(int index) async { + try { + final playlist = ref.read(ProxyPlaylistNotifier.provider); + if (index != playlist.tracks.length - 1) return; + + final track = playlist.tracks.last; + + final pages = await queryClient.fetchInfiniteQueryJob, + 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?.toList() ?? []) + .toList() + .cast(); + + final artists = track.artists!.map((e) => e.name); + + final radio = radios.firstWhere( + (e) { + final validPlaylists = + artists.where((a) => e.description!.contains(a!)); + return e.name == "${track.name} Radio" && + (validPlaylists.length >= 2 || + validPlaylists.length == artists.length) && + e.owner?.displayName != "Spotify"; + }, + orElse: () => radios.first, + ); + + final tracks = + await spotify.playlists.getTracksByPlaylistId(radio.id!).all(); + + await playback.addTracks( + tracks.toList() + ..removeWhere((e) { + final playlist = ref.read(ProxyPlaylistNotifier.provider); + final isDuplicate = playlist.tracks.any((t) => t.id == e.id); + return e.id == track.id || isDuplicate; + }), + ); + } catch (e, stack) { + Catcher2.reportCheckedError(e, stack); + } + } + + // Sometimes user can change settings for which the currentIndexChanged + // might not be called. So we need to check if the current track is the + // last track and if it is then we need to call the listener manually. + if (playlist.active == playlist.tracks.length - 1 && + audioPlayer.isPlaying) { + listener(playlist.active!); + } + + final subscription = + audioPlayer.currentIndexChangedStream.listen(listener); + + return subscription.cancel; + }, + [ + spotify, + playback, + queryClient, + playlist.tracks, + endlessPlayback, + auth, + ], + ); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f79090ae..0628cc43 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -289,5 +289,6 @@ "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?" + "replace_queue_question": "Do you want to replace the current queue or append to it?", + "endless_playback": "Endless Playback" } \ No newline at end of file diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 87be587c..2ff49737 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -15,6 +15,7 @@ import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_update_checker.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; @@ -134,6 +135,8 @@ class RootApp extends HookConsumerWidget { // checks for latest version of the application useUpdateChecker(ref); + useEndlessPlayback(ref); + final backgroundColor = Theme.of(context).scaffoldBackgroundColor; useEffect(() { diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index d36e0713..bd2e33b9 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -221,6 +221,12 @@ class SettingsPlaybackSection extends HookConsumerWidget { preferencesNotifier.setDownloadMusicCodec(value); }, ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.repeat), + title: Text(context.l10n.endless_playback), + value: preferences.endlessPlayback, + onChanged: preferencesNotifier.setEndlessPlayback, + ), ], ); } diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 46569269..875f36cc 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -123,6 +123,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier { audioPlayer.setAudioNormalization(normalize); } + void setEndlessPlayback(bool endless) { + state = state.copyWith(endlessPlayback: endless); + } + Future _getDefaultDownloadDirectory() async { if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index 4244ca67..cf6c0597 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -1,12 +1,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/services/sourced_track/enums.dart'; part 'user_preferences_state.g.dart'; +part 'user_preferences_state.freezed.dart'; @JsonEnum() enum LayoutMode { @@ -53,40 +54,48 @@ enum SearchMode { } } -@JsonSerializable() -final class UserPreferences { - @JsonKey( - defaultValue: SourceQualities.high, - unknownEnumValue: SourceQualities.high, - ) - final SourceQualities audioQuality; +@freezed +class UserPreferences with _$UserPreferences { + const factory UserPreferences({ + @Default(SourceQualities.high) SourceQualities audioQuality, + @Default(true) bool albumColorSync, + @Default(false) bool amoledDarkTheme, + @Default(true) bool checkUpdate, + @Default(false) bool normalizeAudio, + @Default(true) bool showSystemTrayIcon, + @Default(false) bool skipNonMusic, + @Default(false) bool systemTitleBar, + @Default(CloseBehavior.minimizeToTray) CloseBehavior closeBehavior, + @Default(SpotubeColor(0xFF2196F3, name: "Blue")) + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue, + ) + SpotubeColor accentColorScheme, + @Default(LayoutMode.adaptive) LayoutMode layoutMode, + @Default(Locale("system", "system")) + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue, + ) + Locale locale, + @Default(Market.US) Market recommendationMarket, + @Default(SearchMode.youtube) SearchMode searchMode, + @Default("") String downloadLocation, + @Default("https://pipedapi.kavin.rocks") String pipedInstance, + @Default(ThemeMode.system) ThemeMode themeMode, + @Default(AudioSource.youtube) AudioSource audioSource, + @Default(SourceCodecs.weba) SourceCodecs streamMusicCodec, + @Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec, + @Default(true) bool discordPresence, + @Default(true) bool endlessPlayback, + }) = _UserPreferences; + factory UserPreferences.fromJson(Map json) => + _$UserPreferencesFromJson(json); - @JsonKey(defaultValue: true) - final bool albumColorSync; - - @JsonKey(defaultValue: false) - final bool amoledDarkTheme; - - @JsonKey(defaultValue: true) - final bool checkUpdate; - - @JsonKey(defaultValue: false) - final bool normalizeAudio; - - @JsonKey(defaultValue: true) - final bool showSystemTrayIcon; - - @JsonKey(defaultValue: true) - final bool skipNonMusic; - - @JsonKey(defaultValue: false) - final bool systemTitleBar; - - @JsonKey( - defaultValue: CloseBehavior.minimizeToTray, - unknownEnumValue: CloseBehavior.minimizeToTray, - ) - final CloseBehavior closeBehavior; + factory UserPreferences.withDefaults() => UserPreferences.fromJson({}); static SpotubeColor _accentColorSchemeFromJson(Map json) { return SpotubeColor.fromString(json["color"]); @@ -105,23 +114,6 @@ final class UserPreferences { return {"color": color.toString()}; } - static SpotubeColor _defaultAccentColorScheme() => - const SpotubeColor(0xFF2196F3, name: "Blue"); - - @JsonKey( - defaultValue: UserPreferences._defaultAccentColorScheme, - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue, - ) - final SpotubeColor accentColorScheme; - - @JsonKey( - defaultValue: LayoutMode.adaptive, - unknownEnumValue: LayoutMode.adaptive, - ) - final LayoutMode layoutMode; - static Locale _localeFromJson(Map json) { return Locale(json["languageCode"], json["countryCode"]); } @@ -145,144 +137,4 @@ final class UserPreferences { return json[key] as Map?; } - - static Locale _defaultLocaleValue() => const Locale("system", "system"); - - @JsonKey( - defaultValue: UserPreferences._defaultLocaleValue, - toJson: UserPreferences._localeToJson, - fromJson: UserPreferences._localeFromJson, - readValue: UserPreferences._localeReadValue, - ) - final Locale locale; - - @JsonKey( - defaultValue: Market.US, - unknownEnumValue: Market.US, - ) - final Market recommendationMarket; - - @JsonKey( - defaultValue: SearchMode.youtube, - unknownEnumValue: SearchMode.youtube, - ) - final SearchMode searchMode; - - @JsonKey(defaultValue: "") - final String downloadLocation; - - @JsonKey(defaultValue: "https://pipedapi.kavin.rocks") - final String pipedInstance; - - @JsonKey( - defaultValue: ThemeMode.system, - unknownEnumValue: ThemeMode.system, - ) - final ThemeMode themeMode; - - @JsonKey( - defaultValue: AudioSource.youtube, - unknownEnumValue: AudioSource.youtube, - ) - final AudioSource audioSource; - - @JsonKey( - defaultValue: SourceCodecs.weba, - unknownEnumValue: SourceCodecs.weba, - ) - final SourceCodecs streamMusicCodec; - - @JsonKey( - defaultValue: SourceCodecs.m4a, - unknownEnumValue: SourceCodecs.m4a, - ) - final SourceCodecs downloadMusicCodec; - - @JsonKey(defaultValue: true) - final bool discordPresence; - - UserPreferences({ - required this.audioQuality, - required this.albumColorSync, - required this.amoledDarkTheme, - required this.checkUpdate, - required this.normalizeAudio, - required this.showSystemTrayIcon, - required this.skipNonMusic, - required this.systemTitleBar, - required this.closeBehavior, - required this.accentColorScheme, - required this.layoutMode, - required this.locale, - required this.recommendationMarket, - required this.searchMode, - required this.downloadLocation, - required this.pipedInstance, - required this.themeMode, - required this.audioSource, - required this.streamMusicCodec, - required this.downloadMusicCodec, - required this.discordPresence, - }); - - factory UserPreferences.withDefaults() { - return UserPreferences.fromJson({}); - } - - factory UserPreferences.fromJson(Map json) { - return _$UserPreferencesFromJson(json); - } - - Map toJson() { - return _$UserPreferencesToJson(this); - } - - UserPreferences copyWith({ - ThemeMode? themeMode, - SpotubeColor? accentColorScheme, - bool? albumColorSync, - bool? checkUpdate, - SourceQualities? audioQuality, - String? downloadLocation, - LayoutMode? layoutMode, - CloseBehavior? closeBehavior, - bool? showSystemTrayIcon, - Locale? locale, - String? pipedInstance, - SearchMode? searchMode, - bool? skipNonMusic, - AudioSource? audioSource, - Market? recommendationMarket, - bool? saveTrackLyrics, - bool? amoledDarkTheme, - bool? normalizeAudio, - SourceCodecs? downloadMusicCodec, - SourceCodecs? streamMusicCodec, - bool? systemTitleBar, - bool? discordPresence, - }) { - return UserPreferences( - themeMode: themeMode ?? this.themeMode, - accentColorScheme: accentColorScheme ?? this.accentColorScheme, - albumColorSync: albumColorSync ?? this.albumColorSync, - checkUpdate: checkUpdate ?? this.checkUpdate, - audioQuality: audioQuality ?? this.audioQuality, - downloadLocation: downloadLocation ?? this.downloadLocation, - layoutMode: layoutMode ?? this.layoutMode, - closeBehavior: closeBehavior ?? this.closeBehavior, - showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, - locale: locale ?? this.locale, - pipedInstance: pipedInstance ?? this.pipedInstance, - searchMode: searchMode ?? this.searchMode, - skipNonMusic: skipNonMusic ?? this.skipNonMusic, - audioSource: audioSource ?? this.audioSource, - recommendationMarket: recommendationMarket ?? this.recommendationMarket, - amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, - normalizeAudio: normalizeAudio ?? this.normalizeAudio, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - systemTitleBar: systemTitleBar ?? this.systemTitleBar, - discordPresence: discordPresence ?? this.discordPresence, - ); - } } diff --git a/lib/provider/user_preferences/user_preferences_state.freezed.dart b/lib/provider/user_preferences/user_preferences_state.freezed.dart new file mode 100644 index 00000000..4d08d1a9 --- /dev/null +++ b/lib/provider/user_preferences/user_preferences_state.freezed.dart @@ -0,0 +1,697 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user_preferences_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +UserPreferences _$UserPreferencesFromJson(Map json) { + return _UserPreferences.fromJson(json); +} + +/// @nodoc +mixin _$UserPreferences { + SourceQualities get audioQuality => throw _privateConstructorUsedError; + bool get albumColorSync => throw _privateConstructorUsedError; + bool get amoledDarkTheme => throw _privateConstructorUsedError; + bool get checkUpdate => throw _privateConstructorUsedError; + bool get normalizeAudio => throw _privateConstructorUsedError; + bool get showSystemTrayIcon => throw _privateConstructorUsedError; + bool get skipNonMusic => throw _privateConstructorUsedError; + bool get systemTitleBar => throw _privateConstructorUsedError; + CloseBehavior get closeBehavior => throw _privateConstructorUsedError; + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor get accentColorScheme => throw _privateConstructorUsedError; + LayoutMode get layoutMode => throw _privateConstructorUsedError; + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale get locale => throw _privateConstructorUsedError; + Market get recommendationMarket => throw _privateConstructorUsedError; + SearchMode get searchMode => throw _privateConstructorUsedError; + String get downloadLocation => throw _privateConstructorUsedError; + String get pipedInstance => throw _privateConstructorUsedError; + ThemeMode get themeMode => throw _privateConstructorUsedError; + AudioSource get audioSource => throw _privateConstructorUsedError; + SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError; + SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError; + bool get discordPresence => throw _privateConstructorUsedError; + bool get endlessPlayback => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UserPreferencesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserPreferencesCopyWith<$Res> { + factory $UserPreferencesCopyWith( + UserPreferences value, $Res Function(UserPreferences) then) = + _$UserPreferencesCopyWithImpl<$Res, UserPreferences>; + @useResult + $Res call( + {SourceQualities audioQuality, + bool albumColorSync, + bool amoledDarkTheme, + bool checkUpdate, + bool normalizeAudio, + bool showSystemTrayIcon, + bool skipNonMusic, + bool systemTitleBar, + CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor accentColorScheme, + LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale locale, + Market recommendationMarket, + SearchMode searchMode, + String downloadLocation, + String pipedInstance, + ThemeMode themeMode, + AudioSource audioSource, + SourceCodecs streamMusicCodec, + SourceCodecs downloadMusicCodec, + bool discordPresence, + bool endlessPlayback}); +} + +/// @nodoc +class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> + implements $UserPreferencesCopyWith<$Res> { + _$UserPreferencesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? audioQuality = null, + Object? albumColorSync = null, + Object? amoledDarkTheme = null, + Object? checkUpdate = null, + Object? normalizeAudio = null, + Object? showSystemTrayIcon = null, + Object? skipNonMusic = null, + Object? systemTitleBar = null, + Object? closeBehavior = null, + Object? accentColorScheme = null, + Object? layoutMode = null, + Object? locale = null, + Object? recommendationMarket = null, + Object? searchMode = null, + Object? downloadLocation = null, + Object? pipedInstance = null, + Object? themeMode = null, + Object? audioSource = null, + Object? streamMusicCodec = null, + Object? downloadMusicCodec = null, + Object? discordPresence = null, + Object? endlessPlayback = null, + }) { + return _then(_value.copyWith( + audioQuality: null == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as SourceQualities, + albumColorSync: null == albumColorSync + ? _value.albumColorSync + : albumColorSync // ignore: cast_nullable_to_non_nullable + as bool, + amoledDarkTheme: null == amoledDarkTheme + ? _value.amoledDarkTheme + : amoledDarkTheme // ignore: cast_nullable_to_non_nullable + as bool, + checkUpdate: null == checkUpdate + ? _value.checkUpdate + : checkUpdate // ignore: cast_nullable_to_non_nullable + as bool, + normalizeAudio: null == normalizeAudio + ? _value.normalizeAudio + : normalizeAudio // ignore: cast_nullable_to_non_nullable + as bool, + showSystemTrayIcon: null == showSystemTrayIcon + ? _value.showSystemTrayIcon + : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable + as bool, + skipNonMusic: null == skipNonMusic + ? _value.skipNonMusic + : skipNonMusic // ignore: cast_nullable_to_non_nullable + as bool, + systemTitleBar: null == systemTitleBar + ? _value.systemTitleBar + : systemTitleBar // ignore: cast_nullable_to_non_nullable + as bool, + closeBehavior: null == closeBehavior + ? _value.closeBehavior + : closeBehavior // ignore: cast_nullable_to_non_nullable + as CloseBehavior, + accentColorScheme: null == accentColorScheme + ? _value.accentColorScheme + : accentColorScheme // ignore: cast_nullable_to_non_nullable + as SpotubeColor, + layoutMode: null == layoutMode + ? _value.layoutMode + : layoutMode // ignore: cast_nullable_to_non_nullable + as LayoutMode, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + recommendationMarket: null == recommendationMarket + ? _value.recommendationMarket + : recommendationMarket // ignore: cast_nullable_to_non_nullable + as Market, + searchMode: null == searchMode + ? _value.searchMode + : searchMode // ignore: cast_nullable_to_non_nullable + as SearchMode, + downloadLocation: null == downloadLocation + ? _value.downloadLocation + : downloadLocation // ignore: cast_nullable_to_non_nullable + as String, + pipedInstance: null == pipedInstance + ? _value.pipedInstance + : pipedInstance // ignore: cast_nullable_to_non_nullable + as String, + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + audioSource: null == audioSource + ? _value.audioSource + : audioSource // ignore: cast_nullable_to_non_nullable + as AudioSource, + streamMusicCodec: null == streamMusicCodec + ? _value.streamMusicCodec + : streamMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + downloadMusicCodec: null == downloadMusicCodec + ? _value.downloadMusicCodec + : downloadMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + discordPresence: null == discordPresence + ? _value.discordPresence + : discordPresence // ignore: cast_nullable_to_non_nullable + as bool, + endlessPlayback: null == endlessPlayback + ? _value.endlessPlayback + : endlessPlayback // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserPreferencesImplCopyWith<$Res> + implements $UserPreferencesCopyWith<$Res> { + factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value, + $Res Function(_$UserPreferencesImpl) then) = + __$$UserPreferencesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SourceQualities audioQuality, + bool albumColorSync, + bool amoledDarkTheme, + bool checkUpdate, + bool normalizeAudio, + bool showSystemTrayIcon, + bool skipNonMusic, + bool systemTitleBar, + CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor accentColorScheme, + LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale locale, + Market recommendationMarket, + SearchMode searchMode, + String downloadLocation, + String pipedInstance, + ThemeMode themeMode, + AudioSource audioSource, + SourceCodecs streamMusicCodec, + SourceCodecs downloadMusicCodec, + bool discordPresence, + bool endlessPlayback}); +} + +/// @nodoc +class __$$UserPreferencesImplCopyWithImpl<$Res> + extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl> + implements _$$UserPreferencesImplCopyWith<$Res> { + __$$UserPreferencesImplCopyWithImpl( + _$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? audioQuality = null, + Object? albumColorSync = null, + Object? amoledDarkTheme = null, + Object? checkUpdate = null, + Object? normalizeAudio = null, + Object? showSystemTrayIcon = null, + Object? skipNonMusic = null, + Object? systemTitleBar = null, + Object? closeBehavior = null, + Object? accentColorScheme = null, + Object? layoutMode = null, + Object? locale = null, + Object? recommendationMarket = null, + Object? searchMode = null, + Object? downloadLocation = null, + Object? pipedInstance = null, + Object? themeMode = null, + Object? audioSource = null, + Object? streamMusicCodec = null, + Object? downloadMusicCodec = null, + Object? discordPresence = null, + Object? endlessPlayback = null, + }) { + return _then(_$UserPreferencesImpl( + audioQuality: null == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as SourceQualities, + albumColorSync: null == albumColorSync + ? _value.albumColorSync + : albumColorSync // ignore: cast_nullable_to_non_nullable + as bool, + amoledDarkTheme: null == amoledDarkTheme + ? _value.amoledDarkTheme + : amoledDarkTheme // ignore: cast_nullable_to_non_nullable + as bool, + checkUpdate: null == checkUpdate + ? _value.checkUpdate + : checkUpdate // ignore: cast_nullable_to_non_nullable + as bool, + normalizeAudio: null == normalizeAudio + ? _value.normalizeAudio + : normalizeAudio // ignore: cast_nullable_to_non_nullable + as bool, + showSystemTrayIcon: null == showSystemTrayIcon + ? _value.showSystemTrayIcon + : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable + as bool, + skipNonMusic: null == skipNonMusic + ? _value.skipNonMusic + : skipNonMusic // ignore: cast_nullable_to_non_nullable + as bool, + systemTitleBar: null == systemTitleBar + ? _value.systemTitleBar + : systemTitleBar // ignore: cast_nullable_to_non_nullable + as bool, + closeBehavior: null == closeBehavior + ? _value.closeBehavior + : closeBehavior // ignore: cast_nullable_to_non_nullable + as CloseBehavior, + accentColorScheme: null == accentColorScheme + ? _value.accentColorScheme + : accentColorScheme // ignore: cast_nullable_to_non_nullable + as SpotubeColor, + layoutMode: null == layoutMode + ? _value.layoutMode + : layoutMode // ignore: cast_nullable_to_non_nullable + as LayoutMode, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + recommendationMarket: null == recommendationMarket + ? _value.recommendationMarket + : recommendationMarket // ignore: cast_nullable_to_non_nullable + as Market, + searchMode: null == searchMode + ? _value.searchMode + : searchMode // ignore: cast_nullable_to_non_nullable + as SearchMode, + downloadLocation: null == downloadLocation + ? _value.downloadLocation + : downloadLocation // ignore: cast_nullable_to_non_nullable + as String, + pipedInstance: null == pipedInstance + ? _value.pipedInstance + : pipedInstance // ignore: cast_nullable_to_non_nullable + as String, + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + audioSource: null == audioSource + ? _value.audioSource + : audioSource // ignore: cast_nullable_to_non_nullable + as AudioSource, + streamMusicCodec: null == streamMusicCodec + ? _value.streamMusicCodec + : streamMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + downloadMusicCodec: null == downloadMusicCodec + ? _value.downloadMusicCodec + : downloadMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + discordPresence: null == discordPresence + ? _value.discordPresence + : discordPresence // ignore: cast_nullable_to_non_nullable + as bool, + endlessPlayback: null == endlessPlayback + ? _value.endlessPlayback + : endlessPlayback // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserPreferencesImpl implements _UserPreferences { + const _$UserPreferencesImpl( + {this.audioQuality = SourceQualities.high, + this.albumColorSync = true, + this.amoledDarkTheme = false, + this.checkUpdate = true, + this.normalizeAudio = false, + this.showSystemTrayIcon = true, + this.skipNonMusic = false, + this.systemTitleBar = false, + this.closeBehavior = CloseBehavior.minimizeToTray, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"), + this.layoutMode = LayoutMode.adaptive, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + this.locale = const Locale("system", "system"), + this.recommendationMarket = Market.US, + this.searchMode = SearchMode.youtube, + this.downloadLocation = "", + this.pipedInstance = "https://pipedapi.kavin.rocks", + this.themeMode = ThemeMode.system, + this.audioSource = AudioSource.youtube, + this.streamMusicCodec = SourceCodecs.weba, + this.downloadMusicCodec = SourceCodecs.m4a, + this.discordPresence = true, + this.endlessPlayback = true}); + + factory _$UserPreferencesImpl.fromJson(Map json) => + _$$UserPreferencesImplFromJson(json); + + @override + @JsonKey() + final SourceQualities audioQuality; + @override + @JsonKey() + final bool albumColorSync; + @override + @JsonKey() + final bool amoledDarkTheme; + @override + @JsonKey() + final bool checkUpdate; + @override + @JsonKey() + final bool normalizeAudio; + @override + @JsonKey() + final bool showSystemTrayIcon; + @override + @JsonKey() + final bool skipNonMusic; + @override + @JsonKey() + final bool systemTitleBar; + @override + @JsonKey() + final CloseBehavior closeBehavior; + @override + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + final SpotubeColor accentColorScheme; + @override + @JsonKey() + final LayoutMode layoutMode; + @override + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + final Locale locale; + @override + @JsonKey() + final Market recommendationMarket; + @override + @JsonKey() + final SearchMode searchMode; + @override + @JsonKey() + final String downloadLocation; + @override + @JsonKey() + final String pipedInstance; + @override + @JsonKey() + final ThemeMode themeMode; + @override + @JsonKey() + final AudioSource audioSource; + @override + @JsonKey() + final SourceCodecs streamMusicCodec; + @override + @JsonKey() + final SourceCodecs downloadMusicCodec; + @override + @JsonKey() + final bool discordPresence; + @override + @JsonKey() + final bool endlessPlayback; + + @override + String toString() { + return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserPreferencesImpl && + (identical(other.audioQuality, audioQuality) || + other.audioQuality == audioQuality) && + (identical(other.albumColorSync, albumColorSync) || + other.albumColorSync == albumColorSync) && + (identical(other.amoledDarkTheme, amoledDarkTheme) || + other.amoledDarkTheme == amoledDarkTheme) && + (identical(other.checkUpdate, checkUpdate) || + other.checkUpdate == checkUpdate) && + (identical(other.normalizeAudio, normalizeAudio) || + other.normalizeAudio == normalizeAudio) && + (identical(other.showSystemTrayIcon, showSystemTrayIcon) || + other.showSystemTrayIcon == showSystemTrayIcon) && + (identical(other.skipNonMusic, skipNonMusic) || + other.skipNonMusic == skipNonMusic) && + (identical(other.systemTitleBar, systemTitleBar) || + other.systemTitleBar == systemTitleBar) && + (identical(other.closeBehavior, closeBehavior) || + other.closeBehavior == closeBehavior) && + (identical(other.accentColorScheme, accentColorScheme) || + other.accentColorScheme == accentColorScheme) && + (identical(other.layoutMode, layoutMode) || + other.layoutMode == layoutMode) && + (identical(other.locale, locale) || other.locale == locale) && + (identical(other.recommendationMarket, recommendationMarket) || + other.recommendationMarket == recommendationMarket) && + (identical(other.searchMode, searchMode) || + other.searchMode == searchMode) && + (identical(other.downloadLocation, downloadLocation) || + other.downloadLocation == downloadLocation) && + (identical(other.pipedInstance, pipedInstance) || + other.pipedInstance == pipedInstance) && + (identical(other.themeMode, themeMode) || + other.themeMode == themeMode) && + (identical(other.audioSource, audioSource) || + other.audioSource == audioSource) && + (identical(other.streamMusicCodec, streamMusicCodec) || + other.streamMusicCodec == streamMusicCodec) && + (identical(other.downloadMusicCodec, downloadMusicCodec) || + other.downloadMusicCodec == downloadMusicCodec) && + (identical(other.discordPresence, discordPresence) || + other.discordPresence == discordPresence) && + (identical(other.endlessPlayback, endlessPlayback) || + other.endlessPlayback == endlessPlayback)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hashAll([ + runtimeType, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + skipNonMusic, + systemTitleBar, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + recommendationMarket, + searchMode, + downloadLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback + ]); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => + __$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$UserPreferencesImplToJson( + this, + ); + } +} + +abstract class _UserPreferences implements UserPreferences { + const factory _UserPreferences( + {final SourceQualities audioQuality, + final bool albumColorSync, + final bool amoledDarkTheme, + final bool checkUpdate, + final bool normalizeAudio, + final bool showSystemTrayIcon, + final bool skipNonMusic, + final bool systemTitleBar, + final CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + final SpotubeColor accentColorScheme, + final LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + final Locale locale, + final Market recommendationMarket, + final SearchMode searchMode, + final String downloadLocation, + final String pipedInstance, + final ThemeMode themeMode, + final AudioSource audioSource, + final SourceCodecs streamMusicCodec, + final SourceCodecs downloadMusicCodec, + final bool discordPresence, + final bool endlessPlayback}) = _$UserPreferencesImpl; + + factory _UserPreferences.fromJson(Map json) = + _$UserPreferencesImpl.fromJson; + + @override + SourceQualities get audioQuality; + @override + bool get albumColorSync; + @override + bool get amoledDarkTheme; + @override + bool get checkUpdate; + @override + bool get normalizeAudio; + @override + bool get showSystemTrayIcon; + @override + bool get skipNonMusic; + @override + bool get systemTitleBar; + @override + CloseBehavior get closeBehavior; + @override + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor get accentColorScheme; + @override + LayoutMode get layoutMode; + @override + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale get locale; + @override + Market get recommendationMarket; + @override + SearchMode get searchMode; + @override + String get downloadLocation; + @override + String get pipedInstance; + @override + ThemeMode get themeMode; + @override + AudioSource get audioSource; + @override + SourceCodecs get streamMusicCodec; + @override + SourceCodecs get downloadMusicCodec; + @override + bool get discordPresence; + @override + bool get endlessPlayback; + @override + @JsonKey(ignore: true) + _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index 59043601..ce488247 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -6,67 +6,63 @@ part of 'user_preferences_state.dart'; // JsonSerializableGenerator // ************************************************************************** -UserPreferences _$UserPreferencesFromJson(Map json) => - UserPreferences( - audioQuality: $enumDecodeNullable( - _$SourceQualitiesEnumMap, json['audioQuality'], - unknownValue: SourceQualities.high) ?? - SourceQualities.high, +_$UserPreferencesImpl _$$UserPreferencesImplFromJson( + Map json) => + _$UserPreferencesImpl( + audioQuality: + $enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ?? + SourceQualities.high, albumColorSync: json['albumColorSync'] as bool? ?? true, amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, checkUpdate: json['checkUpdate'] as bool? ?? true, normalizeAudio: json['normalizeAudio'] as bool? ?? false, showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? true, - skipNonMusic: json['skipNonMusic'] as bool? ?? true, + skipNonMusic: json['skipNonMusic'] as bool? ?? false, systemTitleBar: json['systemTitleBar'] as bool? ?? false, - closeBehavior: $enumDecodeNullable( - _$CloseBehaviorEnumMap, json['closeBehavior'], - unknownValue: CloseBehavior.minimizeToTray) ?? - CloseBehavior.minimizeToTray, + closeBehavior: + $enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ?? + CloseBehavior.minimizeToTray, accentColorScheme: UserPreferences._accentColorSchemeReadValue( json, 'accentColorScheme') == null - ? UserPreferences._defaultAccentColorScheme() + ? const SpotubeColor(0xFF2196F3, name: "Blue") : UserPreferences._accentColorSchemeFromJson( UserPreferences._accentColorSchemeReadValue( json, 'accentColorScheme') as Map), - layoutMode: $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode'], - unknownValue: LayoutMode.adaptive) ?? - LayoutMode.adaptive, + layoutMode: + $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ?? + LayoutMode.adaptive, locale: UserPreferences._localeReadValue(json, 'locale') == null - ? UserPreferences._defaultLocaleValue() + ? const Locale("system", "system") : UserPreferences._localeFromJson( UserPreferences._localeReadValue(json, 'locale') as Map), - recommendationMarket: $enumDecodeNullable( - _$MarketEnumMap, json['recommendationMarket'], - unknownValue: Market.US) ?? - Market.US, - searchMode: $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode'], - unknownValue: SearchMode.youtube) ?? - SearchMode.youtube, - downloadLocation: json['downloadLocation'] as String? ?? '', + recommendationMarket: + $enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ?? + Market.US, + searchMode: + $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ?? + SearchMode.youtube, + downloadLocation: json['downloadLocation'] as String? ?? "", pipedInstance: - json['pipedInstance'] as String? ?? 'https://pipedapi.kavin.rocks', - themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'], - unknownValue: ThemeMode.system) ?? + json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks", + themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? ThemeMode.system, - audioSource: $enumDecodeNullable( - _$AudioSourceEnumMap, json['audioSource'], - unknownValue: AudioSource.youtube) ?? - AudioSource.youtube, + audioSource: + $enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ?? + AudioSource.youtube, streamMusicCodec: $enumDecodeNullable( - _$SourceCodecsEnumMap, json['streamMusicCodec'], - unknownValue: SourceCodecs.weba) ?? + _$SourceCodecsEnumMap, json['streamMusicCodec']) ?? SourceCodecs.weba, downloadMusicCodec: $enumDecodeNullable( - _$SourceCodecsEnumMap, json['downloadMusicCodec'], - unknownValue: SourceCodecs.m4a) ?? + _$SourceCodecsEnumMap, json['downloadMusicCodec']) ?? SourceCodecs.m4a, discordPresence: json['discordPresence'] as bool? ?? true, + endlessPlayback: json['endlessPlayback'] as bool? ?? true, ); -Map _$UserPreferencesToJson(UserPreferences instance) => +Map _$$UserPreferencesImplToJson( + _$UserPreferencesImpl instance) => { 'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!, 'albumColorSync': instance.albumColorSync, @@ -90,6 +86,7 @@ Map _$UserPreferencesToJson(UserPreferences instance) => 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, 'discordPresence': instance.discordPresence, + 'endlessPlayback': instance.endlessPlayback, }; const _$SourceQualitiesEnumMap = { diff --git a/pubspec.lock b/pubspec.lock index 1a95f20e..0d1b2993 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -957,8 +957,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + url: "https://pub.dev" + source: hosted + version: "2.4.6" freezed_annotation: - dependency: transitive + dependency: "direct main" description: name: freezed_annotation sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d diff --git a/pubspec.yaml b/pubspec.yaml index 1b3d15b0..d3fb5630 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -122,6 +122,7 @@ dependencies: app_links: ^3.5.0 win32_registry: ^1.1.2 flutter_sharing_intent: ^1.1.0 + freezed_annotation: ^2.4.1 dev_dependencies: build_runner: ^2.3.2 @@ -138,6 +139,7 @@ dev_dependencies: json_serializable: ^6.6.2 pub_api_client: ^2.4.0 pubspec_parse: ^1.2.2 + freezed: ^2.4.6 dependency_overrides: http: ^1.1.0 diff --git a/untranslated_messages.json b/untranslated_messages.json index 748a9906..4240c8c0 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -2,108 +2,126 @@ "ar": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "bn": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "ca": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "de": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "es": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "fa": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "fr": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "hi": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "it": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "ja": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "ne": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "nl": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "pl": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "pt": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "ru": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "tr": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "uk": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ], "zh": [ "start_a_radio", "how_to_start_radio", - "replace_queue_question" + "replace_queue_question", + "endless_playback" ] }