feat: pause playback when no internet connection

This commit is contained in:
Kingkor Roy Tirtho 2025-01-20 22:04:12 +06:00
parent 086107b2cd
commit 4fead5f504
12 changed files with 233 additions and 149 deletions

View 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]);
}

View 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();
}
};
}, []);
}

View File

@ -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(

View File

@ -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(

View File

@ -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;
}

View File

@ -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: [],

View File

@ -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"))

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
bonsoir_windows
connectivity_plus
desktop_webview_window
file_selector_windows
flutter_inappwebview_windows