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)
|
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user