chore: format
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user