Merge pull request #6 from Doezer/doezer/run-dart-format

Ran dart format using this pwsh command
This commit is contained in:
Doezer
2025-11-13 22:56:08 +01:00
committed by GitHub
38 changed files with 530 additions and 371 deletions
+2 -1
View File
@@ -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)';
}
+1 -1
View File
@@ -27,4 +27,4 @@ class PlexVideoPlaybackData {
/// Returns true if there are multiple media versions available
bool get hasMultipleVersions => availableVersions.length > 1;
}
}
+4 -1
View File
@@ -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
+1 -3
View File
@@ -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(
+22 -14
View File
@@ -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(
+12 -11
View File
@@ -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,
+5 -1
View File
@@ -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',
+40 -16
View File
@@ -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),
+6 -2
View File
@@ -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),
+36 -49
View File
@@ -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'),
),
),
],
+3 -1
View File
@@ -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,
),
);
+12 -6
View File
@@ -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),
),
);
}
}
+53 -18
View File
@@ -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);
},
);
},
),
],
+3 -3
View File
@@ -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()))),
);
}
}
}
+2 -2
View File
@@ -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);
}
+28 -9
View File
@@ -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',
+4 -2
View File
@@ -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
+19 -9
View File
@@ -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;
}
+8 -8
View File
@@ -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
View File
@@ -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;
}
+6 -11
View File
@@ -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;
}
+3 -1
View File
@@ -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,
),
);
+3 -1
View File
@@ -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;
}
+1 -3
View File
@@ -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,
);
+68 -21
View File
@@ -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(
+8 -15
View File
@@ -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
View File
@@ -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,
),
),
],
],
+16 -9
View File
@@ -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(
+3 -1
View File
@@ -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(),
+10 -10
View File
@@ -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),