lint: enforce unawaited_futures, prefer_final_locals, etc.
This commit is contained in:
@@ -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
|
||||
|
||||
+524
-1461
File diff suppressed because it is too large
Load Diff
+22
-15
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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>{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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((_) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 (_) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user