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)
if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail &&
(itemType == 'movie' || itemType == 'show')) {
if (mixedHubContext && mode == EpisodePosterMode.episodeThumbnail && (itemType == 'movie' || itemType == 'show')) {
return art ?? thumb;
}
@@ -344,7 +343,8 @@ class PlexMetadata with MultiServerFields {
return true;
}
// 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')) {
return true;
}
+7 -1
View File
@@ -46,7 +46,13 @@ class DiscoverScreen extends StatefulWidget {
}
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);
@override
+4 -6
View File
@@ -253,10 +253,8 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
final episodePosterMode = context.watch<SettingsProvider>().episodePosterMode;
// Determine hub content type for layout decisions
final hasEpisodes = _filteredItems.any((item) =>
item.usesWideAspectRatio(episodePosterMode));
final hasNonEpisodes = _filteredItems.any((item) =>
!item.usesWideAspectRatio(episodePosterMode));
final hasEpisodes = _filteredItems.any((item) => item.usesWideAspectRatio(episodePosterMode));
final hasNonEpisodes = _filteredItems.any((item) => !item.usesWideAspectRatio(episodePosterMode));
// Mixed hub = has both episodes AND non-episodes
final isMixedHub = hasEpisodes && hasNonEpisodes;
@@ -265,8 +263,8 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
final isEpisodeOnlyHub = hasEpisodes && !hasNonEpisodes;
// Use 16:9 for episode-only hubs OR mixed hubs (with episode thumbnail mode)
final useWideLayout = episodePosterMode == EpisodePosterMode.episodeThumbnail &&
(isEpisodeOnlyHub || isMixedHub);
final useWideLayout =
episodePosterMode == EpisodePosterMode.episodeThumbnail && (isEpisodeOnlyHub || isMixedHub);
return MediaGridSliver(
items: _filteredItems,
+3 -12
View File
@@ -1135,9 +1135,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
controller: _tabController,
// Disable swipe on desktop - trackpad scrolling triggers accidental tab switches
// See: https://github.com/flutter/flutter/issues/11132
physics: PlatformDetector.isDesktop(context)
? const NeverScrollableScrollPhysics()
: null,
physics: PlatformDetector.isDesktop(context) ? const NeverScrollableScrollPhysics() : null,
children: [
LibraryRecommendedTab(
key: _recommendedTabKey,
@@ -1235,11 +1233,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
final key = _tileKeys[_focusedIndex];
final context = key?.currentContext;
if (context != null) {
Scrollable.ensureVisible(
context,
alignment: 0.25,
duration: const Duration(milliseconds: 200),
);
Scrollable.ensureVisible(context, alignment: 0.25, duration: const Duration(milliseconds: 200));
}
});
}
@@ -1442,10 +1436,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const AppIcon(Symbols.close_rounded, fill: 1),
onPressed: () => Navigator.pop(context),
),
IconButton(icon: const AppIcon(Symbols.close_rounded, fill: 1), onPressed: () => Navigator.pop(context)),
],
),
body: Focus(
+25 -25
View File
@@ -48,10 +48,7 @@ class DataAggregationService {
/// Fetch "On Deck" (Continue Watching) from all servers and merge by recency
/// Items are automatically tagged with server info by PlexClient
Future<List<PlexMetadata>> getOnDeckFromAllServers({
int? limit,
Set<String>? hiddenLibraryKeys,
}) async {
Future<List<PlexMetadata>> getOnDeckFromAllServers({int? limit, Set<String>? hiddenLibraryKeys}) async {
final allOnDeck = await _perServer<PlexMetadata>(
operationName: 'fetching on deck',
operation: (serverId, client, server) async {
@@ -162,29 +159,32 @@ class DataAggregationService {
// Filter out items from hidden libraries if specified
if (hiddenLibraryKeys != null && hiddenLibraryKeys.isNotEmpty) {
return hubs.map((hub) {
final filteredItems = hub.items.where((item) {
// Build the global key for the item's library section
final librarySectionId = item.librarySectionID;
if (librarySectionId == null) return true; // Keep if no section ID
final globalKey = '$serverId:$librarySectionId';
return !hiddenLibraryKeys.contains(globalKey);
}).toList();
return hubs
.map((hub) {
final filteredItems = hub.items.where((item) {
// Build the global key for the item's library section
final librarySectionId = item.librarySectionID;
if (librarySectionId == null) return true; // Keep if no section ID
final globalKey = '$serverId:$librarySectionId';
return !hiddenLibraryKeys.contains(globalKey);
}).toList();
if (filteredItems.isEmpty) return null;
if (filteredItems.isEmpty) return null;
return PlexHub(
hubKey: hub.hubKey,
title: hub.title,
type: hub.type,
hubIdentifier: hub.hubIdentifier,
size: filteredItems.length,
more: hub.more,
items: filteredItems,
serverId: hub.serverId,
serverName: hub.serverName,
);
}).whereType<PlexHub>().toList();
return PlexHub(
hubKey: hub.hubKey,
title: hub.title,
type: hub.type,
hubIdentifier: hub.hubIdentifier,
size: filteredItems.length,
more: hub.more,
items: filteredItems,
serverId: hub.serverId,
serverName: hub.serverName,
);
})
.whereType<PlexHub>()
.toList();
}
return hubs;
+2 -8
View File
@@ -360,10 +360,7 @@ class PlexClient {
///
/// When [forceRefresh] is true, bypasses cache to get fresh OnDeck data.
/// Use this when cross-device sync is needed (e.g., after app resume).
Future<Map<String, dynamic>> getMetadataWithImagesAndOnDeck(
String ratingKey, {
bool forceRefresh = false,
}) async {
Future<Map<String, dynamic>> getMetadataWithImagesAndOnDeck(String ratingKey, {bool forceRefresh = false}) async {
// Cache key is always the base endpoint (no query params)
final cacheKey = '/library/metadata/$ratingKey';
@@ -1265,10 +1262,7 @@ class PlexClient {
/// This matches the official Plex client's home page layout.
Future<List<PlexHub>> getGlobalHubs({int limit = 10}) async {
try {
final response = await _dio.get(
'/hubs',
queryParameters: {'count': limit, 'includeGuids': 1},
);
final response = await _dio.get('/hubs', queryParameters: {'count': limit, 'includeGuids': 1});
final container = _getMediaContainer(response);
if (container != null && container['Hub'] != null) {
@@ -382,11 +382,7 @@ class _SessionCodeRow extends StatelessWidget {
),
),
const SizedBox(width: 4),
Icon(
Symbols.content_copy_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
Icon(Symbols.content_copy_rounded, size: 14, color: theme.colorScheme.onSurfaceVariant),
],
),
),
@@ -395,8 +391,6 @@ class _SessionCodeRow extends StatelessWidget {
void _copySessionCode(BuildContext context) {
Clipboard.setData(ClipboardData(text: sessionId));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.watchTogether.sessionCodeCopied)),
);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.watchTogether.sessionCodeCopied)));
}
}
@@ -186,9 +186,7 @@ class _SessionMenuSheet extends StatelessWidget {
children: [
Text(
'${t.watchTogether.sessionCode}: ',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
),
Text(
provider.sessionId!,
@@ -198,11 +196,7 @@ class _SessionMenuSheet extends StatelessWidget {
),
),
const SizedBox(width: 8),
Icon(
Symbols.content_copy_rounded,
size: 16,
color: theme.colorScheme.onSurfaceVariant,
),
Icon(Symbols.content_copy_rounded, size: 16, color: theme.colorScheme.onSurfaceVariant),
],
),
),
@@ -261,9 +255,7 @@ class _SessionMenuSheet extends StatelessWidget {
void _copySessionCode(BuildContext context, String sessionId) {
Clipboard.setData(ClipboardData(text: sessionId));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.watchTogether.sessionCodeCopied)),
);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.watchTogether.sessionCodeCopied)));
}
void _confirmLeave(BuildContext context) {
+14 -2
View File
@@ -274,7 +274,13 @@ class _MediaCardGrid extends StatelessWidget {
children: [
ClipRRect(
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),
],
@@ -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;
IconData fallbackIcon = Symbols.movie_rounded;
+110 -109
View File
@@ -1283,7 +1283,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
children: [
// Keep-alive: 1px widget that continuously repaints to prevent
// 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
// Also handles long-press for 2x speed
Positioned.fill(
@@ -1389,119 +1390,119 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
opacity: _showControls ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
onTapUp: (details) => _handleControlsOverlayTap(details, constraints),
onLongPressStart: (_) => _handleLongPressStart(),
onLongPressEnd: (_) => _handleLongPressEnd(),
onLongPressCancel: _handleLongPressCancel,
behavior: HitTestBehavior.deferToChild,
child: ValueListenableBuilder<bool>(
valueListenable: widget.hasFirstFrame ?? ValueNotifier(true),
builder: (context, hasFrame, child) {
return Container(
decoration: BoxDecoration(
// Use solid black when loading, gradient when loaded
color: hasFrame ? null : Colors.black,
gradient: hasFrame
? LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
stops: const [0.0, 0.2, 0.8, 1.0],
)
: null,
),
child: child,
);
},
child: isMobile
? Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartHideTimerIfPlaying(),
child: MobileVideoControls(
player: widget.player,
metadata: widget.metadata,
chapters: _chapters,
chaptersLoaded: _chaptersLoaded,
seekTimeSmall: _seekTimeSmall,
trackChapterControls: _buildTrackChapterControlsWidget(),
onSeek: _throttledSeek,
onSeekEnd: _finalizeSeek,
onSeekCompleted: widget.onSeekCompleted,
onPlayPause: () {}, // Not used, handled internally
onCancelAutoHide: () => _hideTimer?.cancel(),
onStartAutoHide: _startHideTimer,
onBack: widget.onBack,
onNext: widget.onNext,
onPrevious: widget.onPrevious,
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,
),
builder: (context, constraints) {
return GestureDetector(
onTapUp: (details) => _handleControlsOverlayTap(details, constraints),
onLongPressStart: (_) => _handleLongPressStart(),
onLongPressEnd: (_) => _handleLongPressEnd(),
onLongPressCancel: _handleLongPressCancel,
behavior: HitTestBehavior.deferToChild,
child: ValueListenableBuilder<bool>(
valueListenable: widget.hasFirstFrame ?? ValueNotifier(true),
builder: (context, hasFrame, child) {
return Container(
decoration: BoxDecoration(
// Use solid black when loading, gradient when loaded
color: hasFrame ? null : Colors.black,
gradient: hasFrame
? LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
stops: const [0.0, 0.2, 0.8, 1.0],
)
: null,
),
child: child,
);
},
child: isMobile
? Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartHideTimerIfPlaying(),
child: MobileVideoControls(
player: widget.player,
metadata: widget.metadata,
chapters: _chapters,
chaptersLoaded: _chaptersLoaded,
seekTimeSmall: _seekTimeSmall,
trackChapterControls: _buildTrackChapterControlsWidget(),
onSeek: _throttledSeek,
onSeekEnd: _finalizeSeek,
onSeekCompleted: widget.onSeekCompleted,
onPlayPause: () {}, // Not used, handled internally
onCancelAutoHide: () => _hideTimer?.cancel(),
onStartAutoHide: _startHideTimer,
onBack: widget.onBack,
onNext: widget.onNext,
onPrevious: widget.onPrevious,
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
if (isMobile && _showDoubleTapFeedback)
Positioned.fill(