feat: piped instance picker on settings

This commit is contained in:
Kingkor Roy Tirtho 2023-06-04 22:02:14 +06:00
parent 3aeb026776
commit bed0d3bd70
16 changed files with 142 additions and 80 deletions

View File

@ -79,4 +79,5 @@ abstract class SpotubeIcons {
static const colorSync = FeatherIcons.activity; static const colorSync = FeatherIcons.activity;
static const language = FeatherIcons.globe; static const language = FeatherIcons.globe;
static const error = FeatherIcons.alertTriangle; static const error = FeatherIcons.alertTriangle;
static const piped = FeatherIcons.cloud;
} }

View File

@ -14,6 +14,13 @@ class AdaptiveSelectTile<T> extends HookWidget {
final Breakpoints breakAfterOr; final Breakpoints breakAfterOr;
/// Show the smaller value when the breakpoint is reached
///
/// If false, the control will be hidden when the breakpoint is reached
///
/// Defaults to `true`
final bool showValueWhenUnfolded;
const AdaptiveSelectTile({ const AdaptiveSelectTile({
required this.title, required this.title,
required this.value, required this.value,
@ -23,6 +30,7 @@ class AdaptiveSelectTile<T> extends HookWidget {
this.subtitle, this.subtitle,
this.secondary, this.secondary,
this.breakAfterOr = Breakpoints.md, this.breakAfterOr = Breakpoints.md,
this.showValueWhenUnfolded = true,
super.key, super.key,
}); });
@ -49,7 +57,8 @@ class AdaptiveSelectTile<T> extends HookWidget {
final control = breakpoint >= breakAfterOr final control = breakpoint >= breakAfterOr
? rawControl ? rawControl
: Container( : showValueWhenUnfolded
? Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
@ -64,7 +73,8 @@ class AdaptiveSelectTile<T> extends HookWidget {
), ),
child: controlPlaceholder, child: controlPlaceholder,
), ),
); )
: const SizedBox.shrink();
return ListTile( return ListTile(
title: title, title: title,

View File

@ -182,5 +182,7 @@
"success_message": "এখন আপনি সফলভাবে আপনার Spotify অ্যাকাউন্ট দিয়ে লগ ইন করেছেন। সাধুভাত আপনাকে", "success_message": "এখন আপনি সফলভাবে আপনার Spotify অ্যাকাউন্ট দিয়ে লগ ইন করেছেন। সাধুভাত আপনাকে",
"step_4": "ধাপ 4", "step_4": "ধাপ 4",
"step_4_steps": "কপি করা \"sp_dc\" এবং \"sp_key\" এর মান সংশ্লিষ্ট ফিল্ডে পেস্ট করুন", "step_4_steps": "কপি করা \"sp_dc\" এবং \"sp_key\" এর মান সংশ্লিষ্ট ফিল্ডে পেস্ট করুন",
"something_went_wrong": "কিছু ভুল হয়েছে" "something_went_wrong": "কিছু ভুল হয়েছে",
"piped_instance": "Piped সার্ভার এড্রেস",
"piped_description": "গান ম্যাচ করার জন্য ব্যবহৃত পাইপড সার্ভার\n এগুলোর মধ্যে কিছু ভাল কাজ নাও করতে পারে৷ তাই নিজ দায়িত্বে ব্যবহার করুন"
} }

View File

@ -182,5 +182,7 @@
"success_message": "Now you're successfully Logged In with your Spotify account. Good Job, mate!", "success_message": "Now you're successfully Logged In with your Spotify account. Good Job, mate!",
"step_4": "Step 4", "step_4": "Step 4",
"step_4_steps": "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields", "step_4_steps": "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields",
"something_went_wrong": "Something went wrong" "something_went_wrong": "Something went wrong",
"piped_instance": "Piped Server Instance",
"piped_description": "The Piped server instance to use for track matching\nSome of them might not work well. So use at your own risk"
} }

View File

@ -182,5 +182,7 @@
"success_message": "Vous êtes maintenant connecté avec succès à votre compte Spotify. Bon travail, mon ami!", "success_message": "Vous êtes maintenant connecté avec succès à votre compte Spotify. Bon travail, mon ami!",
"step_4": "Étape 4", "step_4": "Étape 4",
"step_4_steps": "Collez les valeurs copiées de \"sp_dc\" et \"sp_key\" dans les champs respectifs", "step_4_steps": "Collez les valeurs copiées de \"sp_dc\" et \"sp_key\" dans les champs respectifs",
"something_went_wrong": "Quelque chose s'est mal passé" "something_went_wrong": "Quelque chose s'est mal passé",
"piped_instance": "Instance pipée",
"piped_description": "L'instance de serveur Piped à utiliser pour la correspondance des pistes\nCertaines d'entre elles peuvent ne pas fonctionner correctement. Alors utilisez à vos risques et périls"
} }

View File

@ -182,5 +182,7 @@
"success_message": "अब आप अपने स्पॉटिफाई अकाउंट से सफलतापूर्वक लॉगइन हो गए हैं। अच्छा काम किया!", "success_message": "अब आप अपने स्पॉटिफाई अकाउंट से सफलतापूर्वक लॉगइन हो गए हैं। अच्छा काम किया!",
"step_4": "स्टेप 4", "step_4": "स्टेप 4",
"step_4_steps": "कॉपी की गई \"sp_dc\" और \"sp_key\" मानों को संबंधित फील्ड में पेस्ट करें", "step_4_steps": "कॉपी की गई \"sp_dc\" और \"sp_key\" मानों को संबंधित फील्ड में पेस्ट करें",
"something_went_wrong": "कुछ गलत हो गया" "something_went_wrong": "कुछ गलत हो गया",
"piped_instance": "पाइप्ड सर्वर",
"piped_description": "पाइप किए गए सर्वर\n गानों का मिलान करने के लिए उपयोग किए जाते हैं, हो सकता है कि उनमें से कुछ के साथ ठीक से काम न करें इसलिए अपने जोखिम पर उपयोग करें"
} }

View File

@ -27,7 +27,6 @@ import 'package:spotube/provider/downloader_provider.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/youtube.dart';
import 'package:spotube/themes/theme.dart'; import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/custom_toast_handler.dart'; import 'package:spotube/utils/custom_toast_handler.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
@ -206,10 +205,6 @@ class SpotubeState extends ConsumerState<Spotube> {
void initState() { void initState() {
super.initState(); super.initState();
SharedPreferences.getInstance().then(((value) => localStorage = value)); SharedPreferences.getInstance().then(((value) => localStorage = value));
/// Doing the initialization here to avoid loading time
/// when in offline mode
PipedSpotube.initialize();
} }
@override @override

View File

@ -8,7 +8,6 @@ import 'package:spotube/extensions/album_simple.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/matched_track.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/youtube.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -67,7 +66,10 @@ class SpotubeTrack extends Track {
} }
} }
static Future<List<PipedSearchItemStream>> fetchSiblings(Track track) async { static Future<List<PipedSearchItemStream>> fetchSiblings(
Track track,
PipedClient client,
) async {
final artists = (track.artists ?? []) final artists = (track.artists ?? [])
.map((ar) => ar.name) .map((ar) => ar.name)
.toList() .toList()
@ -80,7 +82,7 @@ class SpotubeTrack extends Track {
onlyCleanArtist: true, onlyCleanArtist: true,
).trim(); ).trim();
final List<PipedSearchItemStream> siblings = await PipedSpotube.client final List<PipedSearchItemStream> siblings = await client
.search( .search(
"$title - ${artists.join(", ")}", "$title - ${artists.join(", ")}",
PipedFilter.musicSongs, PipedFilter.musicSongs,
@ -112,18 +114,19 @@ class SpotubeTrack extends Track {
static Future<SpotubeTrack> fetchFromTrack( static Future<SpotubeTrack> fetchFromTrack(
Track track, Track track,
UserPreferences preferences, UserPreferences preferences,
PipedClient client,
) async { ) async {
final matchedCachedTrack = await MatchedTrack.box.get(track.id!); final matchedCachedTrack = await MatchedTrack.box.get(track.id!);
var siblings = <PipedSearchItemStream>[]; var siblings = <PipedSearchItemStream>[];
PipedStreamResponse ytVideo; PipedStreamResponse ytVideo;
if (matchedCachedTrack != null) { if (matchedCachedTrack != null) {
ytVideo = await PipedSpotube.client.streams(matchedCachedTrack.youtubeId); ytVideo = await client.streams(matchedCachedTrack.youtubeId);
} else { } else {
siblings = await fetchSiblings(track); siblings = await fetchSiblings(track, client);
if (siblings.isEmpty) { if (siblings.isEmpty) {
throw Exception("Failed to find any results for ${track.name}"); throw Exception("Failed to find any results for ${track.name}");
} }
ytVideo = await PipedSpotube.client.streams(siblings.first.id); ytVideo = await client.streams(siblings.first.id);
await MatchedTrack.box.put( await MatchedTrack.box.put(
track.id!, track.id!,
@ -167,10 +170,11 @@ class SpotubeTrack extends Track {
Future<SpotubeTrack?> swappedCopy( Future<SpotubeTrack?> swappedCopy(
PipedSearchItemStream video, PipedSearchItemStream video,
UserPreferences preferences, UserPreferences preferences,
PipedClient client,
) async { ) async {
if (siblings.none((element) => element.id == video.id)) return null; if (siblings.none((element) => element.id == video.id)) return null;
final ytVideo = await PipedSpotube.client.streams(video.id); final ytVideo = await client.streams(video.id);
final ytStream = getStreamInfo(ytVideo, preferences.audioQuality); final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
@ -225,10 +229,10 @@ class SpotubeTrack extends Track {
); );
} }
Future<SpotubeTrack> populatedCopy() async { Future<SpotubeTrack> populatedCopy(PipedClient client) async {
if (this.siblings.isNotEmpty) return this; if (this.siblings.isNotEmpty) return this;
final siblings = await fetchSiblings(this); final siblings = await fetchSiblings(this, client);
return SpotubeTrack.fromTrack( return SpotubeTrack.fromTrack(
track: this, track: this,

View File

@ -1,10 +1,12 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.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';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/language_codes.dart';
@ -21,6 +23,7 @@ import 'package:spotube/l10n/l10n.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/downloader_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/provider/piped_provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class SettingsPage extends HookConsumerWidget { class SettingsPage extends HookConsumerWidget {
@ -286,6 +289,41 @@ class SettingsPage extends HookConsumerWidget {
} }
}, },
), ),
Consumer(builder: (context, ref, child) {
final instanceList =
ref.watch(pipedInstancesFutureProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary: const Icon(SpotubeIcons.piped),
title: Text(context.l10n.piped_instance),
subtitle: Text(context.l10n.piped_description),
value: preferences.pipedInstance,
showValueWhenUnfolded: false,
options: data
.sortedBy((e) => e.name)
.map(
(e) => DropdownMenuItem(
value: e.apiUrl,
child: Text(e.name),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
preferences.setPipedInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) =>
Text(error.toString()),
);
}),
SwitchListTile( SwitchListTile(
secondary: const Icon(SpotubeIcons.download), secondary: const Icon(SpotubeIcons.download),
title: Text(context.l10n.pre_download_play), title: Text(context.l10n.pre_download_play),

View File

@ -11,6 +11,7 @@ import 'package:spotify/spotify.dart' hide Image, Queue;
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/piped_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -50,6 +51,7 @@ class Downloader with ChangeNotifier {
final track = await SpotubeTrack.fetchFromTrack( final track = await SpotubeTrack.fetchFromTrack(
baseTrack, baseTrack,
ref.read(userPreferencesProvider), ref.read(userPreferencesProvider),
ref.read(pipedClientProvider),
); );
_queue.add(() async { _queue.add(() async {

View File

@ -0,0 +1,18 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
PipedClient _defaultClient = PipedClient();
final pipedClientProvider = Provider((ref) {
final instanceUrl =
ref.watch(userPreferencesProvider.select((s) => s.pipedInstance));
if (instanceUrl == "https://pipedapi.kavin.rocks") return _defaultClient;
return PipedClient(instance: instanceUrl);
});
final pipedInstancesFutureProvider = FutureProvider<List<PipedInstance>>(
(ref) async => _defaultClient.instanceList(),
);

View File

@ -1,6 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/matched_track.dart';
@ -11,7 +12,8 @@ import 'package:spotube/services/supabase.dart';
mixin NextFetcher on StateNotifier<ProxyPlaylist> { mixin NextFetcher on StateNotifier<ProxyPlaylist> {
Future<List<SpotubeTrack>> fetchTracks( Future<List<SpotubeTrack>> fetchTracks(
UserPreferences preferences, { UserPreferences preferences,
PipedClient pipedClient, {
int count = 3, int count = 3,
int offset = 0, int offset = 0,
}) async { }) async {
@ -25,7 +27,11 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
/// fetch [bareTracks] one by one with 100ms delay /// fetch [bareTracks] one by one with 100ms delay
final fetchedTracks = await Future.wait( final fetchedTracks = await Future.wait(
bareTracks.mapIndexed((i, track) async { bareTracks.mapIndexed((i, track) async {
final future = SpotubeTrack.fetchFromTrack(track, preferences); final future = SpotubeTrack.fetchFromTrack(
track,
preferences,
pipedClient,
);
if (i == 0) { if (i == 0) {
return await future; return await future;
} }

View File

@ -15,7 +15,7 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/services/audio_services/audio_services.dart';
import 'package:spotube/services/youtube.dart'; import 'package:spotube/provider/piped_provider.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -45,6 +45,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
late final AudioServices notificationService; late final AudioServices notificationService;
UserPreferences get preferences => ref.read(userPreferencesProvider); UserPreferences get preferences => ref.read(userPreferencesProvider);
PipedClient get pipedClient => ref.read(pipedClientProvider);
ProxyPlaylist get playlist => state; ProxyPlaylist get playlist => state;
BlackListNotifier get blacklist => BlackListNotifier get blacklist =>
ref.read(BlackListNotifier.provider.notifier); ref.read(BlackListNotifier.provider.notifier);
@ -157,7 +158,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
final nthFetchedTrack = switch (track.runtimeType) { final nthFetchedTrack = switch (track.runtimeType) {
SpotubeTrack => track as SpotubeTrack, SpotubeTrack => track as SpotubeTrack,
_ => await SpotubeTrack.fetchFromTrack(track, preferences), _ => await SpotubeTrack.fetchFromTrack(track, preferences, pipedClient),
}; };
await audioPlayer.replaceSource( await audioPlayer.replaceSource(
@ -220,6 +221,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
final addableTrack = await SpotubeTrack.fetchFromTrack( final addableTrack = await SpotubeTrack.fetchFromTrack(
tracks.elementAtOrNull(initialIndex) ?? tracks.first, tracks.elementAtOrNull(initialIndex) ?? tracks.first,
preferences, preferences,
pipedClient,
); );
state = state.copyWith( state = state.copyWith(
@ -307,7 +309,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
Future<void> populateSibling() async { Future<void> populateSibling() async {
if (state.activeTrack is SpotubeTrack) { if (state.activeTrack is SpotubeTrack) {
final activeTrackWithSiblingsForSure = final activeTrackWithSiblingsForSure =
await (state.activeTrack as SpotubeTrack).populatedCopy(); await (state.activeTrack as SpotubeTrack).populatedCopy(pipedClient);
state = state.copyWith( state = state.copyWith(
tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks), tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks),
@ -321,7 +323,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
if (state.activeTrack is SpotubeTrack && video is PipedSearchItemStream) { if (state.activeTrack is SpotubeTrack && video is PipedSearchItemStream) {
populateSibling(); populateSibling();
final newTrack = await (state.activeTrack as SpotubeTrack) final newTrack = await (state.activeTrack as SpotubeTrack)
.swappedCopy(video, preferences); .swappedCopy(video, preferences, pipedClient);
if (newTrack == null) return; if (newTrack == null) return;
state = state.copyWith( state = state.copyWith(
tracks: mergeTracks([newTrack], state.tracks), tracks: mergeTracks([newTrack], state.tracks),
@ -438,14 +440,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
onInit() async { onInit() async {
if (state.tracks.isEmpty) return null; if (state.tracks.isEmpty) return null;
if (await PipedSpotube.initialized) {
await load( await load(
state.tracks, state.tracks,
initialIndex: state.active ?? 0, initialIndex: state.active ?? 0,
autoPlay: false, autoPlay: false,
); );
} }
}
@override @override
FutureOr<ProxyPlaylist> fromJson(Map<String, dynamic> json) { FutureOr<ProxyPlaylist> fromJson(Map<String, dynamic> json) {

View File

@ -50,6 +50,8 @@ class UserPreferences extends PersistedChangeNotifier {
Locale locale; Locale locale;
String pipedInstance;
final Ref ref; final Ref ref;
UserPreferences( UserPreferences(
@ -67,6 +69,7 @@ class UserPreferences extends PersistedChangeNotifier {
this.closeBehavior = CloseBehavior.minimizeToTray, this.closeBehavior = CloseBehavior.minimizeToTray,
this.showSystemTrayIcon = true, this.showSystemTrayIcon = true,
this.locale = const Locale("system", "system"), this.locale = const Locale("system", "system"),
this.pipedInstance = "https://pipedapi.kavin.rocks",
}) : super() { }) : super() {
if (downloadLocation.isEmpty) { if (downloadLocation.isEmpty) {
_getDefaultDownloadDirectory().then( _getDefaultDownloadDirectory().then(
@ -161,6 +164,12 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence(); updatePersistence();
} }
void setPipedInstance(String instance) {
pipedInstance = instance;
notifyListeners();
updatePersistence();
}
Future<String> _getDefaultDownloadDirectory() async { Future<String> _getDefaultDownloadDirectory() async {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
@ -206,6 +215,8 @@ class UserPreferences extends PersistedChangeNotifier {
final localeMap = map["locale"] != null ? jsonDecode(map["locale"]) : null; final localeMap = map["locale"] != null ? jsonDecode(map["locale"]) : null;
locale = locale =
localeMap != null ? Locale(localeMap?["lc"], localeMap?["cc"]) : locale; localeMap != null ? Locale(localeMap?["lc"], localeMap?["cc"]) : locale;
pipedInstance = map["pipedInstance"] ?? pipedInstance;
} }
@override @override
@ -225,6 +236,7 @@ class UserPreferences extends PersistedChangeNotifier {
"showSystemTrayIcon": showSystemTrayIcon, "showSystemTrayIcon": showSystemTrayIcon,
"locale": "locale":
jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}), jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}),
"pipedInstance": pipedInstance,
}; };
} }
} }

View File

@ -1,32 +0,0 @@
import 'dart:async';
import 'package:piped_client/piped_client.dart';
PipedClient _defaultClient = PipedClient();
class PipedSpotube {
static final Completer<bool> _initialized = Completer();
static Future<bool> get initialized => _initialized.future;
/// Checks for a working instance of piped.video
///
/// To distribute the load, in each startup it randomizes public instances
/// and selects a working instance and uses that throughout the session
static Future<void> initialize() async {
final pipedInstances = await _defaultClient.instanceList();
pipedInstances.shuffle();
for (final instance in pipedInstances) {
final client = PipedClient(instance: instance.apiUrl);
try {
await client.streams("dQw4w9WgXcQ");
_defaultClient = client;
_initialized.complete(true);
break;
} catch (e) {
continue;
}
}
}
static PipedClient get client => _defaultClient;
}

View File

@ -10,7 +10,7 @@ ThemeData theme(Color seed, Brightness brightness) {
useMaterial3: true, useMaterial3: true,
colorScheme: scheme, colorScheme: scheme,
listTileTheme: ListTileThemeData( listTileTheme: ListTileThemeData(
horizontalTitleGap: 0, horizontalTitleGap: 5,
iconColor: scheme.onSurface, iconColor: scheme.onSurface,
), ),
appBarTheme: const AppBarTheme(surfaceTintColor: Colors.transparent), appBarTheme: const AppBarTheme(surfaceTintColor: Colors.transparent),