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:
- 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:
extends:
- package:dart_code_linter/presets/recommended.yaml
File diff suppressed because it is too large Load Diff
+22 -15
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io' show Platform, ProcessInfo;
import 'dart:ui' show AppExitResponse;
import 'package:flutter/foundation.dart';
// ignore: depend_on_referenced_packages
import 'package:shared_preferences_foundation/shared_preferences_foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
@@ -138,7 +139,7 @@ Future<void> _bootstrapApp() async {
final savedLocale = settings.read(SettingsService.appLocale);
// Initialize localization with saved locale
LocaleSettings.setLocale(savedLocale);
unawaited(LocaleSettings.setLocale(savedLocale));
// Needed for formatting dates in different locales
await initializeDateFormatting(savedLocale.languageCode, null);
@@ -212,7 +213,7 @@ Future<void> _bootstrapApp() async {
// Desktop-only services
if (PlatformDetector.isDesktopOS()) {
DiscordRPCService.instance.initialize();
unawaited(DiscordRPCService.instance.initialize());
}
// Trakt scrobble service (all platforms)
@@ -251,7 +252,7 @@ FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
if (instance != null && !instance.read(SettingsService.crashReporting)) return null;
// Drop unactionable errors
var exceptions = event.exceptions;
final exceptions = event.exceptions;
if (exceptions != null) {
bool shouldDrop(SentryException e) {
final v = e.value;
@@ -917,7 +918,7 @@ class _SetupScreenState extends State<SetupScreen> {
// Check network connectivity early to fast-path airplane mode.
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
bool hasNetwork;
Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup'));
unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup')));
try {
final connectivityResult = await Connectivity().checkConnectivity().timeout(
const Duration(seconds: 3),
@@ -929,7 +930,9 @@ class _SetupScreenState extends State<SetupScreen> {
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) {
_setStatus(t.common.refreshingServers);
@@ -941,7 +944,7 @@ class _SetupScreenState extends State<SetupScreen> {
if (refreshResult == ServerRefreshResult.authError) {
await storage.clearCredentials();
if (mounted) {
Navigator.pushReplacement(context, fadeRoute(const AuthScreen()));
unawaited(Navigator.pushReplacement(context, fadeRoute(const AuthScreen())));
}
return;
}
@@ -954,7 +957,7 @@ class _SetupScreenState extends State<SetupScreen> {
if (servers.isEmpty) {
if (mounted) {
Navigator.pushReplacement(context, fadeRoute(const AuthScreen()));
unawaited(Navigator.pushReplacement(context, fadeRoute(const AuthScreen())));
}
return;
}
@@ -966,11 +969,13 @@ class _SetupScreenState extends State<SetupScreen> {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
unawaited(Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))));
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);
// Populate per-server status for splash display
@@ -1006,16 +1011,18 @@ class _SetupScreenState extends State<SetupScreen> {
if (result.hasConnections && result.firstClient != null) {
// Resume any downloads that were interrupted by app kill
final downloadProvider = context.read<DownloadProvider>();
downloadProvider.ensureInitialized().then((_) {
downloadProvider.resumeQueuedDownloads(result.firstClient!);
});
unawaited(
downloadProvider.ensureInitialized().then((_) {
downloadProvider.resumeQueuedDownloads(result.firstClient!);
}),
);
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!)));
unawaited(Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))));
} else {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
unawaited(Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))));
}
} catch (e, 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);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
unawaited(Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))));
}
}
}
@@ -10,62 +10,44 @@ RemoteDevice _$RemoteDeviceFromJson(Map<String, dynamic> json) => RemoteDevice(
id: json['id'] as String,
name: json['name'] as String,
platform: json['platform'] as String,
connectedAt: json['connectedAt'] == null
? null
: DateTime.parse(json['connectedAt'] as String),
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as bool),
),
connectedAt: json['connectedAt'] == null ? null : 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) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'platform': instance.platform,
'connectedAt': instance.connectedAt.toIso8601String(),
'capabilities': instance.capabilities,
};
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
RemoteSession(
role: $enumDecode(
_$RemoteSessionRoleEnumMap,
json['role'],
unknownValue: RemoteSessionRole.remote,
),
status:
$enumDecodeNullable(
_$RemoteSessionStatusEnumMap,
json['status'],
unknownValue: RemoteSessionStatus.disconnected,
) ??
RemoteSessionStatus.disconnected,
connectedDevice: json['connectedDevice'] == null
? null
: RemoteDevice.fromJson(
json['connectedDevice'] as Map<String, dynamic>,
),
createdAt: json['createdAt'] == null
? null
: DateTime.parse(json['createdAt'] as String),
errorMessage: json['errorMessage'] as String?,
);
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
<String, dynamic>{
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice,
'createdAt': instance.createdAt.toIso8601String(),
'errorMessage': instance.errorMessage,
};
const _$RemoteSessionRoleEnumMap = {
RemoteSessionRole.host: 'host',
RemoteSessionRole.remote: 'remote',
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'platform': instance.platform,
'connectedAt': instance.connectedAt.toIso8601String(),
'capabilities': instance.capabilities,
};
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession(
role: $enumDecode(_$RemoteSessionRoleEnumMap, json['role'], unknownValue: RemoteSessionRole.remote),
status:
$enumDecodeNullable(
_$RemoteSessionStatusEnumMap,
json['status'],
unknownValue: RemoteSessionStatus.disconnected,
) ??
RemoteSessionStatus.disconnected,
connectedDevice: json['connectedDevice'] == null
? null
: RemoteDevice.fromJson(json['connectedDevice'] as Map<String, dynamic>),
createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String),
errorMessage: json['errorMessage'] as String?,
);
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => <String, dynamic>{
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
'connectedDevice': instance.connectedDevice,
'createdAt': instance.createdAt.toIso8601String(),
'errorMessage': instance.errorMessage,
};
const _$RemoteSessionRoleEnumMap = {RemoteSessionRole.host: 'host', RemoteSessionRole.remote: 'remote'};
const _$RemoteSessionStatusEnumMap = {
RemoteSessionStatus.disconnected: 'disconnected',
RemoteSessionStatus.connecting: 'connecting',
+12 -18
View File
@@ -6,21 +6,15 @@ part of 'play_queue_response.dart';
// JsonSerializableGenerator
// **************************************************************************
PlayQueueResponse _$PlayQueueResponseFromJson(Map<String, dynamic> json) =>
PlayQueueResponse(
playQueueID: (json['playQueueID'] as num).toInt(),
playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?)
?.toInt(),
playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?)
?.toInt(),
playQueueSelectedMetadataItemID:
json['playQueueSelectedMetadataItemID'] as String?,
playQueueShuffled: flexibleBool(json['playQueueShuffled']),
playQueueSourceURI: json['playQueueSourceURI'] as String?,
playQueueTotalCount: (json['playQueueTotalCount'] as num?)?.toInt(),
playQueueVersion: (json['playQueueVersion'] as num).toInt(),
size: (json['size'] as num?)?.toInt(),
items: (json['Metadata'] as List<dynamic>?)
?.map((e) => PlexMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
);
PlayQueueResponse _$PlayQueueResponseFromJson(Map<String, dynamic> json) => PlayQueueResponse(
playQueueID: (json['playQueueID'] as num).toInt(),
playQueueSelectedItemID: (json['playQueueSelectedItemID'] as num?)?.toInt(),
playQueueSelectedItemOffset: (json['playQueueSelectedItemOffset'] as num?)?.toInt(),
playQueueSelectedMetadataItemID: json['playQueueSelectedMetadataItemID'] as String?,
playQueueShuffled: flexibleBool(json['playQueueShuffled']),
playQueueSourceURI: json['playQueueSourceURI'] as String?,
playQueueTotalCount: (json['playQueueTotalCount'] as num?)?.toInt(),
playQueueVersion: (json['playQueueVersion'] as num).toInt(),
size: (json['size'] as num?)?.toInt(),
items: (json['Metadata'] as List<dynamic>?)?.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? ?? '',
guestEnabled: json['guestEnabled'] as bool? ?? false,
subscription: json['subscription'] as bool? ?? false,
users:
(json['users'] as List<dynamic>?)
?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
users: (json['users'] as List<dynamic>?)?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>)).toList() ?? [],
);
Map<String, dynamic> _$PlexHomeToJson(PlexHome instance) => <String, dynamic>{
+15 -16
View File
@@ -22,19 +22,18 @@ PlexHomeUser _$PlexHomeUserFromJson(Map<String, dynamic> json) => PlexHomeUser(
protected: json['protected'] as bool? ?? false,
);
Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) =>
<String, dynamic>{
'id': instance.id,
'uuid': instance.uuid,
'title': instance.title,
'username': instance.username,
'email': instance.email,
'friendlyName': instance.friendlyName,
'thumb': instance.thumb,
'hasPassword': instance.hasPassword,
'restricted': instance.restricted,
'updatedAt': instance.updatedAt,
'admin': instance.admin,
'guest': instance.guest,
'protected': instance.protected,
};
Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) => <String, dynamic>{
'id': instance.id,
'uuid': instance.uuid,
'title': instance.title,
'username': instance.username,
'email': instance.email,
'friendlyName': instance.friendlyName,
'thumb': instance.thumb,
'hasPassword': instance.hasPassword,
'restricted': instance.restricted,
'updatedAt': instance.updatedAt,
'admin': instance.admin,
'guest': instance.guest,
'protected': instance.protected,
};
+12 -13
View File
@@ -19,16 +19,15 @@ PlexLibrary _$PlexLibraryFromJson(Map<String, dynamic> json) => PlexLibrary(
hidden: (json['hidden'] as num?)?.toInt(),
);
Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) =>
<String, dynamic>{
'key': instance.key,
'title': instance.title,
'type': instance.type,
'agent': instance.agent,
'scanner': instance.scanner,
'language': instance.language,
'uuid': instance.uuid,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
'hidden': instance.hidden,
};
Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) => <String, dynamic>{
'key': instance.key,
'title': instance.title,
'type': instance.type,
'agent': instance.agent,
'scanner': instance.scanner,
'language': instance.language,
'uuid': instance.uuid,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
'hidden': instance.hidden,
};
+1 -1
View File
@@ -413,7 +413,7 @@ class PlexMetadata with MultiServerFields {
final images = json['Image'] as List?;
if (images == null) return null;
for (var image in images) {
for (final image in images) {
if (image is Map && image['type'] == imageType) {
return image['url'] as String?;
}
+56 -59
View File
@@ -42,9 +42,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
leafCount: (json['leafCount'] as num?)?.toInt(),
viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(),
childCount: flexibleInt(json['childCount']),
role: (json['Role'] as List<dynamic>?)
?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>))
.toList(),
role: (json['Role'] as List<dynamic>?)?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>)).toList(),
mediaVersions: (json['Media'] as List<dynamic>?)
?.map((e) => PlexMediaVersion.fromJson(e as Map<String, dynamic>))
.toList(),
@@ -76,59 +74,58 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
backgroundSquare: json['backgroundSquare'] as String?,
);
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) =>
<String, dynamic>{
'ratingKey': instance.ratingKey,
'key': instance.key,
'guid': instance.guid,
'studio': instance.studio,
'type': instance.type,
'title': instance.title,
'titleSort': instance.titleSort,
'contentRating': instance.contentRating,
'summary': instance.summary,
'rating': instance.rating,
'audienceRating': instance.audienceRating,
'userRating': instance.userRating,
'year': instance.year,
'originallyAvailableAt': instance.originallyAvailableAt,
'thumb': instance.thumb,
'art': instance.art,
'duration': instance.duration,
'addedAt': instance.addedAt,
'updatedAt': instance.updatedAt,
'lastViewedAt': instance.lastViewedAt,
'grandparentTitle': instance.grandparentTitle,
'grandparentThumb': instance.grandparentThumb,
'grandparentArt': instance.grandparentArt,
'grandparentRatingKey': instance.grandparentRatingKey,
'parentTitle': instance.parentTitle,
'parentThumb': instance.parentThumb,
'parentRatingKey': instance.parentRatingKey,
'parentIndex': instance.parentIndex,
'index': instance.index,
'grandparentTheme': instance.grandparentTheme,
'viewOffset': instance.viewOffset,
'viewCount': instance.viewCount,
'leafCount': instance.leafCount,
'viewedLeafCount': instance.viewedLeafCount,
'childCount': instance.childCount,
'Role': instance.role,
'audioLanguage': instance.audioLanguage,
'subtitleLanguage': instance.subtitleLanguage,
'subtitleMode': instance.subtitleMode,
'playlistItemID': instance.playlistItemID,
'playQueueItemID': instance.playQueueItemID,
'librarySectionID': instance.librarySectionID,
'librarySectionTitle': instance.librarySectionTitle,
'ratingImage': instance.ratingImage,
'audienceRatingImage': instance.audienceRatingImage,
'tagline': instance.tagline,
'originalTitle': instance.originalTitle,
'editionTitle': instance.editionTitle,
'subtype': instance.subtype,
'extraType': instance.extraType,
'primaryExtraKey': instance.primaryExtraKey,
'clearLogo': instance.clearLogo,
'backgroundSquare': instance.backgroundSquare,
};
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dynamic>{
'ratingKey': instance.ratingKey,
'key': instance.key,
'guid': instance.guid,
'studio': instance.studio,
'type': instance.type,
'title': instance.title,
'titleSort': instance.titleSort,
'contentRating': instance.contentRating,
'summary': instance.summary,
'rating': instance.rating,
'audienceRating': instance.audienceRating,
'userRating': instance.userRating,
'year': instance.year,
'originallyAvailableAt': instance.originallyAvailableAt,
'thumb': instance.thumb,
'art': instance.art,
'duration': instance.duration,
'addedAt': instance.addedAt,
'updatedAt': instance.updatedAt,
'lastViewedAt': instance.lastViewedAt,
'grandparentTitle': instance.grandparentTitle,
'grandparentThumb': instance.grandparentThumb,
'grandparentArt': instance.grandparentArt,
'grandparentRatingKey': instance.grandparentRatingKey,
'parentTitle': instance.parentTitle,
'parentThumb': instance.parentThumb,
'parentRatingKey': instance.parentRatingKey,
'parentIndex': instance.parentIndex,
'index': instance.index,
'grandparentTheme': instance.grandparentTheme,
'viewOffset': instance.viewOffset,
'viewCount': instance.viewCount,
'leafCount': instance.leafCount,
'viewedLeafCount': instance.viewedLeafCount,
'childCount': instance.childCount,
'Role': instance.role,
'audioLanguage': instance.audioLanguage,
'subtitleLanguage': instance.subtitleLanguage,
'subtitleMode': instance.subtitleMode,
'playlistItemID': instance.playlistItemID,
'playQueueItemID': instance.playQueueItemID,
'librarySectionID': instance.librarySectionID,
'librarySectionTitle': instance.librarySectionTitle,
'ratingImage': instance.ratingImage,
'audienceRatingImage': instance.audienceRatingImage,
'tagline': instance.tagline,
'originalTitle': instance.originalTitle,
'editionTitle': instance.editionTitle,
'subtype': instance.subtype,
'extraType': instance.extraType,
'primaryExtraKey': instance.primaryExtraKey,
'clearLogo': instance.clearLogo,
'backgroundSquare': instance.backgroundSquare,
};
+19 -20
View File
@@ -26,23 +26,22 @@ PlexPlaylist _$PlexPlaylistFromJson(Map<String, dynamic> json) => PlexPlaylist(
thumb: json['thumb'] as String?,
);
Map<String, dynamic> _$PlexPlaylistToJson(PlexPlaylist instance) =>
<String, dynamic>{
'ratingKey': instance.ratingKey,
'key': instance.key,
'type': instance.type,
'title': instance.title,
'summary': instance.summary,
'smart': instance.smart,
'playlistType': instance.playlistType,
'duration': instance.duration,
'leafCount': instance.leafCount,
'composite': instance.composite,
'addedAt': instance.addedAt,
'updatedAt': instance.updatedAt,
'lastViewedAt': instance.lastViewedAt,
'viewCount': instance.viewCount,
'content': instance.content,
'guid': instance.guid,
'thumb': instance.thumb,
};
Map<String, dynamic> _$PlexPlaylistToJson(PlexPlaylist instance) => <String, dynamic>{
'ratingKey': instance.ratingKey,
'key': instance.key,
'type': instance.type,
'title': instance.title,
'summary': instance.summary,
'smart': instance.smart,
'playlistType': instance.playlistType,
'duration': instance.duration,
'leafCount': instance.leafCount,
'composite': instance.composite,
'addedAt': instance.addedAt,
'updatedAt': instance.updatedAt,
'lastViewedAt': instance.lastViewedAt,
'viewCount': instance.viewCount,
'content': instance.content,
'guid': instance.guid,
'thumb': instance.thumb,
};
+1 -1
View File
@@ -351,7 +351,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
3 => 'quit',
4 => 'error',
5 => 'redirect',
String s => s,
final String s => s,
_ => null,
};
if (reason == 'eof') {
+1 -1
View File
@@ -188,7 +188,7 @@ class CompanionRemoteProvider with ChangeNotifier {
/// Stop the host server and LAN broadcasting.
Future<void> stopHostServer() async {
_intentionalDisconnect = true;
_discoveryService?.stopBroadcasting();
await _discoveryService?.stopBroadcasting();
if (_peerService != null) {
await _peerService!.disconnect();
+1 -1
View File
@@ -126,7 +126,7 @@ class LibrariesProvider extends ChangeNotifier {
}
// 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
final orderedLibraries = <PlexLibrary>[];
+2 -2
View File
@@ -51,7 +51,7 @@ class _AuthScreenState extends State<AuthScreen> {
setState(() {
_useQrFlow = true;
});
_startAuthentication();
unawaited(_startAuthentication());
}
}
@@ -115,7 +115,7 @@ class _AuthScreenState extends State<AuthScreen> {
await profileFuture;
if (!mounted) return;
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!)));
unawaited(Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))));
} catch (e) {
appLogger.e('Failed to connect to servers', error: e);
if (!mounted) return;
+10 -7
View File
@@ -416,8 +416,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Center the active dot when possible
final center = _currentHeroIndex;
int start = (center - 2).clamp(0, totalDots - 5);
int end = start + 4; // 5 dots total (0-4 inclusive)
final int start = (center - 2).clamp(0, totalDots - 5);
final int end = start + 4; // 5 dots total (0-4 inclusive)
return (start: start, end: end);
}
@@ -461,6 +461,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Get hidden libraries for filtering
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
await hiddenLibrariesProvider.ensureInitialized();
if (!mounted) return;
_lastSeenHiddenKeys = Set.of(hiddenLibrariesProvider.hiddenLibraryKeys);
// 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
if (Platform.isAndroid) {
_syncWatchNext(onDeck);
unawaited(_syncWatchNext(onDeck));
}
// Sync PageController to first page after OnDeck loads
@@ -622,7 +623,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
// Sync to Android TV Watch Next row
if (Platform.isAndroid) {
_syncWatchNext(onDeck);
unawaited(_syncWatchNext(onDeck));
}
appLogger.d('Continue Watching refreshed successfully');
@@ -806,9 +807,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
playbackStateProvider.clearShuffle();
if (mounted) {
Navigator.of(
context,
).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => const AuthScreen()), (route) => false);
unawaited(
Navigator.of(
context,
).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/material.dart';
import 'package:plezy/widgets/app_icon.dart';
@@ -174,7 +176,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
}
if (libraryGlobalKeyToLoad != null && mounted) {
_loadLibraryContent(libraryGlobalKeyToLoad);
unawaited(_loadLibraryContent(libraryGlobalKeyToLoad));
}
}
@@ -536,7 +538,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
.toList();
if (visibleLibraries.isNotEmpty) {
_loadLibraryContent(visibleLibraries.first.globalKey);
unawaited(_loadLibraryContent(visibleLibraries.first.globalKey));
}
}
}
@@ -600,16 +602,16 @@ class _LibrariesScreenState extends State<LibrariesScreen>
switch (action) {
case 'scan':
_scanLibrary(library);
unawaited(_scanLibrary(library));
break;
case 'analyze':
_analyzeLibrary(library);
unawaited(_analyzeLibrary(library));
break;
case 'refresh':
_refreshLibraryMetadata(library);
unawaited(_refreshLibraryMetadata(library));
break;
case 'empty_trash':
_emptyLibraryTrash(library);
unawaited(_emptyLibraryTrash(library));
break;
}
}
@@ -611,8 +611,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
final storage = await StorageService.getInstance();
await storage.saveLibraryFilters(filters, sectionId: widget.library.globalKey);
_loadItems();
_loadFirstCharacters();
unawaited(_loadItems());
unawaited(_loadFirstCharacters());
},
),
);
+3 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.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)
_loadFavorites(multiServer);
unawaited(_loadFavorites(multiServer));
if (allChannels.isNotEmpty && PlatformDetector.shouldUseSideNavigation(context)) {
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
if (_companionRemoteSetup && mounted) {
_autoStartCompanionRemoteServer(context.read<CompanionRemoteProvider>());
unawaited(_autoStartCompanionRemoteServer(context.read<CompanionRemoteProvider>()));
}
// 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
_checkForUpdatesOnStartup();
unawaited(_checkForUpdatesOnStartup());
});
}
@@ -327,7 +327,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
final contentId = await watchNext.getInitialDeepLink();
if (contentId != null && mounted) {
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;
navigateToVideoPlayer(context, metadata: metadata);
unawaited(navigateToVideoPlayer(context, metadata: metadata));
} catch (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;
}
}
SystemNavigator.pop();
unawaited(SystemNavigator.pop());
});
}
@@ -823,7 +823,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
}
// Reset other provider states
hiddenLibrariesProvider.refresh();
unawaited(hiddenLibrariesProvider.refresh());
playbackStateProvider.clearShuffle();
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
if (!_showEpisodesDirectly && _seasons.isNotEmpty) {
_fetchSeasonEpisodes(_selectedSeasonIndex);
unawaited(_fetchSeasonEpisodes(_selectedSeasonIndex));
}
} else if (widget.metadata.isSeason) {
await _fetchAllEpisodes();
@@ -872,8 +872,8 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
context,
isWatched ? t.messages.markedAsUnwatchedOffline : t.messages.markedAsWatchedOffline,
);
_updateWatchStateOffline();
_loadOfflineOnDeckEpisode();
unawaited(_updateWatchStateOffline());
unawaited(_loadOfflineOnDeckEpisode());
}
} else {
// Online mode: send to server
@@ -1237,7 +1237,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
if (widget.metadata.isShow) {
_loadSeasonsFromDownloads();
// Get offline OnDeck episode
_loadOfflineOnDeckEpisode();
unawaited(_loadOfflineOnDeckEpisode());
} else if (widget.metadata.isSeason) {
_seasons = [widget.metadata];
_showEpisodesDirectly = true;
@@ -1286,16 +1286,16 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
// Load seasons if it's a show
if (metadata.isShow) {
_loadSeasons();
unawaited(_loadSeasons());
} else if (metadata.isSeason) {
_seasons = [widget.metadata];
_showEpisodesDirectly = true;
_fetchAllEpisodes();
unawaited(_fetchAllEpisodes());
}
// Load extras (trailers, behind-the-scenes, etc.)
_loadExtras();
_loadRelatedHubs();
unawaited(_loadExtras());
unawaited(_loadRelatedHubs());
return;
}
@@ -1307,11 +1307,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
});
if (widget.metadata.isShow) {
_loadSeasons();
unawaited(_loadSeasons());
} else if (widget.metadata.isSeason) {
_seasons = [widget.metadata];
_showEpisodesDirectly = true;
_fetchAllEpisodes();
unawaited(_fetchAllEpisodes());
}
} catch (e) {
// Fallback to passed metadata on error
@@ -1322,11 +1322,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
});
if (widget.metadata.isShow) {
_loadSeasons();
unawaited(_loadSeasons());
} else if (widget.metadata.isSeason) {
_seasons = [widget.metadata];
_showEpisodesDirectly = true;
_fetchAllEpisodes();
unawaited(_fetchAllEpisodes());
}
}
}
@@ -1383,7 +1383,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
await _fetchAllEpisodes();
} else if (seasonsWithServerId.isNotEmpty) {
// Fetch episodes for the auto-selected season
_fetchSeasonEpisodes(onDeckSeasonIndex);
unawaited(_fetchSeasonEpisodes(onDeckSeasonIndex));
}
} catch (e) {
setStateIfMounted(() {
@@ -2376,7 +2376,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
if (context.mounted) {
await navigateToVideoPlayer(context, metadata: firstEpisode);
// Refresh metadata when returning from video player
_loadFullMetadata();
unawaited(_loadFullMetadata());
}
} catch (e) {
// Close loading indicator if it's still open
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
@@ -128,7 +129,7 @@ class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
);
if (value != null) {
await _settingsService.write(settings.SettingsService.appLocale, value);
LocaleSettings.setLocale(value);
unawaited(LocaleSettings.setLocale(value));
_restartApp();
}
},
+28 -25
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -141,34 +142,36 @@ class _LogsScreenState extends State<LogsScreen> {
final data = response.data is String ? jsonDecode(response.data) : response.data;
final id = (data as Map<String, dynamic>)['id'] as String;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(t.messages.logsUploaded),
content: Row(
children: [
Text('${t.messages.logId}: '),
SelectableText(
id,
style: const TextStyle(fontWeight: FontWeight.bold, fontFamily: 'monospace', fontSize: 18),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.copy, size: 20),
onPressed: () {
Clipboard.setData(ClipboardData(text: id));
showSuccessSnackBar(context, t.messages.logsCopied);
},
unawaited(
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(t.messages.logsUploaded),
content: Row(
children: [
Text('${t.messages.logId}: '),
SelectableText(
id,
style: const TextStyle(fontWeight: FontWeight.bold, fontFamily: 'monospace', fontSize: 18),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.copy, size: 20),
onPressed: () {
Clipboard.setData(ClipboardData(text: id));
showSuccessSnackBar(context, t.messages.logsCopied);
},
),
],
),
actions: [
FocusableButton(
autofocus: true,
onPressed: () => Navigator.of(ctx).pop(),
child: TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(t.common.close)),
),
],
),
actions: [
FocusableButton(
autofocus: true,
onPressed: () => Navigator.of(ctx).pop(),
child: TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(t.common.close)),
),
],
),
);
} catch (_) {
+40 -38
View File
@@ -548,46 +548,48 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
final storageService = DownloadStorageService.instance;
final isCustom = storageService.isUsingCustomPath();
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(t.settings.downloads),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.settings.downloadLocationDescription),
const SizedBox(height: 16),
FutureBuilder<String>(
future: storageService.getCurrentDownloadPathDisplay(),
builder: (context, snapshot) {
return Text(
t.settings.currentPath(path: snapshot.data ?? '...'),
style: Theme.of(context).textTheme.bodySmall,
);
},
),
],
),
actions: [
if (isCustom)
unawaited(
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(t.settings.downloads),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.settings.downloadLocationDescription),
const SizedBox(height: 16),
FutureBuilder<String>(
future: storageService.getCurrentDownloadPathDisplay(),
builder: (context, snapshot) {
return Text(
t.settings.currentPath(path: snapshot.data ?? '...'),
style: Theme.of(context).textTheme.bodySmall,
);
},
),
],
),
actions: [
if (isCustom)
DialogActionButton(
onPressed: () async {
Navigator.pop(dialogContext);
await _resetDownloadLocation();
},
label: t.settings.resetToDefault,
),
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
DialogActionButton(
onPressed: () async {
Navigator.pop(dialogContext);
await _resetDownloadLocation();
await _selectDownloadLocation();
},
label: t.settings.resetToDefault,
label: t.settings.selectFolder,
isPrimary: true,
),
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
DialogActionButton(
onPressed: () async {
Navigator.pop(dialogContext);
await _selectDownloadLocation();
},
label: t.settings.selectFolder,
isPrimary: true,
),
],
],
),
),
);
}
@@ -725,7 +727,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
await _keyboardService?.resetToDefaults();
if (mounted) {
showSuccessSnackBar(context, t.settings.resetSettingsSuccess);
_loadSettings();
unawaited(_loadSettings());
}
}
@@ -766,7 +768,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
if (!mounted) return;
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([
themeProvider.reload(),
settingsProvider.reload(),
@@ -776,7 +778,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
unawaited(librariesProvider.refresh());
if (!mounted) return;
_loadSettings();
unawaited(_loadSettings());
showSuccessSnackBar(context, t.settings.importSettingsSuccess);
} on NoUserSignedInException {
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 {
Color initialColor = _hexToColor(currentColor);
final Color initialColor = _hexToColor(currentColor);
final Color selectedColor = await showColorPickerDialog(
context,
+80 -70
View File
@@ -789,7 +789,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Apply saved volume (clamped to max volume)
final savedVolume = settingsService.read(SettingsService.volume).clamp(0.0, maxVolume.toDouble());
player!.setVolume(savedVolume);
unawaited(player!.setVolume(savedVolume));
// Notify that player is ready
if (mounted) {
@@ -804,7 +804,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
}
// Enable wakelock to prevent screen from turning off during playback
_setWakelock(true);
unawaited(_setWakelock(true));
appLogger.d('Wakelock enabled for video playback');
}
@@ -822,8 +822,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
OrientationHelper.setLandscapeOrientation();
} else {
// Unlocked: Allow all orientations immediately
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
unawaited(SystemChrome.setPreferredOrientations(DeviceOrientation.values));
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky));
}
} catch (e) {
appLogger.w('Failed to set orientation', error: e);
@@ -903,7 +903,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_liveStreamFallbackLevel = 0;
if (!_hasFirstFrame.value) {
_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
if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) {
@@ -958,7 +958,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await _initializeServices();
// Load next/previous episodes (fire-and-forget)
_loadAdjacentEpisodes();
unawaited(_loadAdjacentEpisodes());
} catch (e) {
appLogger.e('Failed to initialize player', error: e);
if (mounted) {
@@ -1038,10 +1038,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await player!.play();
}
Sentry.addBreadcrumb(
Breadcrumb(
message: 'Frame rate matching: ${fps}fps, switched=$didSwitch, delay=${delaySec}s',
category: 'player',
unawaited(
Sentry.addBreadcrumb(
Breadcrumb(
message: 'Frame rate matching: ${fps}fps, switched=$didSwitch, delay=${delaySec}s',
category: 'player',
),
),
);
appLogger.d('Frame rate matching: Set display to ${fps}fps (duration: ${durationMs}ms, switched=$didSwitch)');
@@ -1057,7 +1059,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
try {
await player!.clearVideoFrameRate();
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');
} catch (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
if (client != null) {
DiscordRPCService.instance.startPlayback(_currentMetadata, client);
TraktScrobbleService.instance.startPlayback(_currentMetadata, client, isLive: widget.isLive);
TrackerCoordinator.instance.startPlayback(_currentMetadata, client, isLive: widget.isLive);
unawaited(DiscordRPCService.instance.startPlayback(_currentMetadata, client));
unawaited(TraktScrobbleService.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) {
appLogger.e('Failed to start live TV playback', error: e);
_sendLiveTimeline('stopped');
unawaited(_sendLiveTimeline('stopped'));
if (mounted) {
showErrorSnackBar(context, e.toString());
_handleBackButton();
unawaited(_handleBackButton());
}
}
return;
@@ -1652,7 +1654,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_videoFilterManager?.enterPipMode();
};
if (player!.state.playing) {
_videoPIPManager!.updateAutoPipState(isPlaying: true);
unawaited(_videoPIPManager!.updateAutoPipState(isPlaying: true));
}
}
@@ -1747,10 +1749,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
}
}
Sentry.addBreadcrumb(
Breadcrumb(
message: 'Pre-playback frame rate: ${preKnownFps}fps, switched=$didSwitch, delay=${delaySec}s',
category: 'player',
unawaited(
Sentry.addBreadcrumb(
Breadcrumb(
message: 'Pre-playback frame rate: ${preKnownFps}fps, switched=$didSwitch, delay=${delaySec}s',
category: 'player',
),
),
);
}
@@ -2008,7 +2012,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Persist ambient lighting state
final settings = await SettingsService.getInstance();
settings.write(SettingsService.ambientLighting, ambientLighting.isEnabled);
unawaited(settings.write(SettingsService.ambientLighting, ambientLighting.isEnabled));
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)
_isReplacingWithVideo = true;
navigateToVideoPlayer(context, metadata: metadata, usePushReplacement: true);
unawaited(navigateToVideoPlayer(context, metadata: metadata, usePushReplacement: true));
}
void _setupCompanionRemoteCallbacks() {
@@ -2150,23 +2154,23 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final settings = await SettingsService.getInstance();
final maxVol = settings.read(SettingsService.maxVolume).toDouble();
final newVolume = (player!.state.volume + 10).clamp(0.0, maxVol);
player!.setVolume(newVolume);
settings.write(SettingsService.volume, newVolume);
unawaited(player!.setVolume(newVolume));
unawaited(settings.write(SettingsService.volume, newVolume));
};
receiver.onVolumeDown = () async {
if (player == null) return;
final settings = await SettingsService.getInstance();
final maxVol = settings.read(SettingsService.maxVolume).toDouble();
final newVolume = (player!.state.volume - 10).clamp(0.0, maxVol);
player!.setVolume(newVolume);
settings.write(SettingsService.volume, newVolume);
unawaited(player!.setVolume(newVolume));
unawaited(settings.write(SettingsService.volume, newVolume));
};
receiver.onVolumeMute = () async {
if (player == null) return;
final settings = await SettingsService.getInstance();
final newVolume = player!.state.volume > 0 ? 0.0 : 100.0;
player!.setVolume(newVolume);
settings.write(SettingsService.volume, newVolume);
unawaited(player!.setVolume(newVolume));
unawaited(settings.write(SettingsService.volume, newVolume));
};
receiver.onSubtitles = _cycleSubtitleTrack;
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
// never fires false. Normalize all playback-dependent state.
_setWakelock(false);
_progressTracker?.sendProgress('paused');
unawaited(_setWakelock(false));
unawaited(_progressTracker?.sendProgress('paused'));
_updateMediaControlsPlaybackState();
DiscordRPCService.instance.pausePlayback();
TraktScrobbleService.instance.pausePlayback();
unawaited(DiscordRPCService.instance.pausePlayback());
unawaited(TraktScrobbleService.instance.pausePlayback());
if (_autoPipEnabled) {
_videoPIPManager?.updateAutoPipState(isPlaying: false);
unawaited(_videoPIPManager?.updateAutoPipState(isPlaying: false));
}
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
if (PipService().isPipActive.value) {
_playNext();
unawaited(_playNext());
return;
}
@@ -2514,7 +2518,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
}
} else if (_nextEpisode == null && !_completionTriggered) {
_completionTriggered = true;
_handleBackButton();
unawaited(_handleBackButton());
}
}
@@ -2560,7 +2564,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Future<void> _showServerLimitDialog() async {
if (!mounted) return;
await showServerLimitDialog(context);
if (mounted) _handleBackButton();
if (mounted) unawaited(_handleBackButton());
}
/// 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 {
if (!_isPlayerInitialized || !mounted) return;
_setWakelock(player?.state.isActive ?? false);
unawaited(_setWakelock(player?.state.isActive ?? false));
final manager = _mediaControlsManager;
final currentPlayer = player;
@@ -2770,7 +2774,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
widget.liveDvrKey == null) {
appLogger.w('Cannot retry live stream — missing session info');
showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed');
_handleBackButton();
unawaited(_handleBackButton());
return;
}
@@ -2783,7 +2787,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final tuneResult = await client.tuneChannel(widget.liveDvrKey!, channel.key);
if (tuneResult == null || !mounted) {
showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed');
_handleBackButton();
unawaited(_handleBackButton());
return;
}
@@ -2800,7 +2804,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
);
if (streamPath == null || !mounted) {
showGlobalErrorSnackBar(_lastLogError ?? 'Live stream failed');
_handleBackButton();
unawaited(_handleBackButton());
return;
}
@@ -3128,18 +3132,20 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_isReplacingWithVideo = true;
// Clear Discord Rich Presence + Trakt scrobble before switching episodes
DiscordRPCService.instance.stopPlayback();
TraktScrobbleService.instance.stopPlayback();
TrackerCoordinator.instance.stopPlayback();
unawaited(DiscordRPCService.instance.stopPlayback());
unawaited(TraktScrobbleService.instance.stopPlayback());
unawaited(TrackerCoordinator.instance.stopPlayback());
// If player isn't available, navigate without preserving settings
if (player == null) {
if (mounted) {
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
usePushReplacement: true,
isOffline: widget.isOffline,
unawaited(
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
usePushReplacement: true,
isOffline: widget.isOffline,
),
);
}
return;
@@ -3150,11 +3156,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
if (currentPlayer == null) {
// Player already disposed, navigate without preserving settings
if (mounted) {
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
usePushReplacement: true,
isOffline: widget.isOffline,
unawaited(
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
usePushReplacement: true,
isOffline: widget.isOffline,
),
);
}
return;
@@ -3165,7 +3173,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final currentSecondarySubtitleTrack = currentPlayer.state.track.secondarySubtitle;
// Pause and stop current playback
currentPlayer.pause();
unawaited(currentPlayer.pause());
await _progressTracker?.sendProgress('stopped');
_progressTracker?.stopTracking();
@@ -3174,14 +3182,16 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Navigate to the episode using pushReplacement to destroy current player
if (mounted) {
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack,
usePushReplacement: true,
isOffline: widget.isOffline,
unawaited(
navigateToVideoPlayer(
context,
metadata: episodeMetadata,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack,
usePushReplacement: true,
isOffline: widget.isOffline,
),
);
}
}
@@ -3209,9 +3219,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_progressTracker?.stopTracking();
_progressTracker?.dispose();
_progressTracker = null;
DiscordRPCService.instance.stopPlayback();
TraktScrobbleService.instance.stopPlayback();
TrackerCoordinator.instance.stopPlayback();
unawaited(DiscordRPCService.instance.stopPlayback());
unawaited(TraktScrobbleService.instance.stopPlayback());
unawaited(TrackerCoordinator.instance.stopPlayback());
_currentMetadata = episodeMetadata;
_activeRatingKey = episodeMetadata.ratingKey;
@@ -3323,9 +3333,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
}
if (client != null) {
DiscordRPCService.instance.startPlayback(episodeMetadata, client);
TraktScrobbleService.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive);
TrackerCoordinator.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive);
unawaited(DiscordRPCService.instance.startPlayback(episodeMetadata, client));
unawaited(TraktScrobbleService.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive));
unawaited(TrackerCoordinator.instance.startPlayback(episodeMetadata, client, isLive: widget.isLive));
}
try {
@@ -3337,7 +3347,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
await _loadAdjacentEpisodes();
if (_autoPipEnabled) {
_videoPIPManager?.updateAutoPipState(isPlaying: currentPlayer.state.playing);
unawaited(_videoPIPManager?.updateAutoPipState(isPlaying: currentPlayer.state.playing));
}
} catch (e) {
_isSwappingEpisode = false;
@@ -153,7 +153,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
} else {
request.response.statusCode = HttpStatus.notFound;
request.response.close();
unawaited(request.response.close());
}
});
@@ -229,7 +229,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
deviceName == null ||
platform == null) {
socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed');
unawaited(socket.close(4003, 'Authentication failed'));
return;
}
@@ -240,7 +240,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_recordFailedAuth(sourceIp);
appLogger.w('CompanionRemote: Auth failed — unknown user');
socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed');
unawaited(socket.close(4003, 'Authentication failed'));
return;
}
@@ -261,7 +261,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_recordFailedAuth(sourceIp);
appLogger.w('CompanionRemote: Auth failed — invalid auth tag');
socket.add(jsonEncode({'type': 'authFailed'}));
socket.close(4003, 'Authentication failed');
unawaited(socket.close(4003, 'Authentication failed'));
return;
}
@@ -275,7 +275,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
// Close existing client if present
if (_clientSocket != null) {
appLogger.d('CompanionRemote: Replacing existing client connection');
_clientSocket!.close(4004, 'Replaced by new connection');
unawaited(_clientSocket!.close(4004, 'Replaced by new connection'));
}
_clientSocket = socket;
@@ -298,7 +298,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
sendDeviceInfo(hostDeviceName, hostPlatform);
} else {
appLogger.w('CompanionRemote: Expected auth, got ${json['type']}');
socket.close(4002, 'Authentication required');
unawaited(socket.close(4002, 'Authentication required'));
}
} else {
// Encrypted command — data is binary
+4 -4
View File
@@ -112,7 +112,7 @@ class DiscordRPCService {
if (_isEnabled && _isConnected) {
// Upload thumbnail in background, don't block playback
_uploadThumbnailAndUpdatePresence();
unawaited(_uploadThumbnailAndUpdatePresence());
}
}
@@ -183,7 +183,7 @@ class DiscordRPCService {
/// Clear the presence
Future<void> clearPresence() async {
try {
_rpc?.clearPresence();
unawaited(_rpc?.clearPresence());
} catch (e) {
appLogger.d('Failed to clear Discord presence', error: e);
}
@@ -237,7 +237,7 @@ class DiscordRPCService {
_disconnectedSubscription = null;
_errorSubscription = null;
try {
_rpc?.dispose();
unawaited(_rpc?.dispose());
} catch (e) {
appLogger.d('DiscordRPC: dispose ignored', error: e);
}
@@ -258,7 +258,7 @@ class DiscordRPCService {
_errorSubscription = null;
try {
_rpc?.dispose();
unawaited(_rpc?.dispose());
} catch (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.
Future<void> recoverInterruptedDownloads() async {
try {
Sentry.addBreadcrumb(Breadcrumb(message: 'Initializing FileDownloader', category: 'downloads'));
unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'Initializing FileDownloader', category: 'downloads')));
await _initializeFileDownloader();
// 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();
if (rescheduled.isNotEmpty) {
appLogger.i('Rescheduled ${rescheduled.length} killed download task(s)');
@@ -251,7 +251,7 @@ class DownloadManagerService {
}
// 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();
for (final item in allDownloads) {
if (item.status == DownloadStatus.downloading.index) {
@@ -496,7 +496,7 @@ class DownloadManagerService {
_emitProgress(globalKey, DownloadStatus.queued, 0);
// Start processing if not already
_processQueue(client);
unawaited(_processQueue(client));
}
/// Process the download queue — prepares and enqueues items with background_downloader.
@@ -812,7 +812,7 @@ class DownloadManagerService {
await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey);
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.
@@ -860,7 +860,7 @@ class DownloadManagerService {
// Only advance the queue if the download actually started transferring.
// Instant failures (DNS, connection) would just cause the next item to fail too.
if (hadProgress) _processQueue(client);
if (hadProgress) unawaited(_processQueue(client));
} else {
if (isNetworkError) {
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
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.
@@ -913,7 +913,7 @@ class DownloadManagerService {
await _database.updateBgTaskId(globalKey, null);
await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey);
_processQueue(client);
unawaited(_processQueue(client));
}
/// Handle a completed video download — store path, download supplementary content, mark done.
@@ -1034,7 +1034,7 @@ class DownloadManagerService {
_completingKeys.remove(globalKey);
// Always advance the queue, even after errors
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 _database.addToQueue(mediaGlobalKey: globalKey);
final resolvedClient = _getClient(parseGlobalKey(globalKey)?.serverId) ?? client;
_processQueue(resolvedClient);
unawaited(_processQueue(resolvedClient));
}
/// Retry a failed download
@@ -1384,7 +1384,7 @@ class DownloadManagerService {
await _transitionStatus(globalKey, DownloadStatus.queued);
await _database.addToQueue(mediaGlobalKey: globalKey);
final resolvedClient = _getClient(parseGlobalKey(globalKey)?.serverId) ?? client;
_processQueue(resolvedClient);
unawaited(_processQueue(resolvedClient));
}
/// Cancel a download
+11 -7
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -89,13 +91,15 @@ class EpisodeNavigationService {
// Navigate to the new episode
if (context.mounted) {
navigateToVideoPlayer(
context,
metadata: episode,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack,
usePushReplacement: usePushReplacement,
unawaited(
navigateToVideoPlayer(
context,
metadata: episode,
preferredAudioTrack: currentAudioTrack,
preferredSubtitleTrack: currentSubtitleTrack,
preferredSecondarySubtitleTrack: currentSecondarySubtitleTrack,
usePushReplacement: usePushReplacement,
),
);
}
}
+4 -4
View File
@@ -86,7 +86,7 @@ class GamepadService with WindowListener {
_windowFocused = await windowManager.isFocused();
}
_subscription?.cancel();
unawaited(_subscription?.cancel());
_subscription = Gamepad.instance.events.listen(
_handleGamepadEvent,
onError: (e) => appLogger.e('GamepadService: Stream error', error: e),
@@ -138,11 +138,11 @@ class GamepadService with WindowListener {
void _handleGamepadEvent(GamepadEvent event) {
switch (event) {
case GamepadConnectionEvent e:
case final GamepadConnectionEvent e:
appLogger.i('GamepadService: Gamepad ${e.connected ? "connected" : "disconnected"}: ${e.info.name}');
case GamepadButtonEvent e:
case final GamepadButtonEvent e:
_handleButton(e);
case GamepadAxisEvent e:
case final GamepadAxisEvent e:
_handleAxis(e);
}
}
+8 -2
View File
@@ -206,7 +206,9 @@ class MultiServerManager {
}
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
// every reconnect.
@@ -531,7 +533,11 @@ class MultiServerManager {
if (offline.isEmpty) return;
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) {
final storage = await StorageService.getInstance();
+11 -7
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -253,13 +255,15 @@ class PlayQueueLauncher {
// Show loading indicator
if (showLoading && context.mounted) {
loadingVisible = true;
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
loadingDialogContext = dialogContext;
return const Center(child: CircularProgressIndicator());
},
unawaited(
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
loadingDialogContext = dialogContext;
return const Center(child: CircularProgressIndicator());
},
),
);
}
+16 -14
View File
@@ -128,20 +128,22 @@ class PlaybackProgressTracker {
_resetBackoff();
} else {
// Fire-and-forget for playing/paused — avoid blocking the Dart event loop
_sendOnlineProgress(state, position, duration)
.then((_) {
_resetBackoff();
})
.catchError((Object e) {
_consecutiveFailures++;
// Exponential backoff: skip 1, 2, 4, 8... ticks (capped at 6 ≈ 60s)
_ticksToSkip = (1 << (_consecutiveFailures - 1)).clamp(1, 6);
appLogger.d(
'Progress update failed ($_consecutiveFailures consecutive), '
'skipping next $_ticksToSkip tick(s)',
error: e,
);
});
unawaited(
_sendOnlineProgress(state, position, duration)
.then((_) {
_resetBackoff();
})
.catchError((Object e) {
_consecutiveFailures++;
// Exponential backoff: skip 1, 2, 4, 8... ticks (capped at 6 ≈ 60s)
_ticksToSkip = (1 << (_consecutiveFailures - 1)).clamp(1, 6);
appLogger.d(
'Progress update failed ($_consecutiveFailures consecutive), '
'skipping next $_ticksToSkip tick(s)',
error: e,
);
}),
);
}
// Emit watch state event on stop for UI updates across screens.
+29 -27
View File
@@ -462,36 +462,38 @@ class PlexServer {
appLogger.d('Running connection race to find first working endpoint', error: {'candidateCount': totalCandidates});
for (final candidate in candidates) {
PlexClient.testConnectionWithLatency(
candidate.url,
accessToken,
timeout: raceTimeout,
clientIdentifier: clientIdentifier,
).then((result) {
completedTests++;
unawaited(
PlexClient.testConnectionWithLatency(
candidate.url,
accessToken,
timeout: raceTimeout,
clientIdentifier: clientIdentifier,
).then((result) {
completedTests++;
if (!result.success) {
appLogger.w(
'Connection candidate failed',
error: {
'url': candidate.url,
'type': candidate.connection.displayType,
'https': candidate.isHttps,
'error': result.error,
'latencyMs': result.latencyMs,
},
);
}
if (!result.success) {
appLogger.w(
'Connection candidate failed',
error: {
'url': candidate.url,
'type': candidate.connection.displayType,
'https': candidate.isHttps,
'error': result.error,
'latencyMs': result.latencyMs,
},
);
}
if (result.success && !completer.isCompleted) {
if (result.transcoderVideo != null) onTranscoderCapability?.call(result.transcoderVideo!);
completer.complete(candidate);
}
if (result.success && !completer.isCompleted) {
if (result.transcoderVideo != null) onTranscoderCapability?.call(result.transcoderVideo!);
completer.complete(candidate);
}
if (completedTests == candidates.length && !completer.isCompleted) {
completer.complete(null);
}
});
if (completedTests == candidates.length && !completer.isCompleted) {
completer.complete(null);
}
}),
);
}
firstCandidate = await completer.future;
+8 -8
View File
@@ -922,7 +922,7 @@ class PlexClient {
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?;
if (streamType == PlexStreamType.video) {
@@ -1382,7 +1382,7 @@ class PlexClient {
Map<String, dynamic>? videoStream;
Map<String, dynamic>? audioStream;
for (var stream in streams) {
for (final stream in streams) {
final streamType = stream['streamType'] as int?;
if (streamType == PlexStreamType.video && videoStream == null) {
videoStream = stream;
@@ -2801,13 +2801,13 @@ class PlexClient {
: (timeline is Map ? timeline : null);
if (op is Map) {
if (op['Metadata'] case [Map firstMetadata, ...]) {
if (firstMetadata['Media'] case [Map firstMedia, ...]) {
if (op['Metadata'] case [final Map firstMetadata, ...]) {
if (firstMetadata['Media'] case [final Map firstMedia, ...]) {
final rawBeginsAt = firstMedia['beginsAt'];
beginsAt = switch (rawBeginsAt) {
num n => n.toInt(),
String s => int.tryParse(s),
final num n => n.toInt(),
final String s => int.tryParse(s),
_ => null,
};
@@ -2878,8 +2878,8 @@ class PlexClient {
if (firstMedia is Map<String, dynamic>) {
final rawBeginsAt = firstMedia['beginsAt'];
beginsAt = switch (rawBeginsAt) {
num n => n.toInt(),
String s => int.tryParse(s),
final num n => n.toInt(),
final String s => int.tryParse(s),
_ => null,
};
}
+1 -1
View File
@@ -122,7 +122,7 @@ class TrackManager {
// play() failed — clear the flag immediately since playbackRestart won't fire
appLogger.w('Resume after subtitle load failed, applying track selection directly', error: e);
waitingForExternalSubsTrackSelection = false;
applyTrackSelection();
unawaited(applyTrackSelection());
return;
}
+8 -8
View File
@@ -418,21 +418,21 @@ class TrackSelectionService {
final preferredLanguage = getLanguage(preferred);
// 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) {
return track;
}
}
// Try to match: title and language
for (var track in validTracks) {
for (final track in validTracks) {
if (getTitle(track) == preferredTitle && getLanguage(track) == preferredLanguage) {
return track;
}
}
// Try to match: language only
for (var track in validTracks) {
for (final track in validTracks) {
if (getLanguage(track) == preferredLanguage) {
return track;
}
@@ -490,7 +490,7 @@ class TrackSelectionService {
String Function(T) _,
String _,
) {
for (var track in tracks) {
for (final track in tracks) {
final trackLang = getLanguage(track)?.toLowerCase();
if (trackLang != null && languageVariations.any((lang) => trackLang.startsWith(lang))) {
return track;
@@ -682,7 +682,7 @@ class TrackSelectionService {
appLogger.d(
'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)
if (audioResult.priority == TrackSelectionPriority.navigation && onAudioTrackChanged != null) {
@@ -697,7 +697,7 @@ class TrackSelectionService {
? 'OFF'
: (selectedSubtitleTrack.title ?? selectedSubtitleTrack.language ?? 'Track ${selectedSubtitleTrack.id}');
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)
if (subtitleResult.priority == TrackSelectionPriority.navigation && onSubtitleTrackChanged != null) {
@@ -714,13 +714,13 @@ class TrackSelectionService {
appLogger.d(
'Secondary subtitle: ${secondaryMatch.title ?? secondaryMatch.language ?? "Track ${secondaryMatch.id}"}',
);
player.selectSecondarySubtitleTrack(secondaryMatch);
unawaited(player.selectSecondarySubtitleTrack(secondaryMatch));
}
}
// Apply default playback speed from settings
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(),
trackShape: const GappedTrackShape(),
tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 2),
// ignore: deprecated_member_use — opting into the 2024 slider appearance until the default flips
year2023: false,
),
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
/// (XML-to-JSON conversion).
int? flexibleInt(Object? v) => switch (v) {
num n => n.toInt(),
String s => int.tryParse(s),
final num n => n.toInt(),
final String s => int.tryParse(s),
_ => null,
};
@@ -12,16 +12,16 @@ int? flexibleInt(Object? v) => switch (v) {
/// Returns `false` for `null` or unrecognised values.
/// Handles Plex API responses where boolean fields may arrive as integers.
bool flexibleBool(Object? v) => switch (v) {
bool b => b,
int n => n == 1,
String s => s == '1',
final bool b => b,
final int n => n == 1,
final String s => s == '1',
_ => false,
};
/// Parse a value that may be [double], [num], or [String] to [double].
double? flexibleDouble(Object? v) => switch (v) {
num n => n.toDouble(),
String s => double.tryParse(s),
final num n => n.toDouble(),
final String s => double.tryParse(s),
_ => null,
};
@@ -36,6 +36,6 @@ Object? readStringField(Map json, String key) => json[key]?.toString();
/// Returns `null` when the value is `null`.
List<dynamic>? flexibleList(Object? v) => switch (v) {
null => null,
List l => l,
final List l => l,
_ => <dynamic>[v],
};
+3 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../models/livetv_channel.dart';
@@ -43,5 +45,5 @@ Future<void> navigateToLiveTv(
reverseTransitionDuration: Duration.zero,
);
navigator.push<bool>(route);
unawaited(navigator.push<bool>(route));
}
@@ -348,10 +348,10 @@ class WatchTogetherProvider with ChangeNotifier {
_syncManager?.announceLeave();
// Clean up subscriptions
_peerConnectedSubscription?.cancel();
_peerDisconnectedSubscription?.cancel();
_messageSubscription?.cancel();
_errorSubscription?.cancel();
unawaited(_peerConnectedSubscription?.cancel());
unawaited(_peerDisconnectedSubscription?.cancel());
unawaited(_messageSubscription?.cancel());
unawaited(_errorSubscription?.cancel());
_peerConnectedSubscription = null;
_peerDisconnectedSubscription = null;
@@ -432,7 +432,7 @@ class WatchTogetherPeerService with KeepaliveMixin {
_reconnectTimer = null;
stopKeepalive();
_channelSubscription?.cancel();
unawaited(_channelSubscription?.cancel());
_channelSubscription = null;
try {
@@ -276,7 +276,7 @@ class _SessionMenuSheet extends StatelessWidget {
);
if (confirmed) {
provider.leaveSession();
unawaited(provider.leaveSession());
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 iconColor = needsContrastSwap ? Theme.of(context).colorScheme.onError : widget.iconColor;
Widget tile = MouseRegion(
final Widget tile = MouseRegion(
onEnter: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = true) : null,
onExit: widget.hoverColor != null ? (_) => setState(() => _isHoveredOrFocused = false) : null,
child: ListTile(
+1 -1
View File
@@ -37,7 +37,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
final PhysicalKeyboardKey key = keyEvent.physicalKey;
// 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.contains(key))
.toList();
+1 -1
View File
@@ -143,7 +143,7 @@ class _ServerActivitiesButtonState extends State<ServerActivitiesButton> {
_pollTimer?.cancel();
_pollTimer = null;
_panelNotifier.value = _PanelData.loading;
_fetchActivities();
unawaited(_fetchActivities());
}
Widget _buildOverlay(BuildContext overlayContext, {required double right, required double top}) {
@@ -26,8 +26,8 @@ class TrackFilterHelper {
static bool _isAllowedTrack<T>(T track) {
final id = switch (track) {
AudioTrack t => t.id,
SubtitleTrack t => t.id,
final AudioTrack 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)),
trailing: isSelected ? AppIcon(Symbols.check_rounded, fill: 1, color: primary) : null,
onTap: () async {
widget.player.setRate(speed);
await widget.player.setRate(speed);
// Save as default playback speed
final settings = await SettingsService.getInstance();
await settings.write(SettingsService.defaultPlaybackSpeed, speed);
+22 -18
View File
@@ -669,9 +669,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Apply rotation lock setting
if (_isRotationLocked) {
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
unawaited(
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]),
);
} else {
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
unawaited(SystemChrome.setPreferredOrientations(DeviceOrientation.values));
}
}
}
@@ -994,10 +996,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
if (_isRotationLocked) {
// Locked: Allow landscape orientations only
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
await SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
} else {
// 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) {
// Parse chapters
if (metadataJson['Chapter'] != null) {
for (var chapter in metadataJson['Chapter'] as List) {
for (final chapter in metadataJson['Chapter'] as List) {
chapters.add(
PlexChapter(
id: chapter['id'] as int,
@@ -1155,7 +1157,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Parse markers
if (metadataJson['Marker'] != null) {
for (var marker in metadataJson['Marker'] as List) {
for (final marker in metadataJson['Marker'] as List) {
markers.add(
PlexMarker(
id: marker['id'] as int,
@@ -2611,19 +2613,21 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Navigate to new player screen with the updated selection
// Use PageRouteBuilder with zero-duration transitions to prevent orientation reset
if (mounted) {
Navigator.pushReplacement(
context,
PageRouteBuilder<bool>(
pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen(
metadata: widget.metadata.copyWith(viewOffset: currentPosition.inMilliseconds),
selectedMediaIndex: effectiveMediaIndex,
selectedQualityPreset: effectivePreset,
selectedAudioStreamId: effectiveAudioStreamId,
reusedSessionIdentifier: sessionId,
reusedTranscodeSessionId: transcodeSessionId,
unawaited(
Navigator.pushReplacement(
context,
PageRouteBuilder<bool>(
pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen(
metadata: widget.metadata.copyWith(viewOffset: currentPosition.inMilliseconds),
selectedMediaIndex: effectiveMediaIndex,
selectedQualityPreset: effectivePreset,
selectedAudioStreamId: effectiveAudioStreamId,
reusedSessionIdentifier: sessionId,
reusedTranscodeSessionId: transcodeSessionId,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
}
@@ -67,7 +67,7 @@ class VideoControlsHeader extends StatelessWidget {
final seriesName = metadata.grandparentTitle ?? metadata.title!;
final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null;
List<String> parts = [seriesName];
final List<String> parts = [seriesName];
if (hasEpisodeInfo) {
parts.add('S${metadata.parentIndex}E${metadata.index}');
@@ -83,7 +83,7 @@ class VideoControlsHeader extends StatelessWidget {
}
Widget _buildMultiLineTitle() {
List<String> secondLineParts = [];
final List<String> secondLineParts = [];
if (metadata.parentIndex != null && metadata.index != null) {
secondLineParts.add('S${metadata.parentIndex}');
@@ -87,7 +87,7 @@ class _VolumeControlState extends State<VolumeControl> {
Future<void> _adjustVolume(double delta) async {
final currentVolume = widget.player.state.volume;
final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble());
widget.player.setVolume(newVolume);
await widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance();
await settings.write(SettingsService.volume, newVolume);
}
@@ -168,7 +168,7 @@ class _VolumeControlState extends State<VolumeControl> {
),
onPressed: () async {
final newVolume = isMuted ? 100.0 : 0.0;
widget.player.setVolume(newVolume);
await widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance();
await settings.write(SettingsService.volume, newVolume);
},