mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: pause playback when no internet connection
This commit is contained in:
parent
086107b2cd
commit
4fead5f504
46
lib/modules/root/use_downloader_dialogs.dart
Normal file
46
lib/modules/root/use_downloader_dialogs.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
|
||||
void useDownloaderDialogs(WidgetRef ref) {
|
||||
final context = useContext();
|
||||
final showingDialogCompleter = useRef(Completer()..complete());
|
||||
final downloader = ref.watch(downloadManagerProvider);
|
||||
|
||||
useEffect(() {
|
||||
downloader.onFileExists = (track) async {
|
||||
if (!context.mounted) 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]);
|
||||
}
|
127
lib/modules/root/use_global_subscriptions.dart
Normal file
127
lib/modules/root/use_global_subscriptions.dart
Normal file
@ -0,0 +1,127 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/server/routes/connect.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
void useGlobalSubscriptions(WidgetRef ref) {
|
||||
final context = useContext();
|
||||
final theme = Theme.of(context);
|
||||
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ServiceUtils.checkForUpdates(context, ref);
|
||||
});
|
||||
|
||||
StreamSubscription? audioPlayerSubscription;
|
||||
bool pausedByStream = false;
|
||||
|
||||
final subscriptions = [
|
||||
ConnectionCheckerService.instance.onConnectivityChanged
|
||||
.listen((connected) async {
|
||||
audioPlayerSubscription?.cancel();
|
||||
|
||||
/// Pausing or resuming based on connectivity to avoid MPV skipping
|
||||
/// audio while retrying to connect
|
||||
if (audioPlayer.currentIndex >= 0) {
|
||||
if (connected && audioPlayer.isPaused && pausedByStream) {
|
||||
await audioPlayer.resume();
|
||||
pausedByStream = false;
|
||||
} else if (!connected && audioPlayer.isPlaying) {
|
||||
if ((audioPlayer.bufferedPosition - const Duration(seconds: 1)) <=
|
||||
audioPlayer.position) {
|
||||
await audioPlayer.pause();
|
||||
pausedByStream = true;
|
||||
} else {
|
||||
audioPlayerSubscription =
|
||||
audioPlayer.positionStream.listen((position) async {
|
||||
if (ConnectionCheckerService.instance.isConnectedSync) return;
|
||||
|
||||
final bufferedPosition =
|
||||
audioPlayer.bufferedPosition - const Duration(seconds: 1);
|
||||
final duration =
|
||||
audioPlayer.duration - const Duration(seconds: 1);
|
||||
|
||||
if (bufferedPosition <= position || position >= duration) {
|
||||
audioPlayer.pause();
|
||||
pausedByStream = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification for connection related issues
|
||||
if (!context.mounted) return;
|
||||
|
||||
showToast(
|
||||
context: context,
|
||||
location: ToastLocation.bottomCenter,
|
||||
builder: (context, overlay) {
|
||||
if (connected) {
|
||||
return SurfaceCard(
|
||||
child: Basic(
|
||||
leading: const Icon(SpotubeIcons.wifi),
|
||||
title: Text(context.l10n.connection_restored),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SurfaceCard(
|
||||
fillColor: theme.colorScheme.destructive,
|
||||
filled: true,
|
||||
child: Basic(
|
||||
leading: Icon(
|
||||
SpotubeIcons.noWifi,
|
||||
color: theme.colorScheme.destructiveForeground,
|
||||
),
|
||||
trailing: Text(
|
||||
context.l10n.you_are_offline,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.destructiveForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
connectRoutes.connectClientStream.listen((clientOrigin) {
|
||||
if (!context.mounted) return;
|
||||
showToast(
|
||||
context: context,
|
||||
location: ToastLocation.topRight,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
fillColor: Colors.yellow[600],
|
||||
filled: true,
|
||||
child: Basic(
|
||||
leading: const Icon(
|
||||
SpotubeIcons.error,
|
||||
color: Colors.black,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.connect_client_alert(clientOrigin),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
];
|
||||
|
||||
return () {
|
||||
for (final subscription in subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
@ -1,26 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/side_bar_tiles.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/framework/app_pop_scope.dart';
|
||||
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
|
||||
import 'package:spotube/modules/root/bottom_player.dart';
|
||||
import 'package:spotube/modules/root/sidebar.dart';
|
||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||
import 'package:spotube/modules/root/use_downloader_dialogs.dart';
|
||||
import 'package:spotube/modules/root/use_global_subscriptions.dart';
|
||||
import 'package:spotube/pages/home/home.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/glance/glance.dart';
|
||||
import 'package:spotube/provider/server/routes/connect.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class RootApp extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
@ -31,138 +25,14 @@ class RootApp extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final showingDialogCompleter = useRef(Completer()..complete());
|
||||
final downloader = ref.watch(downloadManagerProvider);
|
||||
|
||||
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||
|
||||
ref.listen(glanceProvider, (_, __) {});
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ServiceUtils.checkForUpdates(context, ref);
|
||||
});
|
||||
|
||||
final subscriptions = [
|
||||
ConnectionCheckerService.instance.onConnectivityChanged
|
||||
.listen((status) {
|
||||
if (!context.mounted) return;
|
||||
if (status) {
|
||||
showToast(
|
||||
context: context,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
fillColor: theme.colorScheme.primary,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
SpotubeIcons.wifi,
|
||||
color: theme.colorScheme.primaryForeground,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(context.l10n.connection_restored),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showToast(
|
||||
context: context,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
fillColor: theme.colorScheme.destructive,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
SpotubeIcons.noWifi,
|
||||
color: theme.colorScheme.destructiveForeground,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(context.l10n.you_are_offline),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
connectRoutes.connectClientStream.listen((clientOrigin) {
|
||||
if (!context.mounted) return;
|
||||
showToast(
|
||||
context: context,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
fillColor: Colors.yellow[600],
|
||||
child: 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 (!context.mounted) 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
|
||||
|
||||
useEndlessPlayback(ref);
|
||||
|
||||
final backgroundColor = Theme.of(context).colorScheme.background;
|
||||
final brightness = Theme.of(context).brightness;
|
||||
|
||||
ref.listen(glanceProvider, (_, __) {});
|
||||
useGlobalSubscriptions(ref);
|
||||
useDownloaderDialogs(ref);
|
||||
useEndlessPlayback(ref);
|
||||
|
||||
useEffect(() {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
|
@ -45,7 +45,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
|
||||
var playlist =
|
||||
await database.select(database.playlistTable).getSingleOrNull();
|
||||
var medias = await database.select(database.playlistMediaTable).get();
|
||||
final medias = await database.select(database.playlistMediaTable).get();
|
||||
|
||||
if (playlist == null) {
|
||||
await database.into(database.playlistTable).insert(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
@ -34,6 +35,10 @@ class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
Connectivity().onConnectivityChanged.listen((event) async {
|
||||
await isConnected;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -77,8 +82,9 @@ class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
return interfaces.any(
|
||||
(interface) =>
|
||||
vpnNames.any((name) => interface.name.toLowerCase().contains(name)),
|
||||
(interface) => vpnNames.any(
|
||||
(name) => interface.name.toLowerCase().contains(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -109,10 +115,10 @@ class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
|
||||
Future<bool> get isConnected async {
|
||||
final connected = await _isConnected();
|
||||
isConnectedSync = connected;
|
||||
if (connected != isConnectedSync /*previous value*/) {
|
||||
_connectionStreamController.add(connected);
|
||||
}
|
||||
isConnectedSync = connected;
|
||||
return connected;
|
||||
}
|
||||
|
||||
|
@ -82,14 +82,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
);
|
||||
}
|
||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(
|
||||
cachedSource.sourceId,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
final manifest = await youtubeClient.videos.streamsClient.getManifest(
|
||||
cachedSource.sourceId,
|
||||
requireWatchPage: false,
|
||||
ytClients: [YoutubeApiClient.tv],
|
||||
);
|
||||
return YoutubeSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: [],
|
||||
|
@ -9,6 +9,7 @@ import app_links
|
||||
import audio_service
|
||||
import audio_session
|
||||
import bonsoir_darwin
|
||||
import connectivity_plus
|
||||
import desktop_webview_window
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
@ -33,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
|
@ -8,6 +8,9 @@ PODS:
|
||||
- bonsoir_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- desktop_webview_window (0.0.1):
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
@ -79,6 +82,7 @@ DEPENDENCIES:
|
||||
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`)
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||
- bonsoir_darwin (from `Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
@ -116,6 +120,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||
bonsoir_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
|
||||
desktop_webview_window:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos
|
||||
device_info_plus:
|
||||
@ -166,6 +172,7 @@ SPEC CHECKSUMS:
|
||||
audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9
|
||||
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
|
||||
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
|
24
pubspec.lock
24
pubspec.lock
@ -375,6 +375,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1542,6 +1558,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nm
|
||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -23,6 +23,7 @@ dependencies:
|
||||
bonsoir: ^5.1.10
|
||||
cached_network_image: ^3.3.1
|
||||
collection: ^1.18.0
|
||||
connectivity_plus: ^6.1.2
|
||||
desktop_webview_window:
|
||||
git:
|
||||
path: packages/desktop_webview_window
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <bonsoir_windows/bonsoir_windows_plugin_c_api.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||
@ -27,6 +28,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
BonsoirWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
bonsoir_windows
|
||||
connectivity_plus
|
||||
desktop_webview_window
|
||||
file_selector_windows
|
||||
flutter_inappwebview_windows
|
||||
|
Loading…
Reference in New Issue
Block a user