mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00

* refactor: remove SourcedTrack based audio player and utilize mediakit playback system * feat: implement local (loopback) server to resolve stream source and leverage the media_kit playback API * feat: add source change support and re-add prefetching tracks * fix: assign lastId when track fetch completes regardless of error * chore: remove print statements * fix: remote queue not working * fix: increase mpv network timeout to reduce auto-skipping * fix: do not pre-fetch local tracks * fix(proxy-playlist): reset collections on load * chore: fix lint warnings * fix(mobile): player overlay should not be visible when the player is not playing * chore: fix typo in turkish translation * cd: checkout PR branch * cd: upgrade flutter version * chore: fix lint errors
251 lines
7.9 KiB
Dart
251 lines
7.9 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:spotube/collections/spotube_icons.dart';
|
|
import 'package:spotube/components/player/player_queue.dart';
|
|
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
|
|
import 'package:spotube/components/root/bottom_player.dart';
|
|
import 'package:spotube/components/root/sidebar.dart';
|
|
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
|
import 'package:spotube/extensions/context.dart';
|
|
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
|
import 'package:spotube/hooks/configurators/use_update_checker.dart';
|
|
import 'package:spotube/provider/connect/server.dart';
|
|
import 'package:spotube/provider/download_manager_provider.dart';
|
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|
import 'package:spotube/services/connectivity_adapter.dart';
|
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
|
|
|
const rootPaths = {
|
|
"/": 0,
|
|
"/search": 1,
|
|
"/library": 2,
|
|
"/lyrics": 3,
|
|
};
|
|
|
|
class RootApp extends HookConsumerWidget {
|
|
final Widget child;
|
|
const RootApp({
|
|
required this.child,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final isMounted = useIsMounted();
|
|
final showingDialogCompleter = useRef(Completer()..complete());
|
|
final downloader = ref.watch(downloadManagerProvider);
|
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
|
final theme = Theme.of(context);
|
|
final location = GoRouterState.of(context).matchedLocation;
|
|
|
|
useEffect(() {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
final sharedPreferences = await SharedPreferences.getInstance();
|
|
|
|
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
|
|
context.mounted) {
|
|
await PersistedStateNotifier.showNoEncryptionDialog(context);
|
|
}
|
|
});
|
|
|
|
final subscriptions = [
|
|
ConnectionCheckerService.instance.onConnectivityChanged
|
|
.listen((status) {
|
|
if (status) {
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
Icon(
|
|
SpotubeIcons.wifi,
|
|
color: theme.colorScheme.onPrimary,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(context.l10n.connection_restored),
|
|
],
|
|
),
|
|
backgroundColor: theme.colorScheme.primary,
|
|
showCloseIcon: true,
|
|
width: 350,
|
|
),
|
|
);
|
|
} else {
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
Icon(
|
|
SpotubeIcons.noWifi,
|
|
color: theme.colorScheme.onError,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(context.l10n.you_are_offline),
|
|
],
|
|
),
|
|
backgroundColor: theme.colorScheme.error,
|
|
showCloseIcon: true,
|
|
width: 300,
|
|
),
|
|
);
|
|
}
|
|
}),
|
|
connectClientStream.listen((clientOrigin) {
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(
|
|
backgroundColor: Colors.yellow[600],
|
|
behavior: SnackBarBehavior.floating,
|
|
content: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
SpotubeIcons.error,
|
|
color: Colors.black,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
context.l10n.connect_client_alert(clientOrigin),
|
|
style: const TextStyle(color: Colors.black),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
})
|
|
];
|
|
|
|
return () {
|
|
for (final subscription in subscriptions) {
|
|
subscription.cancel();
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() {
|
|
downloader.onFileExists = (track) async {
|
|
if (!isMounted()) return false;
|
|
|
|
if (!showingDialogCompleter.value.isCompleted) {
|
|
await showingDialogCompleter.value.future;
|
|
}
|
|
|
|
final replaceAll = ref.read(replaceDownloadedFileState);
|
|
|
|
if (replaceAll != null) return replaceAll;
|
|
|
|
showingDialogCompleter.value = Completer();
|
|
|
|
if (context.mounted) {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => ReplaceDownloadedDialog(
|
|
track: track,
|
|
),
|
|
) ??
|
|
false;
|
|
|
|
showingDialogCompleter.value.complete();
|
|
return result;
|
|
}
|
|
|
|
// it'll never reach here as root_app is always mounted
|
|
return false;
|
|
};
|
|
return null;
|
|
}, [downloader]);
|
|
|
|
// checks for latest version of the application
|
|
useUpdateChecker(ref);
|
|
|
|
useEndlessPlayback(ref);
|
|
|
|
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
|
|
|
|
useEffect(() {
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
SystemUiOverlayStyle(
|
|
statusBarColor: backgroundColor, // status bar color
|
|
statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179
|
|
? Brightness.dark
|
|
: Brightness.light,
|
|
),
|
|
);
|
|
return null;
|
|
}, [backgroundColor]);
|
|
|
|
void onSelectIndexChanged(int d) {
|
|
final invertedRouteMap =
|
|
rootPaths.map((key, value) => MapEntry(value, key));
|
|
|
|
if (context.mounted) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
GoRouter.of(context).go(invertedRouteMap[d]!);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ignore: deprecated_member_use
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
if (rootPaths[location] != 0) {
|
|
onSelectIndexChanged(0);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
child: Scaffold(
|
|
body: Sidebar(
|
|
selectedIndex: rootPaths[location],
|
|
onSelectedIndexChanged: onSelectIndexChanged,
|
|
child: child,
|
|
),
|
|
extendBody: true,
|
|
drawerScrimColor: Colors.transparent,
|
|
endDrawer: DesktopTools.platform.isDesktop
|
|
? Container(
|
|
constraints: const BoxConstraints(maxWidth: 800),
|
|
decoration: BoxDecoration(
|
|
boxShadow: theme.brightness == Brightness.light
|
|
? null
|
|
: kElevationToShadow[8],
|
|
),
|
|
margin: const EdgeInsets.only(
|
|
top: 40,
|
|
bottom: 100,
|
|
),
|
|
child: Consumer(
|
|
builder: (context, ref, _) {
|
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
|
final playlistNotifier =
|
|
ref.read(ProxyPlaylistNotifier.notifier);
|
|
|
|
return PlayerQueue.fromProxyPlaylistNotifier(
|
|
floating: true,
|
|
playlist: playlist,
|
|
notifier: playlistNotifier,
|
|
);
|
|
},
|
|
),
|
|
)
|
|
: null,
|
|
bottomNavigationBar: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
BottomPlayer(),
|
|
SpotubeNavigationBar(
|
|
selectedIndex: rootPaths[location],
|
|
onSelectedIndexChanged: onSelectIndexChanged,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|