lint: enforce unawaited_futures, prefer_final_locals, etc.

This commit is contained in:
edde746
2026-04-25 12:27:28 +02:00
parent c2cd2dea37
commit feba5b6f74
52 changed files with 1072 additions and 1978 deletions
+10
View File
@@ -7,6 +7,16 @@ analyzer:
plugins: plugins:
- dart_code_linter - dart_code_linter
linter:
rules:
unawaited_futures: true
cancel_subscriptions: true
close_sinks: true
use_super_parameters: true
prefer_final_locals: true
prefer_final_in_for_each: true
avoid_print: true
dart_code_linter: dart_code_linter:
extends: extends:
- package:dart_code_linter/presets/recommended.yaml - package:dart_code_linter/presets/recommended.yaml
File diff suppressed because it is too large Load Diff
+20 -13
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io' show Platform, ProcessInfo; import 'dart:io' show Platform, ProcessInfo;
import 'dart:ui' show AppExitResponse; import 'dart:ui' show AppExitResponse;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
// ignore: depend_on_referenced_packages
import 'package:shared_preferences_foundation/shared_preferences_foundation.dart'; import 'package:shared_preferences_foundation/shared_preferences_foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -138,7 +139,7 @@ Future<void> _bootstrapApp() async {
final savedLocale = settings.read(SettingsService.appLocale); final savedLocale = settings.read(SettingsService.appLocale);
// Initialize localization with saved locale // Initialize localization with saved locale
LocaleSettings.setLocale(savedLocale); unawaited(LocaleSettings.setLocale(savedLocale));
// Needed for formatting dates in different locales // Needed for formatting dates in different locales
await initializeDateFormatting(savedLocale.languageCode, null); await initializeDateFormatting(savedLocale.languageCode, null);
@@ -212,7 +213,7 @@ Future<void> _bootstrapApp() async {
// Desktop-only services // Desktop-only services
if (PlatformDetector.isDesktopOS()) { if (PlatformDetector.isDesktopOS()) {
DiscordRPCService.instance.initialize(); unawaited(DiscordRPCService.instance.initialize());
} }
// Trakt scrobble service (all platforms) // Trakt scrobble service (all platforms)
@@ -251,7 +252,7 @@ FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
if (instance != null && !instance.read(SettingsService.crashReporting)) return null; if (instance != null && !instance.read(SettingsService.crashReporting)) return null;
// Drop unactionable errors // Drop unactionable errors
var exceptions = event.exceptions; final exceptions = event.exceptions;
if (exceptions != null) { if (exceptions != null) {
bool shouldDrop(SentryException e) { bool shouldDrop(SentryException e) {
final v = e.value; final v = e.value;
@@ -917,7 +918,7 @@ class _SetupScreenState extends State<SetupScreen> {
// Check network connectivity early to fast-path airplane mode. // Check network connectivity early to fast-path airplane mode.
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close. // Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
bool hasNetwork; bool hasNetwork;
Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup')); unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup')));
try { try {
final connectivityResult = await Connectivity().checkConnectivity().timeout( final connectivityResult = await Connectivity().checkConnectivity().timeout(
const Duration(seconds: 3), const Duration(seconds: 3),
@@ -929,7 +930,9 @@ class _SetupScreenState extends State<SetupScreen> {
hasNetwork = true; hasNetwork = true;
} }
Sentry.addBreadcrumb(Breadcrumb(message: 'Network check done: hasNetwork=$hasNetwork', category: 'setup')); unawaited(
Sentry.addBreadcrumb(Breadcrumb(message: 'Network check done: hasNetwork=$hasNetwork', category: 'setup')),
);
if (hasNetwork) { if (hasNetwork) {
_setStatus(t.common.refreshingServers); _setStatus(t.common.refreshingServers);
@@ -941,7 +944,7 @@ class _SetupScreenState extends State<SetupScreen> {
if (refreshResult == ServerRefreshResult.authError) { if (refreshResult == ServerRefreshResult.authError) {
await storage.clearCredentials(); await storage.clearCredentials();
if (mounted) { if (mounted) {
Navigator.pushReplacement(context, fadeRoute(const AuthScreen())); unawaited(Navigator.pushReplacement(context, fadeRoute(const AuthScreen())));
} }
return; return;
} }
@@ -954,7 +957,7 @@ class _SetupScreenState extends State<SetupScreen> {
if (servers.isEmpty) { if (servers.isEmpty) {
if (mounted) { if (mounted) {
Navigator.pushReplacement(context, fadeRoute(const AuthScreen())); unawaited(Navigator.pushReplacement(context, fadeRoute(const AuthScreen())));
} }
return; return;
} }
@@ -966,11 +969,13 @@ class _SetupScreenState extends State<SetupScreen> {
_setStatus(t.common.startingOfflineMode); _setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized(); await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return; if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))); unawaited(Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))));
return; return;
} }
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'setup')); unawaited(
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'setup')),
);
_setStatus(t.common.connectingToServers); _setStatus(t.common.connectingToServers);
// Populate per-server status for splash display // Populate per-server status for splash display
@@ -1006,16 +1011,18 @@ class _SetupScreenState extends State<SetupScreen> {
if (result.hasConnections && result.firstClient != null) { if (result.hasConnections && result.firstClient != null) {
// Resume any downloads that were interrupted by app kill // Resume any downloads that were interrupted by app kill
final downloadProvider = context.read<DownloadProvider>(); final downloadProvider = context.read<DownloadProvider>();
unawaited(
downloadProvider.ensureInitialized().then((_) { downloadProvider.ensureInitialized().then((_) {
downloadProvider.resumeQueuedDownloads(result.firstClient!); downloadProvider.resumeQueuedDownloads(result.firstClient!);
}); }),
);
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))); unawaited(Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))));
} else { } else {
_setStatus(t.common.startingOfflineMode); _setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized(); await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return; if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))); unawaited(Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))));
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
appLogger.e('Error during multi-server connection', error: e, stackTrace: stackTrace); appLogger.e('Error during multi-server connection', error: e, stackTrace: stackTrace);
@@ -1024,7 +1031,7 @@ class _SetupScreenState extends State<SetupScreen> {
_setStatus(t.common.startingOfflineMode); _setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized(); await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return; if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))); unawaited(Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))));
} }
} }
} }
@@ -10,16 +10,11 @@ RemoteDevice _$RemoteDeviceFromJson(Map<String, dynamic> json) => RemoteDevice(
id: json['id'] as String, id: json['id'] as String,
name: json['name'] as String, name: json['name'] as String,
platform: json['platform'] as String, platform: json['platform'] as String,
connectedAt: json['connectedAt'] == null connectedAt: json['connectedAt'] == null ? null : DateTime.parse(json['connectedAt'] as String),
? null capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map((k, e) => MapEntry(k, e as bool)),
: DateTime.parse(json['connectedAt'] as String),
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as bool),
),
); );
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) => Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) => <String, dynamic>{
<String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'platform': instance.platform, 'platform': instance.platform,
@@ -27,13 +22,8 @@ Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) =>
'capabilities': instance.capabilities, 'capabilities': instance.capabilities,
}; };
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession(
RemoteSession( role: $enumDecode(_$RemoteSessionRoleEnumMap, json['role'], unknownValue: RemoteSessionRole.remote),
role: $enumDecode(
_$RemoteSessionRoleEnumMap,
json['role'],
unknownValue: RemoteSessionRole.remote,
),
status: status:
$enumDecodeNullable( $enumDecodeNullable(
_$RemoteSessionStatusEnumMap, _$RemoteSessionStatusEnumMap,
@@ -43,17 +33,12 @@ RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
RemoteSessionStatus.disconnected, RemoteSessionStatus.disconnected,
connectedDevice: json['connectedDevice'] == null connectedDevice: json['connectedDevice'] == null
? null ? null
: RemoteDevice.fromJson( : RemoteDevice.fromJson(json['connectedDevice'] as Map<String, dynamic>),
json['connectedDevice'] as Map<String, dynamic>, createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String),
),
createdAt: json['createdAt'] == null
? null
: DateTime.parse(json['createdAt'] as String),
errorMessage: json['errorMessage'] as String?, errorMessage: json['errorMessage'] as String?,
); );
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => <String, dynamic>{
<String, dynamic>{
'role': _$RemoteSessionRoleEnumMap[instance.role]!, 'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!, 'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice, 'connectedDevice': instance.connectedDevice,
@@ -61,10 +46,7 @@ Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
'errorMessage': instance.errorMessage, 'errorMessage': instance.errorMessage,
}; };
const _$RemoteSessionRoleEnumMap = { const _$RemoteSessionRoleEnumMap = {RemoteSessionRole.host: 'host', RemoteSessionRole.remote: 'remote'};
RemoteSessionRole.host: 'host',
RemoteSessionRole.remote: 'remote',
};
const _$RemoteSessionStatusEnumMap = { const _$RemoteSessionStatusEnumMap = {
RemoteSessionStatus.disconnected: 'disconnected', RemoteSessionStatus.disconnected: 'disconnected',
+5 -11
View File
@@ -6,21 +6,15 @@ part of 'play_queue_response.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
PlayQueueResponse _$PlayQueueResponseFromJson(Map<String, dynamic> json) => PlayQueueResponse _$PlayQueueResponseFromJson(Map<String, dynamic> json) => PlayQueueResponse(
PlayQueueResponse(
playQueueID: (json['playQueueID'] as num).toInt(), playQueueID: (json['playQueueID'] as num).toInt(),
playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?) playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?)?.toInt(),
?.toInt(), playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?)?.toInt(),
playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?) playQueueSelectedMetadataItemID: json['playQueueSelectedMetadataItemID'] as String?,
?.toInt(),
playQueueSelectedMetadataItemID:
json['playQueueSelectedMetadataItemID'] as String?,
playQueueShuffled: flexibleBool(json['playQueueShuffled']), playQueueShuffled: flexibleBool(json['playQueueShuffled']),
playQueueSourceURI: json['playQueueSourceURI'] as String?, playQueueSourceURI: json['playQueueSourceURI'] as String?,
playQueueTotalCount: (json['playQueueTotalCount'] as num?)?.toInt(), playQueueTotalCount: (json['playQueueTotalCount'] as num?)?.toInt(),
playQueueVersion: (json['playQueueVersion'] as num).toInt(), playQueueVersion: (json['playQueueVersion'] as num).toInt(),
size: (json['size'] as num?)?.toInt(), size: (json['size'] as num?)?.toInt(),
items: (json['Metadata'] as List<dynamic>?) items: (json['Metadata'] as List<dynamic>?)?.map((e) => PlexMetadata.fromJson(e as Map<String, dynamic>)).toList(),
?.map((e) => PlexMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
); );
+1 -5
View File
@@ -13,11 +13,7 @@ PlexHome _$PlexHomeFromJson(Map<String, dynamic> json) => PlexHome(
guestUserUUID: json['guestUserUUID'] as String? ?? '', guestUserUUID: json['guestUserUUID'] as String? ?? '',
guestEnabled: json['guestEnabled'] as bool? ?? false, guestEnabled: json['guestEnabled'] as bool? ?? false,
subscription: json['subscription'] as bool? ?? false, subscription: json['subscription'] as bool? ?? false,
users: users: (json['users'] as List<dynamic>?)?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>)).toList() ?? [],
(json['users'] as List<dynamic>?)
?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
); );
Map<String, dynamic> _$PlexHomeToJson(PlexHome instance) => <String, dynamic>{ Map<String, dynamic> _$PlexHomeToJson(PlexHome instance) => <String, dynamic>{
+1 -2
View File
@@ -22,8 +22,7 @@ PlexHomeUser _$PlexHomeUserFromJson(Map<String, dynamic> json) => PlexHomeUser(
protected: json['protected'] as bool? ?? false, protected: json['protected'] as bool? ?? false,
); );
Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) => Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) => <String, dynamic>{
<String, dynamic>{
'id': instance.id, 'id': instance.id,
'uuid': instance.uuid, 'uuid': instance.uuid,
'title': instance.title, 'title': instance.title,
+1 -2
View File
@@ -19,8 +19,7 @@ PlexLibrary _$PlexLibraryFromJson(Map<String, dynamic> json) => PlexLibrary(
hidden: (json['hidden'] as num?)?.toInt(), hidden: (json['hidden'] as num?)?.toInt(),
); );
Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) => Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) => <String, dynamic>{
<String, dynamic>{
'key': instance.key, 'key': instance.key,
'title': instance.title, 'title': instance.title,
'type': instance.type, 'type': instance.type,
+1 -1
View File
@@ -413,7 +413,7 @@ class PlexMetadata with MultiServerFields {
final images = json['Image'] as List?; final images = json['Image'] as List?;
if (images == null) return null; if (images == null) return null;
for (var image in images) { for (final image in images) {
if (image is Map && image['type'] == imageType) { if (image is Map && image['type'] == imageType) {
return image['url'] as String?; return image['url'] as String?;
} }
+2 -5
View File
@@ -42,9 +42,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
leafCount: (json['leafCount'] as num?)?.toInt(), leafCount: (json['leafCount'] as num?)?.toInt(),
viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(), viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(),
childCount: flexibleInt(json['childCount']), childCount: flexibleInt(json['childCount']),
role: (json['Role'] as List<dynamic>?) role: (json['Role'] as List<dynamic>?)?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>)).toList(),
?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>))
.toList(),
mediaVersions: (json['Media'] as List<dynamic>?) mediaVersions: (json['Media'] as List<dynamic>?)
?.map((e) => PlexMediaVersion.fromJson(e as Map<String, dynamic>)) ?.map((e) => PlexMediaVersion.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
@@ -76,8 +74,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
backgroundSquare: json['backgroundSquare'] as String?, backgroundSquare: json['backgroundSquare'] as String?,
); );
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dynamic>{
<String, dynamic>{
'ratingKey': instance.ratingKey, 'ratingKey': instance.ratingKey,
'key': instance.key, 'key': instance.key,
'guid': instance.guid, 'guid': instance.guid,
+1 -2
View File
@@ -26,8 +26,7 @@ PlexPlaylist _$PlexPlaylistFromJson(Map<String, dynamic> json) => PlexPlaylist(
thumb: json['thumb'] as String?, thumb: json['thumb'] as String?,
); );
Map<String, dynamic> _$PlexPlaylistToJson(PlexPlaylist instance) => Map<String, dynamic> _$PlexPlaylistToJson(PlexPlaylist instance) => <String, dynamic>{
<String, dynamic>{
'ratingKey': instance.ratingKey, 'ratingKey': instance.ratingKey,
'key': instance.key, 'key': instance.key,
'type': instance.type, 'type': instance.type,
+1 -1
View File
@@ -351,7 +351,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
3 => 'quit', 3 => 'quit',
4 => 'error', 4 => 'error',
5 => 'redirect', 5 => 'redirect',
String s => s, final String s => s,
_ => null, _ => null,
}; };
if (reason == 'eof') { if (reason == 'eof') {
+1 -1
View File
@@ -188,7 +188,7 @@ class CompanionRemoteProvider with ChangeNotifier {
/// Stop the host server and LAN broadcasting. /// Stop the host server and LAN broadcasting.
Future<void> stopHostServer() async { Future<void> stopHostServer() async {
_intentionalDisconnect = true; _intentionalDisconnect = true;
_discoveryService?.stopBroadcasting(); await _discoveryService?.stopBroadcasting();
if (_peerService != null) { if (_peerService != null) {
await _peerService!.disconnect(); await _peerService!.disconnect();
+1 -1
View File
@@ -126,7 +126,7 @@ class LibrariesProvider extends ChangeNotifier {
} }
// Create a map for quick lookup // Create a map for quick lookup
final libraryMap = {for (var lib in libraries) lib.globalKey: lib}; final libraryMap = {for (final lib in libraries) lib.globalKey: lib};
// Build ordered list based on saved order // Build ordered list based on saved order
final orderedLibraries = <PlexLibrary>[]; final orderedLibraries = <PlexLibrary>[];
+2 -2
View File
@@ -51,7 +51,7 @@ class _AuthScreenState extends State<AuthScreen> {
setState(() { setState(() {
_useQrFlow = true; _useQrFlow = true;
}); });
_startAuthentication(); unawaited(_startAuthentication());
} }
} }
@@ -115,7 +115,7 @@ class _AuthScreenState extends State<AuthScreen> {
await profileFuture; await profileFuture;
if (!mounted) return; if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))); unawaited(Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))));
} catch (e) { } catch (e) {
appLogger.e('Failed to connect to servers', error: e); appLogger.e('Failed to connect to servers', error: e);
if (!mounted) return; if (!mounted) return;
+8 -5
View File
@@ -416,8 +416,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Center the active dot when possible // Center the active dot when possible
final center = _currentHeroIndex; final center = _currentHeroIndex;
int start = (center - 2).clamp(0, totalDots - 5); final int start = (center - 2).clamp(0, totalDots - 5);
int end = start + 4; // 5 dots total (0-4 inclusive) final int end = start + 4; // 5 dots total (0-4 inclusive)
return (start: start, end: end); return (start: start, end: end);
} }
@@ -461,6 +461,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Get hidden libraries for filtering // Get hidden libraries for filtering
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false); final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
await hiddenLibrariesProvider.ensureInitialized(); await hiddenLibrariesProvider.ensureInitialized();
if (!mounted) return;
_lastSeenHiddenKeys = Set.of(hiddenLibrariesProvider.hiddenLibraryKeys); _lastSeenHiddenKeys = Set.of(hiddenLibrariesProvider.hiddenLibraryKeys);
// Get settings for hub mode preference (ensure initialized before accessing) // Get settings for hub mode preference (ensure initialized before accessing)
@@ -509,7 +510,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Sync to Android TV Watch Next row // Sync to Android TV Watch Next row
if (Platform.isAndroid) { if (Platform.isAndroid) {
_syncWatchNext(onDeck); unawaited(_syncWatchNext(onDeck));
} }
// Sync PageController to first page after OnDeck loads // Sync PageController to first page after OnDeck loads
@@ -622,7 +623,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Sync to Android TV Watch Next row // Sync to Android TV Watch Next row
if (Platform.isAndroid) { if (Platform.isAndroid) {
_syncWatchNext(onDeck); unawaited(_syncWatchNext(onDeck));
} }
appLogger.d('Continue Watching refreshed successfully'); appLogger.d('Continue Watching refreshed successfully');
@@ -806,9 +807,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
playbackStateProvider.clearShuffle(); playbackStateProvider.clearShuffle();
if (mounted) { if (mounted) {
unawaited(
Navigator.of( Navigator.of(
context, context,
).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => const AuthScreen()), (route) => false); ).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => const AuthScreen()), (route) => false),
);
} }
} }
} }
+8 -6
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart'; import 'package:plezy/widgets/app_icon.dart';
@@ -174,7 +176,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
} }
if (libraryGlobalKeyToLoad != null && mounted) { if (libraryGlobalKeyToLoad != null && mounted) {
_loadLibraryContent(libraryGlobalKeyToLoad); unawaited(_loadLibraryContent(libraryGlobalKeyToLoad));
} }
} }
@@ -536,7 +538,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
.toList(); .toList();
if (visibleLibraries.isNotEmpty) { if (visibleLibraries.isNotEmpty) {
_loadLibraryContent(visibleLibraries.first.globalKey); unawaited(_loadLibraryContent(visibleLibraries.first.globalKey));
} }
} }
} }
@@ -600,16 +602,16 @@ class _LibrariesScreenState extends State<LibrariesScreen>
switch (action) { switch (action) {
case 'scan': case 'scan':
_scanLibrary(library); unawaited(_scanLibrary(library));
break; break;
case 'analyze': case 'analyze':
_analyzeLibrary(library); unawaited(_analyzeLibrary(library));
break; break;
case 'refresh': case 'refresh':
_refreshLibraryMetadata(library); unawaited(_refreshLibraryMetadata(library));
break; break;
case 'empty_trash': case 'empty_trash':
_emptyLibraryTrash(library); unawaited(_emptyLibraryTrash(library));
break; break;
} }
} }
@@ -611,8 +611,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
final storage = await StorageService.getInstance(); final storage = await StorageService.getInstance();
await storage.saveLibraryFilters(filters, sectionId: widget.library.globalKey); await storage.saveLibraryFilters(filters, sectionId: widget.library.globalKey);
_loadItems(); unawaited(_loadItems());
_loadFirstCharacters(); unawaited(_loadFirstCharacters());
}, },
), ),
); );
+3 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -188,7 +190,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
}); });
// Load favorites from the first available server (favorites are cloud-synced) // Load favorites from the first available server (favorites are cloud-synced)
_loadFavorites(multiServer); unawaited(_loadFavorites(multiServer));
if (allChannels.isNotEmpty && PlatformDetector.shouldUseSideNavigation(context)) { if (allChannels.isNotEmpty && PlatformDetector.shouldUseSideNavigation(context)) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
+6 -6
View File
@@ -175,7 +175,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
// Auto-start companion remote server now that home data is available // Auto-start companion remote server now that home data is available
if (_companionRemoteSetup && mounted) { if (_companionRemoteSetup && mounted) {
_autoStartCompanionRemoteServer(context.read<CompanionRemoteProvider>()); unawaited(_autoStartCompanionRemoteServer(context.read<CompanionRemoteProvider>()));
} }
// Ensure first login (or any unset profile state) requires explicit selection. // Ensure first login (or any unset profile state) requires explicit selection.
@@ -189,7 +189,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
} }
// Check for updates on startup // Check for updates on startup
_checkForUpdatesOnStartup(); unawaited(_checkForUpdatesOnStartup());
}); });
} }
@@ -327,7 +327,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
final contentId = await watchNext.getInitialDeepLink(); final contentId = await watchNext.getInitialDeepLink();
if (contentId != null && mounted) { if (contentId != null && mounted) {
appLogger.d('Watch Next initial deep link: $contentId'); appLogger.d('Watch Next initial deep link: $contentId');
_handleWatchNextContentId(contentId); unawaited(_handleWatchNextContentId(contentId));
} }
}); });
} }
@@ -357,7 +357,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
if (metadata == null || !mounted) return; if (metadata == null || !mounted) return;
navigateToVideoPlayer(context, metadata: metadata); unawaited(navigateToVideoPlayer(context, metadata: metadata));
} catch (e) { } catch (e) {
appLogger.e('Watch Next: failed to navigate to media', error: e); appLogger.e('Watch Next: failed to navigate to media', error: e);
} }
@@ -697,7 +697,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
if (!result.confirmed) return; if (!result.confirmed) return;
} }
} }
SystemNavigator.pop(); unawaited(SystemNavigator.pop());
}); });
} }
@@ -823,7 +823,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
} }
// Reset other provider states // Reset other provider states
hiddenLibrariesProvider.refresh(); unawaited(hiddenLibrariesProvider.refresh());
playbackStateProvider.clearShuffle(); playbackStateProvider.clearShuffle();
appLogger.d('Cleared all provider states for profile switch'); appLogger.d('Cleared all provider states for profile switch');
+14 -14
View File
@@ -323,7 +323,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
}); });
// Re-fetch episodes for the currently selected season // Re-fetch episodes for the currently selected season
if (!_showEpisodesDirectly && _seasons.isNotEmpty) { if (!_showEpisodesDirectly && _seasons.isNotEmpty) {
_fetchSeasonEpisodes(_selectedSeasonIndex); unawaited(_fetchSeasonEpisodes(_selectedSeasonIndex));
} }
} else if (widget.metadata.isSeason) { } else if (widget.metadata.isSeason) {
await _fetchAllEpisodes(); await _fetchAllEpisodes();
@@ -872,8 +872,8 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
context, context,
isWatched ? t.messages.markedAsUnwatchedOffline : t.messages.markedAsWatchedOffline, isWatched ? t.messages.markedAsUnwatchedOffline : t.messages.markedAsWatchedOffline,
); );
_updateWatchStateOffline(); unawaited(_updateWatchStateOffline());
_loadOfflineOnDeckEpisode(); unawaited(_loadOfflineOnDeckEpisode());
} }
} else { } else {
// Online mode: send to server // Online mode: send to server
@@ -1237,7 +1237,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
if (widget.metadata.isShow) { if (widget.metadata.isShow) {
_loadSeasonsFromDownloads(); _loadSeasonsFromDownloads();
// Get offline OnDeck episode // Get offline OnDeck episode
_loadOfflineOnDeckEpisode(); unawaited(_loadOfflineOnDeckEpisode());
} else if (widget.metadata.isSeason) { } else if (widget.metadata.isSeason) {
_seasons = [widget.metadata]; _seasons = [widget.metadata];
_showEpisodesDirectly = true; _showEpisodesDirectly = true;
@@ -1286,16 +1286,16 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
// Load seasons if it's a show // Load seasons if it's a show
if (metadata.isShow) { if (metadata.isShow) {
_loadSeasons(); unawaited(_loadSeasons());
} else if (metadata.isSeason) { } else if (metadata.isSeason) {
_seasons = [widget.metadata]; _seasons = [widget.metadata];
_showEpisodesDirectly = true; _showEpisodesDirectly = true;
_fetchAllEpisodes(); unawaited(_fetchAllEpisodes());
} }
// Load extras (trailers, behind-the-scenes, etc.) // Load extras (trailers, behind-the-scenes, etc.)
_loadExtras(); unawaited(_loadExtras());
_loadRelatedHubs(); unawaited(_loadRelatedHubs());
return; return;
} }
@@ -1307,11 +1307,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
}); });
if (widget.metadata.isShow) { if (widget.metadata.isShow) {
_loadSeasons(); unawaited(_loadSeasons());
} else if (widget.metadata.isSeason) { } else if (widget.metadata.isSeason) {
_seasons = [widget.metadata]; _seasons = [widget.metadata];
_showEpisodesDirectly = true; _showEpisodesDirectly = true;
_fetchAllEpisodes(); unawaited(_fetchAllEpisodes());
} }
} catch (e) { } catch (e) {
// Fallback to passed metadata on error // Fallback to passed metadata on error
@@ -1322,11 +1322,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
}); });
if (widget.metadata.isShow) { if (widget.metadata.isShow) {
_loadSeasons(); unawaited(_loadSeasons());
} else if (widget.metadata.isSeason) { } else if (widget.metadata.isSeason) {
_seasons = [widget.metadata]; _seasons = [widget.metadata];
_showEpisodesDirectly = true; _showEpisodesDirectly = true;
_fetchAllEpisodes(); unawaited(_fetchAllEpisodes());
} }
} }
} }
@@ -1383,7 +1383,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
await _fetchAllEpisodes(); await _fetchAllEpisodes();
} else if (seasonsWithServerId.isNotEmpty) { } else if (seasonsWithServerId.isNotEmpty) {
// Fetch episodes for the auto-selected season // Fetch episodes for the auto-selected season
_fetchSeasonEpisodes(onDeckSeasonIndex); unawaited(_fetchSeasonEpisodes(onDeckSeasonIndex));
} }
} catch (e) { } catch (e) {
setStateIfMounted(() { setStateIfMounted(() {
@@ -2376,7 +2376,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
if (context.mounted) { if (context.mounted) {
await navigateToVideoPlayer(context, metadata: firstEpisode); await navigateToVideoPlayer(context, metadata: firstEpisode);
// Refresh metadata when returning from video player // Refresh metadata when returning from video player
_loadFullMetadata(); unawaited(_loadFullMetadata());
} }
} catch (e) { } catch (e) {
// Close loading indicator if it's still open // Close loading indicator if it's still open
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -128,7 +129,7 @@ class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
); );
if (value != null) { if (value != null) {
await _settingsService.write(settings.SettingsService.appLocale, value); await _settingsService.write(settings.SettingsService.appLocale, value);
LocaleSettings.setLocale(value); unawaited(LocaleSettings.setLocale(value));
_restartApp(); _restartApp();
} }
}, },
+3
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
@@ -141,6 +142,7 @@ class _LogsScreenState extends State<LogsScreen> {
final data = response.data is String ? jsonDecode(response.data) : response.data; final data = response.data is String ? jsonDecode(response.data) : response.data;
final id = (data as Map<String, dynamic>)['id'] as String; final id = (data as Map<String, dynamic>)['id'] as String;
unawaited(
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
@@ -170,6 +172,7 @@ class _LogsScreenState extends State<LogsScreen> {
), ),
], ],
), ),
),
); );
} catch (_) { } catch (_) {
if (!mounted) return; if (!mounted) return;
+5 -3
View File
@@ -548,6 +548,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
final storageService = DownloadStorageService.instance; final storageService = DownloadStorageService.instance;
final isCustom = storageService.isUsingCustomPath(); final isCustom = storageService.isUsingCustomPath();
unawaited(
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
@@ -589,6 +590,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
), ),
], ],
), ),
),
); );
} }
@@ -725,7 +727,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
await _keyboardService?.resetToDefaults(); await _keyboardService?.resetToDefaults();
if (mounted) { if (mounted) {
showSuccessSnackBar(context, t.settings.resetSettingsSuccess); showSuccessSnackBar(context, t.settings.resetSettingsSuccess);
_loadSettings(); unawaited(_loadSettings());
} }
} }
@@ -766,7 +768,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
if (!mounted) return; if (!mounted) return;
if (result == null) return; // user cancelled file picker if (result == null) return; // user cancelled file picker
LocaleSettings.setLocale(_settingsService.read(settings.SettingsService.appLocale)); unawaited(LocaleSettings.setLocale(_settingsService.read(settings.SettingsService.appLocale)));
await Future.wait([ await Future.wait([
themeProvider.reload(), themeProvider.reload(),
settingsProvider.reload(), settingsProvider.reload(),
@@ -776,7 +778,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
unawaited(librariesProvider.refresh()); unawaited(librariesProvider.refresh());
if (!mounted) return; if (!mounted) return;
_loadSettings(); unawaited(_loadSettings());
showSuccessSnackBar(context, t.settings.importSettingsSuccess); showSuccessSnackBar(context, t.settings.importSettingsSuccess);
} on NoUserSignedInException { } on NoUserSignedInException {
if (mounted) showErrorSnackBar(context, t.settings.importSettingsNoUser); if (mounted) showErrorSnackBar(context, t.settings.importSettingsNoUser);
@@ -71,7 +71,7 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
} }
Future<void> _showColorPicker(String title, String currentColor, Function(String) onColorSelected) async { Future<void> _showColorPicker(String title, String currentColor, Function(String) onColorSelected) async {
Color initialColor = _hexToColor(currentColor); final Color initialColor = _hexToColor(currentColor);
final Color selectedColor = await showColorPickerDialog( final Color selectedColor = await showColorPickerDialog(
context, context,
+54 -44
View File
@@ -789,7 +789,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Apply saved volume (clamped to max volume) // Apply saved volume (clamped to max volume)
final savedVolume = settingsService.read(SettingsService.volume).clamp(0.0, maxVolume.toDouble()); final savedVolume = settingsService.read(SettingsService.volume).clamp(0.0, maxVolume.toDouble());
player!.setVolume(savedVolume); unawaited(player!.setVolume(savedVolume));
// Notify that player is ready // Notify that player is ready
if (mounted) { if (mounted) {
@@ -804,7 +804,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
// Enable wakelock to prevent screen from turning off during playback // Enable wakelock to prevent screen from turning off during playback
_setWakelock(true); unawaited(_setWakelock(true));
appLogger.d('Wakelock enabled for video playback'); appLogger.d('Wakelock enabled for video playback');
} }
@@ -822,8 +822,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
OrientationHelper.setLandscapeOrientation(); OrientationHelper.setLandscapeOrientation();
} else { } else {
// Unlocked: Allow all orientations immediately // Unlocked: Allow all orientations immediately
SystemChrome.setPreferredOrientations(DeviceOrientation.values); unawaited(SystemChrome.setPreferredOrientations(DeviceOrientation.values));
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky));
} }
} catch (e) { } catch (e) {
appLogger.w('Failed to set orientation', error: e); appLogger.w('Failed to set orientation', error: e);
@@ -903,7 +903,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_liveStreamFallbackLevel = 0; _liveStreamFallbackLevel = 0;
if (!_hasFirstFrame.value) { if (!_hasFirstFrame.value) {
_hasFirstFrame.value = true; _hasFirstFrame.value = true;
Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')); unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')));
// Apply frame rate matching on Android if enabled // Apply frame rate matching on Android if enabled
if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) { if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) {
@@ -958,7 +958,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await _initializeServices(); await _initializeServices();
// Load next/previous episodes (fire-and-forget) // Load next/previous episodes (fire-and-forget)
_loadAdjacentEpisodes(); unawaited(_loadAdjacentEpisodes());
} catch (e) { } catch (e) {
appLogger.e('Failed to initialize player', error: e); appLogger.e('Failed to initialize player', error: e);
if (mounted) { if (mounted) {
@@ -1038,11 +1038,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await player!.play(); await player!.play();
} }
unawaited(
Sentry.addBreadcrumb( Sentry.addBreadcrumb(
Breadcrumb( Breadcrumb(
message: 'Frame rate matching: ${fps}fps, switched=$didSwitch, delay=${delaySec}s', message: 'Frame rate matching: ${fps}fps, switched=$didSwitch, delay=${delaySec}s',
category: 'player', category: 'player',
), ),
),
); );
appLogger.d('Frame rate matching: Set display to ${fps}fps (duration: ${durationMs}ms, switched=$didSwitch)'); appLogger.d('Frame rate matching: Set display to ${fps}fps (duration: ${durationMs}ms, switched=$didSwitch)');
} catch (e) { } catch (e) {
@@ -1057,7 +1059,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
try { try {
await player!.clearVideoFrameRate(); await player!.clearVideoFrameRate();
await player!.setProperty('video-sync', 'audio'); await player!.setProperty('video-sync', 'audio');
Sentry.addBreadcrumb(Breadcrumb(message: 'Frame rate matching cleared', category: 'player')); unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Frame rate matching cleared', category: 'player')));
appLogger.d('Frame rate matching: Cleared, restored default display mode'); appLogger.d('Frame rate matching: Cleared, restored default display mode');
} catch (e) { } catch (e) {
appLogger.d('Failed to clear frame rate matching', error: e); appLogger.d('Failed to clear frame rate matching', error: e);
@@ -1241,9 +1243,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Start Discord Rich Presence for current media // Start Discord Rich Presence for current media
if (client != null) { if (client != null) {
DiscordRPCService.instance.startPlayback(_currentMetadata, client); unawaited(DiscordRPCService.instance.startPlayback(_currentMetadata, client));
TraktScrobbleService.instance.startPlayback(_currentMetadata, client, isLive: widget.isLive); unawaited(TraktScrobbleService.instance.startPlayback(_currentMetadata, client, isLive: widget.isLive));
TrackerCoordinator.instance.startPlayback(_currentMetadata, client, isLive: widget.isLive); unawaited(TrackerCoordinator.instance.startPlayback(_currentMetadata, client, isLive: widget.isLive));
} }
} }
@@ -1479,10 +1481,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
} catch (e) { } catch (e) {
appLogger.e('Failed to start live TV playback', error: e); appLogger.e('Failed to start live TV playback', error: e);
_sendLiveTimeline('stopped'); unawaited(_sendLiveTimeline('stopped'));
if (mounted) { if (mounted) {
showErrorSnackBar(context, e.toString()); showErrorSnackBar(context, e.toString());
_handleBackButton(); unawaited(_handleBackButton());
} }
} }
return; return;
@@ -1652,7 +1654,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_videoFilterManager?.enterPipMode(); _videoFilterManager?.enterPipMode();
}; };
if (player!.state.playing) { if (player!.state.playing) {
_videoPIPManager!.updateAutoPipState(isPlaying: true); unawaited(_videoPIPManager!.updateAutoPipState(isPlaying: true));
} }
} }
@@ -1747,11 +1749,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
} }
unawaited(
Sentry.addBreadcrumb( Sentry.addBreadcrumb(
Breadcrumb( Breadcrumb(
message: 'Pre-playback frame rate: ${preKnownFps}fps, switched=$didSwitch, delay=${delaySec}s', message: 'Pre-playback frame rate: ${preKnownFps}fps, switched=$didSwitch, delay=${delaySec}s',
category: 'player', category: 'player',
), ),
),
); );
} }
} }
@@ -2008,7 +2012,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Persist ambient lighting state // Persist ambient lighting state
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
settings.write(SettingsService.ambientLighting, ambientLighting.isEnabled); unawaited(settings.write(SettingsService.ambientLighting, ambientLighting.isEnabled));
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
@@ -2109,7 +2113,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Use same navigation as local episode change (pushReplacement from player context) // Use same navigation as local episode change (pushReplacement from player context)
_isReplacingWithVideo = true; _isReplacingWithVideo = true;
navigateToVideoPlayer(context, metadata: metadata, usePushReplacement: true); unawaited(navigateToVideoPlayer(context, metadata: metadata, usePushReplacement: true));
} }
void _setupCompanionRemoteCallbacks() { void _setupCompanionRemoteCallbacks() {
@@ -2150,23 +2154,23 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final maxVol = settings.read(SettingsService.maxVolume).toDouble(); final maxVol = settings.read(SettingsService.maxVolume).toDouble();
final newVolume = (player!.state.volume + 10).clamp(0.0, maxVol); final newVolume = (player!.state.volume + 10).clamp(0.0, maxVol);
player!.setVolume(newVolume); unawaited(player!.setVolume(newVolume));
settings.write(SettingsService.volume, newVolume); unawaited(settings.write(SettingsService.volume, newVolume));
}; };
receiver.onVolumeDown = () async { receiver.onVolumeDown = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final maxVol = settings.read(SettingsService.maxVolume).toDouble(); final maxVol = settings.read(SettingsService.maxVolume).toDouble();
final newVolume = (player!.state.volume - 10).clamp(0.0, maxVol); final newVolume = (player!.state.volume - 10).clamp(0.0, maxVol);
player!.setVolume(newVolume); unawaited(player!.setVolume(newVolume));
settings.write(SettingsService.volume, newVolume); unawaited(settings.write(SettingsService.volume, newVolume));
}; };
receiver.onVolumeMute = () async { receiver.onVolumeMute = () async {
if (player == null) return; if (player == null) return;
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
final newVolume = player!.state.volume > 0 ? 0.0 : 100.0; final newVolume = player!.state.volume > 0 ? 0.0 : 100.0;
player!.setVolume(newVolume); unawaited(player!.setVolume(newVolume));
settings.write(SettingsService.volume, newVolume); unawaited(settings.write(SettingsService.volume, newVolume));
}; };
receiver.onSubtitles = _cycleSubtitleTrack; receiver.onSubtitles = _cycleSubtitleTrack;
receiver.onAudioTracks = _cycleAudioTrack; receiver.onAudioTracks = _cycleAudioTrack;
@@ -2470,13 +2474,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// mpv does not flip the `pause` property on EOF, so _onPlayingStateChanged // mpv does not flip the `pause` property on EOF, so _onPlayingStateChanged
// never fires false. Normalize all playback-dependent state. // never fires false. Normalize all playback-dependent state.
_setWakelock(false); unawaited(_setWakelock(false));
_progressTracker?.sendProgress('paused'); unawaited(_progressTracker?.sendProgress('paused'));
_updateMediaControlsPlaybackState(); _updateMediaControlsPlaybackState();
DiscordRPCService.instance.pausePlayback(); unawaited(DiscordRPCService.instance.pausePlayback());
TraktScrobbleService.instance.pausePlayback(); unawaited(TraktScrobbleService.instance.pausePlayback());
if (_autoPipEnabled) { if (_autoPipEnabled) {
_videoPIPManager?.updateAutoPipState(isPlaying: false); unawaited(_videoPIPManager?.updateAutoPipState(isPlaying: false));
} }
if (_nextEpisode != null && !_showPlayNextDialog && !_showStillWatchingPrompt && !_completionTriggered) { if (_nextEpisode != null && !_showPlayNextDialog && !_showStillWatchingPrompt && !_completionTriggered) {
@@ -2484,7 +2488,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// PiP: skip dialog (user can't interact), auto-play immediately // PiP: skip dialog (user can't interact), auto-play immediately
if (PipService().isPipActive.value) { if (PipService().isPipActive.value) {
_playNext(); unawaited(_playNext());
return; return;
} }
@@ -2514,7 +2518,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
} else if (_nextEpisode == null && !_completionTriggered) { } else if (_nextEpisode == null && !_completionTriggered) {
_completionTriggered = true; _completionTriggered = true;
_handleBackButton(); unawaited(_handleBackButton());
} }
} }
@@ -2560,7 +2564,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Future<void> _showServerLimitDialog() async { Future<void> _showServerLimitDialog() async {
if (!mounted) return; if (!mounted) return;
await showServerLimitDialog(context); await showServerLimitDialog(context);
if (mounted) _handleBackButton(); if (mounted) unawaited(_handleBackButton());
} }
/// Handle notification when native player switched from ExoPlayer to MPV /// Handle notification when native player switched from ExoPlayer to MPV
@@ -2606,7 +2610,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Future<void> _restoreMediaControlsAfterResume() async { Future<void> _restoreMediaControlsAfterResume() async {
if (!_isPlayerInitialized || !mounted) return; if (!_isPlayerInitialized || !mounted) return;
_setWakelock(player?.state.isActive ?? false); unawaited(_setWakelock(player?.state.isActive ?? false));
final manager = _mediaControlsManager; final manager = _mediaControlsManager;
final currentPlayer = player; final currentPlayer = player;
@@ -2770,7 +2774,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
widget.liveDvrKey == null) { widget.liveDvrKey == null) {
appLogger.w('Cannot retry live stream — missing session info'); appLogger.w('Cannot retry live stream — missing session info');
showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed'); showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed');
_handleBackButton(); unawaited(_handleBackButton());
return; return;
} }
@@ -2783,7 +2787,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final tuneResult = await client.tuneChannel(widget.liveDvrKey!, channel.key); final tuneResult = await client.tuneChannel(widget.liveDvrKey!, channel.key);
if (tuneResult == null || !mounted) { if (tuneResult == null || !mounted) {
showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed'); showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed');
_handleBackButton(); unawaited(_handleBackButton());
return; return;
} }
@@ -2800,7 +2804,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
); );
if (streamPath == null || !mounted) { if (streamPath == null || !mounted) {
showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed'); showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed');
_handleBackButton(); unawaited(_handleBackButton());
return; return;
} }
@@ -3128,18 +3132,20 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_isReplacingWithVideo = true; _isReplacingWithVideo = true;
// Clear Discord Rich Presence + Trakt scrobble before switching episodes // Clear Discord Rich Presence + Trakt scrobble before switching episodes
DiscordRPCService.instance.stopPlayback(); unawaited(DiscordRPCService.instance.stopPlayback());
TraktScrobbleService.instance.stopPlayback(); unawaited(TraktScrobbleService.instance.stopPlayback());
TrackerCoordinator.instance.stopPlayback(); unawaited(TrackerCoordinator.instance.stopPlayback());
// If player isn't available, navigate without preserving settings // If player isn't available, navigate without preserving settings
if (player == null) { if (player == null) {
if (mounted) { if (mounted) {
unawaited(
navigateToVideoPlayer( navigateToVideoPlayer(
context, context,
metadata: episodeMetadata, metadata: episodeMetadata,
usePushReplacement: true, usePushReplacement: true,
isOffline: widget.isOffline, isOffline: widget.isOffline,
),
); );
} }
return; return;
@@ -3150,11 +3156,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
if (currentPlayer == null) { if (currentPlayer == null) {
// Player already disposed, navigate without preserving settings // Player already disposed, navigate without preserving settings
if (mounted) { if (mounted) {
unawaited(
navigateToVideoPlayer( navigateToVideoPlayer(
context, context,
metadata: episodeMetadata, metadata: episodeMetadata,
usePushReplacement: true, usePushReplacement: true,
isOffline: widget.isOffline, isOffline: widget.isOffline,
),
); );
} }
return; return;
@@ -3165,7 +3173,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final currentSecondarySubtitleTrack = currentPlayer.state.track.secondarySubtitle; final currentSecondarySubtitleTrack = currentPlayer.state.track.secondarySubtitle;
// Pause and stop current playback // Pause and stop current playback
currentPlayer.pause(); unawaited(currentPlayer.pause());
await _progressTracker?.sendProgress('stopped'); await _progressTracker?.sendProgress('stopped');
_progressTracker?.stopTracking(); _progressTracker?.stopTracking();
@@ -3174,6 +3182,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Navigate to the episode using pushReplacement to destroy current player // Navigate to the episode using pushReplacement to destroy current player
if (mounted) { if (mounted) {
unawaited(
navigateToVideoPlayer( navigateToVideoPlayer(
context, context,
metadata: episodeMetadata, metadata: episodeMetadata,
@@ -3182,6 +3191,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack, preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack,
usePushReplacement: true, usePushReplacement: true,
isOffline: widget.isOffline, isOffline: widget.isOffline,
),
); );
} }
} }
@@ -3209,9 +3219,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_progressTracker?.stopTracking(); _progressTracker?.stopTracking();
_progressTracker?.dispose(); _progressTracker?.dispose();
_progressTracker = null; _progressTracker = null;
DiscordRPCService.instance.stopPlayback(); unawaited(DiscordRPCService.instance.stopPlayback());
TraktScrobbleService.instance.stopPlayback(); unawaited(TraktScrobbleService.instance.stopPlayback());
TrackerCoordinator.instance.stopPlayback(); unawaited(TrackerCoordinator.instance.stopPlayback());
_currentMetadata = episodeMetadata; _currentMetadata = episodeMetadata;
_activeRatingKey = episodeMetadata.ratingKey; _activeRatingKey = episodeMetadata.ratingKey;
@@ -3323,9 +3333,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
if (client != null) { if (client != null) {
DiscordRPCService.instance.startPlayback(episodeMetadata, client); unawaited(DiscordRPCService.instance.startPlayback(episodeMetadata, client));
TraktScrobbleService.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive); unawaited(TraktScrobbleService.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive));
TrackerCoordinator.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive); unawaited(TrackerCoordinator.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive));
} }
try { try {
@@ -3337,7 +3347,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await _loadAdjacentEpisodes(); await _loadAdjacentEpisodes();
if (_autoPipEnabled) { if (_autoPipEnabled) {
_videoPIPManager?.updateAutoPipState(isPlaying: currentPlayer.state.playing); unawaited(_videoPIPManager?.updateAutoPipState(isPlaying: currentPlayer.state.playing));
} }
} catch (e) { } catch (e) {
_isSwappingEpisode = false; _isSwappingEpisode = false;
@@ -153,7 +153,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
} else { } else {
request.response.statusCode = HttpStatus.notFound; request.response.statusCode = HttpStatus.notFound;
request.response.close(); unawaited(request.response.close());
} }
}); });
@@ -229,7 +229,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
deviceName == null || deviceName == null ||
platform == null) { platform == null) {
socket.add(jsonEncode({'type': 'authFailed'})); socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed'); unawaited(socket.close(4003, 'Authentication failed'));
return; return;
} }
@@ -240,7 +240,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_recordFailedAuth(sourceIp); _recordFailedAuth(sourceIp);
appLogger.w('CompanionRemote: Auth failed — unknown user'); appLogger.w('CompanionRemote: Auth failed — unknown user');
socket.add(jsonEncode({'type': 'authFailed'})); socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed'); unawaited(socket.close(4003, 'Authentication failed'));
return; return;
} }
@@ -261,7 +261,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_recordFailedAuth(sourceIp); _recordFailedAuth(sourceIp);
appLogger.w('CompanionRemote: Auth failed — invalid auth tag'); appLogger.w('CompanionRemote: Auth failed — invalid auth tag');
socket.add(jsonEncode({'type': 'authFailed'})); socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed'); unawaited(socket.close(4003, 'Authentication failed'));
return; return;
} }
@@ -275,7 +275,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
// Close existing client if present // Close existing client if present
if (_clientSocket != null) { if (_clientSocket != null) {
appLogger.d('CompanionRemote: Replacing existing client connection'); appLogger.d('CompanionRemote: Replacing existing client connection');
_clientSocket!.close(4004, 'Replaced by new connection'); unawaited(_clientSocket!.close(4004, 'Replaced by new connection'));
} }
_clientSocket = socket; _clientSocket = socket;
@@ -298,7 +298,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
sendDeviceInfo(hostDeviceName, hostPlatform); sendDeviceInfo(hostDeviceName, hostPlatform);
} else { } else {
appLogger.w('CompanionRemote: Expected auth, got ${json['type']}'); appLogger.w('CompanionRemote: Expected auth, got ${json['type']}');
socket.close(4002, 'Authentication required'); unawaited(socket.close(4002, 'Authentication required'));
} }
} else { } else {
// Encrypted command — data is binary // Encrypted command — data is binary
+4 -4
View File
@@ -112,7 +112,7 @@ class DiscordRPCService {
if (_isEnabled && _isConnected) { if (_isEnabled && _isConnected) {
// Upload thumbnail in background, don't block playback // Upload thumbnail in background, don't block playback
_uploadThumbnailAndUpdatePresence(); unawaited(_uploadThumbnailAndUpdatePresence());
} }
} }
@@ -183,7 +183,7 @@ class DiscordRPCService {
/// Clear the presence /// Clear the presence
Future<void> clearPresence() async { Future<void> clearPresence() async {
try { try {
_rpc?.clearPresence(); unawaited(_rpc?.clearPresence());
} catch (e) { } catch (e) {
appLogger.d('Failed to clear Discord presence', error: e); appLogger.d('Failed to clear Discord presence', error: e);
} }
@@ -237,7 +237,7 @@ class DiscordRPCService {
_disconnectedSubscription = null; _disconnectedSubscription = null;
_errorSubscription = null; _errorSubscription = null;
try { try {
_rpc?.dispose(); unawaited(_rpc?.dispose());
} catch (e) { } catch (e) {
appLogger.d('DiscordRPC: dispose ignored', error: e); appLogger.d('DiscordRPC: dispose ignored', error: e);
} }
@@ -258,7 +258,7 @@ class DiscordRPCService {
_errorSubscription = null; _errorSubscription = null;
try { try {
_rpc?.dispose(); unawaited(_rpc?.dispose());
} catch (e) { } catch (e) {
appLogger.d('Error disposing Discord RPC', error: e); appLogger.d('Error disposing Discord RPC', error: e);
} }
+11 -11
View File
@@ -201,11 +201,11 @@ class DownloadManagerService {
/// then scans drift for orphaned items. /// then scans drift for orphaned items.
Future<void> recoverInterruptedDownloads() async { Future<void> recoverInterruptedDownloads() async {
try { try {
Sentry.addBreadcrumb(Breadcrumb(message: 'Initializing FileDownloader', category: 'downloads')); unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Initializing FileDownloader', category: 'downloads')));
await _initializeFileDownloader(); await _initializeFileDownloader();
// Let background_downloader re-enqueue tasks killed by the OS // Let background_downloader re-enqueue tasks killed by the OS
Sentry.addBreadcrumb(Breadcrumb(message: 'Rescheduling killed tasks', category: 'downloads')); unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Rescheduling killed tasks', category: 'downloads')));
final (rescheduled, _) = await FileDownloader().rescheduleKilledTasks(); final (rescheduled, _) = await FileDownloader().rescheduleKilledTasks();
if (rescheduled.isNotEmpty) { if (rescheduled.isNotEmpty) {
appLogger.i('Rescheduled ${rescheduled.length} killed download task(s)'); appLogger.i('Rescheduled ${rescheduled.length} killed download task(s)');
@@ -251,7 +251,7 @@ class DownloadManagerService {
} }
// Scan drift for orphaned items stuck in 'downloading' // Scan drift for orphaned items stuck in 'downloading'
Sentry.addBreadcrumb(Breadcrumb(message: 'Scanning for orphaned downloads', category: 'downloads')); unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Scanning for orphaned downloads', category: 'downloads')));
final allDownloads = await _database.select(_database.downloadedMedia).get(); final allDownloads = await _database.select(_database.downloadedMedia).get();
for (final item in allDownloads) { for (final item in allDownloads) {
if (item.status == DownloadStatus.downloading.index) { if (item.status == DownloadStatus.downloading.index) {
@@ -496,7 +496,7 @@ class DownloadManagerService {
_emitProgress(globalKey, DownloadStatus.queued, 0); _emitProgress(globalKey, DownloadStatus.queued, 0);
// Start processing if not already // Start processing if not already
_processQueue(client); unawaited(_processQueue(client));
} }
/// Process the download queue — prepares and enqueues items with background_downloader. /// Process the download queue — prepares and enqueues items with background_downloader.
@@ -812,7 +812,7 @@ class DownloadManagerService {
await _transitionStatus(globalKey, DownloadStatus.queued); await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey); await _database.addToQueue(mediaGlobalKey: globalKey);
final client = _getClient(parseGlobalKey(globalKey)?.serverId); final client = _getClient(parseGlobalKey(globalKey)?.serverId);
if (client != null) _processQueue(client); if (client != null) unawaited(_processQueue(client));
} }
/// Handle a failed download — auto-retry if retries remain, otherwise permanently fail. /// Handle a failed download — auto-retry if retries remain, otherwise permanently fail.
@@ -860,7 +860,7 @@ class DownloadManagerService {
// Only advance the queue if the download actually started transferring. // Only advance the queue if the download actually started transferring.
// Instant failures (DNS, connection) would just cause the next item to fail too. // Instant failures (DNS, connection) would just cause the next item to fail too.
if (hadProgress) _processQueue(client); if (hadProgress) unawaited(_processQueue(client));
} else { } else {
if (isNetworkError) { if (isNetworkError) {
appLogger.w('Network error for $globalKey, failing permanently (no auto-retry): $errorMessage'); appLogger.w('Network error for $globalKey, failing permanently (no auto-retry): $errorMessage');
@@ -891,7 +891,7 @@ class DownloadManagerService {
// Try to enqueue more items from the queue // Try to enqueue more items from the queue
final client = _getClient(parseGlobalKey(globalKey)?.serverId); final client = _getClient(parseGlobalKey(globalKey)?.serverId);
if (client != null) _processQueue(client); if (client != null) unawaited(_processQueue(client));
} }
/// Execute an app-level auto-retry: transition back to queued and re-enqueue. /// Execute an app-level auto-retry: transition back to queued and re-enqueue.
@@ -913,7 +913,7 @@ class DownloadManagerService {
await _database.updateBgTaskId(globalKey, null); await _database.updateBgTaskId(globalKey, null);
await _transitionStatus(globalKey, DownloadStatus.queued); await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey); await _database.addToQueue(mediaGlobalKey: globalKey);
_processQueue(client); unawaited(_processQueue(client));
} }
/// Handle a completed video download — store path, download supplementary content, mark done. /// Handle a completed video download — store path, download supplementary content, mark done.
@@ -1034,7 +1034,7 @@ class DownloadManagerService {
_completingKeys.remove(globalKey); _completingKeys.remove(globalKey);
// Always advance the queue, even after errors // Always advance the queue, even after errors
final nextClient = _getClient(parseGlobalKey(globalKey)?.serverId); final nextClient = _getClient(parseGlobalKey(globalKey)?.serverId);
if (nextClient != null) _processQueue(nextClient); if (nextClient != null) unawaited(_processQueue(nextClient));
} }
} }
@@ -1372,7 +1372,7 @@ class DownloadManagerService {
await _transitionStatus(globalKey, DownloadStatus.queued); await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey); await _database.addToQueue(mediaGlobalKey: globalKey);
final resolvedClient = _getClient(parseGlobalKey(globalKey)?.serverId) ?? client; final resolvedClient = _getClient(parseGlobalKey(globalKey)?.serverId) ?? client;
_processQueue(resolvedClient); unawaited(_processQueue(resolvedClient));
} }
/// Retry a failed download /// Retry a failed download
@@ -1384,7 +1384,7 @@ class DownloadManagerService {
await _transitionStatus(globalKey, DownloadStatus.queued); await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey); await _database.addToQueue(mediaGlobalKey: globalKey);
final resolvedClient = _getClient(parseGlobalKey(globalKey)?.serverId) ?? client; final resolvedClient = _getClient(parseGlobalKey(globalKey)?.serverId) ?? client;
_processQueue(resolvedClient); unawaited(_processQueue(resolvedClient));
} }
/// Cancel a download /// Cancel a download
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -89,6 +91,7 @@ class EpisodeNavigationService {
// Navigate to the new episode // Navigate to the new episode
if (context.mounted) { if (context.mounted) {
unawaited(
navigateToVideoPlayer( navigateToVideoPlayer(
context, context,
metadata: episode, metadata: episode,
@@ -96,6 +99,7 @@ class EpisodeNavigationService {
preferredSubtitleTrack: currentSubtitleTrack, preferredSubtitleTrack: currentSubtitleTrack,
preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack, preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack,
usePushReplacement: usePushReplacement, usePushReplacement: usePushReplacement,
),
); );
} }
} }
+4 -4
View File
@@ -86,7 +86,7 @@ class GamepadService with WindowListener {
_windowFocused = await windowManager.isFocused(); _windowFocused = await windowManager.isFocused();
} }
_subscription?.cancel(); unawaited(_subscription?.cancel());
_subscription = Gamepad.instance.events.listen( _subscription = Gamepad.instance.events.listen(
_handleGamepadEvent, _handleGamepadEvent,
onError: (e) => appLogger.e('GamepadService: Stream error', error: e), onError: (e) => appLogger.e('GamepadService: Stream error', error: e),
@@ -138,11 +138,11 @@ class GamepadService with WindowListener {
void _handleGamepadEvent(GamepadEvent event) { void _handleGamepadEvent(GamepadEvent event) {
switch (event) { switch (event) {
case GamepadConnectionEvent e: case final GamepadConnectionEvent e:
appLogger.i('GamepadService: Gamepad ${e.connected ? "connected" : "disconnected"}: ${e.info.name}'); appLogger.i('GamepadService: Gamepad ${e.connected ? "connected" : "disconnected"}: ${e.info.name}');
case GamepadButtonEvent e: case final GamepadButtonEvent e:
_handleButton(e); _handleButton(e);
case GamepadAxisEvent e: case final GamepadAxisEvent e:
_handleAxis(e); _handleAxis(e);
} }
} }
+8 -2
View File
@@ -206,7 +206,9 @@ class MultiServerManager {
} }
appLogger.i('Connecting to ${servers.length} servers...'); appLogger.i('Connecting to ${servers.length} servers...');
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'servers')); unawaited(
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'servers')),
);
// Re-use the persisted client ID so Plex doesn't see a "new device" on // Re-use the persisted client ID so Plex doesn't see a "new device" on
// every reconnect. // every reconnect.
@@ -531,7 +533,11 @@ class MultiServerManager {
if (offline.isEmpty) return; if (offline.isEmpty) return;
appLogger.d('Attempting reconnection for ${offline.length} offline servers'); appLogger.d('Attempting reconnection for ${offline.length} offline servers');
Sentry.addBreadcrumb(Breadcrumb(message: 'Reconnecting ${offline.length} offline server(s)', category: 'servers')); unawaited(
Sentry.addBreadcrumb(
Breadcrumb(message: 'Reconnecting ${offline.length} offline server(s)', category: 'servers'),
),
);
if (forceRediscovery) { if (forceRediscovery) {
final storage = await StorageService.getInstance(); final storage = await StorageService.getInstance();
+4
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -253,6 +255,7 @@ class PlayQueueLauncher {
// Show loading indicator // Show loading indicator
if (showLoading && context.mounted) { if (showLoading && context.mounted) {
loadingVisible = true; loadingVisible = true;
unawaited(
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -260,6 +263,7 @@ class PlayQueueLauncher {
loadingDialogContext = dialogContext; loadingDialogContext = dialogContext;
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
}, },
),
); );
} }
+3 -1
View File
@@ -128,6 +128,7 @@ class PlaybackProgressTracker {
_resetBackoff(); _resetBackoff();
} else { } else {
// Fire-and-forget for playing/paused — avoid blocking the Dart event loop // Fire-and-forget for playing/paused — avoid blocking the Dart event loop
unawaited(
_sendOnlineProgress(state, position, duration) _sendOnlineProgress(state, position, duration)
.then((_) { .then((_) {
_resetBackoff(); _resetBackoff();
@@ -141,7 +142,8 @@ class PlaybackProgressTracker {
'skipping next $_ticksToSkip tick(s)', 'skipping next $_ticksToSkip tick(s)',
error: e, error: e,
); );
}); }),
);
} }
// Emit watch state event on stop for UI updates across screens. // Emit watch state event on stop for UI updates across screens.
+3 -1
View File
@@ -462,6 +462,7 @@ class PlexServer {
appLogger.d('Running connection race to find first working endpoint', error: {'candidateCount': totalCandidates}); appLogger.d('Running connection race to find first working endpoint', error: {'candidateCount': totalCandidates});
for (final candidate in candidates) { for (final candidate in candidates) {
unawaited(
PlexClient.testConnectionWithLatency( PlexClient.testConnectionWithLatency(
candidate.url, candidate.url,
accessToken, accessToken,
@@ -491,7 +492,8 @@ class PlexServer {
if (completedTests == candidates.length && !completer.isCompleted) { if (completedTests == candidates.length && !completer.isCompleted) {
completer.complete(null); completer.complete(null);
} }
}); }),
);
} }
firstCandidate = await completer.future; firstCandidate = await completer.future;
+8 -8
View File
@@ -922,7 +922,7 @@ class PlexClient {
if (streams == null) return (audio: audioTracks, subtitles: subtitleTracks, frameRate: frameRate); if (streams == null) return (audio: audioTracks, subtitles: subtitleTracks, frameRate: frameRate);
for (var stream in streams) { for (final stream in streams) {
final streamType = stream['streamType'] as int?; final streamType = stream['streamType'] as int?;
if (streamType == PlexStreamType.video) { if (streamType == PlexStreamType.video) {
@@ -1382,7 +1382,7 @@ class PlexClient {
Map<String, dynamic>? videoStream; Map<String, dynamic>? videoStream;
Map<String, dynamic>? audioStream; Map<String, dynamic>? audioStream;
for (var stream in streams) { for (final stream in streams) {
final streamType = stream['streamType'] as int?; final streamType = stream['streamType'] as int?;
if (streamType == PlexStreamType.video && videoStream == null) { if (streamType == PlexStreamType.video && videoStream == null) {
videoStream = stream; videoStream = stream;
@@ -2801,13 +2801,13 @@ class PlexClient {
: (timeline is Map ? timeline : null); : (timeline is Map ? timeline : null);
if (op is Map) { if (op is Map) {
if (op['Metadata'] case [Map firstMetadata, ...]) { if (op['Metadata'] case [final Map firstMetadata, ...]) {
if (firstMetadata['Media'] case [Map firstMedia, ...]) { if (firstMetadata['Media'] case [final Map firstMedia, ...]) {
final rawBeginsAt = firstMedia['beginsAt']; final rawBeginsAt = firstMedia['beginsAt'];
beginsAt = switch (rawBeginsAt) { beginsAt = switch (rawBeginsAt) {
num n => n.toInt(), final num n => n.toInt(),
String s => int.tryParse(s), final String s => int.tryParse(s),
_ => null, _ => null,
}; };
@@ -2878,8 +2878,8 @@ class PlexClient {
if (firstMedia is Map<String, dynamic>) { if (firstMedia is Map<String, dynamic>) {
final rawBeginsAt = firstMedia['beginsAt']; final rawBeginsAt = firstMedia['beginsAt'];
beginsAt = switch (rawBeginsAt) { beginsAt = switch (rawBeginsAt) {
num n => n.toInt(), final num n => n.toInt(),
String s => int.tryParse(s), final String s => int.tryParse(s),
_ => null, _ => null,
}; };
} }
+1 -1
View File
@@ -122,7 +122,7 @@ class TrackManager {
// play() failed — clear the flag immediately since playbackRestart won't fire // play() failed — clear the flag immediately since playbackRestart won't fire
appLogger.w('Resume after subtitle load failed, applying track selection directly', error: e); appLogger.w('Resume after subtitle load failed, applying track selection directly', error: e);
waitingForExternalSubsTrackSelection = false; waitingForExternalSubsTrackSelection = false;
applyTrackSelection(); unawaited(applyTrackSelection());
return; return;
} }
+8 -8
View File
@@ -418,21 +418,21 @@ class TrackSelectionService {
final preferredLanguage = getLanguage(preferred); final preferredLanguage = getLanguage(preferred);
// Try to match: id, title, and language // Try to match: id, title, and language
for (var track in validTracks) { for (final track in validTracks) {
if (getId(track) == preferredId && getTitle(track) == preferredTitle && getLanguage(track) == preferredLanguage) { if (getId(track) == preferredId && getTitle(track) == preferredTitle && getLanguage(track) == preferredLanguage) {
return track; return track;
} }
} }
// Try to match: title and language // Try to match: title and language
for (var track in validTracks) { for (final track in validTracks) {
if (getTitle(track) == preferredTitle && getLanguage(track) == preferredLanguage) { if (getTitle(track) == preferredTitle && getLanguage(track) == preferredLanguage) {
return track; return track;
} }
} }
// Try to match: language only // Try to match: language only
for (var track in validTracks) { for (final track in validTracks) {
if (getLanguage(track) == preferredLanguage) { if (getLanguage(track) == preferredLanguage) {
return track; return track;
} }
@@ -490,7 +490,7 @@ class TrackSelectionService {
String Function(T) _, String Function(T) _,
String _, String _,
) { ) {
for (var track in tracks) { for (final track in tracks) {
final trackLang = getLanguage(track)?.toLowerCase(); final trackLang = getLanguage(track)?.toLowerCase();
if (trackLang != null && languageVariations.any((lang) => trackLang.startsWith(lang))) { if (trackLang != null && languageVariations.any((lang) => trackLang.startsWith(lang))) {
return track; return track;
@@ -682,7 +682,7 @@ class TrackSelectionService {
appLogger.d( appLogger.d(
'Audio: ${selectedAudioTrack.title ?? selectedAudioTrack.language ?? "Track ${selectedAudioTrack.id}"} [${audioResult.priority.name}]', 'Audio: ${selectedAudioTrack.title ?? selectedAudioTrack.language ?? "Track ${selectedAudioTrack.id}"} [${audioResult.priority.name}]',
); );
player.selectAudioTrack(selectedAudioTrack); unawaited(player.selectAudioTrack(selectedAudioTrack));
// Save to Plex if this was user's navigation preference (Priority 1) // Save to Plex if this was user's navigation preference (Priority 1)
if (audioResult.priority == TrackSelectionPriority.navigation && onAudioTrackChanged != null) { if (audioResult.priority == TrackSelectionPriority.navigation && onAudioTrackChanged != null) {
@@ -697,7 +697,7 @@ class TrackSelectionService {
? 'OFF' ? 'OFF'
: (selectedSubtitleTrack.title ?? selectedSubtitleTrack.language ?? 'Track ${selectedSubtitleTrack.id}'); : (selectedSubtitleTrack.title ?? selectedSubtitleTrack.language ?? 'Track ${selectedSubtitleTrack.id}');
appLogger.d('Subtitle: $subtitleName [${subtitleResult.priority.name}]'); appLogger.d('Subtitle: $subtitleName [${subtitleResult.priority.name}]');
player.selectSubtitleTrack(selectedSubtitleTrack); unawaited(player.selectSubtitleTrack(selectedSubtitleTrack));
// Save to Plex if this was user's navigation preference (Priority 1) // Save to Plex if this was user's navigation preference (Priority 1)
if (subtitleResult.priority == TrackSelectionPriority.navigation && onSubtitleTrackChanged != null) { if (subtitleResult.priority == TrackSelectionPriority.navigation && onSubtitleTrackChanged != null) {
@@ -714,13 +714,13 @@ class TrackSelectionService {
appLogger.d( appLogger.d(
'Secondary subtitle: ${secondaryMatch.title ?? secondaryMatch.language ?? "Track ${secondaryMatch.id}"}', 'Secondary subtitle: ${secondaryMatch.title ?? secondaryMatch.language ?? "Track ${secondaryMatch.id}"}',
); );
player.selectSecondarySubtitleTrack(secondaryMatch); unawaited(player.selectSecondarySubtitleTrack(secondaryMatch));
} }
} }
// Apply default playback speed from settings // Apply default playback speed from settings
if (defaultPlaybackSpeed != null && defaultPlaybackSpeed != 1.0) { if (defaultPlaybackSpeed != null && defaultPlaybackSpeed != 1.0) {
player.setRate(defaultPlaybackSpeed); unawaited(player.setRate(defaultPlaybackSpeed));
} }
} }
} }
+1
View File
@@ -108,6 +108,7 @@ ThemeData monoTheme({required bool dark, bool oled = false}) {
thumbShape: const HandleThumbShape(), thumbShape: const HandleThumbShape(),
trackShape: const GappedTrackShape(), trackShape: const GappedTrackShape(),
tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 2), tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 2),
// ignore: deprecated_member_use — opting into the 2024 slider appearance until the default flips
year2023: false, year2023: false,
), ),
dividerTheme: DividerThemeData(space: 0, thickness: 1, color: c.outline), dividerTheme: DividerThemeData(space: 0, thickness: 1, color: c.outline),
+8 -8
View File
@@ -3,8 +3,8 @@
/// to handle Plex API responses where numeric fields may arrive as strings /// to handle Plex API responses where numeric fields may arrive as strings
/// (XML-to-JSON conversion). /// (XML-to-JSON conversion).
int? flexibleInt(Object? v) => switch (v) { int? flexibleInt(Object? v) => switch (v) {
num n => n.toInt(), final num n => n.toInt(),
String s => int.tryParse(s), final String s => int.tryParse(s),
_ => null, _ => null,
}; };
@@ -12,16 +12,16 @@ int? flexibleInt(Object? v) => switch (v) {
/// Returns `false` for `null` or unrecognised values. /// Returns `false` for `null` or unrecognised values.
/// Handles Plex API responses where boolean fields may arrive as integers. /// Handles Plex API responses where boolean fields may arrive as integers.
bool flexibleBool(Object? v) => switch (v) { bool flexibleBool(Object? v) => switch (v) {
bool b => b, final bool b => b,
int n => n == 1, final int n => n == 1,
String s => s == '1', final String s => s == '1',
_ => false, _ => false,
}; };
/// Parse a value that may be [double], [num], or [String] to [double]. /// Parse a value that may be [double], [num], or [String] to [double].
double? flexibleDouble(Object? v) => switch (v) { double? flexibleDouble(Object? v) => switch (v) {
num n => n.toDouble(), final num n => n.toDouble(),
String s => double.tryParse(s), final String s => double.tryParse(s),
_ => null, _ => null,
}; };
@@ -36,6 +36,6 @@ Object? readStringField(Map json, String key) => json[key]?.toString();
/// Returns `null` when the value is `null`. /// Returns `null` when the value is `null`.
List<dynamic>? flexibleList(Object? v) => switch (v) { List<dynamic>? flexibleList(Object? v) => switch (v) {
null => null, null => null,
List l => l, final List l => l,
_ => <dynamic>[v], _ => <dynamic>[v],
}; };
+3 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/livetv_channel.dart'; import '../models/livetv_channel.dart';
@@ -43,5 +45,5 @@ Future<void> navigateToLiveTv(
reverseTransitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero,
); );
navigator.push<bool>(route); unawaited(navigator.push<bool>(route));
} }
@@ -348,10 +348,10 @@ class WatchTogetherProvider with ChangeNotifier {
_syncManager?.announceLeave(); _syncManager?.announceLeave();
// Clean up subscriptions // Clean up subscriptions
_peerConnectedSubscription?.cancel(); unawaited(_peerConnectedSubscription?.cancel());
_peerDisconnectedSubscription?.cancel(); unawaited(_peerDisconnectedSubscription?.cancel());
_messageSubscription?.cancel(); unawaited(_messageSubscription?.cancel());
_errorSubscription?.cancel(); unawaited(_errorSubscription?.cancel());
_peerConnectedSubscription = null; _peerConnectedSubscription = null;
_peerDisconnectedSubscription = null; _peerDisconnectedSubscription = null;
@@ -432,7 +432,7 @@ class WatchTogetherPeerService with KeepaliveMixin {
_reconnectTimer = null; _reconnectTimer = null;
stopKeepalive(); stopKeepalive();
_channelSubscription?.cancel(); unawaited(_channelSubscription?.cancel());
_channelSubscription = null; _channelSubscription = null;
try { try {
@@ -276,7 +276,7 @@ class _SessionMenuSheet extends StatelessWidget {
); );
if (confirmed) { if (confirmed) {
provider.leaveSession(); unawaited(provider.leaveSession());
onLeaveSession?.call(); onLeaveSession?.call();
} }
} }
+1 -1
View File
@@ -116,7 +116,7 @@ class _FocusableListTileState extends State<FocusableListTile> with FocusableTil
final textColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.textColor; final textColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.textColor;
final iconColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.iconColor; final iconColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.iconColor;
Widget tile = MouseRegion( final Widget tile = MouseRegion(
onEnter: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = true) : null, onEnter: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = true) : null,
onExit: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = false) : null, onExit: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = false) : null,
child: ListTile( child: ListTile(
+1 -1
View File
@@ -37,7 +37,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
final PhysicalKeyboardKey key = keyEvent.physicalKey; final PhysicalKeyboardKey key = keyEvent.physicalKey;
// Detect which modifiers are currently held down, excluding the primary key // Detect which modifiers are currently held down, excluding the primary key
List<HotKeyModifier> modifiers = HotKeyModifier.values final List<HotKeyModifier> modifiers = HotKeyModifier.values
.where((m) => m.physicalKeys.any(physicalKeysPressed.contains)) .where((m) => m.physicalKeys.any(physicalKeysPressed.contains))
.where((m) => !m.physicalKeys.contains(key)) .where((m) => !m.physicalKeys.contains(key))
.toList(); .toList();
+1 -1
View File
@@ -143,7 +143,7 @@ class _ServerActivitiesButtonState extends State<ServerActivitiesButton> {
_pollTimer?.cancel(); _pollTimer?.cancel();
_pollTimer = null; _pollTimer = null;
_panelNotifier.value = _PanelData.loading; _panelNotifier.value = _PanelData.loading;
_fetchActivities(); unawaited(_fetchActivities());
} }
Widget _buildOverlay(BuildContext overlayContext, {required double right, required double top}) { Widget _buildOverlay(BuildContext overlayContext, {required double right, required double top}) {
@@ -26,8 +26,8 @@ class TrackFilterHelper {
static bool _isAllowedTrack<T>(T track) { static bool _isAllowedTrack<T>(T track) {
final id = switch (track) { final id = switch (track) {
AudioTrack t => t.id, final AudioTrack t => t.id,
SubtitleTrack t => t.id, final SubtitleTrack t => t.id,
_ => '', _ => '',
}; };
@@ -559,7 +559,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
title: Text(label, style: TextStyle(color: isSelected ? primary : null)), title: Text(label, style: TextStyle(color: isSelected ? primary : null)),
trailing: isSelected ? AppIcon(Symbols.check_rounded, fill: 1, color: primary) : null, trailing: isSelected ? AppIcon(Symbols.check_rounded, fill: 1, color: primary) : null,
onTap: () async { onTap: () async {
widget.player.setRate(speed); await widget.player.setRate(speed);
// Save as default playback speed // Save as default playback speed
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.write(SettingsService.defaultPlaybackSpeed, speed); await settings.write(SettingsService.defaultPlaybackSpeed, speed);
+10 -6
View File
@@ -669,9 +669,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Apply rotation lock setting // Apply rotation lock setting
if (_isRotationLocked) { if (_isRotationLocked) {
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); unawaited(
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]),
);
} else { } else {
SystemChrome.setPreferredOrientations(DeviceOrientation.values); unawaited(SystemChrome.setPreferredOrientations(DeviceOrientation.values));
} }
} }
} }
@@ -994,10 +996,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
if (_isRotationLocked) { if (_isRotationLocked) {
// Locked: Allow landscape orientations only // Locked: Allow landscape orientations only
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); await SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
} else { } else {
// Unlocked: Allow all orientations including portrait // Unlocked: Allow all orientations including portrait
SystemChrome.setPreferredOrientations(DeviceOrientation.values); await SystemChrome.setPreferredOrientations(DeviceOrientation.values);
} }
} }
@@ -1139,7 +1141,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
if (metadataJson != null) { if (metadataJson != null) {
// Parse chapters // Parse chapters
if (metadataJson['Chapter'] != null) { if (metadataJson['Chapter'] != null) {
for (var chapter in metadataJson['Chapter'] as List) { for (final chapter in metadataJson['Chapter'] as List) {
chapters.add( chapters.add(
PlexChapter( PlexChapter(
id: chapter['id'] as int, id: chapter['id'] as int,
@@ -1155,7 +1157,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Parse markers // Parse markers
if (metadataJson['Marker'] != null) { if (metadataJson['Marker'] != null) {
for (var marker in metadataJson['Marker'] as List) { for (final marker in metadataJson['Marker'] as List) {
markers.add( markers.add(
PlexMarker( PlexMarker(
id: marker['id'] as int, id: marker['id'] as int,
@@ -2611,6 +2613,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Navigate to new player screen with the updated selection // Navigate to new player screen with the updated selection
// Use PageRouteBuilder with zero-duration transitions to prevent orientation reset // Use PageRouteBuilder with zero-duration transitions to prevent orientation reset
if (mounted) { if (mounted) {
unawaited(
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
PageRouteBuilder<bool>( PageRouteBuilder<bool>(
@@ -2625,6 +2628,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
transitionDuration: Duration.zero, transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero,
), ),
),
); );
} }
} catch (e) { } catch (e) {
@@ -67,7 +67,7 @@ class VideoControlsHeader extends StatelessWidget {
final seriesName = metadata.grandparentTitle ?? metadata.title!; final seriesName = metadata.grandparentTitle ?? metadata.title!;
final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null; final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null;
List<String> parts = [seriesName]; final List<String> parts = [seriesName];
if (hasEpisodeInfo) { if (hasEpisodeInfo) {
parts.add('S${metadata.parentIndex}E${metadata.index}'); parts.add('S${metadata.parentIndex}E${metadata.index}');
@@ -83,7 +83,7 @@ class VideoControlsHeader extends StatelessWidget {
} }
Widget _buildMultiLineTitle() { Widget _buildMultiLineTitle() {
List<String> secondLineParts = []; final List<String> secondLineParts = [];
if (metadata.parentIndex != null && metadata.index != null) { if (metadata.parentIndex != null && metadata.index != null) {
secondLineParts.add('S${metadata.parentIndex}'); secondLineParts.add('S${metadata.parentIndex}');
@@ -87,7 +87,7 @@ class _VolumeControlState extends State<VolumeControl> {
Future<void> _adjustVolume(double delta) async { Future<void> _adjustVolume(double delta) async {
final currentVolume = widget.player.state.volume; final currentVolume = widget.player.state.volume;
final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble()); final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble());
widget.player.setVolume(newVolume); await widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.write(SettingsService.volume, newVolume); await settings.write(SettingsService.volume, newVolume);
} }
@@ -168,7 +168,7 @@ class _VolumeControlState extends State<VolumeControl> {
), ),
onPressed: () async { onPressed: () async {
final newVolume = isMuted ? 100.0 : 0.0; final newVolume = isMuted ? 100.0 : 0.0;
widget.player.setVolume(newVolume); await widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.write(SettingsService.volume, newVolume); await settings.write(SettingsService.volume, newVolume);
}, },