mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-11 01:17:30 +00:00
Merge branch 'dev' into master
This commit is contained in:
commit
44ebada3bf
@ -520,10 +520,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Pashto, Pushto",
|
// name: "Pashto, Pushto",
|
||||||
// nativeName: "پښتو",
|
// nativeName: "پښتو",
|
||||||
// ),
|
// ),
|
||||||
// "pt": const ISOLanguageName(
|
"pt": const ISOLanguageName(
|
||||||
// name: "Portuguese",
|
name: "Portuguese",
|
||||||
// nativeName: "Português",
|
nativeName: "Português",
|
||||||
// ),
|
),
|
||||||
// "qu": const ISOLanguageName(
|
// "qu": const ISOLanguageName(
|
||||||
// name: "Quechua",
|
// name: "Quechua",
|
||||||
// nativeName: "Runa Simi, Kichwa",
|
// nativeName: "Runa Simi, Kichwa",
|
||||||
|
|||||||
@ -128,8 +128,8 @@ final router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/mini-player",
|
path: "/mini-player",
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: MiniLyricsPage(),
|
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@ -93,4 +93,5 @@ abstract class SpotubeIcons {
|
|||||||
static const skip = FeatherIcons.fastForward;
|
static const skip = FeatherIcons.fastForward;
|
||||||
static const noWifi = FeatherIcons.wifiOff;
|
static const noWifi = FeatherIcons.wifiOff;
|
||||||
static const wifi = FeatherIcons.wifi;
|
static const wifi = FeatherIcons.wifi;
|
||||||
|
static const window = Icons.window_rounded;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,8 +50,13 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
() => playlist.containsCollection(album.id!),
|
() => playlist.containsCollection(album.id!),
|
||||||
[playlist, album.id],
|
[playlist, album.id],
|
||||||
);
|
);
|
||||||
final int marginH =
|
|
||||||
useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
final marginH = useBreakpointValue<int>(
|
||||||
|
xs: 10,
|
||||||
|
sm: 10,
|
||||||
|
md: 15,
|
||||||
|
others: 20,
|
||||||
|
);
|
||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
|||||||
@ -3,13 +3,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/album/album_card.dart';
|
import 'package:spotube/components/album/album_card.dart';
|
||||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||||
|
import 'package:spotube/components/shared/waypoint.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
@ -23,76 +24,86 @@ class UserAlbums extends HookConsumerWidget {
|
|||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
final albumsQuery = useQueries.album.ofMine(ref);
|
final albumsQuery = useQueries.album.ofMine(ref);
|
||||||
|
|
||||||
final spacing = useBreakpointValue<double>(
|
final controller = useScrollController();
|
||||||
xs: 0,
|
|
||||||
sm: 0,
|
|
||||||
others: 20,
|
|
||||||
);
|
|
||||||
|
|
||||||
final searchText = useState('');
|
final searchText = useState('');
|
||||||
|
|
||||||
|
final allAlbums = useMemoized(
|
||||||
|
() => albumsQuery.pages
|
||||||
|
.expand((element) => element.items ?? <AlbumSimple>[]),
|
||||||
|
[albumsQuery.pages],
|
||||||
|
);
|
||||||
|
|
||||||
final albums = useMemoized(() {
|
final albums = useMemoized(() {
|
||||||
if (searchText.value.isEmpty) {
|
if (searchText.value.isEmpty) {
|
||||||
return albumsQuery.data?.toList() ?? [];
|
return allAlbums;
|
||||||
}
|
}
|
||||||
return albumsQuery.data
|
return allAlbums
|
||||||
?.map((e) => (
|
.map((e) => (
|
||||||
weightedRatio(e.name!, searchText.value),
|
weightedRatio(e.name!, searchText.value),
|
||||||
e,
|
e,
|
||||||
))
|
))
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||||
.where((e) => e.$1 > 50)
|
.where((e) => e.$1 > 50)
|
||||||
.map((e) => e.$2)
|
.map((e) => e.$2)
|
||||||
.toList() ??
|
.toList();
|
||||||
[];
|
}, [allAlbums, searchText.value]);
|
||||||
}, [albumsQuery.data, searchText.value]);
|
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth == null) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await albumsQuery.refresh();
|
await albumsQuery.refresh();
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SafeArea(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
child: Scaffold(
|
||||||
child: Padding(
|
appBar: PreferredSize(
|
||||||
padding: const EdgeInsets.all(8.0),
|
preferredSize: const Size.fromHeight(50),
|
||||||
child: SafeArea(
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: ColoredBox(
|
||||||
children: [
|
color: theme.scaffoldBackgroundColor,
|
||||||
SearchBar(
|
child: SearchBar(
|
||||||
onChanged: (value) => searchText.value = value,
|
onChanged: (value) => searchText.value = value,
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
hintText: context.l10n.filter_albums,
|
hintText: context.l10n.filter_artist,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
AnimatedCrossFade(
|
),
|
||||||
duration: const Duration(milliseconds: 300),
|
),
|
||||||
firstChild: Container(
|
body: SizedBox.expand(
|
||||||
alignment: Alignment.topLeft,
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: const ShimmerPlaybuttonCard(count: 7),
|
controller: controller,
|
||||||
),
|
child: Wrap(
|
||||||
secondChild: Wrap(
|
runSpacing: 20,
|
||||||
spacing: spacing, // gap between adjacent chips
|
alignment: WrapAlignment.center,
|
||||||
runSpacing: 20, // gap between lines
|
runAlignment: WrapAlignment.center,
|
||||||
alignment: WrapAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: albums
|
children: [
|
||||||
.map((album) => AlbumCard(
|
if (albums.isEmpty)
|
||||||
TypeConversionUtils.simpleAlbum_X_Album(album),
|
Container(
|
||||||
))
|
alignment: Alignment.topLeft,
|
||||||
.toList(),
|
padding: const EdgeInsets.all(16.0),
|
||||||
),
|
child: const ShimmerPlaybuttonCard(count: 4),
|
||||||
crossFadeState: albumsQuery.isLoading ||
|
),
|
||||||
!albumsQuery.hasData ||
|
for (final album in albums)
|
||||||
searchText.value.isNotEmpty
|
AlbumCard(
|
||||||
? CrossFadeState.showFirst
|
TypeConversionUtils.simpleAlbum_X_Album(album),
|
||||||
: CrossFadeState.showSecond,
|
),
|
||||||
),
|
if (albumsQuery.hasNextPage)
|
||||||
],
|
Waypoint(
|
||||||
|
controller: controller,
|
||||||
|
isGrid: true,
|
||||||
|
onTouchEdge: albumsQuery.fetchNext,
|
||||||
|
child: const ShimmerPlaybuttonCard(count: 1),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -92,6 +92,8 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
tooltip: context.l10n.mini_player,
|
tooltip: context.l10n.mini_player,
|
||||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
final prevSize =
|
||||||
|
await DesktopTools.window.getSize();
|
||||||
await DesktopTools.window.setMinimumSize(
|
await DesktopTools.window.setMinimumSize(
|
||||||
const Size(300, 300),
|
const Size(300, 300),
|
||||||
);
|
);
|
||||||
@ -106,7 +108,10 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
await Future.delayed(
|
await Future.delayed(
|
||||||
const Duration(milliseconds: 100),
|
const Duration(milliseconds: 100),
|
||||||
() async {
|
() async {
|
||||||
GoRouter.of(context).go('/mini-player');
|
GoRouter.of(context).go(
|
||||||
|
'/mini-player',
|
||||||
|
extra: prevSize,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
class ConfirmDownloadDialog extends StatelessWidget {
|
class ConfirmDownloadDialog extends StatelessWidget {
|
||||||
@ -24,8 +25,9 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: Padding(
|
content: Container(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
|
constraints: BoxConstraints(maxWidth: Breakpoints.sm),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -87,7 +89,7 @@ class BulletPoint extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text("●"),
|
const Text("\u2022"),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Flexible(child: Text(text)),
|
Flexible(child: Text(text)),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
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/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -188,6 +189,7 @@ class AlbumHeartButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final client = useQueryClient();
|
||||||
final me = useQueries.user.me(ref);
|
final me = useQueries.user.me(ref);
|
||||||
|
|
||||||
final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
|
final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
|
||||||
@ -196,10 +198,10 @@ class AlbumHeartButton extends HookConsumerWidget {
|
|||||||
final toggleAlbumLike = useMutations.album.toggleFavorite(
|
final toggleAlbumLike = useMutations.album.toggleFavorite(
|
||||||
ref,
|
ref,
|
||||||
album.id!,
|
album.id!,
|
||||||
refreshQueries: [
|
refreshQueries: [albumIsSaved.key],
|
||||||
albumIsSaved.key,
|
onData: (_, __) async {
|
||||||
"current-user-albums",
|
await client.refreshInfiniteQueryAllPages("current-user-albums");
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (me.isLoading || !me.hasData) {
|
if (me.isLoading || !me.hasData) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ final closeNotification = DesktopTools.createNotification(
|
|||||||
windowManager.close();
|
windowManager.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
class PageWindowTitleBar extends StatefulHookWidget
|
class PageWindowTitleBar extends StatefulHookConsumerWidget
|
||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final bool automaticallyImplyLeading;
|
final bool automaticallyImplyLeading;
|
||||||
@ -60,23 +60,23 @@ class PageWindowTitleBar extends StatefulHookWidget
|
|||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PageWindowTitleBar> createState() => _PageWindowTitleBarState();
|
ConsumerState<PageWindowTitleBar> createState() => _PageWindowTitleBarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PageWindowTitleBarState extends State<PageWindowTitleBar> {
|
class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
||||||
|
void onDrag(details) {
|
||||||
|
final systemTitleBar =
|
||||||
|
ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
|
||||||
|
if (kIsDesktop && !systemTitleBar) {
|
||||||
|
DesktopTools.window.startDragging();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onHorizontalDragStart: (details) {
|
onHorizontalDragStart: onDrag,
|
||||||
if (kIsDesktop) {
|
onVerticalDragStart: onDrag,
|
||||||
windowManager.startDragging();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onVerticalDragStart: (details) {
|
|
||||||
if (kIsDesktop) {
|
|
||||||
windowManager.startDragging();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
leading: widget.leading,
|
leading: widget.leading,
|
||||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||||
@ -108,13 +108,12 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final closeBehavior =
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.closeBehavior));
|
|
||||||
final isMaximized = useState<bool?>(null);
|
final isMaximized = useState<bool?>(null);
|
||||||
const type = ThemeType.auto;
|
const type = ThemeType.auto;
|
||||||
|
|
||||||
Future<void> onClose() async {
|
Future<void> onClose() async {
|
||||||
if (closeBehavior == CloseBehavior.close) {
|
if (preferences.closeBehavior == CloseBehavior.close) {
|
||||||
await windowManager.close();
|
await windowManager.close();
|
||||||
} else {
|
} else {
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
@ -131,7 +130,7 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!kIsDesktop || kIsMacOS) {
|
if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_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/user_preferences_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
final trackCollectionSortState =
|
final trackCollectionSortState =
|
||||||
@ -55,7 +56,9 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
TextStyle tableHeadStyle =
|
final apiType =
|
||||||
|
ref.watch(userPreferencesProvider.select((s) => s.youtubeApiType));
|
||||||
|
final tableHeadStyle =
|
||||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||||
|
|
||||||
final selected = useState<List<String>>([]);
|
final selected = useState<List<String>>([]);
|
||||||
@ -188,12 +191,13 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case "download":
|
case "download":
|
||||||
{
|
{
|
||||||
final confirmed = await showDialog(
|
final confirmed = apiType == YoutubeApiType.piped ||
|
||||||
context: context,
|
await showDialog(
|
||||||
builder: (context) {
|
context: context,
|
||||||
return const ConfirmDownloadDialog();
|
builder: (context) {
|
||||||
},
|
return const ConfirmDownloadDialog();
|
||||||
);
|
},
|
||||||
|
);
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
await downloader
|
await downloader
|
||||||
.batchAddToQueue(selectedTracks.toList());
|
.batchAddToQueue(selectedTracks.toList());
|
||||||
|
|||||||
@ -258,5 +258,6 @@
|
|||||||
"piped_api_down": "Piped API is down",
|
"piped_api_down": "Piped API is down",
|
||||||
"piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change",
|
"piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change",
|
||||||
"you_are_offline": "You are currently offline",
|
"you_are_offline": "You are currently offline",
|
||||||
"connection_restored": "Your internet connection was restored"
|
"connection_restored": "Your internet connection was restored",
|
||||||
|
"use_system_title_bar": "Use system title bar"
|
||||||
}
|
}
|
||||||
260
lib/l10n/app_pt.arb
Normal file
260
lib/l10n/app_pt.arb
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
{
|
||||||
|
"guest": "Visitante",
|
||||||
|
"browse": "Explorar",
|
||||||
|
"search": "Buscar",
|
||||||
|
"library": "Biblioteca",
|
||||||
|
"lyrics": "Letras",
|
||||||
|
"settings": "Configurações",
|
||||||
|
"genre_categories_filter": "Filtrar categorias ou gêneros...",
|
||||||
|
"genre": "Gênero",
|
||||||
|
"personalized": "Personalizado",
|
||||||
|
"featured": "Destaque",
|
||||||
|
"new_releases": "Novos Lançamentos",
|
||||||
|
"songs": "Músicas",
|
||||||
|
"playing_track": "Tocando {track}",
|
||||||
|
"queue_clear_alert": "Isso irá limpar a fila atual. {track_length} músicas serão removidas.\nDeseja continuar?",
|
||||||
|
"load_more": "Carregar mais",
|
||||||
|
"playlists": "Playlists",
|
||||||
|
"artists": "Artistas",
|
||||||
|
"albums": "Álbuns",
|
||||||
|
"tracks": "Faixas",
|
||||||
|
"downloads": "Downloads",
|
||||||
|
"filter_playlists": "Filtrar suas playlists...",
|
||||||
|
"liked_tracks": "Músicas Curtidas",
|
||||||
|
"liked_tracks_description": "Todas as suas músicas curtidas",
|
||||||
|
"create_playlist": "Criar Playlist",
|
||||||
|
"create_a_playlist": "Criar uma playlist",
|
||||||
|
"create": "Criar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"playlist_name": "Nome da Playlist",
|
||||||
|
"name_of_playlist": "Nome da playlist",
|
||||||
|
"description": "Descrição",
|
||||||
|
"public": "Pública",
|
||||||
|
"collaborative": "Colaborativa",
|
||||||
|
"search_local_tracks": "Buscar músicas locais...",
|
||||||
|
"play": "Reproduzir",
|
||||||
|
"delete": "Excluir",
|
||||||
|
"none": "Nenhum",
|
||||||
|
"sort_a_z": "Ordenar de A-Z",
|
||||||
|
"sort_z_a": "Ordenar de Z-A",
|
||||||
|
"sort_artist": "Ordenar por Artista",
|
||||||
|
"sort_album": "Ordenar por Álbum",
|
||||||
|
"sort_tracks": "Ordenar Faixas",
|
||||||
|
"currently_downloading": "Baixando no momento ({tracks_length})",
|
||||||
|
"cancel_all": "Cancelar Tudo",
|
||||||
|
"filter_artist": "Filtrar artistas...",
|
||||||
|
"followers": "{followers} Seguidores",
|
||||||
|
"add_artist_to_blacklist": "Adicionar artista à lista negra",
|
||||||
|
"top_tracks": "Principais Músicas",
|
||||||
|
"fans_also_like": "Fãs também curtiram",
|
||||||
|
"loading": "Carregando...",
|
||||||
|
"artist": "Artista",
|
||||||
|
"blacklisted": "Na Lista Negra",
|
||||||
|
"following": "Seguindo",
|
||||||
|
"follow": "Seguir",
|
||||||
|
"artist_url_copied": "URL do artista copiada para a área de transferência",
|
||||||
|
"added_to_queue": "Adicionadas {tracks} músicas à fila",
|
||||||
|
"filter_albums": "Filtrar álbuns...",
|
||||||
|
"synced": "Sincronizado",
|
||||||
|
"plain": "Simples",
|
||||||
|
"shuffle": "Aleatório",
|
||||||
|
"search_tracks": "Buscar músicas...",
|
||||||
|
"released": "Lançado",
|
||||||
|
"error": "Erro {error}",
|
||||||
|
"title": "Título",
|
||||||
|
"time": "Tempo",
|
||||||
|
"more_actions": "Mais ações",
|
||||||
|
"download_count": "Baixar ({count})",
|
||||||
|
"add_count_to_playlist": "Adicionar ({count}) à Playlist",
|
||||||
|
"add_count_to_queue": "Adicionar ({count}) à Fila",
|
||||||
|
"play_count_next": "Reproduzir ({count}) em seguida",
|
||||||
|
"album": "Álbum",
|
||||||
|
"copied_to_clipboard": "{data} copiado para a área de transferência",
|
||||||
|
"add_to_following_playlists": "Adicionar {track} às Playlists Seguintes",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"added_track_to_queue": "Adicionada {track} à fila",
|
||||||
|
"add_to_queue": "Adicionar à fila",
|
||||||
|
"track_will_play_next": "{track} será reproduzida em seguida",
|
||||||
|
"play_next": "Reproduzir em seguida",
|
||||||
|
"removed_track_from_queue": "{track} removida da fila",
|
||||||
|
"remove_from_queue": "Remover da fila",
|
||||||
|
"remove_from_favorites": "Remover dos favoritos",
|
||||||
|
"save_as_favorite": "Salvar como favorita",
|
||||||
|
"add_to_playlist": "Adicionar à playlist",
|
||||||
|
"remove_from_playlist": "Remover da playlist",
|
||||||
|
"add_to_blacklist": "Adicionar à lista negra",
|
||||||
|
"remove_from_blacklist": "Remover da lista negra",
|
||||||
|
"share": "Compartilhar",
|
||||||
|
"mini_player": "Mini Player",
|
||||||
|
"slide_to_seek": "Arraste para avançar ou retroceder",
|
||||||
|
"shuffle_playlist": "Embaralhar playlist",
|
||||||
|
"unshuffle_playlist": "Desembaralhar playlist",
|
||||||
|
"previous_track": "Faixa anterior",
|
||||||
|
"next_track": "Próxima faixa",
|
||||||
|
"pause_playback": "Pausar Reprodução",
|
||||||
|
"resume_playback": "Continuar Reprodução",
|
||||||
|
"loop_track": "Repetir faixa",
|
||||||
|
"repeat_playlist": "Repetir playlist",
|
||||||
|
"queue": "Fila",
|
||||||
|
"alternative_track_sources": "Fontes alternativas de faixas",
|
||||||
|
"download_track": "Baixar faixa",
|
||||||
|
"tracks_in_queue": "{tracks} músicas na fila",
|
||||||
|
"clear_all": "Limpar tudo",
|
||||||
|
"show_hide_ui_on_hover": "Mostrar/Ocultar UI ao passar o mouse",
|
||||||
|
"always_on_top": "Sempre no topo",
|
||||||
|
"exit_mini_player": "Sair do Mini player",
|
||||||
|
"download_location": "Local de download",
|
||||||
|
"account": "Conta",
|
||||||
|
"login_with_spotify": "Fazer login com sua conta do Spotify",
|
||||||
|
"connect_with_spotify": "Conectar ao Spotify",
|
||||||
|
"logout": "Sair",
|
||||||
|
"logout_of_this_account": "Sair desta conta",
|
||||||
|
"language_region": "Idioma e Região",
|
||||||
|
"language": "Idioma",
|
||||||
|
"system_default": "Padrão do Sistema",
|
||||||
|
"market_place_region": "Região da Loja",
|
||||||
|
"recommendation_country": "País de Recomendação",
|
||||||
|
"appearance": "Aparência",
|
||||||
|
"layout_mode": "Modo de Layout",
|
||||||
|
"override_layout_settings": "Substituir configurações do modo de layout responsivo",
|
||||||
|
"adaptive": "Adaptável",
|
||||||
|
"compact": "Compacto",
|
||||||
|
"extended": "Estendido",
|
||||||
|
"theme": "Tema",
|
||||||
|
"dark": "Escuro",
|
||||||
|
"light": "Claro",
|
||||||
|
"system": "Sistema",
|
||||||
|
"accent_color": "Cor de Destaque",
|
||||||
|
"sync_album_color": "Sincronizar cor do álbum",
|
||||||
|
"sync_album_color_description": "Usa a cor predominante da capa do álbum como cor de destaque",
|
||||||
|
"playback": "Reprodução",
|
||||||
|
"audio_quality": "Qualidade do Áudio",
|
||||||
|
"high": "Alta",
|
||||||
|
"low": "Baixa",
|
||||||
|
"pre_download_play": "Pré-download e reprodução",
|
||||||
|
"pre_download_play_description": "Em vez de transmitir áudio, baixar bytes e reproduzir (recomendado para usuários com maior largura de banda)",
|
||||||
|
"skip_non_music": "Pular segmentos não musicais (SponsorBlock)",
|
||||||
|
"blacklist_description": "Faixas e artistas na lista negra",
|
||||||
|
"wait_for_download_to_finish": "Aguarde o download atual ser concluído",
|
||||||
|
"download_lyrics": "Baixar letras junto com as faixas",
|
||||||
|
"desktop": "Desktop",
|
||||||
|
"close_behavior": "Comportamento de Fechamento",
|
||||||
|
"close": "Fechar",
|
||||||
|
"minimize_to_tray": "Minimizar para a bandeja",
|
||||||
|
"show_tray_icon": "Mostrar ícone na bandeja do sistema",
|
||||||
|
"about": "Sobre",
|
||||||
|
"u_love_spotube": "Sabemos que você adora o Spotube",
|
||||||
|
"check_for_updates": "Verificar atualizações",
|
||||||
|
"about_spotube": "Sobre o Spotube",
|
||||||
|
"blacklist": "Lista Negra",
|
||||||
|
"please_sponsor": "Por favor, patrocine/doe",
|
||||||
|
"spotube_description": "Spotube, um cliente leve, multiplataforma e gratuito para o Spotify",
|
||||||
|
"version": "Versão",
|
||||||
|
"build_number": "Número de Build",
|
||||||
|
"founder": "Fundador",
|
||||||
|
"repository": "Repositório",
|
||||||
|
"bug_issues": "Bugs/Problemas",
|
||||||
|
"made_with": "Feito com ❤️ em Bangladesh🇧🇩",
|
||||||
|
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||||
|
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||||
|
"license": "Licença",
|
||||||
|
"add_spotify_credentials": "Adicione suas credenciais do Spotify para começar",
|
||||||
|
"credentials_will_not_be_shared_disclaimer": "Não se preocupe, suas credenciais não serão coletadas nem compartilhadas com ninguém",
|
||||||
|
"know_how_to_login": "Não sabe como fazer isso?",
|
||||||
|
"follow_step_by_step_guide": "Siga o guia passo a passo",
|
||||||
|
"spotify_cookie": "Cookie do Spotify {name}",
|
||||||
|
"cookie_name_cookie": "Cookie {name}",
|
||||||
|
"fill_in_all_fields": "Preencha todos os campos, por favor",
|
||||||
|
"submit": "Enviar",
|
||||||
|
"exit": "Sair",
|
||||||
|
"previous": "Anterior",
|
||||||
|
"next": "Próximo",
|
||||||
|
"done": "Concluído",
|
||||||
|
"step_1": "Passo 1",
|
||||||
|
"first_go_to": "Primeiro, vá para",
|
||||||
|
"login_if_not_logged_in": "e faça login/cadastro se ainda não estiver logado",
|
||||||
|
"step_2": "Passo 2",
|
||||||
|
"step_2_steps": "1. Uma vez logado, pressione F12 ou clique com o botão direito do mouse > Inspecionar para abrir as ferramentas de desenvolvimento do navegador.\n2. Em seguida, vá para a guia \"Aplicativo\" (Chrome, Edge, Brave, etc.) ou \"Armazenamento\" (Firefox, Palemoon, etc.)\n3. Acesse a seção \"Cookies\" e depois a subseção \"https://accounts.spotify.com\"",
|
||||||
|
"step_3": "Passo 3",
|
||||||
|
"step_3_steps": "Copie os valores dos Cookies \"sp_dc\" e \"sp_key\" (ou sp_gaid)",
|
||||||
|
"success_emoji": "Sucesso🥳",
|
||||||
|
"success_message": "Agora você está logado com sucesso em sua conta do Spotify. Bom trabalho!",
|
||||||
|
"step_4": "Passo 4",
|
||||||
|
"step_4_steps": "Cole os valores copiados \"sp_dc\" e \"sp_key\" (ou sp_gaid) nos campos correspondentes",
|
||||||
|
"something_went_wrong": "Algo deu errado",
|
||||||
|
"piped_instance": "Instância do Servidor Piped",
|
||||||
|
"piped_description": "A instância do servidor Piped a ser usada para correspondência de faixas",
|
||||||
|
"piped_warning": "Algumas delas podem não funcionar bem. Use por sua conta e risco",
|
||||||
|
"generate_playlist": "Gerar Playlist",
|
||||||
|
"track_exists": "A faixa {track} já existe",
|
||||||
|
"replace_downloaded_tracks": "Substituir todas as faixas baixadas",
|
||||||
|
"skip_download_tracks": "Pular o download de todas as faixas baixadas",
|
||||||
|
"do_you_want_to_replace": "Deseja substituir a faixa existente?",
|
||||||
|
"replace": "Substituir",
|
||||||
|
"skip": "Pular",
|
||||||
|
"select_up_to_count_type": "Selecione até {count} {type}",
|
||||||
|
"select_genres": "Selecionar Gêneros",
|
||||||
|
"add_genres": "Adicionar Gêneros",
|
||||||
|
"country": "País",
|
||||||
|
"number_of_tracks_generate": "Número de faixas a gerar",
|
||||||
|
"acousticness": "Acústica",
|
||||||
|
"danceability": "Dançabilidade",
|
||||||
|
"energy": "Energia",
|
||||||
|
"instrumentalness": "Instrumentalidade",
|
||||||
|
"liveness": "Vivacidade",
|
||||||
|
"loudness": "Volume",
|
||||||
|
"speechiness": "Discurso",
|
||||||
|
"valence": "Valência",
|
||||||
|
"popularity": "Popularidade",
|
||||||
|
"key": "Tonalidade",
|
||||||
|
"duration": "Duração (s)",
|
||||||
|
"tempo": "Tempo (BPM)",
|
||||||
|
"mode": "Modo",
|
||||||
|
"time_signature": "Assinatura de tempo",
|
||||||
|
"short": "Curto",
|
||||||
|
"medium": "Médio",
|
||||||
|
"long": "Longo",
|
||||||
|
"min": "Min",
|
||||||
|
"max": "Máx",
|
||||||
|
"target": "Alvo",
|
||||||
|
"moderate": "Moderado",
|
||||||
|
"deselect_all": "Desmarcar Todos",
|
||||||
|
"select_all": "Selecionar Todos",
|
||||||
|
"are_you_sure": "Tem certeza?",
|
||||||
|
"generating_playlist": "Gerando sua playlist personalizada...",
|
||||||
|
"selected_count_tracks": "{count} faixas selecionadas",
|
||||||
|
"download_warning": "Se você baixar todas as faixas em massa, estará claramente pirateando música e causando danos à sociedade criativa da música. Espero que você esteja ciente disso. Sempre tente respeitar e apoiar o trabalho árduo dos artistas",
|
||||||
|
"download_ip_ban_warning": "Além disso, seu IP pode ser bloqueado no YouTube devido a solicitações de download excessivas. O bloqueio de IP significa que você não poderá usar o YouTube (mesmo se estiver conectado) por pelo menos 2-3 meses a partir do dispositivo IP. E o Spotube não se responsabiliza se isso acontecer",
|
||||||
|
"by_clicking_accept_terms": "Ao clicar em 'aceitar', você concorda com os seguintes termos:",
|
||||||
|
"download_agreement_1": "Eu sei que estou pirateando música. Sou mau",
|
||||||
|
"download_agreement_2": "Vou apoiar o artista onde puder e estou fazendo isso porque não tenho dinheiro para comprar sua arte",
|
||||||
|
"download_agreement_3": "Estou completamente ciente de que meu IP pode ser bloqueado no YouTube e não responsabilizo o Spotube ou seus proprietários/colaboradores por quaisquer acidentes causados pela minha ação atual",
|
||||||
|
"decline": "Recusar",
|
||||||
|
"accept": "Aceitar",
|
||||||
|
"details": "Detalhes",
|
||||||
|
"youtube": "YouTube",
|
||||||
|
"channel": "Canal",
|
||||||
|
"likes": "Curtidas",
|
||||||
|
"dislikes": "Descurtidas",
|
||||||
|
"views": "Visualizações",
|
||||||
|
"streamUrl": "URL do Stream",
|
||||||
|
"stop": "Parar",
|
||||||
|
"sort_newest": "Ordenar por mais recente adicionado",
|
||||||
|
"sort_oldest": "Ordenar por mais antigo adicionado",
|
||||||
|
"sleep_timer": "Temporizador de Sono",
|
||||||
|
"mins": "{minutes} Minutos",
|
||||||
|
"hours": "{hours} Horas",
|
||||||
|
"hour": "{hours} Hora",
|
||||||
|
"custom_hours": "Horas Personalizadas",
|
||||||
|
"logs": "Registros",
|
||||||
|
"developers": "Desenvolvedores",
|
||||||
|
"not_logged_in": "Você não está logado",
|
||||||
|
"search_mode": "Modo de Busca",
|
||||||
|
"youtube_api_type": "Tipo de API",
|
||||||
|
"ok": "Ok",
|
||||||
|
"failed_to_encrypt": "Falha ao criptografar",
|
||||||
|
"encryption_failed_warning": "O Spotube usa criptografia para armazenar seus dados com segurança, mas falhou em fazê-lo. Portanto, ele voltará para o armazenamento não seguro.\nSe você estiver usando o Linux, certifique-se de ter algum serviço secreto (gnome-keyring, kde-wallet, keepassxc, etc.) instalado",
|
||||||
|
"querying_info": "Consultando informações...",
|
||||||
|
"piped_api_down": "A API do Piped está fora do ar",
|
||||||
|
"piped_down_error_instructions": "A instância do Piped {pipedInstance} está fora do ar no momento\n\nMude a instância ou altere o 'Tipo de API' para a API oficial do YouTube\n\nCertifique-se de reiniciar o aplicativo após a alteração"
|
||||||
|
}
|
||||||
@ -20,5 +20,6 @@ class L10n {
|
|||||||
const Locale('zh', 'CN'),
|
const Locale('zh', 'CN'),
|
||||||
const Locale('pl', 'PL'),
|
const Locale('pl', 'PL'),
|
||||||
const Locale('ru', 'RU'),
|
const Locale('ru', 'RU'),
|
||||||
|
const Locale('pt', 'PT'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,12 @@ final officialMusicRegex = RegExp(
|
|||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class TrackNotFoundException implements Exception {
|
||||||
|
factory TrackNotFoundException(Track track) {
|
||||||
|
throw Exception("Failed to find any results for ${track.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SpotubeTrack extends Track {
|
class SpotubeTrack extends Track {
|
||||||
final YoutubeVideoInfo ytTrack;
|
final YoutubeVideoInfo ytTrack;
|
||||||
final String ytUri;
|
final String ytUri;
|
||||||
@ -157,7 +163,7 @@ class SpotubeTrack extends Track {
|
|||||||
} else {
|
} else {
|
||||||
siblings = await fetchSiblings(track, client);
|
siblings = await fetchSiblings(track, client);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw Exception("Failed to find any results for ${track.name}");
|
throw TrackNotFoundException(track);
|
||||||
}
|
}
|
||||||
(ytVideo, ytStreamUrl) =
|
(ytVideo, ytStreamUrl) =
|
||||||
await client.video(siblings.first.id, siblings.first.searchMode);
|
await client.video(siblings.first.id, siblings.first.searchMode);
|
||||||
|
|||||||
@ -19,13 +19,13 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class MiniLyricsPage extends HookConsumerWidget {
|
class MiniLyricsPage extends HookConsumerWidget {
|
||||||
const MiniLyricsPage({Key? key}) : super(key: key);
|
final Size prevSize;
|
||||||
|
const MiniLyricsPage({Key? key, required this.prevSize}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final update = useForceUpdate();
|
final update = useForceUpdate();
|
||||||
final prevSize = useRef<Size?>(null);
|
|
||||||
final wasMaximized = useRef<bool>(false);
|
final wasMaximized = useRef<bool>(false);
|
||||||
|
|
||||||
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
@ -35,7 +35,6 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
prevSize.value = await DesktopTools.window.getSize();
|
|
||||||
wasMaximized.value = await DesktopTools.window.isMaximized();
|
wasMaximized.value = await DesktopTools.window.isMaximized();
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@ -213,7 +212,7 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
if (wasMaximized.value) {
|
if (wasMaximized.value) {
|
||||||
await DesktopTools.window.maximize();
|
await DesktopTools.window.maximize();
|
||||||
} else {
|
} else {
|
||||||
await DesktopTools.window.setSize(prevSize.value!);
|
await DesktopTools.window.setSize(prevSize);
|
||||||
}
|
}
|
||||||
await DesktopTools.window
|
await DesktopTools.window
|
||||||
.setAlignment(Alignment.center);
|
.setAlignment(Alignment.center);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_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:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Offset;
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/lyrics/zoom_controls.dart';
|
import 'package:spotube/components/lyrics/zoom_controls.dart';
|
||||||
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
||||||
@ -11,9 +11,11 @@ import 'package:spotube/hooks/use_auto_scroll_controller.dart';
|
|||||||
import 'package:spotube/hooks/use_synced_lyrics.dart';
|
import 'package:spotube/hooks/use_synced_lyrics.dart';
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:stroke_text/stroke_text.dart';
|
||||||
|
|
||||||
final _delay = StateProvider<int>((ref) => 0);
|
final _delay = StateProvider<int>((ref) => 0);
|
||||||
|
|
||||||
@ -114,9 +116,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
? Container(
|
? Container(
|
||||||
padding: index == lyricValue.lyrics.length - 1
|
padding: index == lyricValue.lyrics.length - 1
|
||||||
? EdgeInsets.only(
|
? EdgeInsets.only(
|
||||||
bottom:
|
bottom: mediaQuery.size.height / 2,
|
||||||
MediaQuery.of(context).size.height /
|
|
||||||
2,
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
)
|
)
|
||||||
@ -130,19 +130,40 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
child: AnimatedDefaultTextStyle(
|
child: AnimatedDefaultTextStyle(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isActive
|
|
||||||
? Colors.white
|
|
||||||
: palette.bodyTextColor,
|
|
||||||
fontWeight: isActive
|
fontWeight: isActive
|
||||||
? FontWeight.w500
|
? FontWeight.w500
|
||||||
: FontWeight.normal,
|
: FontWeight.normal,
|
||||||
fontSize: (isActive ? 28 : 26) *
|
fontSize: (isActive ? 28 : 26) *
|
||||||
(textZoomLevel.value / 100),
|
(textZoomLevel.value / 100),
|
||||||
shadows: kElevationToShadow[9],
|
|
||||||
),
|
),
|
||||||
child: Text(
|
textAlign: TextAlign.center,
|
||||||
lyricSlice.text,
|
child: InkWell(
|
||||||
textAlign: TextAlign.center,
|
onTap: () async {
|
||||||
|
final duration =
|
||||||
|
await audioPlayer.duration ??
|
||||||
|
Duration.zero;
|
||||||
|
final time = Duration(
|
||||||
|
seconds:
|
||||||
|
lyricSlice.time.inSeconds - delay,
|
||||||
|
);
|
||||||
|
if (time > duration || time.isNegative) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioPlayer.seek(time);
|
||||||
|
},
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
return StrokeText(
|
||||||
|
text: lyricSlice.text,
|
||||||
|
textStyle:
|
||||||
|
DefaultTextStyle.of(context).style,
|
||||||
|
textColor: isActive
|
||||||
|
? Colors.white
|
||||||
|
: palette.bodyTextColor,
|
||||||
|
strokeColor: isActive
|
||||||
|
? Colors.black
|
||||||
|
: Colors.transparent,
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -336,15 +336,17 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: context
|
text: context
|
||||||
.l10n.piped_description),
|
.l10n.piped_description,
|
||||||
|
style:
|
||||||
|
theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
const TextSpan(text: "\n"),
|
const TextSpan(text: "\n"),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text:
|
text:
|
||||||
context.l10n.piped_warning,
|
context.l10n.piped_warning,
|
||||||
style: Theme.of(context)
|
style:
|
||||||
.textTheme
|
theme.textTheme.labelMedium,
|
||||||
.labelMedium,
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -496,6 +498,12 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
value: preferences.showSystemTrayIcon,
|
value: preferences.showSystemTrayIcon,
|
||||||
onChanged: preferences.setShowSystemTrayIcon,
|
onChanged: preferences.setShowSystemTrayIcon,
|
||||||
),
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(SpotubeIcons.window),
|
||||||
|
title: Text(context.l10n.use_system_title_bar),
|
||||||
|
value: preferences.systemTitleBar,
|
||||||
|
onChanged: preferences.setSystemTitleBar,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:spotube/provider/user_preferences_provider.dart';
|
|||||||
import 'package:spotube/provider/youtube_provider.dart';
|
import 'package:spotube/provider/youtube_provider.dart';
|
||||||
import 'package:spotube/services/download_manager/download_manager.dart';
|
import 'package:spotube/services/download_manager/download_manager.dart';
|
||||||
import 'package:spotube/services/youtube/youtube.dart';
|
import 'package:spotube/services/youtube/youtube.dart';
|
||||||
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class DownloadManagerProvider extends ChangeNotifier {
|
class DownloadManagerProvider extends ChangeNotifier {
|
||||||
@ -58,7 +59,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
album: track.album?.name,
|
album: track.album?.name,
|
||||||
albumArtist: track.artists?.map((a) => a.name).join(", "),
|
albumArtist: track.artists?.map((a) => a.name).join(", "),
|
||||||
year: track.album?.releaseDate != null
|
year: track.album?.releaseDate != null
|
||||||
? int.tryParse(track.album!.releaseDate!) ?? 1969
|
? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969
|
||||||
: 1969,
|
: 1969,
|
||||||
trackNumber: track.trackNumber,
|
trackNumber: track.trackNumber,
|
||||||
discNumber: track.discNumber,
|
discNumber: track.discNumber,
|
||||||
@ -85,7 +86,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
final Ref<DownloadManagerProvider> ref;
|
final Ref<DownloadManagerProvider> ref;
|
||||||
|
|
||||||
YoutubeEndpoints get yt => ref.read(downloadYoutubeProvider);
|
YoutubeEndpoints get yt => ref.read(youtubeProvider);
|
||||||
String get downloadDirectory =>
|
String get downloadDirectory =>
|
||||||
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
String getTrackFileUrl(Track track) {
|
String getTrackFileUrl(Track track) {
|
||||||
final name =
|
final name =
|
||||||
"${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? <Artist>[])}.m4a";
|
"${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? <Artist>[])}.m4a";
|
||||||
return join(downloadDirectory, name);
|
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isActive(Track track) {
|
bool isActive(Track track) {
|
||||||
|
|||||||
@ -86,21 +86,22 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
|||||||
} else if (track is LocalTrack) {
|
} else if (track is LocalTrack) {
|
||||||
return track.path;
|
return track.path;
|
||||||
} else {
|
} else {
|
||||||
return "https://youtube.com/unplayable.m4a?id=${track.id}&title=${track.name?.replaceAll(
|
return trackToUnplayableSource(track);
|
||||||
RegExp(r'\s+', caseSensitive: false),
|
|
||||||
'-',
|
|
||||||
)}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String trackToUnplayableSource(Track track) {
|
||||||
|
return "https://youtube.com/unplayable.m4a?id=${track.id}&title=${Uri.encodeComponent(track.name!)}";
|
||||||
|
}
|
||||||
|
|
||||||
List<Track> mapSourcesToTracks(List<String> sources) {
|
List<Track> mapSourcesToTracks(List<String> sources) {
|
||||||
return sources
|
return sources
|
||||||
.map((source) {
|
.map((source) {
|
||||||
final track = state.tracks.firstWhereOrNull(
|
final track = state.tracks.firstWhereOrNull(
|
||||||
(track) {
|
(track) =>
|
||||||
final newSource = makeAppropriateSource(track);
|
trackToUnplayableSource(track) == source ||
|
||||||
return newSource == source;
|
(track is SpotubeTrack && track.ytUri == source) ||
|
||||||
},
|
(track is LocalTrack && track.path == source),
|
||||||
);
|
);
|
||||||
return track;
|
return track;
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:catcher/catcher.dart';
|
import 'package:catcher/catcher.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
@ -68,7 +70,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
() async {
|
() async {
|
||||||
notificationService = await AudioServices.create(ref, this);
|
notificationService = await AudioServices.create(ref, this);
|
||||||
|
|
||||||
({String source, List<SkipSegment> segments})? currentSegments;
|
// listeners state
|
||||||
|
final currentSegments =
|
||||||
|
// using source as unique id because alternative track source support
|
||||||
|
ObjectRef<({String source, List<SkipSegment> segments})?>(null);
|
||||||
|
final isPreSearching = ObjectRef(false);
|
||||||
|
final isFetchingSegments = ObjectRef(false);
|
||||||
|
|
||||||
audioPlayer.activeSourceChangedStream.listen((newActiveSource) async {
|
audioPlayer.activeSourceChangedStream.listen((newActiveSource) async {
|
||||||
try {
|
try {
|
||||||
@ -112,19 +119,18 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bool isPreSearching = false;
|
|
||||||
|
|
||||||
listenTo2Percent(int percent) async {
|
listenTo2Percent(int percent) async {
|
||||||
if (isPreSearching ||
|
if (isPreSearching.value ||
|
||||||
audioPlayer.currentSource == null ||
|
audioPlayer.currentSource == null ||
|
||||||
audioPlayer.nextSource == null ||
|
audioPlayer.nextSource == null ||
|
||||||
isPlayable(audioPlayer.nextSource!)) return;
|
isPlayable(audioPlayer.nextSource!)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isPreSearching = true;
|
isPreSearching.value = true;
|
||||||
|
|
||||||
final oldTrack =
|
final oldTrack =
|
||||||
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||||
|
|
||||||
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
||||||
|
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
@ -138,51 +144,64 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
|
// Removing tracks that were not found to avoid queue interruption
|
||||||
|
// TODO: Add a flag to enable/disable skip not found tracks
|
||||||
|
if (e is TrackNotFoundException) {
|
||||||
|
final oldTrack =
|
||||||
|
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||||
|
await removeTrack(oldTrack!.id!);
|
||||||
|
}
|
||||||
Catcher.reportCheckedError(e, stackTrace);
|
Catcher.reportCheckedError(e, stackTrace);
|
||||||
} finally {
|
} finally {
|
||||||
isPreSearching = false;
|
isPreSearching.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audioPlayer.percentCompletedStream(2).listen(listenTo2Percent);
|
audioPlayer.percentCompletedStream(2).listen(listenTo2Percent);
|
||||||
|
|
||||||
bool isFetchingSegments = false;
|
|
||||||
|
|
||||||
audioPlayer.positionStream.listen((position) async {
|
audioPlayer.positionStream.listen((position) async {
|
||||||
|
if (state.activeTrack == null || state.activeTrack is LocalTrack) {
|
||||||
|
isFetchingSegments.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (state.activeTrack == null || state.activeTrack is LocalTrack) {
|
final isYTMusicMode =
|
||||||
isFetchingSegments = false;
|
preferences.youtubeApiType == YoutubeApiType.piped &&
|
||||||
return;
|
preferences.searchMode == SearchMode.youtubeMusic;
|
||||||
}
|
|
||||||
// skipping in very first second breaks stream
|
|
||||||
if ((preferences.youtubeApiType == YoutubeApiType.piped &&
|
|
||||||
preferences.searchMode == SearchMode.youtubeMusic) ||
|
|
||||||
!preferences.skipNonMusic) return;
|
|
||||||
|
|
||||||
final notSameSegmentId =
|
if (isYTMusicMode || !preferences.skipNonMusic) return;
|
||||||
currentSegments?.source != audioPlayer.currentSource;
|
|
||||||
|
|
||||||
if (currentSegments == null ||
|
final isNotSameSegmentId =
|
||||||
(notSameSegmentId && !isFetchingSegments)) {
|
currentSegments.value?.source != audioPlayer.currentSource;
|
||||||
isFetchingSegments = true;
|
|
||||||
|
if (currentSegments.value == null ||
|
||||||
|
(isNotSameSegmentId && !isFetchingSegments.value)) {
|
||||||
|
isFetchingSegments.value = true;
|
||||||
try {
|
try {
|
||||||
currentSegments = (
|
currentSegments.value = (
|
||||||
source: audioPlayer.currentSource!,
|
source: audioPlayer.currentSource!,
|
||||||
segments: await getAndCacheSkipSegments(
|
segments: await getAndCacheSkipSegments(
|
||||||
(state.activeTrack as SpotubeTrack).ytTrack.id,
|
(state.activeTrack as SpotubeTrack).ytTrack.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
currentSegments.value = (
|
||||||
|
source: audioPlayer.currentSource!,
|
||||||
|
segments: [],
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingSegments = false;
|
isFetchingSegments.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final (source: _, :segments) = currentSegments!;
|
final (source: _, :segments) = currentSegments.value!;
|
||||||
|
|
||||||
|
// skipping in first 2 second breaks stream
|
||||||
if (segments.isEmpty || position < const Duration(seconds: 3)) return;
|
if (segments.isEmpty || position < const Duration(seconds: 3)) return;
|
||||||
|
|
||||||
for (final segment in segments) {
|
for (final segment in segments) {
|
||||||
if ((position.inSeconds >= segment.start &&
|
if (position.inSeconds >= segment.start &&
|
||||||
position.inSeconds < segment.end)) {
|
position.inSeconds < segment.end) {
|
||||||
await audioPlayer.seek(Duration(seconds: segment.end));
|
await audioPlayer.seek(Duration(seconds: segment.end));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,15 +338,18 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
Future<void> jumpTo(int index) async {
|
Future<void> jumpTo(int index) async {
|
||||||
final oldTrack =
|
final oldTrack =
|
||||||
mapSourcesToTracks([audioPlayer.currentSource!]).firstOrNull;
|
mapSourcesToTracks([audioPlayer.currentSource!]).firstOrNull;
|
||||||
|
|
||||||
state = state.copyWith(active: index);
|
state = state.copyWith(active: index);
|
||||||
await audioPlayer.pause();
|
await audioPlayer.pause();
|
||||||
final track = await ensureSourcePlayable(audioPlayer.sources[index]);
|
final track = await ensureSourcePlayable(audioPlayer.sources[index]);
|
||||||
|
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
tracks: mergeTracks([track], state.tracks),
|
tracks: mergeTracks([track], state.tracks),
|
||||||
active: index,
|
active: index,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await audioPlayer.jumpTo(index);
|
await audioPlayer.jumpTo(index);
|
||||||
|
|
||||||
if (oldTrack != null || track != null) {
|
if (oldTrack != null || track != null) {
|
||||||
@ -419,13 +441,16 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (audioPlayer.nextSource == null) return;
|
if (audioPlayer.nextSource == null) return;
|
||||||
final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
active: state.tracks
|
active: state.tracks
|
||||||
.toList()
|
.toList()
|
||||||
.indexWhere((element) => element.id == oldTrack?.id),
|
.indexWhere((element) => element.id == oldTrack?.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
await audioPlayer.pause();
|
await audioPlayer.pause();
|
||||||
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
||||||
|
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
tracks: mergeTracks([track], state.tracks),
|
tracks: mergeTracks([track], state.tracks),
|
||||||
@ -515,7 +540,13 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
final cached = await SkipSegment.box.get(id);
|
final cached = await SkipSegment.box.get(id);
|
||||||
if (cached != null && cached.isNotEmpty) {
|
if (cached != null && cached.isNotEmpty) {
|
||||||
return List.castFrom<dynamic, SkipSegment>(
|
return List.castFrom<dynamic, SkipSegment>(
|
||||||
(cached as List).map((json) => SkipSegment.fromJson(json)).toList(),
|
(cached as List)
|
||||||
|
.map(
|
||||||
|
(json) => SkipSegment.fromJson(
|
||||||
|
Map.castFrom<dynamic, dynamic, String, dynamic>(json),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,7 +618,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
final oldCollections = state.collections;
|
final oldCollections = state.collections;
|
||||||
await load(
|
await load(
|
||||||
state.tracks,
|
state.tracks,
|
||||||
initialIndex: state.active ?? 0,
|
initialIndex: max(state.active ?? 0, 0),
|
||||||
autoPlay: false,
|
autoPlay: false,
|
||||||
);
|
);
|
||||||
state = state.copyWith(collections: oldCollections);
|
state = state.copyWith(collections: oldCollections);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||||
@ -65,6 +66,8 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
|
|
||||||
YoutubeApiType youtubeApiType;
|
YoutubeApiType youtubeApiType;
|
||||||
|
|
||||||
|
bool systemTitleBar;
|
||||||
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
UserPreferences(
|
UserPreferences(
|
||||||
@ -85,6 +88,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
this.searchMode = SearchMode.youtube,
|
this.searchMode = SearchMode.youtube,
|
||||||
this.skipNonMusic = true,
|
this.skipNonMusic = true,
|
||||||
this.youtubeApiType = YoutubeApiType.youtube,
|
this.youtubeApiType = YoutubeApiType.youtube,
|
||||||
|
this.systemTitleBar = false,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
if (downloadLocation.isEmpty && !kIsWeb) {
|
if (downloadLocation.isEmpty && !kIsWeb) {
|
||||||
_getDefaultDownloadDirectory().then(
|
_getDefaultDownloadDirectory().then(
|
||||||
@ -197,6 +201,15 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
updatePersistence();
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setSystemTitleBar(bool isSystemTitleBar) {
|
||||||
|
systemTitleBar = isSystemTitleBar;
|
||||||
|
DesktopTools.window.setTitleBarStyle(
|
||||||
|
systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
|
||||||
|
);
|
||||||
|
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";
|
||||||
|
|
||||||
@ -257,6 +270,10 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
(type) => type.name == map["youtubeApiType"],
|
(type) => type.name == map["youtubeApiType"],
|
||||||
orElse: () => YoutubeApiType.youtube,
|
orElse: () => YoutubeApiType.youtube,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
systemTitleBar = map["systemTitleBar"] ?? systemTitleBar;
|
||||||
|
// updates the title bar
|
||||||
|
setSystemTitleBar(systemTitleBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -279,6 +296,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
"searchMode": searchMode.name,
|
"searchMode": searchMode.name,
|
||||||
"skipNonMusic": skipNonMusic,
|
"skipNonMusic": skipNonMusic,
|
||||||
"youtubeApiType": youtubeApiType.name,
|
"youtubeApiType": youtubeApiType.name,
|
||||||
|
'systemTitleBar': systemTitleBar,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,3 @@ final youtubeProvider = Provider<YoutubeEndpoints>((ref) {
|
|||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
return YoutubeEndpoints(preferences);
|
return YoutubeEndpoints(preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
// this provider overrides the API provider to use piped.video for downloading
|
|
||||||
final downloadYoutubeProvider = Provider<YoutubeEndpoints>((ref) {
|
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
|
||||||
return YoutubeEndpoints(
|
|
||||||
preferences.copyWith(
|
|
||||||
youtubeApiType: YoutubeApiType.piped,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@ -111,7 +111,7 @@ abstract class AudioPlayerInterface {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PlaybackLoopMode> get loopMode async {
|
PlaybackLoopMode get loopMode {
|
||||||
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
// return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
// return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
||||||
|
|||||||
@ -156,6 +156,12 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
|
|
||||||
String? get nextSource {
|
String? get nextSource {
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
|
|
||||||
|
if (loopMode == PlaybackLoopMode.all &&
|
||||||
|
_mkPlayer.playlist.index == _mkPlayer.playlist.medias.length - 1) {
|
||||||
|
return sources.first;
|
||||||
|
}
|
||||||
|
|
||||||
return _mkPlayer.playlist.medias
|
return _mkPlayer.playlist.medias
|
||||||
.elementAtOrNull(_mkPlayer.playlist.index + 1)
|
.elementAtOrNull(_mkPlayer.playlist.index + 1)
|
||||||
?.uri;
|
?.uri;
|
||||||
@ -169,6 +175,10 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? get previousSource {
|
String? get previousSource {
|
||||||
|
if (loopMode == PlaybackLoopMode.all && _mkPlayer.playlist.index == 0) {
|
||||||
|
return sources.last;
|
||||||
|
}
|
||||||
|
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
return _mkPlayer.playlist.medias
|
return _mkPlayer.playlist.medias
|
||||||
.elementAtOrNull(_mkPlayer.playlist.index - 1)
|
.elementAtOrNull(_mkPlayer.playlist.index - 1)
|
||||||
|
|||||||
@ -159,7 +159,7 @@ class MkPlayerWithState extends Player {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (_playlist == null || _playlist!.index + 1 >= _playlist!.medias.length) {
|
if (_playlist == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +170,7 @@ class MkPlayerWithState extends Player {
|
|||||||
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
||||||
} else if (!isLast) {
|
} else if (!isLast) {
|
||||||
playlist = _playlist!.copyWith(index: _playlist!.index + 1);
|
playlist = _playlist!.copyWith(index: _playlist!.index + 1);
|
||||||
|
|
||||||
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,7 +191,7 @@ class MkPlayerWithState extends Player {
|
|||||||
@override
|
@override
|
||||||
Future<void> jump(int index) async {
|
Future<void> jump(int index) async {
|
||||||
if (_playlist == null || index < 0 || index >= _playlist!.medias.length) {
|
if (_playlist == null || index < 0 || index >= _playlist!.medias.length) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist = _playlist!.copyWith(index: index);
|
playlist = _playlist!.copyWith(index: index);
|
||||||
@ -233,30 +234,30 @@ class MkPlayerWithState extends Player {
|
|||||||
|
|
||||||
final isOldUrlPlaying = _playlist!.medias[_playlist!.index].uri == oldUrl;
|
final isOldUrlPlaying = _playlist!.medias[_playlist!.index].uri == oldUrl;
|
||||||
|
|
||||||
for (var i = 0; i < _playlist!.medias.length - 1; i++) {
|
// ends the loop where match is found
|
||||||
final media = _playlist!.medias[i];
|
// tends to be a bit more efficient than forEach
|
||||||
if (media.uri == oldUrl) {
|
_playlist!.medias.firstWhereIndexedOrNull((i, media) {
|
||||||
if (isOldUrlPlaying) {
|
if (media.uri != oldUrl) return false;
|
||||||
pause();
|
if (isOldUrlPlaying) {
|
||||||
}
|
pause();
|
||||||
final newMedias = _playlist!.medias.toList();
|
|
||||||
newMedias[i] = Media(newUrl, extras: media.extras);
|
|
||||||
playlist = _playlist!.copyWith(medias: newMedias);
|
|
||||||
if (isOldUrlPlaying) {
|
|
||||||
super.open(
|
|
||||||
newMedias[i],
|
|
||||||
play: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace in the _tempMedias if it's not null
|
|
||||||
if (shuffled && _tempMedias != null) {
|
|
||||||
final tempIndex = _tempMedias!.indexOf(media);
|
|
||||||
_tempMedias![tempIndex] = Media(newUrl, extras: media.extras);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
final copyMedias = [..._playlist!.medias];
|
||||||
|
copyMedias[i] = Media(newUrl, extras: media.extras);
|
||||||
|
playlist = _playlist!.copyWith(medias: copyMedias);
|
||||||
|
if (isOldUrlPlaying) {
|
||||||
|
super.open(
|
||||||
|
copyMedias[i],
|
||||||
|
play: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace in the _tempMedias if it's not null
|
||||||
|
if (shuffled && _tempMedias != null) {
|
||||||
|
final tempIndex = _tempMedias!.indexOf(media);
|
||||||
|
_tempMedias![tempIndex] = Media(newUrl, extras: media.extras);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:smtc_windows/smtc_windows.dart'
|
import 'package:smtc_windows/smtc_windows.dart';
|
||||||
if (dart.library.html) 'package:spotube/services/audio_services/smtc_windows_web.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|||||||
@ -9,6 +9,8 @@ class AlbumMutations {
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
String albumId, {
|
String albumId, {
|
||||||
List<String>? refreshQueries,
|
List<String>? refreshQueries,
|
||||||
|
List<String>? refreshInfiniteQueries,
|
||||||
|
MutationOnDataFn<bool, dynamic>? onData,
|
||||||
}) {
|
}) {
|
||||||
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
|
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
|
||||||
"toggle-album-like/$albumId",
|
"toggle-album-like/$albumId",
|
||||||
@ -22,6 +24,8 @@ class AlbumMutations {
|
|||||||
},
|
},
|
||||||
ref: ref,
|
ref: ref,
|
||||||
refreshQueries: refreshQueries,
|
refreshQueries: refreshQueries,
|
||||||
|
refreshInfiniteQueries: refreshInfiniteQueries,
|
||||||
|
onData: onData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,12 +9,20 @@ import 'package:spotube/provider/user_preferences_provider.dart';
|
|||||||
class AlbumQueries {
|
class AlbumQueries {
|
||||||
const AlbumQueries();
|
const AlbumQueries();
|
||||||
|
|
||||||
Query<Iterable<AlbumSimple>, dynamic> ofMine(WidgetRef ref) {
|
InfiniteQuery<Page<AlbumSimple>, dynamic, int> ofMine(WidgetRef ref) {
|
||||||
return useSpotifyQuery<Iterable<AlbumSimple>, dynamic>(
|
return useSpotifyInfiniteQuery<Page<AlbumSimple>, dynamic, int>(
|
||||||
"current-user-albums",
|
"current-user-albums",
|
||||||
(spotify) {
|
(page, spotify) {
|
||||||
return spotify.me.savedAlbums().all();
|
return spotify.me.savedAlbums().getPage(
|
||||||
|
20,
|
||||||
|
page * 20,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
initialPage: 0,
|
||||||
|
nextPage: (lastPage, lastPageData) =>
|
||||||
|
(lastPageData.items?.length ?? 0) < 20 || lastPageData.isLast
|
||||||
|
? null
|
||||||
|
: lastPage + 1,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,4 +57,8 @@ abstract class PrimitiveUtils {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String toSafeFileName(String str) {
|
||||||
|
return str.replaceAll(RegExp(r'[^\w\s\.\-_]'), "_");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -257,11 +257,34 @@ abstract class ServiceUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void navigate(BuildContext context, String location, {Object? extra}) {
|
static void navigate(BuildContext context, String location, {Object? extra}) {
|
||||||
|
if (GoRouterState.of(context).matchedLocation == location) return;
|
||||||
GoRouter.of(context).go(location, extra: extra);
|
GoRouter.of(context).go(location, extra: extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void push(BuildContext context, String location, {Object? extra}) {
|
static void push(BuildContext context, String location, {Object? extra}) {
|
||||||
GoRouter.of(context).push(location, extra: extra);
|
final router = GoRouter.of(context);
|
||||||
|
final routerState = GoRouterState.of(context);
|
||||||
|
final routerStack = router.routerDelegate.currentConfiguration.matches
|
||||||
|
.map((e) => e.matchedLocation);
|
||||||
|
|
||||||
|
if (routerState.matchedLocation == location ||
|
||||||
|
routerStack.contains(location)) return;
|
||||||
|
router.push(location, extra: extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DateTime parseSpotifyAlbumDate(AlbumSimple? album) {
|
||||||
|
if (album == null || album.releaseDate == null) {
|
||||||
|
return DateTime.parse("1975-01-01");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (album.releaseDatePrecision ?? DatePrecision.year) {
|
||||||
|
case DatePrecision.day:
|
||||||
|
return DateTime.parse(album.releaseDate!);
|
||||||
|
case DatePrecision.month:
|
||||||
|
return DateTime.parse("${album.releaseDate}-01");
|
||||||
|
case DatePrecision.year:
|
||||||
|
return DateTime.parse("${album.releaseDate}-01-01");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<T> sortTracks<T extends Track>(List<T> tracks, SortBy sortBy) {
|
static List<T> sortTracks<T extends Track>(List<T> tracks, SortBy sortBy) {
|
||||||
@ -278,12 +301,12 @@ abstract class ServiceUtils {
|
|||||||
case SortBy.ascending:
|
case SortBy.ascending:
|
||||||
return a.name?.compareTo(b.name ?? "") ?? 0;
|
return a.name?.compareTo(b.name ?? "") ?? 0;
|
||||||
case SortBy.oldest:
|
case SortBy.oldest:
|
||||||
final aDate = DateTime.parse(a.album?.releaseDate ?? "2069-01-01");
|
final aDate = parseSpotifyAlbumDate(a.album);
|
||||||
final bDate = DateTime.parse(b.album?.releaseDate ?? "2069-01-01");
|
final bDate = parseSpotifyAlbumDate(b.album);
|
||||||
return aDate.compareTo(bDate);
|
return aDate.compareTo(bDate);
|
||||||
case SortBy.newest:
|
case SortBy.newest:
|
||||||
final aDate = DateTime.parse(a.album?.releaseDate ?? "2069-01-01");
|
final aDate = parseSpotifyAlbumDate(a.album);
|
||||||
final bDate = DateTime.parse(b.album?.releaseDate ?? "2069-01-01");
|
final bDate = parseSpotifyAlbumDate(b.album);
|
||||||
return bDate.compareTo(aDate);
|
return bDate.compareTo(aDate);
|
||||||
case SortBy.descending:
|
case SortBy.descending:
|
||||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||||
|
|||||||
@ -8,9 +8,6 @@ import 'package:path/path.dart';
|
|||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
import 'package:spotube/components/shared/links/anchor_button.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/matched_track.dart';
|
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
|
||||||
import 'package:spotube/services/youtube/youtube.dart';
|
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -67,7 +64,7 @@ abstract class TypeConversionUtils {
|
|||||||
if (onRouteChange != null) {
|
if (onRouteChange != null) {
|
||||||
onRouteChange("/artist/${artist.value.id}");
|
onRouteChange("/artist/${artist.value.id}");
|
||||||
} else {
|
} else {
|
||||||
ServiceUtils.navigate(
|
ServiceUtils.push(
|
||||||
context,
|
context,
|
||||||
"/artist/${artist.value.id}",
|
"/artist/${artist.value.id}",
|
||||||
);
|
);
|
||||||
@ -122,29 +119,12 @@ abstract class TypeConversionUtils {
|
|||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SpotubeTrack localTrack_X_Track(
|
static Track localTrack_X_Track(
|
||||||
File file, {
|
File file, {
|
||||||
Metadata? metadata,
|
Metadata? metadata,
|
||||||
String? art,
|
String? art,
|
||||||
}) {
|
}) {
|
||||||
final track = SpotubeTrack(
|
final track = Track();
|
||||||
YoutubeVideoInfo(
|
|
||||||
searchMode: SearchMode.youtube,
|
|
||||||
id: "dQw4w9WgXcQ",
|
|
||||||
title: basenameWithoutExtension(file.path),
|
|
||||||
duration: Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0),
|
|
||||||
dislikes: 0,
|
|
||||||
likes: 0,
|
|
||||||
thumbnailUrl: art ?? "",
|
|
||||||
views: 0,
|
|
||||||
channelName: metadata?.albumArtist ?? "Spotube",
|
|
||||||
channelId: metadata?.albumArtist ?? "Spotube",
|
|
||||||
publishedAt:
|
|
||||||
metadata?.year != null ? DateTime(metadata!.year!) : DateTime(2003),
|
|
||||||
),
|
|
||||||
file.path,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
track.album = Album()
|
track.album = Album()
|
||||||
..name = metadata?.album ?? "Spotube"
|
..name = metadata?.album ?? "Spotube"
|
||||||
..images = [if (art != null) Image()..url = art]
|
..images = [if (art != null) Image()..url = art]
|
||||||
|
|||||||
@ -11,8 +11,8 @@ installed_size: 24400
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- mpv
|
- mpv
|
||||||
- libappindicator3-1
|
- libappindicator3-1 | libayatana-appindicator3-1
|
||||||
- gir1.2-appindicator3-0.1
|
- gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1
|
||||||
- libsecret-1-0
|
- libsecret-1-0
|
||||||
- libnotify-bin
|
- libnotify-bin
|
||||||
- libjsoncpp25
|
- libjsoncpp25
|
||||||
|
|||||||
12
pubspec.lock
12
pubspec.lock
@ -1074,10 +1074,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_libs_android_audio
|
name: media_kit_libs_android_audio
|
||||||
sha256: "767a93c44da73b7103a1fcbe2346f7211b7c44fa727f359410e690a156f630c5"
|
sha256: f16e67d4c5a85cb603290da253456bc8ea3d85d932c778e3afd11195db2dc26d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
media_kit_libs_ios_audio:
|
media_kit_libs_ios_audio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1715,6 +1715,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
stroke_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: stroke_text
|
||||||
|
sha256: "0ec0e526c0eae7d21ce628d78eb9ae9be634259f26b0f1735f9ed540890d8cf6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
supabase:
|
supabase:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -69,7 +69,7 @@ dependencies:
|
|||||||
logger: ^1.1.0
|
logger: ^1.1.0
|
||||||
media_kit: ^1.1.3
|
media_kit: ^1.1.3
|
||||||
media_kit_native_event_loop: ^1.0.7
|
media_kit_native_event_loop: ^1.0.7
|
||||||
media_kit_libs_android_audio: ^1.3.1
|
media_kit_libs_android_audio: ^1.3.2
|
||||||
media_kit_libs_ios_audio: ^1.1.2
|
media_kit_libs_ios_audio: ^1.1.2
|
||||||
media_kit_libs_macos_audio: ^1.1.2
|
media_kit_libs_macos_audio: ^1.1.2
|
||||||
media_kit_libs_windows_audio: ^1.0.6
|
media_kit_libs_windows_audio: ^1.0.6
|
||||||
@ -103,6 +103,7 @@ dependencies:
|
|||||||
ref: a738913c8ce2c9f47515382d40827e794a334274
|
ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||||
path: plugins/window_size
|
path: plugins/window_size
|
||||||
youtube_explode_dart: ^2.0.1
|
youtube_explode_dart: ^2.0.1
|
||||||
|
stroke_text: ^0.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.2
|
build_runner: ^2.3.2
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ca": [
|
"ca": [
|
||||||
@ -11,53 +12,67 @@
|
|||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hi": [
|
"hi": [
|
||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pt": [
|
||||||
|
"you_are_offline",
|
||||||
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
"piped_api_down",
|
"piped_api_down",
|
||||||
"piped_down_error_instructions",
|
"piped_down_error_instructions",
|
||||||
"you_are_offline",
|
"you_are_offline",
|
||||||
"connection_restored"
|
"connection_restored",
|
||||||
|
"use_system_title_bar"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user