fix: add const constructors and remove unused imports

This commit is contained in:
edde746
2026-02-27 15:43:34 +01:00
parent ab2f8cf6ff
commit 00667fe8b8
21 changed files with 206 additions and 220 deletions
+14 -27
View File
@@ -137,7 +137,7 @@ void main() async {
void _registerShaderLicenses() {
LicenseRegistry.addLicense(() async* {
yield LicenseEntryWithLineBreaks(
yield const LicenseEntryWithLineBreaks(
['Anime4K'],
'MIT License\n'
'\n'
@@ -162,7 +162,7 @@ void _registerShaderLicenses() {
'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE '
'SOFTWARE.',
);
yield LicenseEntryWithLineBreaks(
yield const LicenseEntryWithLineBreaks(
['NVIDIA Image Scaling (NVScaler)'],
'The MIT License (MIT)\n'
'\n'
@@ -390,9 +390,10 @@ class _SetupScreenState extends State<SetupScreen> {
// Check network connectivity early to fast-path airplane mode.
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
final connectivityResult = await Connectivity()
.checkConnectivity()
.timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]);
final connectivityResult = await Connectivity().checkConnectivity().timeout(
const Duration(seconds: 3),
onTimeout: () => [ConnectivityResult.other],
);
final hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
if (hasNetwork) {
@@ -430,10 +431,7 @@ class _SetupScreenState extends State<SetupScreen> {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(
context,
fadeRoute(const MainScreen(isOfflineMode: true)),
);
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
return;
}
@@ -457,18 +455,12 @@ class _SetupScreenState extends State<SetupScreen> {
downloadProvider.resumeQueuedDownloads(result.firstClient!);
});
Navigator.pushReplacement(
context,
fadeRoute(MainScreen(client: result.firstClient!)),
);
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!)));
} else {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(
context,
fadeRoute(const MainScreen(isOfflineMode: true)),
);
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
}
} catch (e, stackTrace) {
appLogger.e('Error during multi-server connection', error: e, stackTrace: stackTrace);
@@ -477,10 +469,7 @@ class _SetupScreenState extends State<SetupScreen> {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(
context,
fadeRoute(const MainScreen(isOfflineMode: true)),
);
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
}
}
}
@@ -493,9 +482,7 @@ class _SetupScreenState extends State<SetupScreen> {
children: [
// Icon dead-center, matching Android 12+ splash position.
// 192dp accounts for the 16% inset in ic_launcher.xml.
Center(
child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288),
),
Center(child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288)),
// Status text below center, independent of icon position.
Positioned(
left: 0,
@@ -507,9 +494,9 @@ class _SetupScreenState extends State<SetupScreen> {
_statusMessage,
key: ValueKey(_statusMessage),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
),
),
),
+2 -5
View File
@@ -113,10 +113,7 @@ class _AuthScreenState extends State<AuthScreen> {
await profileFuture;
if (!mounted) return;
Navigator.pushReplacement(
context,
fadeRoute(MainScreen(client: result.firstClient!)),
);
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!)));
} catch (e) {
appLogger.e('Failed to connect to servers', error: e);
setState(() {
@@ -455,7 +452,7 @@ class _AuthScreenState extends State<AuthScreen> {
padding: const EdgeInsets.symmetric(vertical: 12),
side: BorderSide(color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5)),
),
child: Text(t.auth.debugEnterToken, style: TextStyle(fontSize: 12)),
child: Text(t.auth.debugEnterToken, style: const TextStyle(fontSize: 12)),
),
],
if (_errorMessage != null) ...[
@@ -201,7 +201,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle),
decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle),
),
],
),
@@ -648,7 +648,7 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> {
hintText: t.companionRemote.remote.searchHint,
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(icon: const Icon(Icons.send), onPressed: () => _submit(_controller.text)),
border: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(100))),
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(100))),
),
onSubmitted: _submit,
),
+35 -23
View File
@@ -258,7 +258,6 @@ class _DiscoverScreenState extends State<DiscoverScreen>
_loadContent();
}
/// Handle key events for the hero section
late final _handleHeroKeyEvent = dpadKeyHandler(
onDown: () {
@@ -285,7 +284,6 @@ class _DiscoverScreenState extends State<DiscoverScreen>
},
);
@override
void dispose() {
_hiddenLibrariesProvider?.removeListener(_onHiddenLibrariesChanged);
@@ -781,12 +779,18 @@ class _DiscoverScreenState extends State<DiscoverScreen>
PopupMenuItem(
value: 'switch_profile',
child: Row(
children: [AppIcon(Symbols.people_rounded, fill: 1), SizedBox(width: 8), Text(t.discover.switchProfile)],
children: [
AppIcon(Symbols.people_rounded, fill: 1),
const SizedBox(width: 8),
Text(t.discover.switchProfile),
],
),
),
PopupMenuItem(
value: 'logout',
child: Row(children: [AppIcon(Symbols.logout_rounded, fill: 1), SizedBox(width: 8), Text(t.common.logout)]),
child: Row(
children: [AppIcon(Symbols.logout_rounded, fill: 1), const SizedBox(width: 8), Text(t.common.logout)],
),
),
],
).then((value) {
@@ -838,14 +842,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
onNavigateLeft: _navigateToSidebar,
onNavigateDown: _focusContentFromAppBar,
actions: [
FocusableAction(
icon: Symbols.refresh_rounded,
iconColor: Colors.white,
onPressed: _loadContent,
),
FocusableAction(icon: Symbols.refresh_rounded, iconColor: Colors.white, onPressed: _loadContent),
// Watch Together
FocusableAction(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())),
onPressed: () =>
Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())),
child: Stack(
children: [
IconButton(
@@ -854,7 +855,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
fill: watchTogether.isInSession ? 1 : 0,
color: watchTogether.isInSession ? Theme.of(context).colorScheme.primary : Colors.white,
),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WatchTogetherScreen()),
),
tooltip: 'Watch Together',
),
if (watchTogether.isInSession && watchTogether.participantCount > 1)
@@ -886,7 +890,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
if (isDesktop) {
RemoteSessionDialog.show(context);
} else {
Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen()));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MobileRemoteScreen()),
);
}
},
child: Stack(
@@ -895,13 +902,18 @@ class _DiscoverScreenState extends State<DiscoverScreen>
icon: AppIcon(
Symbols.phone_android_rounded,
fill: companionRemote.isConnected ? 1 : 0,
color: companionRemote.isConnected ? Theme.of(context).colorScheme.primary : Colors.white,
color: companionRemote.isConnected
? Theme.of(context).colorScheme.primary
: Colors.white,
),
onPressed: () {
if (isDesktop) {
RemoteSessionDialog.show(context);
} else {
Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen()));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MobileRemoteScreen()),
);
}
},
tooltip: t.companionRemote.title,
@@ -913,10 +925,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: const Border.fromBorderSide(BorderSide(color: Colors.white, width: 1)),
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 1)),
),
),
),
@@ -944,7 +956,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
child: Row(
children: [
AppIcon(Symbols.people_rounded, fill: 1),
SizedBox(width: 8),
const SizedBox(width: 8),
Text(t.discover.switchProfile),
],
),
@@ -954,7 +966,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
child: Row(
children: [
AppIcon(Symbols.logout_rounded, fill: 1),
SizedBox(width: 8),
const SizedBox(width: 8),
Text(t.common.logout),
],
),
@@ -1097,11 +1109,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: Colors.grey),
SizedBox(height: 16),
const AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(t.discover.noContentAvailable),
SizedBox(height: 8),
Text(t.discover.addMediaToLibraries, style: TextStyle(color: Colors.grey)),
const SizedBox(height: 8),
Text(t.discover.addMediaToLibraries, style: const TextStyle(color: Colors.grey)),
],
),
),
@@ -1575,7 +1587,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
] else
Text(
t.common.play,
style: TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w600),
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w600),
),
],
),
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../focus/focusable_action_bar.dart';
import '../focus/input_mode_tracker.dart';
import '../focus/key_event_utils.dart';
import '../mixins/grid_focus_node_mixin.dart';
import '../providers/settings_provider.dart';
import '../utils/grid_size_calculator.dart';
-4
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../services/plex_client.dart';
@@ -15,11 +14,8 @@ import '../widgets/focusable_media_card.dart';
import '../widgets/media_grid_delegate.dart';
import '../widgets/desktop_app_bar.dart';
import '../widgets/overlay_sheet.dart';
import 'package:flutter/services.dart';
import '../focus/dpad_navigator.dart';
import '../focus/focusable_action_bar.dart';
import '../focus/input_mode_tracker.dart';
import '../focus/key_event_utils.dart';
import '../mixins/grid_focus_node_mixin.dart';
import 'libraries/sort_bottom_sheet.dart';
import 'libraries/state_messages.dart';
-1
View File
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../focus/dpad_navigator.dart';
import '../../focus/focusable_action_bar.dart';
import '../../i18n/strings.g.dart';
import '../../models/livetv_channel.dart';
+1 -1
View File
@@ -533,7 +533,7 @@ class GuideTabState extends State<GuideTab> {
children: [
Row(
children: [
SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight),
const SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight),
Expanded(
child: SingleChildScrollView(
controller: _headerHorizontalController,
+4 -4
View File
@@ -365,7 +365,7 @@ class _TvPinInputState extends State<_TvPinInput> {
_digits[index] = digit;
_activeIndex = index;
_mobileControllers[index].text = digit.toString();
_mobileControllers[index].selection = TextSelection.collapsed(offset: 1);
_mobileControllers[index].selection = const TextSelection.collapsed(offset: 1);
});
if (index < 3) {
@@ -436,12 +436,12 @@ class _TvPinInputState extends State<_TvPinInput> {
maxLength: 2, // allow overwrite
obscureText: true,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
decoration: InputDecoration(
decoration: const InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(FocusTheme.defaultBorderRadius)),
borderRadius: BorderRadius.all(Radius.circular(FocusTheme.defaultBorderRadius)),
),
contentPadding: const EdgeInsets.symmetric(vertical: 14),
contentPadding: EdgeInsets.symmetric(vertical: 14),
),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => _onMobileDigitChanged(i, value),
+6 -6
View File
@@ -235,16 +235,16 @@ class _SearchScreenState extends State<SearchScreen> with Refreshable, FullRefre
: null,
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(100)),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(100)),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(100)),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(100)),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(100)),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(100)),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+8 -3
View File
@@ -59,7 +59,8 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
bool _suppressNextBackKeyUp = false;
bool _routeSubscribed = false;
String _toGlobalKey(String ratingKey, {String? serverId}) => buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey);
String _toGlobalKey(String ratingKey, {String? serverId}) =>
buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey);
// WatchStateAware: watch all episode ratingKeys
@override
@@ -364,14 +365,18 @@ class _EpisodeCardState extends State<_EpisodeCard> {
);
return Row(
children: [
if (widget.episode.duration != null) Text(formatDurationTimestamp(Duration(milliseconds: widget.episode.duration!)), style: mutedStyle),
if (widget.episode.duration != null)
Text(formatDurationTimestamp(Duration(milliseconds: widget.episode.duration!)), style: mutedStyle),
if (widget.episode.originallyAvailableAt != null) ...[
dot,
Text(formatFullDate(widget.episode.originallyAvailableAt!), style: mutedStyle),
],
if (widget.episode.userRating != null && widget.episode.userRating! > 0) ...[
dot,
Padding(padding: const EdgeInsets.only(top: 2), child: Icon(Symbols.star_rounded, size: 12, fill: 1, color: Colors.amber)),
const Padding(
padding: EdgeInsets.only(top: 2),
child: Icon(Symbols.star_rounded, size: 12, fill: 1, color: Colors.amber),
),
const SizedBox(width: 2),
Text(
(widget.episode.userRating! / 2) == (widget.episode.userRating! / 2).truncateToDouble()
-1
View File
@@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:flutter/services.dart';
import 'package:logger/logger.dart';
+6 -7
View File
@@ -51,10 +51,9 @@ class PlexAuthService {
static Future<PlexAuthService> create() async {
final storage = await StorageService.getInstance();
final dio = Dio(BaseOptions(
connectTimeout: ConnectionTimeouts.plexTvConnect,
receiveTimeout: ConnectionTimeouts.plexTvReceive,
));
final dio = Dio(
BaseOptions(connectTimeout: ConnectionTimeouts.plexTvConnect, receiveTimeout: ConnectionTimeouts.plexTvReceive),
);
// Get or create client identifier
String? clientIdentifier = storage.getClientIdentifier();
@@ -276,7 +275,7 @@ class PlexServer {
factory PlexServer.fromJson(Map<String, dynamic> json) {
// Validate required fields first
if (!_isValidServerJson(json)) {
throw FormatException(
throw const FormatException(
'Invalid server data: missing required fields (name, clientIdentifier, accessToken, or connections)',
);
}
@@ -302,7 +301,7 @@ class PlexServer {
// If no valid connections were parsed, this server is unusable
if (connections.isEmpty) {
throw FormatException('Server has no valid connections');
throw const FormatException('Server has no valid connections');
}
DateTime? lastSeenAt;
@@ -863,7 +862,7 @@ 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 const FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)');
}
return PlexConnection(
+21 -21
View File
@@ -399,27 +399,27 @@ class SettingsService extends BaseSharedPreferencesService {
// HotKey Objects (New implementation)
Map<String, HotKey> getDefaultKeyboardHotkeys() {
return {
'play_pause': HotKey(key: PhysicalKeyboardKey.space),
'volume_up': HotKey(key: PhysicalKeyboardKey.arrowUp),
'volume_down': HotKey(key: PhysicalKeyboardKey.arrowDown),
'seek_forward': HotKey(key: PhysicalKeyboardKey.arrowRight),
'seek_backward': HotKey(key: PhysicalKeyboardKey.arrowLeft),
'seek_forward_large': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]),
'seek_backward_large': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]),
'fullscreen_toggle': HotKey(key: PhysicalKeyboardKey.keyF),
'mute_toggle': HotKey(key: PhysicalKeyboardKey.keyM),
'subtitle_toggle': HotKey(key: PhysicalKeyboardKey.keyS),
'audio_track_next': HotKey(key: PhysicalKeyboardKey.keyA),
'subtitle_track_next': HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]),
'chapter_next': HotKey(key: PhysicalKeyboardKey.keyN),
'chapter_previous': HotKey(key: PhysicalKeyboardKey.keyP),
'speed_increase': HotKey(key: PhysicalKeyboardKey.equal),
'speed_decrease': HotKey(key: PhysicalKeyboardKey.minus),
'speed_reset': HotKey(key: PhysicalKeyboardKey.keyR),
'sub_seek_next': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]),
'sub_seek_prev': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]),
'shader_toggle': HotKey(key: PhysicalKeyboardKey.keyG),
'skip_marker': HotKey(key: PhysicalKeyboardKey.enter),
'play_pause': const HotKey(key: PhysicalKeyboardKey.space),
'volume_up': const HotKey(key: PhysicalKeyboardKey.arrowUp),
'volume_down': const HotKey(key: PhysicalKeyboardKey.arrowDown),
'seek_forward': const HotKey(key: PhysicalKeyboardKey.arrowRight),
'seek_backward': const HotKey(key: PhysicalKeyboardKey.arrowLeft),
'seek_forward_large': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]),
'seek_backward_large': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]),
'fullscreen_toggle': const HotKey(key: PhysicalKeyboardKey.keyF),
'mute_toggle': const HotKey(key: PhysicalKeyboardKey.keyM),
'subtitle_toggle': const HotKey(key: PhysicalKeyboardKey.keyS),
'audio_track_next': const HotKey(key: PhysicalKeyboardKey.keyA),
'subtitle_track_next': const HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]),
'chapter_next': const HotKey(key: PhysicalKeyboardKey.keyN),
'chapter_previous': const HotKey(key: PhysicalKeyboardKey.keyP),
'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal),
'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus),
'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR),
'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]),
'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]),
'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG),
'skip_marker': const HotKey(key: PhysicalKeyboardKey.enter),
};
}
+1 -1
View File
@@ -95,7 +95,7 @@ ThemeData monoTheme({required bool dark, bool oled = false}) {
color: c.surface,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: const BorderRadius.all(Radius.circular(14))),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14))),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
@@ -453,7 +453,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.red, borderRadius: const BorderRadius.all(Radius.circular(4))),
decoration: const BoxDecoration(color: Colors.red, borderRadius: BorderRadius.all(Radius.circular(4))),
child: Text(
t.liveTv.live,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
@@ -199,7 +199,7 @@ class MobileVideoControls extends StatelessWidget {
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.red, borderRadius: const BorderRadius.all(Radius.circular(4))),
decoration: const BoxDecoration(color: Colors.red, borderRadius: BorderRadius.all(Radius.circular(4))),
child: Text(
t.liveTv.live,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
@@ -35,7 +35,6 @@ class ChapterSheet extends StatefulWidget {
}
class _ChapterSheetState extends State<ChapterSheet> {
/// Get the PlexClient for chapters, or null if unavailable (offline mode)
PlexClient? _tryGetClientForChapters(BuildContext context) {
if (widget.serverId == null) return null;
@@ -49,55 +48,53 @@ class _ChapterSheetState extends State<ChapterSheet> {
@override
Widget build(BuildContext context) {
return StreamBuilder<Duration>(
stream: widget.player.streams.position,
initialData: widget.player.state.position,
builder: (context, positionSnapshot) {
final currentPosition = positionSnapshot.data ?? Duration.zero;
final currentPositionMs = currentPosition.inMilliseconds;
stream: widget.player.streams.position,
initialData: widget.player.state.position,
builder: (context, positionSnapshot) {
final currentPosition = positionSnapshot.data ?? Duration.zero;
final currentPositionMs = currentPosition.inMilliseconds;
// Find the current chapter based on position
int? currentChapterIndex;
for (int i = 0; i < widget.chapters.length; i++) {
final chapter = widget.chapters[i];
final startMs = chapter.startTimeOffset ?? 0;
final endMs =
chapter.endTimeOffset ??
(i < widget.chapters.length - 1
? widget.chapters[i + 1].startTimeOffset ?? 0
: double.maxFinite.toInt());
// Find the current chapter based on position
int? currentChapterIndex;
for (int i = 0; i < widget.chapters.length; i++) {
final chapter = widget.chapters[i];
final startMs = chapter.startTimeOffset ?? 0;
final endMs =
chapter.endTimeOffset ??
(i < widget.chapters.length - 1 ? widget.chapters[i + 1].startTimeOffset ?? 0 : double.maxFinite.toInt());
if (currentPositionMs >= startMs && currentPositionMs < endMs) {
currentChapterIndex = i;
break;
}
if (currentPositionMs >= startMs && currentPositionMs < endMs) {
currentChapterIndex = i;
break;
}
}
Widget content;
if (!widget.chaptersLoaded) {
content = const Center(child: CircularProgressIndicator());
} else if (widget.chapters.isEmpty) {
content = Center(
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
);
} else {
content = ListView.builder(
itemCount: widget.chapters.length,
itemBuilder: (context, index) {
final chapter = widget.chapters[index];
final isCurrentChapter = currentChapterIndex == index;
Widget content;
if (!widget.chaptersLoaded) {
content = const Center(child: CircularProgressIndicator());
} else if (widget.chapters.isEmpty) {
content = Center(
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
);
} else {
content = ListView.builder(
itemCount: widget.chapters.length,
itemBuilder: (context, index) {
final chapter = widget.chapters[index];
final isCurrentChapter = currentChapterIndex == index;
// Get local file path for offline chapter thumbnails
final localThumbPath = widget.serverId != null && chapter.thumb != null
? DownloadStorageService.instance.getArtworkPathSync(widget.serverId!, chapter.thumb!)
: null;
// Get local file path for offline chapter thumbnails
final localThumbPath = widget.serverId != null && chapter.thumb != null
? DownloadStorageService.instance.getArtworkPathSync(widget.serverId!, chapter.thumb!)
: null;
return FocusableListTile(
leading: chapter.thumb != null
? SizedBox(
width: 60,
height: 34,
child: Stack(
children: [
return FocusableListTile(
leading: chapter.thumb != null
? SizedBox(
width: 60,
height: 34,
child: Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: PlexOptimizedImage.thumb(
@@ -114,48 +111,48 @@ class _ChapterSheetState extends State<ChapterSheet> {
if (isCurrentChapter)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
border: const Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4)),
border: Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
),
),
),
],
),
)
: null,
title: Text(
chapter.label,
style: TextStyle(
color: isCurrentChapter ? Colors.blue : null,
fontWeight: isCurrentChapter ? FontWeight.bold : FontWeight.normal,
),
],
),
)
: null,
title: Text(
chapter.label,
style: TextStyle(
color: isCurrentChapter ? Colors.blue : null,
fontWeight: isCurrentChapter ? FontWeight.bold : FontWeight.normal,
),
subtitle: Text(
formatDurationTimestamp(chapter.startTime),
style: TextStyle(
color: isCurrentChapter ? Colors.blue.withValues(alpha: 0.7) : tokens(context).textMuted,
fontSize: 12,
),
),
subtitle: Text(
formatDurationTimestamp(chapter.startTime),
style: TextStyle(
color: isCurrentChapter ? Colors.blue.withValues(alpha: 0.7) : tokens(context).textMuted,
fontSize: 12,
),
trailing: isCurrentChapter
? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue)
: null,
onTap: () {
widget.player.seek(chapter.startTime);
OverlaySheetController.of(context).close();
},
);
},
);
}
return BaseVideoControlSheet(
title: t.videoControls.chapters,
icon: Symbols.video_library_rounded,
child: content,
),
trailing: isCurrentChapter
? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue)
: null,
onTap: () {
widget.player.seek(chapter.startTime);
OverlaySheetController.of(context).close();
},
);
},
);
},
);
}
return BaseVideoControlSheet(
title: t.videoControls.chapters,
icon: Symbols.video_library_rounded,
child: content,
);
},
);
}
}
@@ -62,9 +62,7 @@ class QueueSheet extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: isCurrent
? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue)
: null,
trailing: isCurrent ? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue) : null,
onTap: () {
onItemSelected(item);
OverlaySheetController.of(context).close();
@@ -74,11 +72,7 @@ class QueueSheet extends StatelessWidget {
);
}
return BaseVideoControlSheet(
title: t.videoControls.queue,
icon: Symbols.queue_music_rounded,
child: content,
);
return BaseVideoControlSheet(title: t.videoControls.queue, icon: Symbols.queue_music_rounded, child: content);
},
);
}
@@ -109,9 +103,9 @@ class QueueSheet extends StatelessWidget {
if (isCurrent)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
border: const Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4)),
border: Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
),
),
),
@@ -1288,7 +1288,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppIcon(Symbols.fast_forward_rounded, fill: 1, color: Colors.white, size: 16),
const AppIcon(Symbols.fast_forward_rounded, fill: 1, color: Colors.white, size: 16),
const SizedBox(width: 4),
const Text(
'2x',
@@ -1565,7 +1565,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// On Windows/Linux with navigation off, ESC only exits fullscreen —
// never exits the player. Consume all back key events and check
// actual window state asynchronously.
if (!_videoPlayerNavigationEnabled && (Platform.isWindows || Platform.isLinux) && event.logicalKey.isBackKey) {
if (!_videoPlayerNavigationEnabled &&
(Platform.isWindows || Platform.isLinux) &&
event.logicalKey.isBackKey) {
if (event is KeyUpEvent) {
_exitFullscreenIfNeeded();
}
@@ -242,25 +242,25 @@ class _TimelineSliderState extends State<TimelineSlider> {
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
),
child: Semantics(
label: t.videoControls.timelineSlider,
slider: true,
child: Slider(
value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0,
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
onChanged: (value) {
setState(() => _dragValue = value);
widget.onSeek(Duration(milliseconds: value.toInt()));
},
onChangeEnd: (value) {
setState(() => _dragValue = null);
widget.onSeekEnd(Duration(milliseconds: value.toInt()));
},
activeColor: Colors.white,
inactiveColor: Colors.transparent,
label: t.videoControls.timelineSlider,
slider: true,
child: Slider(
value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0,
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
onChanged: (value) {
setState(() => _dragValue = value);
widget.onSeek(Duration(milliseconds: value.toInt()));
},
onChangeEnd: (value) {
setState(() => _dragValue = null);
widget.onSeekEnd(Duration(milliseconds: value.toInt()));
},
activeColor: Colors.white,
inactiveColor: Colors.transparent,
),
),
),
),
),
// Chapter marker indicators
if (widget.chaptersLoaded && widget.chapters.isNotEmpty && widget.duration.inMilliseconds > 0)