Merge pull request #6 from Doezer/doezer/run-dart-format
Ran dart format using this pwsh command
This commit is contained in:
@@ -140,7 +140,8 @@ class PlexFileInfo {
|
||||
/// Format audio channels (e.g., "2 channels (stereo)")
|
||||
String get audioChannelsFormatted {
|
||||
if (audioChannels != null) {
|
||||
String channelText = '$audioChannels channel${audioChannels! > 1 ? 's' : ''}';
|
||||
String channelText =
|
||||
'$audioChannels channel${audioChannels! > 1 ? 's' : ''}';
|
||||
if (audioChannelLayout != null) {
|
||||
channelText += ' ($audioChannelLayout)';
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@ class PlexVideoPlaybackData {
|
||||
|
||||
/// Returns true if there are multiple media versions available
|
||||
bool get hasMultipleVersions => availableVersions.length > 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ class PlaybackStateProvider with ChangeNotifier {
|
||||
/// Gets the next episode in the shuffle queue.
|
||||
/// Returns null if queue is exhausted or current episode is not in queue.
|
||||
/// [loopQueue] - If true, restart from beginning when queue is exhausted
|
||||
PlexMetadata? getNextEpisode(String currentEpisodeKey, {bool loopQueue = false}) {
|
||||
PlexMetadata? getNextEpisode(
|
||||
String currentEpisodeKey, {
|
||||
bool loopQueue = false,
|
||||
}) {
|
||||
if (_shuffleQueue.isEmpty) return null;
|
||||
|
||||
// Find current episode in queue
|
||||
|
||||
@@ -78,9 +78,7 @@ class _AboutScreenState extends State<AboutScreen> {
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.description),
|
||||
title: Text(t.about.openSourceLicenses),
|
||||
subtitle: Text(
|
||||
t.about.viewLicensesDescription,
|
||||
),
|
||||
subtitle: Text(t.about.viewLicensesDescription),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
|
||||
@@ -245,13 +245,16 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/plezy.png', width: 120, height: 120),
|
||||
Image.asset(
|
||||
'assets/plezy.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t.app.title,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -321,7 +324,9 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
ElevatedButton(
|
||||
onPressed: _startAuthentication,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: Text(t.auth.signInWithPlex),
|
||||
),
|
||||
@@ -334,7 +339,9 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
_startAuthentication();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: Text(t.auth.showQRCode),
|
||||
),
|
||||
@@ -343,11 +350,12 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
OutlinedButton(
|
||||
onPressed: _handleDebugTap,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withValues(alpha: 0.5),
|
||||
color: Theme.of(context).colorScheme.outline
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
@@ -380,9 +388,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t.app.title,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
@@ -405,7 +412,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
),
|
||||
child: Text(t.auth.retry),
|
||||
),
|
||||
] else ...[ // add QR button here
|
||||
] else ...[
|
||||
// add QR button here
|
||||
ElevatedButton(
|
||||
onPressed: _startAuthentication,
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
@@ -240,7 +240,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
/// This is called when returning to the home screen to avoid blocking UI
|
||||
Future<void> _refreshContinueWatching() async {
|
||||
appLogger.d('Refreshing Continue Watching in background');
|
||||
|
||||
|
||||
try {
|
||||
final clientProvider = context.plexClient;
|
||||
final client = clientProvider.client;
|
||||
@@ -250,7 +250,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
}
|
||||
|
||||
final onDeck = await client.getOnDeck();
|
||||
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_onDeck = onDeck;
|
||||
@@ -412,11 +412,9 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
|
||||
if (plexToken == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.noPlexToken),
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.noPlexToken)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -627,10 +625,13 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildHorizontalList(_onDeck, isLarge: false, isInContinueWatching: true),
|
||||
_buildHorizontalList(
|
||||
_onDeck,
|
||||
isLarge: false,
|
||||
isInContinueWatching: true,
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
// Recommendation Hubs (Trending, Top in Genre, etc.)
|
||||
for (final hub in _hubs) ...[
|
||||
SliverToBoxAdapter(
|
||||
@@ -1279,8 +1280,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
width: cardWidth,
|
||||
height: posterHeight,
|
||||
onRefresh: updateItem,
|
||||
onRemoveFromContinueWatching: isInContinueWatching
|
||||
? _refreshContinueWatching
|
||||
onRemoveFromContinueWatching: isInContinueWatching
|
||||
? _refreshContinueWatching
|
||||
: null,
|
||||
forceGridMode: true,
|
||||
isInContinueWatching: isInContinueWatching,
|
||||
|
||||
@@ -100,7 +100,11 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
|
||||
|
||||
List<PlexSort> _getDefaultSortOptions() {
|
||||
return [
|
||||
PlexSort(key: 'titleSort', title: t.hubDetail.title, defaultDirection: 'asc'),
|
||||
PlexSort(
|
||||
key: 'titleSort',
|
||||
title: t.hubDetail.title,
|
||||
defaultDirection: 'asc',
|
||||
),
|
||||
PlexSort(
|
||||
key: 'year',
|
||||
descKey: 'year:desc',
|
||||
|
||||
@@ -72,7 +72,10 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
return t.errors.connectionFailed;
|
||||
default:
|
||||
appLogger.e('Error loading $context', error: error);
|
||||
return t.errors.failedToLoad(context: context, error: error.message ?? 'Unknown error');
|
||||
return t.errors.failedToLoad(
|
||||
context: context,
|
||||
error: error.message ?? 'Unknown error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,8 +604,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
label: t.libraries.scanLibraryFiles,
|
||||
requiresConfirmation: true,
|
||||
confirmationTitle: t.libraries.scanLibrary,
|
||||
confirmationMessage:
|
||||
t.libraries.scanLibraryConfirm(title: library.title),
|
||||
confirmationMessage: t.libraries.scanLibraryConfirm(
|
||||
title: library.title,
|
||||
),
|
||||
),
|
||||
ContextMenuItem(
|
||||
value: 'analyze',
|
||||
@@ -610,8 +614,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
label: t.libraries.analyze,
|
||||
requiresConfirmation: true,
|
||||
confirmationTitle: t.libraries.analyzeLibrary,
|
||||
confirmationMessage:
|
||||
t.libraries.analyzeLibraryConfirm(title: library.title),
|
||||
confirmationMessage: t.libraries.analyzeLibraryConfirm(
|
||||
title: library.title,
|
||||
),
|
||||
),
|
||||
ContextMenuItem(
|
||||
value: 'refresh',
|
||||
@@ -619,8 +624,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
label: t.libraries.refreshMetadata,
|
||||
requiresConfirmation: true,
|
||||
confirmationTitle: t.libraries.refreshMetadata,
|
||||
confirmationMessage:
|
||||
t.libraries.refreshMetadataConfirm(title: library.title),
|
||||
confirmationMessage: t.libraries.refreshMetadataConfirm(
|
||||
title: library.title,
|
||||
),
|
||||
isDestructive: true,
|
||||
),
|
||||
ContextMenuItem(
|
||||
@@ -629,8 +635,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
label: t.libraries.emptyTrash,
|
||||
requiresConfirmation: true,
|
||||
confirmationTitle: t.libraries.emptyTrash,
|
||||
confirmationMessage:
|
||||
t.libraries.emptyTrashConfirm(title: library.title),
|
||||
confirmationMessage: t.libraries.emptyTrashConfirm(
|
||||
title: library.title,
|
||||
),
|
||||
isDestructive: true,
|
||||
),
|
||||
];
|
||||
@@ -743,7 +750,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.metadataRefreshStarted(title: library.title)),
|
||||
content: Text(
|
||||
t.messages.metadataRefreshStarted(title: library.title),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
@@ -753,7 +762,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.metadataRefreshFailed(error: e.toString())),
|
||||
content: Text(
|
||||
t.messages.metadataRefreshFailed(error: e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
@@ -1037,7 +1048,11 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.folder_open, size: 64, color: Colors.grey),
|
||||
const Icon(
|
||||
Icons.folder_open,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(t.libraries.thisLibraryIsEmpty),
|
||||
],
|
||||
@@ -1097,7 +1112,9 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t.libraries.loadingLibraryWithCount(count: _items.length),
|
||||
t.libraries.loadingLibraryWithCount(
|
||||
count: _items.length,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
@@ -1393,7 +1410,10 @@ class _FiltersBottomSheetState extends State<_FiltersBottomSheet> {
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
t.libraries.filters,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_tempSelectedFilters.isNotEmpty)
|
||||
@@ -1718,7 +1738,9 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(selectedItem.confirmationTitle ?? t.dialog.confirmAction),
|
||||
title: Text(
|
||||
selectedItem.confirmationTitle ?? t.dialog.confirmAction,
|
||||
),
|
||||
content: Text(
|
||||
selectedItem.confirmationMessage ??
|
||||
t.libraries.confirmActionMessage,
|
||||
@@ -1852,7 +1874,9 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
|
||||
: Icons.visibility,
|
||||
),
|
||||
onPressed: () => widget.onToggleVisibility(library),
|
||||
tooltip: isHidden ? t.libraries.showLibrary : t.libraries.hideLibrary,
|
||||
tooltip: isHidden
|
||||
? t.libraries.showLibrary
|
||||
: t.libraries.hideLibrary,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
|
||||
@@ -93,7 +93,9 @@ class _LicensesScreenState extends State<LicensesScreen> {
|
||||
),
|
||||
subtitle: mergedLicense.licenseEntries.length > 1
|
||||
? Text(
|
||||
t.licenses.licensesCount(count: mergedLicense.licenseEntries.length),
|
||||
t.licenses.licensesCount(
|
||||
count: mergedLicense.licenseEntries.length,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
@@ -178,7 +180,9 @@ class _LicenseDetailScreen extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
isMultipleLicenses
|
||||
? t.licenses.licenseNumber(number: index + 1)
|
||||
? t.licenses.licenseNumber(
|
||||
number: index + 1,
|
||||
)
|
||||
: t.licenses.license,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
|
||||
@@ -40,9 +40,9 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
MemoryLogOutput.clearLogs();
|
||||
_logs = [];
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.logsCleared)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.logsCleared)));
|
||||
}
|
||||
|
||||
void _copyAllLogs() {
|
||||
@@ -55,7 +55,8 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
isFirst = false;
|
||||
|
||||
buffer.write(
|
||||
'[${_formatTime(log.timestamp)}] [${log.level.name.toUpperCase()}] ${log.message}');
|
||||
'[${_formatTime(log.timestamp)}] [${log.level.name.toUpperCase()}] ${log.message}',
|
||||
);
|
||||
if (log.error != null) {
|
||||
buffer.write('\nError: ${log.error}');
|
||||
}
|
||||
@@ -64,9 +65,9 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
}
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: buffer.toString()));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.logsCopied)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.logsCopied)));
|
||||
}
|
||||
|
||||
Color _getLevelColor(Level level) {
|
||||
@@ -131,26 +132,21 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
),
|
||||
if (_logs.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text(t.messages.noLogsAvailable),
|
||||
),
|
||||
child: Center(child: Text(t.messages.noLogsAvailable)),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final log = _logs[index];
|
||||
return _LogEntryCard(
|
||||
log: log,
|
||||
formatTime: _formatTime,
|
||||
levelColor: _getLevelColor(log.level),
|
||||
levelIcon: _getLevelIcon(log.level),
|
||||
);
|
||||
},
|
||||
childCount: _logs.length,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final log = _logs[index];
|
||||
return _LogEntryCard(
|
||||
log: log,
|
||||
formatTime: _formatTime,
|
||||
levelColor: _getLevelColor(log.level),
|
||||
levelIcon: _getLevelIcon(log.level),
|
||||
);
|
||||
}, childCount: _logs.length),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -198,11 +194,7 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
widget.levelIcon,
|
||||
color: widget.levelColor,
|
||||
size: 20,
|
||||
),
|
||||
Icon(widget.levelIcon, color: widget.levelColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -221,9 +213,7 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.formatTime(widget.log.timestamp),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
@@ -244,13 +234,10 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
),
|
||||
if (hasErrorOrStackTrace)
|
||||
Icon(
|
||||
_isExpanded
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withValues(alpha: 0.6),
|
||||
_isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).iconTheme.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -262,9 +249,9 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
Text(
|
||||
t.logs.error,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: widget.levelColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
color: widget.levelColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
@@ -277,9 +264,9 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
),
|
||||
child: SelectableText(
|
||||
widget.log.error.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -288,9 +275,9 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
Text(
|
||||
t.logs.stackTrace,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: widget.levelColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
color: widget.levelColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
@@ -303,9 +290,9 @@ class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
),
|
||||
child: SelectableText(
|
||||
widget.log.stackTrace.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -116,7 +116,9 @@ class ProfileSwitchScreen extends StatelessWidget {
|
||||
} else if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.errors.failedToSwitchProfile(displayName: user.displayName)),
|
||||
content: Text(
|
||||
t.errors.failedToSwitchProfile(displayName: user.displayName),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -75,7 +75,9 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
|
||||
|
||||
String _getErrorMessage(dynamic error) {
|
||||
if (error is ServerParsingException) {
|
||||
return t.serverSelection.malformedServerData(count: error.invalidServerData.length);
|
||||
return t.serverSelection.malformedServerData(
|
||||
count: error.invalidServerData.length,
|
||||
);
|
||||
} else if (error is FormatException) {
|
||||
// Handle JSON parsing errors with more user-friendly messages
|
||||
if (error.message.contains('Invalid server data')) {
|
||||
@@ -85,13 +87,13 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
|
||||
}
|
||||
return t.serverSelection.malformedServerInfo(message: error.message);
|
||||
} else if (error.toString().contains('SocketException') ||
|
||||
error.toString().contains('TimeoutException')) {
|
||||
error.toString().contains('TimeoutException')) {
|
||||
return t.serverSelection.networkConnectionFailed;
|
||||
} else if (error.toString().contains('401') ||
|
||||
error.toString().contains('Unauthorized')) {
|
||||
error.toString().contains('Unauthorized')) {
|
||||
return t.serverSelection.authenticationFailed;
|
||||
} else if (error.toString().contains('404') ||
|
||||
error.toString().contains('Not Found')) {
|
||||
error.toString().contains('Not Found')) {
|
||||
return t.serverSelection.plexServiceUnavailable;
|
||||
}
|
||||
|
||||
@@ -101,7 +103,9 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
|
||||
Future<void> _copyDebugDataToClipboard() async {
|
||||
if (_debugServerData == null) return;
|
||||
|
||||
final jsonString = const JsonEncoder.withIndent(' ').convert(_debugServerData);
|
||||
final jsonString = const JsonEncoder.withIndent(
|
||||
' ',
|
||||
).convert(_debugServerData);
|
||||
await Clipboard.setData(ClipboardData(text: jsonString));
|
||||
|
||||
if (mounted) {
|
||||
@@ -207,7 +211,9 @@ class _ServerSelectionScreenState extends State<ServerSelectionScreen> {
|
||||
// Show error
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result.error ?? t.errors.connectionFailedGeneric)),
|
||||
SnackBar(
|
||||
content: Text(result.error ?? t.errors.connectionFailedGeneric),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +52,15 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
}
|
||||
|
||||
String _colorToHex(Color color) {
|
||||
return '#${((color.r * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}${((color.g * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}${((color.b * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}'.toUpperCase();
|
||||
return '#${((color.r * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}${((color.g * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}${((color.b * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}'
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
Future<void> _showColorPicker(String title, String currentColor, Function(String) onColorSelected) async {
|
||||
Future<void> _showColorPicker(
|
||||
String title,
|
||||
String currentColor,
|
||||
Function(String) onColorSelected,
|
||||
) async {
|
||||
Color initialColor = _hexToColor(currentColor);
|
||||
|
||||
final Color selectedColor = await showColorPickerDialog(
|
||||
@@ -123,7 +128,9 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
t.subtitlingStyling.stylingOptions,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
// Font Size Slider
|
||||
@@ -142,7 +149,10 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Text('30', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const Text(
|
||||
'30',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _fontSize.toDouble(),
|
||||
@@ -160,7 +170,10 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('80', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const Text(
|
||||
'80',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -182,7 +195,9 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
subtitle: Text(_textColor),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showColorPicker(t.subtitlingStyling.textColor, _textColor, (color) {
|
||||
_showColorPicker(t.subtitlingStyling.textColor, _textColor, (
|
||||
color,
|
||||
) {
|
||||
setState(() {
|
||||
_textColor = color;
|
||||
});
|
||||
@@ -207,7 +222,10 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Text('0', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const Text(
|
||||
'0',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _borderSize.toDouble(),
|
||||
@@ -225,7 +243,10 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('5', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const Text(
|
||||
'5',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -247,7 +268,9 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
subtitle: Text(_borderColor),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showColorPicker(t.subtitlingStyling.borderColor, _borderColor, (color) {
|
||||
_showColorPicker(t.subtitlingStyling.borderColor, _borderColor, (
|
||||
color,
|
||||
) {
|
||||
setState(() {
|
||||
_borderColor = color;
|
||||
});
|
||||
@@ -272,7 +295,10 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Text('0%', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const Text(
|
||||
'0%',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _backgroundOpacity.toDouble(),
|
||||
@@ -286,11 +312,16 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
});
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
_settingsService.setSubtitleBackgroundOpacity(_backgroundOpacity);
|
||||
_settingsService.setSubtitleBackgroundOpacity(
|
||||
_backgroundOpacity,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('100%', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const Text(
|
||||
'100%',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -312,12 +343,16 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
subtitle: Text(_backgroundColor),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_showColorPicker(t.subtitlingStyling.backgroundColor, _backgroundColor, (color) {
|
||||
setState(() {
|
||||
_backgroundColor = color;
|
||||
});
|
||||
_settingsService.setSubtitleBackgroundColor(color);
|
||||
});
|
||||
_showColorPicker(
|
||||
t.subtitlingStyling.backgroundColor,
|
||||
_backgroundColor,
|
||||
(color) {
|
||||
setState(() {
|
||||
_backgroundColor = color;
|
||||
});
|
||||
_settingsService.setSubtitleBackgroundColor(color);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -428,9 +428,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +239,8 @@ class KeyboardShortcutsService {
|
||||
|
||||
// Clamp between 0 and video duration
|
||||
final clampedPosition = newPosition.isNegative
|
||||
? Duration.zero
|
||||
: (newPosition > duration ? duration : newPosition);
|
||||
? Duration.zero
|
||||
: (newPosition > duration ? duration : newPosition);
|
||||
|
||||
player.seek(clampedPosition);
|
||||
}
|
||||
|
||||
@@ -271,7 +271,9 @@ class PlexServer {
|
||||
factory PlexServer.fromJson(Map<String, dynamic> json) {
|
||||
// Validate required fields first
|
||||
if (!_isValidServerJson(json)) {
|
||||
throw FormatException('Invalid server data: missing required fields (name, clientIdentifier, accessToken, or connections)');
|
||||
throw FormatException(
|
||||
'Invalid server data: missing required fields (name, clientIdentifier, accessToken, or connections)',
|
||||
);
|
||||
}
|
||||
|
||||
final List<dynamic> connectionsJson = json['connections'] as List<dynamic>;
|
||||
@@ -309,8 +311,10 @@ class PlexServer {
|
||||
|
||||
return PlexServer(
|
||||
name: json['name'] as String, // Safe because validated above
|
||||
clientIdentifier: json['clientIdentifier'] as String, // Safe because validated above
|
||||
accessToken: json['accessToken'] as String, // Safe because validated above
|
||||
clientIdentifier:
|
||||
json['clientIdentifier'] as String, // Safe because validated above
|
||||
accessToken:
|
||||
json['accessToken'] as String, // Safe because validated above
|
||||
connections: connections,
|
||||
owned: json['owned'] as bool? ?? false,
|
||||
product: json['product'] as String?,
|
||||
@@ -326,10 +330,12 @@ class PlexServer {
|
||||
if (json['name'] is! String || (json['name'] as String).isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (json['clientIdentifier'] is! String || (json['clientIdentifier'] as String).isEmpty) {
|
||||
if (json['clientIdentifier'] is! String ||
|
||||
(json['clientIdentifier'] as String).isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (json['accessToken'] is! String || (json['accessToken'] as String).isEmpty) {
|
||||
if (json['accessToken'] is! String ||
|
||||
(json['accessToken'] as String).isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -394,8 +400,16 @@ class PlexServer {
|
||||
final httpCandidates = <_ConnectionCandidate>[];
|
||||
|
||||
for (final connection in connections) {
|
||||
final uriCandidate = _ConnectionCandidate(connection, connection.uri, true);
|
||||
final directCandidate = _ConnectionCandidate(connection, connection.directUrl, false);
|
||||
final uriCandidate = _ConnectionCandidate(
|
||||
connection,
|
||||
connection.uri,
|
||||
true,
|
||||
);
|
||||
final directCandidate = _ConnectionCandidate(
|
||||
connection,
|
||||
connection.directUrl,
|
||||
false,
|
||||
);
|
||||
|
||||
if (connection.protocol == 'https') {
|
||||
httpsCandidates.add(uriCandidate);
|
||||
@@ -574,7 +588,9 @@ class PlexConnection {
|
||||
factory PlexConnection.fromJson(Map<String, dynamic> json) {
|
||||
// Validate required fields
|
||||
if (!_isValidConnectionJson(json)) {
|
||||
throw FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)');
|
||||
throw FormatException(
|
||||
'Invalid connection data: missing required fields (protocol, address, port, or uri)',
|
||||
);
|
||||
}
|
||||
|
||||
return PlexConnection(
|
||||
@@ -634,7 +650,10 @@ class PlexConnection {
|
||||
/// Create an HTTP fallback version of this HTTPS connection
|
||||
/// This allows testing HTTP when HTTPS is unavailable (e.g., certificate issues)
|
||||
PlexConnection toHttpFallback() {
|
||||
assert(protocol == 'https', 'Can only create HTTP fallback for HTTPS connections');
|
||||
assert(
|
||||
protocol == 'https',
|
||||
'Can only create HTTP fallback for HTTPS connections',
|
||||
);
|
||||
|
||||
return PlexConnection(
|
||||
protocol: 'http',
|
||||
|
||||
@@ -37,7 +37,8 @@ class SettingsService {
|
||||
static const String _keySubtitleBorderSize = 'subtitle_border_size';
|
||||
static const String _keySubtitleBorderColor = 'subtitle_border_color';
|
||||
static const String _keySubtitleBackgroundColor = 'subtitle_background_color';
|
||||
static const String _keySubtitleBackgroundOpacity = 'subtitle_background_opacity';
|
||||
static const String _keySubtitleBackgroundOpacity =
|
||||
'subtitle_background_opacity';
|
||||
static const String _keyShuffleUnwatchedOnly = 'shuffle_unwatched_only';
|
||||
static const String _keyShuffleOrderNavigation = 'shuffle_order_navigation';
|
||||
static const String _keyShuffleLoopQueue = 'shuffle_loop_queue';
|
||||
@@ -228,7 +229,8 @@ class SettingsService {
|
||||
}
|
||||
|
||||
bool getRotationLocked() {
|
||||
return _prefs.getBool(_keyRotationLocked) ?? true; // Default: locked (landscape only)
|
||||
return _prefs.getBool(_keyRotationLocked) ??
|
||||
true; // Default: locked (landscape only)
|
||||
}
|
||||
|
||||
// Subtitle Styling Settings
|
||||
|
||||
@@ -68,7 +68,8 @@ class TrackSelectionService {
|
||||
// Mode 1: Shown with foreign audio
|
||||
if (profile.autoSelectSubtitle == 1) {
|
||||
// Check if audio language matches user's preferred subtitle language
|
||||
if (selectedAudioTrack != null && profile.defaultSubtitleLanguage != null) {
|
||||
if (selectedAudioTrack != null &&
|
||||
profile.defaultSubtitleLanguage != null) {
|
||||
final audioLang = selectedAudioTrack.languageCode;
|
||||
final prefLang = profile.defaultSubtitleLanguage;
|
||||
|
||||
@@ -108,10 +109,16 @@ class TrackSelectionService {
|
||||
var candidateTracks = tracks;
|
||||
|
||||
// Apply SDH (hearing impaired) filtering
|
||||
candidateTracks = _filterBySDH(candidateTracks, profile.defaultSubtitleAccessibility);
|
||||
candidateTracks = _filterBySDH(
|
||||
candidateTracks,
|
||||
profile.defaultSubtitleAccessibility,
|
||||
);
|
||||
|
||||
// Apply forced subtitle filtering
|
||||
candidateTracks = _filterByForced(candidateTracks, profile.defaultSubtitleForced);
|
||||
candidateTracks = _filterByForced(
|
||||
candidateTracks,
|
||||
profile.defaultSubtitleForced,
|
||||
);
|
||||
|
||||
// If no candidates after filtering, relax filters
|
||||
if (candidateTracks.isEmpty) {
|
||||
@@ -200,17 +207,20 @@ class TrackSelectionService {
|
||||
|
||||
// Look for common SDH indicators
|
||||
return title.contains('sdh') ||
|
||||
displayTitle.contains('sdh') ||
|
||||
title.contains('cc') ||
|
||||
displayTitle.contains('cc') ||
|
||||
title.contains('hearing impaired') ||
|
||||
displayTitle.contains('hearing impaired');
|
||||
displayTitle.contains('sdh') ||
|
||||
title.contains('cc') ||
|
||||
displayTitle.contains('cc') ||
|
||||
title.contains('hearing impaired') ||
|
||||
displayTitle.contains('hearing impaired');
|
||||
}
|
||||
|
||||
/// Checks if a language code matches a preferred language
|
||||
///
|
||||
/// Handles both 2-letter (ISO 639-1) and 3-letter (ISO 639-2) codes
|
||||
static bool _matchesLanguage(String? trackLanguage, String? preferredLanguage) {
|
||||
static bool _matchesLanguage(
|
||||
String? trackLanguage,
|
||||
String? preferredLanguage,
|
||||
) {
|
||||
if (trackLanguage == null || preferredLanguage == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ class UpdateService {
|
||||
|
||||
/// Check if update checking is enabled via build flag
|
||||
static bool get isUpdateCheckEnabled {
|
||||
const enabled = bool.fromEnvironment('ENABLE_UPDATE_CHECK', defaultValue: false);
|
||||
const enabled = bool.fromEnvironment(
|
||||
'ENABLE_UPDATE_CHECK',
|
||||
defaultValue: false,
|
||||
);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@@ -62,24 +65,21 @@ class UpdateService {
|
||||
|
||||
/// Check for updates on GitHub (manual check, ignores cooldown)
|
||||
/// Returns a map with update info, or null if no update or error
|
||||
static Future<Map<String, dynamic>?> checkForUpdates({bool silent = false}) async {
|
||||
static Future<Map<String, dynamic>?> checkForUpdates({
|
||||
bool silent = false,
|
||||
}) async {
|
||||
if (!isUpdateCheckEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
|
||||
final dio = Dio();
|
||||
final response = await dio.get(
|
||||
'https://api.github.com/repos/$_githubRepo/releases/latest',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
},
|
||||
),
|
||||
options: Options(headers: {'Accept': 'application/vnd.github+json'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
||||
+19
-14
@@ -13,7 +13,10 @@ String _redactSensitiveData(String message) {
|
||||
|
||||
// Redact authorization headers
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'([Aa]uthorization[=:]\s*)([A-Za-z0-9_\-\.]+)', caseSensitive: false),
|
||||
RegExp(
|
||||
r'([Aa]uthorization[=:]\s*)([A-Za-z0-9_\-\.]+)',
|
||||
caseSensitive: false,
|
||||
),
|
||||
(match) => '${match.group(1)}[REDACTED]',
|
||||
);
|
||||
|
||||
@@ -31,7 +34,9 @@ String _redactSensitiveData(String message) {
|
||||
|
||||
// Redact full URLs with tokens in query parameters
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'(https?://[^\s]*[?&])([Xx]-[Pp]lex-[Tt]oken|token)=([A-Za-z0-9_-]+)'),
|
||||
RegExp(
|
||||
r'(https?://[^\s]*[?&])([Xx]-[Pp]lex-[Tt]oken|token)=([A-Za-z0-9_-]+)',
|
||||
),
|
||||
(match) => '${match.group(1)}${match.group(2)}=[REDACTED]',
|
||||
);
|
||||
|
||||
@@ -49,18 +54,18 @@ String _redactSensitiveData(String message) {
|
||||
|
||||
// Redact standalone token-like strings (20+ alphanumeric characters)
|
||||
// Only if they appear in common token contexts
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'\b([A-Za-z0-9_-]{20,})\b'),
|
||||
(match) {
|
||||
final token = match.group(1)!;
|
||||
// Only redact if it looks like a token (mixed case or contains hyphens/underscores)
|
||||
if (token.contains(RegExp(r'[A-Z]')) && token.contains(RegExp(r'[a-z]')) ||
|
||||
token.contains('_') || token.contains('-')) {
|
||||
return '[REDACTED_TOKEN]';
|
||||
}
|
||||
return token;
|
||||
},
|
||||
);
|
||||
redacted = redacted.replaceAllMapped(RegExp(r'\b([A-Za-z0-9_-]{20,})\b'), (
|
||||
match,
|
||||
) {
|
||||
final token = match.group(1)!;
|
||||
// Only redact if it looks like a token (mixed case or contains hyphens/underscores)
|
||||
if (token.contains(RegExp(r'[A-Z]')) && token.contains(RegExp(r'[a-z]')) ||
|
||||
token.contains('_') ||
|
||||
token.contains('-')) {
|
||||
return '[REDACTED_TOKEN]';
|
||||
}
|
||||
return token;
|
||||
});
|
||||
|
||||
return redacted;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ Future<void> handleShufflePlay(
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,9 +41,7 @@ Future<void> handleShufflePlay(
|
||||
if (itemType == 'show') {
|
||||
if (unwatchedOnly) {
|
||||
// Get only unwatched episodes
|
||||
episodes = await client.getAllUnwatchedEpisodes(
|
||||
metadata.ratingKey,
|
||||
);
|
||||
episodes = await client.getAllUnwatchedEpisodes(metadata.ratingKey);
|
||||
} else {
|
||||
// Get all episodes from all seasons
|
||||
final allEpisodes = <PlexMetadata>[];
|
||||
@@ -71,9 +68,7 @@ Future<void> handleShufflePlay(
|
||||
} else {
|
||||
// Get all episodes in season
|
||||
final seasonEpisodes = await client.getChildren(metadata.ratingKey);
|
||||
episodes = seasonEpisodes
|
||||
.where((ep) => ep.type == 'episode')
|
||||
.toList();
|
||||
episodes = seasonEpisodes.where((ep) => ep.type == 'episode').toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +79,9 @@ Future<void> handleShufflePlay(
|
||||
|
||||
if (episodes.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.noEpisodesFound)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.noEpisodesFound)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ class UserSwitchingUtils {
|
||||
} else if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.failedToSwitchProfile(displayName: user.displayName)),
|
||||
content: Text(
|
||||
t.messages.failedToSwitchProfile(displayName: user.displayName),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -41,7 +41,9 @@ Future<bool?> navigateToVideoPlayer(
|
||||
try {
|
||||
final settingsService = await SettingsService.getInstance();
|
||||
final seriesKey = metadata.grandparentRatingKey ?? metadata.ratingKey;
|
||||
final savedPreference = settingsService.getMediaVersionPreference(seriesKey);
|
||||
final savedPreference = settingsService.getMediaVersionPreference(
|
||||
seriesKey,
|
||||
);
|
||||
if (savedPreference != null) {
|
||||
mediaIndex = savedPreference;
|
||||
}
|
||||
|
||||
@@ -167,9 +167,7 @@ class _ContextMenuWrapperState extends State<ContextMenuWrapper> {
|
||||
if (selectedItem.requiresConfirmation) {
|
||||
final confirmed = await _showConfirmationDialog(
|
||||
title: selectedItem.confirmationTitle ?? t.dialog.confirmAction,
|
||||
message:
|
||||
selectedItem.confirmationMessage ??
|
||||
t.dialog.areYouSure,
|
||||
message: selectedItem.confirmationMessage ?? t.dialog.areYouSure,
|
||||
isDestructive: selectedItem.isDestructive,
|
||||
);
|
||||
|
||||
|
||||
@@ -77,30 +77,66 @@ class FileInfoBottomSheet extends StatelessWidget {
|
||||
// Video Section
|
||||
_buildSectionHeader(t.fileInfo.video),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(t.fileInfo.codec, fileInfo.videoCodec ?? t.common.unknown),
|
||||
_buildInfoRow(t.fileInfo.resolution, fileInfo.resolutionFormatted),
|
||||
_buildInfoRow(t.fileInfo.bitrate, fileInfo.bitrateFormatted),
|
||||
_buildInfoRow(t.fileInfo.frameRate, fileInfo.frameRateFormatted),
|
||||
_buildInfoRow(t.fileInfo.aspectRatio, fileInfo.aspectRatioFormatted),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.codec,
|
||||
fileInfo.videoCodec ?? t.common.unknown,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.resolution,
|
||||
fileInfo.resolutionFormatted,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.bitrate,
|
||||
fileInfo.bitrateFormatted,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.frameRate,
|
||||
fileInfo.frameRateFormatted,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.aspectRatio,
|
||||
fileInfo.aspectRatioFormatted,
|
||||
),
|
||||
if (fileInfo.videoProfile != null)
|
||||
_buildInfoRow(t.fileInfo.profile, fileInfo.videoProfile!),
|
||||
if (fileInfo.bitDepth != null)
|
||||
_buildInfoRow(t.fileInfo.bitDepth, '${fileInfo.bitDepth} bit'),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.bitDepth,
|
||||
'${fileInfo.bitDepth} bit',
|
||||
),
|
||||
if (fileInfo.colorSpace != null)
|
||||
_buildInfoRow(t.fileInfo.colorSpace, fileInfo.colorSpace!),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.colorSpace,
|
||||
fileInfo.colorSpace!,
|
||||
),
|
||||
if (fileInfo.colorRange != null)
|
||||
_buildInfoRow(t.fileInfo.colorRange, fileInfo.colorRange!),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.colorRange,
|
||||
fileInfo.colorRange!,
|
||||
),
|
||||
if (fileInfo.colorPrimaries != null)
|
||||
_buildInfoRow(t.fileInfo.colorPrimaries, fileInfo.colorPrimaries!),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.colorPrimaries,
|
||||
fileInfo.colorPrimaries!,
|
||||
),
|
||||
if (fileInfo.chromaSubsampling != null)
|
||||
_buildInfoRow(t.fileInfo.chromaSubsampling, fileInfo.chromaSubsampling!),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.chromaSubsampling,
|
||||
fileInfo.chromaSubsampling!,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Audio Section
|
||||
_buildSectionHeader(t.fileInfo.audio),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(t.fileInfo.codec, fileInfo.audioCodec ?? t.common.unknown),
|
||||
_buildInfoRow(t.fileInfo.channels, fileInfo.audioChannelsFormatted),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.codec,
|
||||
fileInfo.audioCodec ?? t.common.unknown,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.channels,
|
||||
fileInfo.audioChannelsFormatted,
|
||||
),
|
||||
if (fileInfo.audioProfile != null)
|
||||
_buildInfoRow(t.fileInfo.profile, fileInfo.audioProfile!),
|
||||
const SizedBox(height: 20),
|
||||
@@ -109,10 +145,20 @@ class FileInfoBottomSheet extends StatelessWidget {
|
||||
_buildSectionHeader(t.fileInfo.file),
|
||||
const SizedBox(height: 8),
|
||||
if (fileInfo.filePath != null)
|
||||
_buildInfoRow(t.fileInfo.path, fileInfo.filePath!, isMonospace: true),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.path,
|
||||
fileInfo.filePath!,
|
||||
isMonospace: true,
|
||||
),
|
||||
_buildInfoRow(t.fileInfo.size, fileInfo.fileSizeFormatted),
|
||||
_buildInfoRow(t.fileInfo.container, fileInfo.container ?? t.common.unknown),
|
||||
_buildInfoRow(t.fileInfo.duration, fileInfo.durationFormatted),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.container,
|
||||
fileInfo.container ?? t.common.unknown,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.duration,
|
||||
fileInfo.durationFormatted,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Advanced Section
|
||||
@@ -120,11 +166,15 @@ class FileInfoBottomSheet extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.optimizedForStreaming,
|
||||
fileInfo.optimizedForStreaming == true ? t.common.yes : t.common.no,
|
||||
fileInfo.optimizedForStreaming == true
|
||||
? t.common.yes
|
||||
: t.common.no,
|
||||
),
|
||||
_buildInfoRow(
|
||||
t.fileInfo.has64bitOffsets,
|
||||
fileInfo.has64bitOffsets == true ? t.common.yes : t.common.no,
|
||||
fileInfo.has64bitOffsets == true
|
||||
? t.common.yes
|
||||
: t.common.no,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -157,10 +207,7 @@ class FileInfoBottomSheet extends StatelessWidget {
|
||||
width: 140,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
fontSize: 14,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -56,9 +56,9 @@ class _HorizontalScrollWithArrowsState
|
||||
|
||||
void _scrollLeft() {
|
||||
final position = _scrollController.position;
|
||||
final targetScroll = (position.pixels -
|
||||
(position.viewportDimension * widget.scrollAmount))
|
||||
.clamp(0.0, position.maxScrollExtent);
|
||||
final targetScroll =
|
||||
(position.pixels - (position.viewportDimension * widget.scrollAmount))
|
||||
.clamp(0.0, position.maxScrollExtent);
|
||||
|
||||
_scrollController.animateTo(
|
||||
targetScroll,
|
||||
@@ -69,9 +69,9 @@ class _HorizontalScrollWithArrowsState
|
||||
|
||||
void _scrollRight() {
|
||||
final position = _scrollController.position;
|
||||
final targetScroll = (position.pixels +
|
||||
(position.viewportDimension * widget.scrollAmount))
|
||||
.clamp(0.0, position.maxScrollExtent);
|
||||
final targetScroll =
|
||||
(position.pixels + (position.viewportDimension * widget.scrollAmount))
|
||||
.clamp(0.0, position.maxScrollExtent);
|
||||
|
||||
_scrollController.animateTo(
|
||||
targetScroll,
|
||||
@@ -145,10 +145,7 @@ class _NavigationArrow extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _NavigationArrow({
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
});
|
||||
const _NavigationArrow({required this.icon, required this.onPressed});
|
||||
|
||||
@override
|
||||
State<_NavigationArrow> createState() => _NavigationArrowState();
|
||||
@@ -182,11 +179,7 @@ class _NavigationArrowState extends State<_NavigationArrow> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
child: Icon(widget.icon, color: Colors.white, size: 32),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+32
-29
@@ -181,34 +181,31 @@ class _MediaCardGrid extends StatelessWidget {
|
||||
item.displaySubtitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: tokens(context).textMuted,
|
||||
fontSize: 11,
|
||||
height: 1.1,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tokens(context).textMuted,
|
||||
fontSize: 11,
|
||||
height: 1.1,
|
||||
),
|
||||
)
|
||||
else if (item.parentTitle != null)
|
||||
Text(
|
||||
item.parentTitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: tokens(context).textMuted,
|
||||
fontSize: 11,
|
||||
height: 1.1,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tokens(context).textMuted,
|
||||
fontSize: 11,
|
||||
height: 1.1,
|
||||
),
|
||||
)
|
||||
else if (item.year != null)
|
||||
Text(
|
||||
'${item.year}',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: tokens(context).textMuted,
|
||||
fontSize: 11,
|
||||
height: 1.1,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tokens(context).textMuted,
|
||||
fontSize: 11,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -473,10 +470,12 @@ class _MediaCardList extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tokens(context).textMuted.withValues(alpha: 0.9),
|
||||
fontSize: _metadataFontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
color: tokens(
|
||||
context,
|
||||
).textMuted.withValues(alpha: 0.9),
|
||||
fontSize: _metadataFontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
@@ -487,9 +486,11 @@ class _MediaCardList extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tokens(context).textMuted.withValues(alpha: 0.85),
|
||||
fontSize: _subtitleFontSize,
|
||||
),
|
||||
color: tokens(
|
||||
context,
|
||||
).textMuted.withValues(alpha: 0.85),
|
||||
fontSize: _subtitleFontSize,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
@@ -500,10 +501,12 @@ class _MediaCardList extends StatelessWidget {
|
||||
maxLines: _summaryMaxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tokens(context).textMuted.withValues(alpha: 0.7),
|
||||
fontSize: _summaryFontSize,
|
||||
height: 1.3,
|
||||
),
|
||||
color: tokens(
|
||||
context,
|
||||
).textMuted.withValues(alpha: 0.7),
|
||||
fontSize: _summaryFontSize,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -102,7 +102,11 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
if ((itemType == 'episode' || itemType == 'season') &&
|
||||
widget.metadata.grandparentTitle != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(value: 'series', icon: Icons.tv, label: t.mediaMenu.goToSeries),
|
||||
_MenuAction(
|
||||
value: 'series',
|
||||
icon: Icons.tv,
|
||||
label: t.mediaMenu.goToSeries,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -262,7 +266,9 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))),
|
||||
SnackBar(
|
||||
content: Text(t.messages.errorLoading(error: e.toString())),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -312,9 +318,9 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,14 +403,15 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.errorLoadingFileInfo(error: e.toString()))));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.errorLoadingFileInfo(error: e.toString())),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
|
||||
@@ -125,7 +125,9 @@ class _PinEntryDialogState extends State<PinEntryDialog>
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
tooltip: _obscureText ? t.pinEntry.showPin : t.pinEntry.hidePin,
|
||||
tooltip: _obscureText
|
||||
? t.pinEntry.showPin
|
||||
: t.pinEntry.hidePin,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _submit(),
|
||||
|
||||
@@ -95,10 +95,7 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
groupValue: _currentSort,
|
||||
onChanged: (PlexSort? value) {
|
||||
if (value != null) {
|
||||
_handleSortChange(
|
||||
value,
|
||||
value.defaultDirection == 'desc',
|
||||
);
|
||||
_handleSortChange(value, value.defaultDirection == 'desc');
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
@@ -124,7 +121,10 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
),
|
||||
ButtonSegment(
|
||||
value: true,
|
||||
icon: Icon(Icons.arrow_downward, size: 16),
|
||||
icon: Icon(
|
||||
Icons.arrow_downward,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
selected: {_currentDescending},
|
||||
@@ -135,12 +135,12 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
],
|
||||
)
|
||||
: null,
|
||||
leading: Radio<PlexSort>(
|
||||
value: sort,
|
||||
toggleable: false,
|
||||
),
|
||||
leading: Radio<PlexSort>(value: sort, toggleable: false),
|
||||
onTap: () {
|
||||
_handleSortChange(sort, sort.defaultDirection == 'desc');
|
||||
_handleSortChange(
|
||||
sort,
|
||||
sort.defaultDirection == 'desc',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,10 +6,7 @@ import '../../../i18n/strings.g.dart';
|
||||
class AudioTrackSheet extends StatelessWidget {
|
||||
final Player player;
|
||||
|
||||
const AudioTrackSheet({
|
||||
super.key,
|
||||
required this.player,
|
||||
});
|
||||
const AudioTrackSheet({super.key, required this.player});
|
||||
|
||||
static BoxConstraints getBottomSheetConstraints(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
@@ -123,15 +120,13 @@ class AudioTrackSheet extends StatelessWidget {
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isSelected ? Colors.blue : Colors.white,
|
||||
color: isSelected
|
||||
? Colors.blue
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.blue,
|
||||
)
|
||||
? const Icon(Icons.check, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
player.setAudioTrack(audioTrack);
|
||||
|
||||
@@ -73,7 +73,8 @@ class ChapterSheet extends StatelessWidget {
|
||||
for (int i = 0; i < chapters.length; i++) {
|
||||
final chapter = chapters[i];
|
||||
final startMs = chapter.startTimeOffset ?? 0;
|
||||
final endMs = chapter.endTimeOffset ??
|
||||
final endMs =
|
||||
chapter.endTimeOffset ??
|
||||
(i < chapters.length - 1
|
||||
? chapters[i + 1].startTimeOffset ?? 0
|
||||
: double.maxFinite.toInt());
|
||||
@@ -142,41 +143,43 @@ class ChapterSheet extends StatelessWidget {
|
||||
child: Consumer<PlexClientProvider>(
|
||||
builder:
|
||||
(context, clientProvider, child) {
|
||||
final client = clientProvider.client;
|
||||
if (client == null) {
|
||||
return const Icon(
|
||||
Icons.image,
|
||||
color: Colors.white54,
|
||||
size: 34,
|
||||
);
|
||||
}
|
||||
return Image.network(
|
||||
client.getThumbnailUrl(
|
||||
chapter.thumb,
|
||||
),
|
||||
width: 60,
|
||||
height: 34,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
) =>
|
||||
const Icon(
|
||||
Icons.image,
|
||||
color: Colors.white54,
|
||||
size: 34,
|
||||
),
|
||||
);
|
||||
},
|
||||
final client =
|
||||
clientProvider.client;
|
||||
if (client == null) {
|
||||
return const Icon(
|
||||
Icons.image,
|
||||
color: Colors.white54,
|
||||
size: 34,
|
||||
);
|
||||
}
|
||||
return Image.network(
|
||||
client.getThumbnailUrl(
|
||||
chapter.thumb,
|
||||
),
|
||||
width: 60,
|
||||
height: 34,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
) => const Icon(
|
||||
Icons.image,
|
||||
color: Colors.white54,
|
||||
size: 34,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (isCurrentChapter)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
@@ -190,8 +193,9 @@ class ChapterSheet extends StatelessWidget {
|
||||
title: Text(
|
||||
chapter.label,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isCurrentChapter ? Colors.blue : Colors.white,
|
||||
color: isCurrentChapter
|
||||
? Colors.blue
|
||||
: Colors.white,
|
||||
fontWeight: isCurrentChapter
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
|
||||
@@ -5,10 +5,7 @@ import 'package:media_kit/media_kit.dart';
|
||||
class PlaybackSpeedSheet extends StatelessWidget {
|
||||
final Player player;
|
||||
|
||||
const PlaybackSpeedSheet({
|
||||
super.key,
|
||||
required this.player,
|
||||
});
|
||||
const PlaybackSpeedSheet({super.key, required this.player});
|
||||
|
||||
static BoxConstraints getBottomSheetConstraints(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
@@ -37,10 +37,8 @@ class SleepTimerSheet extends StatelessWidget {
|
||||
backgroundColor: Colors.grey[900],
|
||||
isScrollControlled: true,
|
||||
constraints: getBottomSheetConstraints(context),
|
||||
builder: (context) => SleepTimerSheet(
|
||||
player: player,
|
||||
defaultDuration: defaultDuration,
|
||||
),
|
||||
builder: (context) =>
|
||||
SleepTimerSheet(player: player, defaultDuration: defaultDuration),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +84,9 @@ class SleepTimerSheet extends StatelessWidget {
|
||||
sleepTimer.isActive
|
||||
? Icons.bedtime
|
||||
: Icons.bedtime_outlined,
|
||||
color: sleepTimer.isActive ? Colors.amber : Colors.white,
|
||||
color: sleepTimer.isActive
|
||||
? Colors.amber
|
||||
: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
@@ -136,12 +136,15 @@ class SleepTimerSheet extends StatelessWidget {
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(t.videoControls.addTime(amount: "15", unit: " min")),
|
||||
label: Text(
|
||||
t.videoControls.addTime(
|
||||
amount: "15",
|
||||
unit: " min",
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: const BorderSide(
|
||||
color: Colors.white54,
|
||||
),
|
||||
side: const BorderSide(color: Colors.white54),
|
||||
),
|
||||
onPressed: () {
|
||||
sleepTimer.extendTimer(
|
||||
@@ -180,10 +183,7 @@ class SleepTimerSheet extends StatelessWidget {
|
||||
: '${(minutes / 60).toStringAsFixed(minutes % 60 == 0 ? 0 : 1)} ${minutes == 60 ? 'hour' : 'hours'}';
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(
|
||||
Icons.timer,
|
||||
color: Colors.white70,
|
||||
),
|
||||
leading: const Icon(Icons.timer, color: Colors.white70),
|
||||
title: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
@@ -192,31 +192,30 @@ class SleepTimerSheet extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
sleepTimer.startTimer(
|
||||
Duration(minutes: minutes),
|
||||
() {
|
||||
// Pause playback when timer completes
|
||||
player.pause();
|
||||
sleepTimer.startTimer(Duration(minutes: minutes), () {
|
||||
// Pause playback when timer completes
|
||||
player.pause();
|
||||
|
||||
// Show a snackbar notification
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Sleep timer completed - playback paused',
|
||||
),
|
||||
duration: Duration(seconds: 3),
|
||||
// Show a snackbar notification
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Sleep timer completed - playback paused',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
Navigator.pop(context);
|
||||
|
||||
// Show confirmation snackbar
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.sleepTimerSet(label: label)),
|
||||
content: Text(
|
||||
t.messages.sleepTimerSet(label: label),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,10 +6,7 @@ import '../../../i18n/strings.g.dart';
|
||||
class SubtitleTrackSheet extends StatelessWidget {
|
||||
final Player player;
|
||||
|
||||
const SubtitleTrackSheet({
|
||||
super.key,
|
||||
required this.player,
|
||||
});
|
||||
const SubtitleTrackSheet({super.key, required this.player});
|
||||
|
||||
static BoxConstraints getBottomSheetConstraints(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
@@ -115,9 +112,7 @@ class SubtitleTrackSheet extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
player.setSubtitleTrack(
|
||||
SubtitleTrack.no(),
|
||||
);
|
||||
player.setSubtitleTrack(SubtitleTrack.no());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
@@ -162,15 +157,13 @@ class SubtitleTrackSheet extends StatelessWidget {
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isSelected ? Colors.blue : Colors.white,
|
||||
color: isSelected
|
||||
? Colors.blue
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.blue,
|
||||
)
|
||||
? const Icon(Icons.check, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
player.setSubtitleTrack(subtitle);
|
||||
|
||||
@@ -321,7 +321,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
stream: widget.player.stream.audioDevice,
|
||||
initialData: widget.player.state.audioDevice,
|
||||
builder: (context, snapshot) {
|
||||
final currentDevice = snapshot.data ?? widget.player.state.audioDevice;
|
||||
final currentDevice =
|
||||
snapshot.data ?? widget.player.state.audioDevice;
|
||||
final deviceLabel = currentDevice.description.isEmpty
|
||||
? currentDevice.name
|
||||
: currentDevice.description;
|
||||
@@ -338,7 +339,10 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
deviceLabel,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -432,7 +436,9 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(t.videoControls.addTime(amount: "15", unit: " min")),
|
||||
label: Text(
|
||||
t.videoControls.addTime(amount: "15", unit: " min"),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: const BorderSide(color: Colors.white54),
|
||||
@@ -552,7 +558,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
stream: widget.player.stream.audioDevice,
|
||||
initialData: widget.player.state.audioDevice,
|
||||
builder: (context, selectedSnapshot) {
|
||||
final currentDevice = selectedSnapshot.data ?? widget.player.state.audioDevice;
|
||||
final currentDevice =
|
||||
selectedSnapshot.data ?? widget.player.state.audioDevice;
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: devices.length,
|
||||
|
||||
@@ -1602,9 +1602,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,10 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
// Slider
|
||||
Row(
|
||||
children: [
|
||||
Text(t.videoControls.minusTime(amount: "2", unit: "s"), style: const TextStyle(color: Colors.white70)),
|
||||
Text(
|
||||
t.videoControls.minusTime(amount: "2", unit: "s"),
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _currentOffset,
|
||||
@@ -122,7 +125,10 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(t.videoControls.addTime(amount: "2", unit: "s"), style: const TextStyle(color: Colors.white70)),
|
||||
Text(
|
||||
t.videoControls.addTime(amount: "2", unit: "s"),
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Reference in New Issue
Block a user