feat: file version picker
This commit is contained in:
@@ -6,6 +6,7 @@ import '../models/plex_media_info.dart';
|
||||
import '../models/plex_file_info.dart';
|
||||
import '../models/plex_filter.dart';
|
||||
import '../models/plex_sort.dart';
|
||||
import '../models/plex_media_version.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
/// Result of testing a connection, including success status and latency
|
||||
@@ -372,14 +373,22 @@ class PlexClient {
|
||||
}
|
||||
|
||||
/// Get video URL for direct playback
|
||||
Future<String?> getVideoUrl(String ratingKey) async {
|
||||
/// [mediaIndex] specifies which Media item to use (defaults to 0 - first version)
|
||||
Future<String?> getVideoUrl(String ratingKey, {int mediaIndex = 0}) async {
|
||||
final response = await _dio.get('/library/metadata/$ratingKey');
|
||||
final metadataJson = _getFirstMetadataJson(response);
|
||||
|
||||
if (metadataJson != null &&
|
||||
metadataJson['Media'] != null &&
|
||||
(metadataJson['Media'] as List).isNotEmpty) {
|
||||
final media = metadataJson['Media'][0];
|
||||
final mediaList = metadataJson['Media'] as List;
|
||||
|
||||
// Ensure the requested index is valid
|
||||
if (mediaIndex < 0 || mediaIndex >= mediaList.length) {
|
||||
mediaIndex = 0;
|
||||
}
|
||||
|
||||
final media = mediaList[mediaIndex];
|
||||
if (media['Part'] != null && (media['Part'] as List).isNotEmpty) {
|
||||
final part = media['Part'][0];
|
||||
final partKey = part['key'] as String?;
|
||||
@@ -420,14 +429,22 @@ class PlexClient {
|
||||
}
|
||||
|
||||
/// Get detailed media info including chapters and tracks
|
||||
Future<PlexMediaInfo?> getMediaInfo(String ratingKey) async {
|
||||
/// [mediaIndex] specifies which Media item to use (defaults to 0 - first version)
|
||||
Future<PlexMediaInfo?> getMediaInfo(String ratingKey, {int mediaIndex = 0}) async {
|
||||
final response = await _dio.get('/library/metadata/$ratingKey');
|
||||
final metadataJson = _getFirstMetadataJson(response);
|
||||
|
||||
if (metadataJson != null &&
|
||||
metadataJson['Media'] != null &&
|
||||
(metadataJson['Media'] as List).isNotEmpty) {
|
||||
final media = metadataJson['Media'][0];
|
||||
final mediaList = metadataJson['Media'] as List;
|
||||
|
||||
// Ensure the requested index is valid
|
||||
if (mediaIndex < 0 || mediaIndex >= mediaList.length) {
|
||||
mediaIndex = 0;
|
||||
}
|
||||
|
||||
final media = mediaList[mediaIndex];
|
||||
if (media['Part'] != null && (media['Part'] as List).isNotEmpty) {
|
||||
final part = media['Part'][0];
|
||||
final partKey = part['key'] as String?;
|
||||
@@ -506,6 +523,24 @@ class PlexClient {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get all available media versions for a media item
|
||||
/// Returns a list of PlexMediaVersion objects representing different quality/format options
|
||||
Future<List<PlexMediaVersion>> getMediaVersions(String ratingKey) async {
|
||||
final response = await _dio.get('/library/metadata/$ratingKey');
|
||||
final metadataJson = _getFirstMetadataJson(response);
|
||||
|
||||
if (metadataJson != null &&
|
||||
metadataJson['Media'] != null &&
|
||||
(metadataJson['Media'] as List).isNotEmpty) {
|
||||
final mediaList = metadataJson['Media'] as List;
|
||||
return mediaList
|
||||
.map((media) => PlexMediaVersion.fromJson(media as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Get file information for a media item
|
||||
Future<PlexFileInfo?> getFileInfo(String ratingKey) async {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
class PlexMediaVersion {
|
||||
final int id;
|
||||
final String? videoResolution;
|
||||
final String? videoCodec;
|
||||
final int? bitrate;
|
||||
final int? width;
|
||||
final int? height;
|
||||
final String? container;
|
||||
final String partKey;
|
||||
|
||||
PlexMediaVersion({
|
||||
required this.id,
|
||||
this.videoResolution,
|
||||
this.videoCodec,
|
||||
this.bitrate,
|
||||
this.width,
|
||||
this.height,
|
||||
this.container,
|
||||
required this.partKey,
|
||||
});
|
||||
|
||||
/// Creates a PlexMediaVersion from Plex API Media object
|
||||
factory PlexMediaVersion.fromJson(Map<String, dynamic> json) {
|
||||
// Get the first Part key for playback
|
||||
final parts = json['Part'] as List<dynamic>?;
|
||||
final partKey = parts != null && parts.isNotEmpty
|
||||
? parts[0]['key'] as String? ?? ''
|
||||
: '';
|
||||
|
||||
return PlexMediaVersion(
|
||||
id: json['id'] as int? ?? 0,
|
||||
videoResolution: json['videoResolution'] as String?,
|
||||
videoCodec: json['videoCodec'] as String?,
|
||||
bitrate: json['bitrate'] as int?,
|
||||
width: json['width'] as int?,
|
||||
height: json['height'] as int?,
|
||||
container: json['container'] as String?,
|
||||
partKey: partKey,
|
||||
);
|
||||
}
|
||||
|
||||
/// Display label with detailed information: "1080p H.264 MKV (8.5 Mbps)"
|
||||
String get displayLabel {
|
||||
final parts = <String>[];
|
||||
|
||||
// Add resolution
|
||||
if (videoResolution != null && videoResolution!.isNotEmpty) {
|
||||
parts.add('${videoResolution}p');
|
||||
} else if (height != null) {
|
||||
parts.add('${height}p');
|
||||
}
|
||||
|
||||
// Add codec
|
||||
if (videoCodec != null && videoCodec!.isNotEmpty) {
|
||||
parts.add(videoCodec!.toUpperCase());
|
||||
}
|
||||
|
||||
// Add container
|
||||
if (container != null && container!.isNotEmpty) {
|
||||
parts.add(container!.toUpperCase());
|
||||
}
|
||||
|
||||
// Build main label
|
||||
String label = parts.isNotEmpty ? parts.join(' ') : 'Unknown';
|
||||
|
||||
// Add bitrate in parentheses
|
||||
if (bitrate != null && bitrate! > 0) {
|
||||
final bitrateInMbps = (bitrate! / 1000).toStringAsFixed(1);
|
||||
label += ' ($bitrateInMbps Mbps)';
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => displayLabel;
|
||||
}
|
||||
@@ -70,6 +70,74 @@ class PlexMetadata {
|
||||
this.viewedLeafCount,
|
||||
});
|
||||
|
||||
/// Create a copy of this metadata with optional field overrides
|
||||
PlexMetadata copyWith({
|
||||
String? ratingKey,
|
||||
String? key,
|
||||
String? guid,
|
||||
String? studio,
|
||||
String? type,
|
||||
String? title,
|
||||
String? contentRating,
|
||||
String? summary,
|
||||
double? rating,
|
||||
int? year,
|
||||
String? thumb,
|
||||
String? art,
|
||||
int? duration,
|
||||
int? addedAt,
|
||||
int? updatedAt,
|
||||
String? grandparentTitle,
|
||||
String? grandparentThumb,
|
||||
String? grandparentArt,
|
||||
String? grandparentRatingKey,
|
||||
String? parentTitle,
|
||||
String? parentThumb,
|
||||
String? parentRatingKey,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
String? grandparentTheme,
|
||||
int? viewOffset,
|
||||
int? viewCount,
|
||||
int? leafCount,
|
||||
int? viewedLeafCount,
|
||||
}) {
|
||||
final copy = PlexMetadata(
|
||||
ratingKey: ratingKey ?? this.ratingKey,
|
||||
key: key ?? this.key,
|
||||
guid: guid ?? this.guid,
|
||||
studio: studio ?? this.studio,
|
||||
type: type ?? this.type,
|
||||
title: title ?? this.title,
|
||||
contentRating: contentRating ?? this.contentRating,
|
||||
summary: summary ?? this.summary,
|
||||
rating: rating ?? this.rating,
|
||||
year: year ?? this.year,
|
||||
thumb: thumb ?? this.thumb,
|
||||
art: art ?? this.art,
|
||||
duration: duration ?? this.duration,
|
||||
addedAt: addedAt ?? this.addedAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
grandparentTitle: grandparentTitle ?? this.grandparentTitle,
|
||||
grandparentThumb: grandparentThumb ?? this.grandparentThumb,
|
||||
grandparentArt: grandparentArt ?? this.grandparentArt,
|
||||
grandparentRatingKey: grandparentRatingKey ?? this.grandparentRatingKey,
|
||||
parentTitle: parentTitle ?? this.parentTitle,
|
||||
parentThumb: parentThumb ?? this.parentThumb,
|
||||
parentRatingKey: parentRatingKey ?? this.parentRatingKey,
|
||||
parentIndex: parentIndex ?? this.parentIndex,
|
||||
index: index ?? this.index,
|
||||
grandparentTheme: grandparentTheme ?? this.grandparentTheme,
|
||||
viewOffset: viewOffset ?? this.viewOffset,
|
||||
viewCount: viewCount ?? this.viewCount,
|
||||
leafCount: leafCount ?? this.leafCount,
|
||||
viewedLeafCount: viewedLeafCount ?? this.viewedLeafCount,
|
||||
);
|
||||
// Preserve clearLogo
|
||||
copy._clearLogo = _clearLogo;
|
||||
return copy;
|
||||
}
|
||||
|
||||
// Extract clearLogo from Image array in raw JSON
|
||||
void _extractClearLogo(Map<String, dynamic> json) {
|
||||
if (!json.containsKey('Image')) return;
|
||||
|
||||
@@ -14,12 +14,14 @@ import '../services/settings_service.dart';
|
||||
import '../utils/orientation_helper.dart';
|
||||
import '../utils/video_player_navigation.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import '../models/plex_media_version.dart';
|
||||
|
||||
class VideoPlayerScreen extends StatefulWidget {
|
||||
final PlexMetadata metadata;
|
||||
final AudioTrack? preferredAudioTrack;
|
||||
final SubtitleTrack? preferredSubtitleTrack;
|
||||
final double? preferredPlaybackRate;
|
||||
final int selectedMediaIndex;
|
||||
|
||||
const VideoPlayerScreen({
|
||||
super.key,
|
||||
@@ -27,6 +29,7 @@ class VideoPlayerScreen extends StatefulWidget {
|
||||
this.preferredAudioTrack,
|
||||
this.preferredSubtitleTrack,
|
||||
this.preferredPlaybackRate,
|
||||
this.selectedMediaIndex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -44,6 +47,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
bool _showPlayNextDialog = false;
|
||||
PlexClientProvider? _cachedClientProvider;
|
||||
bool _isPhone = false;
|
||||
List<PlexMediaVersion> _availableVersions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -119,6 +123,9 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
// Get the video URL and start playback
|
||||
_startPlayback();
|
||||
|
||||
// Load available media versions
|
||||
_loadMediaVersions();
|
||||
|
||||
// Set fullscreen mode and landscape orientation
|
||||
if (mounted) {
|
||||
try {
|
||||
@@ -182,8 +189,11 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
throw Exception('No client available');
|
||||
}
|
||||
|
||||
// Get the direct file URL from the server
|
||||
final videoUrl = await client.getVideoUrl(widget.metadata.ratingKey);
|
||||
// Get the direct file URL from the server using the selected media index
|
||||
final videoUrl = await client.getVideoUrl(
|
||||
widget.metadata.ratingKey,
|
||||
mediaIndex: widget.selectedMediaIndex,
|
||||
);
|
||||
|
||||
if (videoUrl != null) {
|
||||
// Open video without auto-playing
|
||||
@@ -226,6 +236,24 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load available media versions for this item
|
||||
Future<void> _loadMediaVersions() async {
|
||||
try {
|
||||
final clientProvider = context.plexClient;
|
||||
final client = clientProvider.client;
|
||||
if (client == null) return;
|
||||
|
||||
final versions = await client.getMediaVersions(widget.metadata.ratingKey);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_availableVersions = versions;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.e('Error loading media versions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Stop progress tracking
|
||||
@@ -781,6 +809,8 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
widget.metadata,
|
||||
onNext: _nextEpisode != null ? _playNext : null,
|
||||
onPrevious: _previousEpisode != null ? _playPrevious : null,
|
||||
availableVersions: _availableVersions,
|
||||
selectedMediaIndex: widget.selectedMediaIndex,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,6 +20,7 @@ class SettingsService {
|
||||
static const String _keyUseSeasonPoster = 'use_season_poster';
|
||||
static const String _keySeekTimeSmall = 'seek_time_small';
|
||||
static const String _keySeekTimeLarge = 'seek_time_large';
|
||||
static const String _keyMediaVersionPreferences = 'media_version_preferences';
|
||||
|
||||
static SettingsService? _instance;
|
||||
late SharedPreferences _prefs;
|
||||
@@ -588,6 +589,47 @@ class SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
// Media Version Preferences
|
||||
/// Save media version preference for a series
|
||||
/// [seriesRatingKey] is the grandparentRatingKey for TV series, or ratingKey for movies
|
||||
/// [mediaIndex] is the index of the selected media version
|
||||
Future<void> setMediaVersionPreference(String seriesRatingKey, int mediaIndex) async {
|
||||
final preferences = _getMediaVersionPreferences();
|
||||
preferences[seriesRatingKey] = mediaIndex;
|
||||
|
||||
final jsonString = json.encode(preferences);
|
||||
await _prefs.setString(_keyMediaVersionPreferences, jsonString);
|
||||
}
|
||||
|
||||
/// Get saved media version preference for a series
|
||||
/// Returns null if no preference is saved
|
||||
int? getMediaVersionPreference(String seriesRatingKey) {
|
||||
final preferences = _getMediaVersionPreferences();
|
||||
return preferences[seriesRatingKey];
|
||||
}
|
||||
|
||||
/// Clear media version preference for a series
|
||||
Future<void> clearMediaVersionPreference(String seriesRatingKey) async {
|
||||
final preferences = _getMediaVersionPreferences();
|
||||
preferences.remove(seriesRatingKey);
|
||||
|
||||
final jsonString = json.encode(preferences);
|
||||
await _prefs.setString(_keyMediaVersionPreferences, jsonString);
|
||||
}
|
||||
|
||||
/// Get all media version preferences
|
||||
Map<String, int> _getMediaVersionPreferences() {
|
||||
final jsonString = _prefs.getString(_keyMediaVersionPreferences);
|
||||
if (jsonString == null) return {};
|
||||
|
||||
try {
|
||||
final decoded = json.decode(jsonString) as Map<String, dynamic>;
|
||||
return decoded.map((key, value) => MapEntry(key, value as int));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Reset all settings to defaults
|
||||
Future<void> resetAllSettings() async {
|
||||
await Future.wait([
|
||||
@@ -603,6 +645,7 @@ class SettingsService {
|
||||
_prefs.remove(_keyUseSeasonPoster),
|
||||
_prefs.remove(_keySeekTimeSmall),
|
||||
_prefs.remove(_keySeekTimeLarge),
|
||||
_prefs.remove(_keyMediaVersionPreferences),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../screens/video_player_screen.dart';
|
||||
import '../services/settings_service.dart';
|
||||
|
||||
/// Navigates to the VideoPlayerScreen with instant transitions to prevent white flash.
|
||||
///
|
||||
@@ -15,6 +16,8 @@ import '../screens/video_player_screen.dart';
|
||||
/// - [preferredAudioTrack]: Optional audio track to select on playback start
|
||||
/// - [preferredSubtitleTrack]: Optional subtitle track to select on playback start
|
||||
/// - [preferredPlaybackRate]: Optional playback speed to set on playback start
|
||||
/// - [selectedMediaIndex]: Optional media version index to use; if not provided,
|
||||
/// loads the saved preference for the series/movie. Defaults to 0 if no preference exists.
|
||||
/// - [usePushReplacement]: If true, replaces current route instead of pushing;
|
||||
/// useful for episode-to-episode navigation. Defaults to false.
|
||||
///
|
||||
@@ -26,14 +29,31 @@ Future<bool?> navigateToVideoPlayer(
|
||||
AudioTrack? preferredAudioTrack,
|
||||
SubtitleTrack? preferredSubtitleTrack,
|
||||
double? preferredPlaybackRate,
|
||||
int? selectedMediaIndex,
|
||||
bool usePushReplacement = false,
|
||||
}) async {
|
||||
// Load saved media version preference if not explicitly provided
|
||||
int mediaIndex = selectedMediaIndex ?? 0;
|
||||
if (selectedMediaIndex == null) {
|
||||
try {
|
||||
final settingsService = await SettingsService.getInstance();
|
||||
final seriesKey = metadata.grandparentRatingKey ?? metadata.ratingKey;
|
||||
final savedPreference = settingsService.getMediaVersionPreference(seriesKey);
|
||||
if (savedPreference != null) {
|
||||
mediaIndex = savedPreference;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors loading preference, use default
|
||||
}
|
||||
}
|
||||
|
||||
final route = PageRouteBuilder<bool>(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen(
|
||||
metadata: metadata,
|
||||
preferredAudioTrack: preferredAudioTrack,
|
||||
preferredSubtitleTrack: preferredSubtitleTrack,
|
||||
preferredPlaybackRate: preferredPlaybackRate,
|
||||
selectedMediaIndex: mediaIndex,
|
||||
),
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:window_manager/window_manager.dart';
|
||||
import 'package:macos_window_utils/macos_window_utils.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_media_info.dart';
|
||||
import '../models/plex_media_version.dart';
|
||||
import '../providers/plex_client_provider.dart';
|
||||
import '../services/fullscreen_state_manager.dart';
|
||||
import '../services/keyboard_shortcuts_service.dart';
|
||||
@@ -14,6 +15,7 @@ import '../services/settings_service.dart';
|
||||
import '../utils/desktop_window_padding.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../screens/video_player_screen.dart';
|
||||
import 'app_bar_back_button.dart';
|
||||
|
||||
/// Custom video controls builder for Plex with chapter, audio, and subtitle support
|
||||
@@ -22,12 +24,16 @@ Widget plexVideoControlsBuilder(
|
||||
PlexMetadata metadata, {
|
||||
VoidCallback? onNext,
|
||||
VoidCallback? onPrevious,
|
||||
List<PlexMediaVersion>? availableVersions,
|
||||
int? selectedMediaIndex,
|
||||
}) {
|
||||
return PlexVideoControls(
|
||||
player: player,
|
||||
metadata: metadata,
|
||||
onNext: onNext,
|
||||
onPrevious: onPrevious,
|
||||
availableVersions: availableVersions ?? [],
|
||||
selectedMediaIndex: selectedMediaIndex ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +42,8 @@ class PlexVideoControls extends StatefulWidget {
|
||||
final PlexMetadata metadata;
|
||||
final VoidCallback? onNext;
|
||||
final VoidCallback? onPrevious;
|
||||
final List<PlexMediaVersion> availableVersions;
|
||||
final int selectedMediaIndex;
|
||||
|
||||
const PlexVideoControls({
|
||||
super.key,
|
||||
@@ -43,6 +51,8 @@ class PlexVideoControls extends StatefulWidget {
|
||||
required this.metadata,
|
||||
this.onNext,
|
||||
this.onPrevious,
|
||||
this.availableVersions = const [],
|
||||
this.selectedMediaIndex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -289,6 +299,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
icon: const Icon(Icons.video_library, color: Colors.white),
|
||||
onPressed: _showChapterBottomSheet,
|
||||
),
|
||||
if (widget.availableVersions.length > 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.video_file, color: Colors.white),
|
||||
onPressed: _showVersionBottomSheet,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -1780,6 +1795,119 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
);
|
||||
}
|
||||
|
||||
void _showVersionBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.grey[900],
|
||||
isScrollControlled: true,
|
||||
constraints: _getBottomSheetConstraints(),
|
||||
builder: (context) {
|
||||
final versions = widget.availableVersions;
|
||||
final currentIndex = widget.selectedMediaIndex;
|
||||
|
||||
return SafeArea(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.video_file, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Video Version',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.white24, height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: versions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final version = versions[index];
|
||||
final isSelected = index == currentIndex;
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
version.displayLabel,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.blue : Colors.white,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? const Icon(Icons.check, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_switchMediaVersion(index);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Switch to a different media version
|
||||
Future<void> _switchMediaVersion(int newMediaIndex) async {
|
||||
if (newMediaIndex == widget.selectedMediaIndex) {
|
||||
return; // Already using this version
|
||||
}
|
||||
|
||||
try {
|
||||
// Save current playback position
|
||||
final currentPosition = widget.player.state.position;
|
||||
|
||||
// Save the preference
|
||||
final settingsService = await SettingsService.getInstance();
|
||||
final seriesKey = widget.metadata.grandparentRatingKey ??
|
||||
widget.metadata.ratingKey;
|
||||
await settingsService.setMediaVersionPreference(seriesKey, newMediaIndex);
|
||||
|
||||
// Navigate to new player screen with the selected version
|
||||
// Use PageRouteBuilder with zero-duration transitions to prevent orientation reset
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
PageRouteBuilder<bool>(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen(
|
||||
metadata: widget.metadata.copyWith(
|
||||
viewOffset: currentPosition.inMilliseconds,
|
||||
),
|
||||
selectedMediaIndex: newMediaIndex,
|
||||
),
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error switching version: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
|
||||
Reference in New Issue
Block a user