chore: format

This commit is contained in:
edde746
2026-01-20 13:46:44 +01:00
parent 3b0c53d7f9
commit b5726fe4b7
10 changed files with 173 additions and 185 deletions
+3 -3
View File
@@ -327,8 +327,7 @@ class PlexMetadata with MultiServerFields {
} }
// For movies/shows in mixed hub context with episode thumbnail mode, use art (16:9) // For movies/shows in mixed hub context with episode thumbnail mode, use art (16:9)
if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail && if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail && (itemType == 'movie' || itemType == 'show')) {
(itemType == 'movie' || itemType == 'show')) {
return art ?? thumb; return art ?? thumb;
} }
@@ -344,7 +343,8 @@ class PlexMetadata with MultiServerFields {
return true; return true;
} }
// Movies, shows, and seasons use 16:9 in mixed hubs with episode thumbnail mode // Movies, shows, and seasons use 16:9 in mixed hubs with episode thumbnail mode
if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail && if (mixedHubContext &&
mode == EpisodePosterMode.episodeThumbnail &&
(itemType == 'movie' || itemType == 'show' || itemType == 'season')) { (itemType == 'movie' || itemType == 'show' || itemType == 'season')) {
return true; return true;
} }
+7 -1
View File
@@ -46,7 +46,13 @@ class DiscoverScreen extends StatefulWidget {
} }
class _DiscoverScreenState extends State<DiscoverScreen> class _DiscoverScreenState extends State<DiscoverScreen>
with Refreshable, FullRefreshable, ItemUpdatable, WatchStateAware, SingleTickerProviderStateMixin, WidgetsBindingObserver { with
Refreshable,
FullRefreshable,
ItemUpdatable,
WatchStateAware,
SingleTickerProviderStateMixin,
WidgetsBindingObserver {
static const Duration _heroAutoScrollDuration = Duration(seconds: 8); static const Duration _heroAutoScrollDuration = Duration(seconds: 8);
@override @override
+4 -6
View File
@@ -253,10 +253,8 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
final episodePosterMode = context.watch<SettingsProvider>().episodePosterMode; final episodePosterMode = context.watch<SettingsProvider>().episodePosterMode;
// Determine hub content type for layout decisions // Determine hub content type for layout decisions
final hasEpisodes = _filteredItems.any((item) => final hasEpisodes = _filteredItems.any((item) => item.usesWideAspectRatio(episodePosterMode));
item.usesWideAspectRatio(episodePosterMode)); final hasNonEpisodes = _filteredItems.any((item) => !item.usesWideAspectRatio(episodePosterMode));
final hasNonEpisodes = _filteredItems.any((item) =>
!item.usesWideAspectRatio(episodePosterMode));
// Mixed hub = has both episodes AND non-episodes // Mixed hub = has both episodes AND non-episodes
final isMixedHub = hasEpisodes && hasNonEpisodes; final isMixedHub = hasEpisodes && hasNonEpisodes;
@@ -265,8 +263,8 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
final isEpisodeOnlyHub = hasEpisodes && !hasNonEpisodes; final isEpisodeOnlyHub = hasEpisodes && !hasNonEpisodes;
// Use 16:9 for episode-only hubs OR mixed hubs (with episode thumbnail mode) // Use 16:9 for episode-only hubs OR mixed hubs (with episode thumbnail mode)
final useWideLayout = episodePosterMode == EpisodePosterMode.episodeThumbnail && final useWideLayout =
(isEpisodeOnlyHub || isMixedHub); episodePosterMode == EpisodePosterMode.episodeThumbnail && (isEpisodeOnlyHub || isMixedHub);
return MediaGridSliver( return MediaGridSliver(
items: _filteredItems, items: _filteredItems,
+3 -12
View File
@@ -1135,9 +1135,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
controller: _tabController, controller: _tabController,
// Disable swipe on desktop - trackpad scrolling triggers accidental tab switches // Disable swipe on desktop - trackpad scrolling triggers accidental tab switches
// See: https://github.com/flutter/flutter/issues/11132 // See: https://github.com/flutter/flutter/issues/11132
physics: PlatformDetector.isDesktop(context) physics: PlatformDetector.isDesktop(context) ? const NeverScrollableScrollPhysics() : null,
? const NeverScrollableScrollPhysics()
: null,
children: [ children: [
LibraryRecommendedTab( LibraryRecommendedTab(
key: _recommendedTabKey, key: _recommendedTabKey,
@@ -1235,11 +1233,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
final key = _tileKeys[_focusedIndex]; final key = _tileKeys[_focusedIndex];
final context = key?.currentContext; final context = key?.currentContext;
if (context != null) { if (context != null) {
Scrollable.ensureVisible( Scrollable.ensureVisible(context, alignment: 0.25, duration: const Duration(milliseconds: 200));
context,
alignment: 0.25,
duration: const Duration(milliseconds: 200),
);
} }
}); });
} }
@@ -1442,10 +1436,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
), ),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
actions: [ actions: [
IconButton( IconButton(icon: const AppIcon(Symbols.close_rounded, fill: 1), onPressed: () => Navigator.pop(context)),
icon: const AppIcon(Symbols.close_rounded, fill: 1),
onPressed: () => Navigator.pop(context),
),
], ],
), ),
body: Focus( body: Focus(
+25 -25
View File
@@ -48,10 +48,7 @@ class DataAggregationService {
/// Fetch "On Deck" (Continue Watching) from all servers and merge by recency /// Fetch "On Deck" (Continue Watching) from all servers and merge by recency
/// Items are automatically tagged with server info by PlexClient /// Items are automatically tagged with server info by PlexClient
Future<List<PlexMetadata>> getOnDeckFromAllServers({ Future<List<PlexMetadata>> getOnDeckFromAllServers({int? limit, Set<String>? hiddenLibraryKeys}) async {
int? limit,
Set<String>? hiddenLibraryKeys,
}) async {
final allOnDeck = await _perServer<PlexMetadata>( final allOnDeck = await _perServer<PlexMetadata>(
operationName: 'fetching on deck', operationName: 'fetching on deck',
operation: (serverId, client, server) async { operation: (serverId, client, server) async {
@@ -162,29 +159,32 @@ class DataAggregationService {
// Filter out items from hidden libraries if specified // Filter out items from hidden libraries if specified
if (hiddenLibraryKeys != null && hiddenLibraryKeys.isNotEmpty) { if (hiddenLibraryKeys != null && hiddenLibraryKeys.isNotEmpty) {
return hubs.map((hub) { return hubs
final filteredItems = hub.items.where((item) { .map((hub) {
// Build the global key for the item's library section final filteredItems = hub.items.where((item) {
final librarySectionId = item.librarySectionID; // Build the global key for the item's library section
if (librarySectionId == null) return true; // Keep if no section ID final librarySectionId = item.librarySectionID;
final globalKey = '$serverId:$librarySectionId'; if (librarySectionId == null) return true; // Keep if no section ID
return !hiddenLibraryKeys.contains(globalKey); final globalKey = '$serverId:$librarySectionId';
}).toList(); return !hiddenLibraryKeys.contains(globalKey);
}).toList();
if (filteredItems.isEmpty) return null; if (filteredItems.isEmpty) return null;
return PlexHub( return PlexHub(
hubKey: hub.hubKey, hubKey: hub.hubKey,
title: hub.title, title: hub.title,
type: hub.type, type: hub.type,
hubIdentifier: hub.hubIdentifier, hubIdentifier: hub.hubIdentifier,
size: filteredItems.length, size: filteredItems.length,
more: hub.more, more: hub.more,
items: filteredItems, items: filteredItems,
serverId: hub.serverId, serverId: hub.serverId,
serverName: hub.serverName, serverName: hub.serverName,
); );
}).whereType<PlexHub>().toList(); })
.whereType<PlexHub>()
.toList();
} }
return hubs; return hubs;
+2 -8
View File
@@ -360,10 +360,7 @@ class PlexClient {
/// ///
/// When [forceRefresh] is true, bypasses cache to get fresh OnDeck data. /// When [forceRefresh] is true, bypasses cache to get fresh OnDeck data.
/// Use this when cross-device sync is needed (e.g., after app resume). /// Use this when cross-device sync is needed (e.g., after app resume).
Future<Map<String, dynamic>> getMetadataWithImagesAndOnDeck( Future<Map<String, dynamic>> getMetadataWithImagesAndOnDeck(String ratingKey, {bool forceRefresh = false}) async {
String ratingKey, {
bool forceRefresh = false,
}) async {
// Cache key is always the base endpoint (no query params) // Cache key is always the base endpoint (no query params)
final cacheKey = '/library/metadata/$ratingKey'; final cacheKey = '/library/metadata/$ratingKey';
@@ -1265,10 +1262,7 @@ class PlexClient {
/// This matches the official Plex client's home page layout. /// This matches the official Plex client's home page layout.
Future<List<PlexHub>> getGlobalHubs({int limit = 10}) async { Future<List<PlexHub>> getGlobalHubs({int limit = 10}) async {
try { try {
final response = await _dio.get( final response = await _dio.get('/hubs', queryParameters: {'count': limit, 'includeGuids': 1});
'/hubs',
queryParameters: {'count': limit, 'includeGuids': 1},
);
final container = _getMediaContainer(response); final container = _getMediaContainer(response);
if (container != null && container['Hub'] != null) { if (container != null && container['Hub'] != null) {
@@ -382,11 +382,7 @@ class _SessionCodeRow extends StatelessWidget {
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Icon( Icon(Symbols.content_copy_rounded, size: 14, color: theme.colorScheme.onSurfaceVariant),
Symbols.content_copy_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
], ],
), ),
), ),
@@ -395,8 +391,6 @@ class _SessionCodeRow extends StatelessWidget {
void _copySessionCode(BuildContext context) { void _copySessionCode(BuildContext context) {
Clipboard.setData(ClipboardData(text: sessionId)); Clipboard.setData(ClipboardData(text: sessionId));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.watchTogether.sessionCodeCopied)));
SnackBar(content: Text(t.watchTogether.sessionCodeCopied)),
);
} }
} }
@@ -186,9 +186,7 @@ class _SessionMenuSheet extends StatelessWidget {
children: [ children: [
Text( Text(
'${t.watchTogether.sessionCode}: ', '${t.watchTogether.sessionCode}: ',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
color: theme.colorScheme.onSurfaceVariant,
),
), ),
Text( Text(
provider.sessionId!, provider.sessionId!,
@@ -198,11 +196,7 @@ class _SessionMenuSheet extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Icon( Icon(Symbols.content_copy_rounded, size: 16, color: theme.colorScheme.onSurfaceVariant),
Symbols.content_copy_rounded,
size: 16,
color: theme.colorScheme.onSurfaceVariant,
),
], ],
), ),
), ),
@@ -261,9 +255,7 @@ class _SessionMenuSheet extends StatelessWidget {
void _copySessionCode(BuildContext context, String sessionId) { void _copySessionCode(BuildContext context, String sessionId) {
Clipboard.setData(ClipboardData(text: sessionId)); Clipboard.setData(ClipboardData(text: sessionId));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.watchTogether.sessionCodeCopied)));
SnackBar(content: Text(t.watchTogether.sessionCodeCopied)),
);
} }
void _confirmLeave(BuildContext context) { void _confirmLeave(BuildContext context) {
+14 -2
View File
@@ -274,7 +274,13 @@ class _MediaCardGrid extends StatelessWidget {
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(tokens(context).radiusSm), borderRadius: BorderRadius.circular(tokens(context).radiusSm),
child: _buildPosterImage(context, item, isOffline: isOffline, localPosterPath: localPosterPath, mixedHubContext: mixedHubContext), child: _buildPosterImage(
context,
item,
isOffline: isOffline,
localPosterPath: localPosterPath,
mixedHubContext: mixedHubContext,
),
), ),
_PosterOverlay(item: item), _PosterOverlay(item: item),
], ],
@@ -571,7 +577,13 @@ class _MediaCardList extends StatelessWidget {
} }
} }
Widget _buildPosterImage(BuildContext context, dynamic item, {bool isOffline = false, String? localPosterPath, bool mixedHubContext = false}) { Widget _buildPosterImage(
BuildContext context,
dynamic item, {
bool isOffline = false,
String? localPosterPath,
bool mixedHubContext = false,
}) {
String? posterUrl; String? posterUrl;
IconData fallbackIcon = Symbols.movie_rounded; IconData fallbackIcon = Symbols.movie_rounded;
+110 -109
View File
@@ -1283,7 +1283,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
children: [ children: [
// Keep-alive: 1px widget that continuously repaints to prevent // Keep-alive: 1px widget that continuously repaints to prevent
// Flutter animations from freezing when the frame clock goes idle // Flutter animations from freezing when the frame clock goes idle
if (Platform.isLinux || Platform.isWindows) const Positioned(top: 0, left: 0, child: _LinuxKeepAlive()), if (Platform.isLinux || Platform.isWindows)
const Positioned(top: 0, left: 0, child: _LinuxKeepAlive()),
// Invisible tap detector that always covers the full area // Invisible tap detector that always covers the full area
// Also handles long-press for 2x speed // Also handles long-press for 2x speed
Positioned.fill( Positioned.fill(
@@ -1389,119 +1390,119 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
opacity: _showControls ? 1.0 : 0.0, opacity: _showControls ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return GestureDetector( return GestureDetector(
onTapUp: (details) => _handleControlsOverlayTap(details, constraints), onTapUp: (details) => _handleControlsOverlayTap(details, constraints),
onLongPressStart: (_) => _handleLongPressStart(), onLongPressStart: (_) => _handleLongPressStart(),
onLongPressEnd: (_) => _handleLongPressEnd(), onLongPressEnd: (_) => _handleLongPressEnd(),
onLongPressCancel: _handleLongPressCancel, onLongPressCancel: _handleLongPressCancel,
behavior: HitTestBehavior.deferToChild, behavior: HitTestBehavior.deferToChild,
child: ValueListenableBuilder<bool>( child: ValueListenableBuilder<bool>(
valueListenable: widget.hasFirstFrame ?? ValueNotifier(true), valueListenable: widget.hasFirstFrame ?? ValueNotifier(true),
builder: (context, hasFrame, child) { builder: (context, hasFrame, child) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
// Use solid black when loading, gradient when loaded // Use solid black when loading, gradient when loaded
color: hasFrame ? null : Colors.black, color: hasFrame ? null : Colors.black,
gradient: hasFrame gradient: hasFrame
? LinearGradient( ? LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
Colors.black.withValues(alpha: 0.7), Colors.black.withValues(alpha: 0.7),
Colors.transparent, Colors.transparent,
Colors.transparent, Colors.transparent,
Colors.black.withValues(alpha: 0.7), Colors.black.withValues(alpha: 0.7),
], ],
stops: const [0.0, 0.2, 0.8, 1.0], stops: const [0.0, 0.2, 0.8, 1.0],
) )
: null, : null,
), ),
child: child, child: child,
); );
}, },
child: isMobile child: isMobile
? Listener( ? Listener(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartHideTimerIfPlaying(), onPointerDown: (_) => _restartHideTimerIfPlaying(),
child: MobileVideoControls( child: MobileVideoControls(
player: widget.player, player: widget.player,
metadata: widget.metadata, metadata: widget.metadata,
chapters: _chapters, chapters: _chapters,
chaptersLoaded: _chaptersLoaded, chaptersLoaded: _chaptersLoaded,
seekTimeSmall: _seekTimeSmall, seekTimeSmall: _seekTimeSmall,
trackChapterControls: _buildTrackChapterControlsWidget(), trackChapterControls: _buildTrackChapterControlsWidget(),
onSeek: _throttledSeek, onSeek: _throttledSeek,
onSeekEnd: _finalizeSeek, onSeekEnd: _finalizeSeek,
onSeekCompleted: widget.onSeekCompleted, onSeekCompleted: widget.onSeekCompleted,
onPlayPause: () {}, // Not used, handled internally onPlayPause: () {}, // Not used, handled internally
onCancelAutoHide: () => _hideTimer?.cancel(), onCancelAutoHide: () => _hideTimer?.cancel(),
onStartAutoHide: _startHideTimer, onStartAutoHide: _startHideTimer,
onBack: widget.onBack, onBack: widget.onBack,
onNext: widget.onNext, onNext: widget.onNext,
onPrevious: widget.onPrevious, onPrevious: widget.onPrevious,
canControl: widget.canControl, canControl: widget.canControl,
hasFirstFrame: widget.hasFirstFrame, hasFirstFrame: widget.hasFirstFrame,
),
)
: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartHideTimerIfPlaying(),
child: DesktopVideoControls(
key: _desktopControlsKey,
player: widget.player,
metadata: widget.metadata,
onNext: widget.onNext,
onPrevious: widget.onPrevious,
chapters: _chapters,
chaptersLoaded: _chaptersLoaded,
seekTimeSmall: _seekTimeSmall,
onSeekToPreviousChapter: _seekToPreviousChapter,
onSeekToNextChapter: _seekToNextChapter,
onSeek: _throttledSeek,
onSeekEnd: _finalizeSeek,
getReplayIcon: getReplayIcon,
getForwardIcon: getForwardIcon,
onFocusActivity: _restartHideTimerIfPlaying,
onHideControls: _hideControlsFromKeyboard,
// Track chapter controls data
availableVersions: widget.availableVersions,
selectedMediaIndex: widget.selectedMediaIndex,
boxFitMode: widget.boxFitMode,
audioSyncOffset: _audioSyncOffset,
subtitleSyncOffset: _subtitleSyncOffset,
isFullscreen: _isFullscreen,
isAlwaysOnTop: _isAlwaysOnTop,
onTogglePIPMode: (_isPipSupported && Platform.isAndroid)
? widget.onTogglePIPMode
: null,
onCycleBoxFitMode: widget.onCycleBoxFitMode,
onToggleFullscreen: _toggleFullscreen,
onToggleAlwaysOnTop: _toggleAlwaysOnTop,
onSwitchVersion: _switchMediaVersion,
onAudioTrackChanged: widget.onAudioTrackChanged,
onSubtitleTrackChanged: widget.onSubtitleTrackChanged,
onLoadSeekTimes: () async {
if (mounted) {
await _loadSeekTimes();
}
},
onCancelAutoHide: () => _hideTimer?.cancel(),
onStartAutoHide: _startHideTimer,
serverId: widget.metadata.serverId ?? '',
onBack: widget.onBack,
canControl: widget.canControl,
hasFirstFrame: widget.hasFirstFrame,
),
), ),
), )
); : Listener(
}, behavior: HitTestBehavior.translucent,
), onPointerDown: (_) => _restartHideTimerIfPlaying(),
child: DesktopVideoControls(
key: _desktopControlsKey,
player: widget.player,
metadata: widget.metadata,
onNext: widget.onNext,
onPrevious: widget.onPrevious,
chapters: _chapters,
chaptersLoaded: _chaptersLoaded,
seekTimeSmall: _seekTimeSmall,
onSeekToPreviousChapter: _seekToPreviousChapter,
onSeekToNextChapter: _seekToNextChapter,
onSeek: _throttledSeek,
onSeekEnd: _finalizeSeek,
getReplayIcon: getReplayIcon,
getForwardIcon: getForwardIcon,
onFocusActivity: _restartHideTimerIfPlaying,
onHideControls: _hideControlsFromKeyboard,
// Track chapter controls data
availableVersions: widget.availableVersions,
selectedMediaIndex: widget.selectedMediaIndex,
boxFitMode: widget.boxFitMode,
audioSyncOffset: _audioSyncOffset,
subtitleSyncOffset: _subtitleSyncOffset,
isFullscreen: _isFullscreen,
isAlwaysOnTop: _isAlwaysOnTop,
onTogglePIPMode: (_isPipSupported && Platform.isAndroid)
? widget.onTogglePIPMode
: null,
onCycleBoxFitMode: widget.onCycleBoxFitMode,
onToggleFullscreen: _toggleFullscreen,
onToggleAlwaysOnTop: _toggleAlwaysOnTop,
onSwitchVersion: _switchMediaVersion,
onAudioTrackChanged: widget.onAudioTrackChanged,
onSubtitleTrackChanged: widget.onSubtitleTrackChanged,
onLoadSeekTimes: () async {
if (mounted) {
await _loadSeekTimes();
}
},
onCancelAutoHide: () => _hideTimer?.cancel(),
onStartAutoHide: _startHideTimer,
serverId: widget.metadata.serverId ?? '',
onBack: widget.onBack,
canControl: widget.canControl,
hasFirstFrame: widget.hasFirstFrame,
),
),
),
);
},
), ),
), ),
), ),
), ),
),
// Visual feedback overlay for double-tap // Visual feedback overlay for double-tap
if (isMobile && _showDoubleTapFeedback) if (isMobile && _showDoubleTapFeedback)
Positioned.fill( Positioned.fill(