mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-14 01:37:07 -07:00
feature: Ask for playback type when media is downloaded (#361)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
563d267566
commit
5ef7936c33
9 changed files with 194 additions and 148 deletions
|
|
@ -1220,5 +1220,9 @@
|
||||||
"hasLikedDirector": "Has liked director",
|
"hasLikedDirector": "Has liked director",
|
||||||
"hasLikedActor": "Has liked actor",
|
"hasLikedActor": "Has liked actor",
|
||||||
"latest": "Latest",
|
"latest": "Latest",
|
||||||
"recommended": "Recommended"
|
"recommended": "Recommended",
|
||||||
|
"playbackType": "Playback type",
|
||||||
|
"playbackTypeDirect": "Direct",
|
||||||
|
"playbackTypeTranscode": "Transcode",
|
||||||
|
"playbackTypeOffline": "Offline"
|
||||||
}
|
}
|
||||||
|
|
@ -202,14 +202,28 @@ extension EpisodeListExtensions on List<EpisodeModel> {
|
||||||
final episodes = where((e) => e.season > 0 && e.status == EpisodeStatus.available).toList();
|
final episodes = where((e) => e.season > 0 && e.status == EpisodeStatus.available).toList();
|
||||||
if (episodes.isEmpty) return null;
|
if (episodes.isEmpty) return null;
|
||||||
|
|
||||||
final lastWatchedIndex = [
|
final lastProgressIndex = episodes.lastIndexWhere((e) => e.userData.progress != 0);
|
||||||
episodes.lastIndexWhere((e) => e.userData.progress != 0),
|
final lastPlayedIndex = episodes.lastIndexWhere((e) => e.userData.played);
|
||||||
episodes.lastIndexWhere((e) => e.userData.played),
|
final lastWatchedIndex = [lastProgressIndex, lastPlayedIndex].reduce((a, b) => a > b ? a : b);
|
||||||
].reduce((a, b) => a > b ? a : b);
|
|
||||||
|
|
||||||
if (lastWatchedIndex >= 0 && lastWatchedIndex + 1 < episodes.length) {
|
if (lastWatchedIndex >= 0) {
|
||||||
final next = episodes.sublist(lastWatchedIndex + 1).firstWhereOrNull((e) => e.status == EpisodeStatus.available);
|
final current = episodes[lastWatchedIndex];
|
||||||
if (next != null) return next;
|
if (!current.userData.played && current.userData.progress != 0) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextIndex = lastWatchedIndex + 1;
|
||||||
|
if (nextIndex < episodes.length) {
|
||||||
|
final next = episodes[nextIndex];
|
||||||
|
if (!next.userData.played && next.userData.progress != 0) {
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextUnplayed = episodes.sublist(nextIndex).firstWhereOrNull(
|
||||||
|
(e) => e.status == EpisodeStatus.available && !e.userData.played,
|
||||||
|
);
|
||||||
|
if (nextUnplayed != null) return nextUnplayed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return episodes.firstOrNull;
|
return episodes.firstOrNull;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:chopper/chopper.dart';
|
import 'package:chopper/chopper.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -16,6 +18,7 @@ import 'package:fladder/models/items/series_model.dart';
|
||||||
import 'package:fladder/models/items/trick_play_model.dart';
|
import 'package:fladder/models/items/trick_play_model.dart';
|
||||||
import 'package:fladder/models/playback/direct_playback_model.dart';
|
import 'package:fladder/models/playback/direct_playback_model.dart';
|
||||||
import 'package:fladder/models/playback/offline_playback_model.dart';
|
import 'package:fladder/models/playback/offline_playback_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_options_dialogue.dart';
|
||||||
import 'package:fladder/models/playback/transcode_playback_model.dart';
|
import 'package:fladder/models/playback/transcode_playback_model.dart';
|
||||||
import 'package:fladder/models/syncing/sync_item.dart';
|
import 'package:fladder/models/syncing/sync_item.dart';
|
||||||
import 'package:fladder/models/video_stream_model.dart';
|
import 'package:fladder/models/video_stream_model.dart';
|
||||||
|
|
@ -49,10 +52,10 @@ extension PlaybackModelExtension on PlaybackModel? {
|
||||||
AudioStreamModel? get defaultAudioStream =>
|
AudioStreamModel? get defaultAudioStream =>
|
||||||
this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex);
|
this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex);
|
||||||
|
|
||||||
String? get label => switch (this) {
|
String? label(BuildContext context) => switch (this) {
|
||||||
DirectPlaybackModel _ => PlaybackType.directStream.name,
|
DirectPlaybackModel _ => PlaybackType.directStream.name(context),
|
||||||
TranscodePlaybackModel _ => PlaybackType.transcode.name,
|
TranscodePlaybackModel _ => PlaybackType.transcode.name(context),
|
||||||
OfflinePlaybackModel _ => PlaybackType.offline.name,
|
OfflinePlaybackModel _ => PlaybackType.offline.name(context),
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -118,12 +121,12 @@ class PlaybackModelHelper {
|
||||||
ref.read(videoPlayerProvider).pause();
|
ref.read(videoPlayerProvider).pause();
|
||||||
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(buffering: true));
|
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(buffering: true));
|
||||||
final currentModel = ref.read(playBackModel);
|
final currentModel = ref.read(playBackModel);
|
||||||
final newModel = (await createServerPlaybackModel(
|
final newModel = (await createPlaybackModel(
|
||||||
newItem,
|
|
||||||
null,
|
null,
|
||||||
|
newItem,
|
||||||
oldModel: currentModel,
|
oldModel: currentModel,
|
||||||
)) ??
|
)) ??
|
||||||
await createOfflinePlaybackModel(
|
await _createOfflinePlaybackModel(
|
||||||
newItem,
|
newItem,
|
||||||
ref.read(syncProvider.notifier).getSyncedItem(newItem),
|
ref.read(syncProvider.notifier).getSyncedItem(newItem),
|
||||||
oldModel: currentModel,
|
oldModel: currentModel,
|
||||||
|
|
@ -133,7 +136,7 @@ class PlaybackModelHelper {
|
||||||
return newModel;
|
return newModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<OfflinePlaybackModel?> createOfflinePlaybackModel(
|
Future<OfflinePlaybackModel?> _createOfflinePlaybackModel(
|
||||||
ItemBaseModel item,
|
ItemBaseModel item,
|
||||||
SyncedItem? syncedItem, {
|
SyncedItem? syncedItem, {
|
||||||
PlaybackModel? oldModel,
|
PlaybackModel? oldModel,
|
||||||
|
|
@ -157,46 +160,92 @@ class PlaybackModelHelper {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<EpisodeModel?> getNextUpEpisode(String itemId) async {
|
Future<PlaybackModel?> createPlaybackModel(
|
||||||
final response = await api.showsNextUpGet(parentId: itemId, fields: [ItemFields.overview]);
|
BuildContext? context,
|
||||||
final episode = response.body?.items?.firstOrNull;
|
ItemBaseModel? item, {
|
||||||
if (episode == null) {
|
PlaybackModel? oldModel,
|
||||||
return null;
|
List<ItemBaseModel>? libraryQueue,
|
||||||
|
bool showPlaybackOptions = false,
|
||||||
|
Duration? startPosition,
|
||||||
|
}) async {
|
||||||
|
if (item == null) return null;
|
||||||
|
final userId = ref.read(userProvider)?.id;
|
||||||
|
if (userId?.isEmpty == true) return null;
|
||||||
|
|
||||||
|
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
|
||||||
|
|
||||||
|
final firstItemToPlay = switch (item) {
|
||||||
|
SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp),
|
||||||
|
_ => item,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (firstItemToPlay == null) return null;
|
||||||
|
|
||||||
|
final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body;
|
||||||
|
|
||||||
|
if (fullItem == null) return null;
|
||||||
|
|
||||||
|
SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(fullItem);
|
||||||
|
|
||||||
|
final firstItemIsSynced = syncedItem != null && syncedItem.status == SyncStatus.complete;
|
||||||
|
|
||||||
|
final options = {
|
||||||
|
PlaybackType.directStream,
|
||||||
|
PlaybackType.transcode,
|
||||||
|
if (firstItemIsSynced) PlaybackType.offline,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((showPlaybackOptions || firstItemIsSynced) && context != null) {
|
||||||
|
final playbackType = await showPlaybackTypeSelection(
|
||||||
|
context: context,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!context.mounted) return null;
|
||||||
|
|
||||||
|
return switch (playbackType) {
|
||||||
|
PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel(
|
||||||
|
fullItem,
|
||||||
|
playbackType,
|
||||||
|
oldModel: oldModel,
|
||||||
|
libraryQueue: queue,
|
||||||
|
startPosition: startPosition,
|
||||||
|
),
|
||||||
|
PlaybackType.offline => await _createOfflinePlaybackModel(fullItem, syncedItem),
|
||||||
|
null => null
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return EpisodeModel.fromBaseDto(episode, ref);
|
return (await _createServerPlaybackModel(
|
||||||
|
fullItem,
|
||||||
|
PlaybackType.directStream,
|
||||||
|
startPosition: startPosition,
|
||||||
|
oldModel: oldModel,
|
||||||
|
libraryQueue: queue,
|
||||||
|
)) ??
|
||||||
|
await _createOfflinePlaybackModel(fullItem, syncedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PlaybackModel?> createServerPlaybackModel(
|
Future<PlaybackModel?> _createServerPlaybackModel(
|
||||||
ItemBaseModel? item,
|
ItemBaseModel item,
|
||||||
PlaybackType? type, {
|
PlaybackType? type, {
|
||||||
PlaybackModel? oldModel,
|
PlaybackModel? oldModel,
|
||||||
List<ItemBaseModel>? libraryQueue,
|
required List<ItemBaseModel> libraryQueue,
|
||||||
Duration? startPosition,
|
Duration? startPosition,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
if (item == null) return null;
|
|
||||||
final userId = ref.read(userProvider)?.id;
|
final userId = ref.read(userProvider)?.id;
|
||||||
if (userId?.isEmpty == true) return null;
|
if (userId?.isEmpty == true) return null;
|
||||||
|
|
||||||
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
|
|
||||||
|
|
||||||
final firstItemToPlay = switch (item) {
|
|
||||||
SeriesModel _ || SeasonModel _ => (await getNextUpEpisode(item.id) ?? queue.first),
|
|
||||||
_ => item,
|
|
||||||
};
|
|
||||||
|
|
||||||
final fullItem = await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id);
|
|
||||||
|
|
||||||
Map<Bitrate, bool> qualityOptions = getVideoQualityOptions(
|
Map<Bitrate, bool> qualityOptions = getVideoQualityOptions(
|
||||||
VideoQualitySettings(
|
VideoQualitySettings(
|
||||||
maxBitRate: ref.read(videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate)),
|
maxBitRate: ref.read(videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate)),
|
||||||
videoBitRate: firstItemToPlay.streamModel?.videoStreams.firstOrNull?.bitRate ?? 0,
|
videoBitRate: item.streamModel?.videoStreams.firstOrNull?.bitRate ?? 0,
|
||||||
videoCodec: firstItemToPlay.streamModel?.videoStreams.firstOrNull?.codec,
|
videoCodec: item.streamModel?.videoStreams.firstOrNull?.codec,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final streamModel = firstItemToPlay.streamModel;
|
final streamModel = item.streamModel;
|
||||||
final audioStreamIndex = selectAudioStream(
|
final audioStreamIndex = selectAudioStream(
|
||||||
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
|
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
|
||||||
oldModel?.mediaStreams?.currentAudioStream,
|
oldModel?.mediaStreams?.currentAudioStream,
|
||||||
|
|
@ -209,7 +258,7 @@ class PlaybackModelHelper {
|
||||||
streamModel?.defaultSubStreamIndex);
|
streamModel?.defaultSubStreamIndex);
|
||||||
|
|
||||||
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
||||||
itemId: firstItemToPlay.id,
|
itemId: item.id,
|
||||||
body: PlaybackInfoDto(
|
body: PlaybackInfoDto(
|
||||||
startTimeTicks: startPosition?.toRuntimeTicks,
|
startTimeTicks: startPosition?.toRuntimeTicks,
|
||||||
audioStreamIndex: audioStreamIndex,
|
audioStreamIndex: audioStreamIndex,
|
||||||
|
|
@ -238,9 +287,9 @@ class PlaybackModelHelper {
|
||||||
defaultSubStreamIndex: subStreamIndex,
|
defaultSubStreamIndex: subStreamIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mediaSegments = await api.mediaSegmentsGet(id: firstItemToPlay.id);
|
final mediaSegments = await api.mediaSegmentsGet(id: item.id);
|
||||||
final trickPlay = (await api.getTrickPlay(item: fullItem.body, ref: ref))?.body;
|
final trickPlay = (await api.getTrickPlay(item: item, ref: ref))?.body;
|
||||||
final chapters = fullItem.body?.overview.chapters ?? [];
|
final chapters = item.overview.chapters ?? [];
|
||||||
|
|
||||||
final mediaPath = isValidVideoUrl(mediaSource.path ?? "");
|
final mediaPath = isValidVideoUrl(mediaSource.path ?? "");
|
||||||
|
|
||||||
|
|
@ -263,8 +312,8 @@ class PlaybackModelHelper {
|
||||||
final playbackUrl = joinAll([ref.read(userProvider)!.server, "Videos", mediaSource.id!, "stream?$params"]);
|
final playbackUrl = joinAll([ref.read(userProvider)!.server, "Videos", mediaSource.id!, "stream?$params"]);
|
||||||
|
|
||||||
return DirectPlaybackModel(
|
return DirectPlaybackModel(
|
||||||
item: fullItem.body ?? item,
|
item: item,
|
||||||
queue: queue,
|
queue: libraryQueue,
|
||||||
mediaSegments: mediaSegments?.body,
|
mediaSegments: mediaSegments?.body,
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
playbackInfo: playbackInfo,
|
playbackInfo: playbackInfo,
|
||||||
|
|
@ -275,8 +324,8 @@ class PlaybackModelHelper {
|
||||||
);
|
);
|
||||||
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
||||||
return TranscodePlaybackModel(
|
return TranscodePlaybackModel(
|
||||||
item: fullItem.body ?? item,
|
item: item,
|
||||||
queue: queue,
|
queue: libraryQueue,
|
||||||
mediaSegments: mediaSegments?.body,
|
mediaSegments: mediaSegments?.body,
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
trickPlay: trickPlay,
|
trickPlay: trickPlay,
|
||||||
|
|
|
||||||
57
lib/models/playback/playback_options_dialogue.dart
Normal file
57
lib/models/playback/playback_options_dialogue.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/video_stream_model.dart';
|
||||||
|
import 'package:fladder/screens/shared/adaptive_dialog.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
|
Future<PlaybackType?> showPlaybackTypeSelection({
|
||||||
|
required BuildContext context,
|
||||||
|
required Set<PlaybackType> options,
|
||||||
|
}) async {
|
||||||
|
PlaybackType? playbackType;
|
||||||
|
|
||||||
|
await showDialogAdaptive(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return PlaybackDialogue(
|
||||||
|
options: options,
|
||||||
|
onClose: (type) {
|
||||||
|
playbackType = type;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return playbackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaybackDialogue extends StatelessWidget {
|
||||||
|
final Set<PlaybackType> options;
|
||||||
|
final Function(PlaybackType type) onClose;
|
||||||
|
const PlaybackDialogue({required this.options, required this.onClose, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16).add(const EdgeInsets.only(top: 16, bottom: 8)),
|
||||||
|
child: Text(
|
||||||
|
context.localized.playbackType,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
...options.map((type) => ListTile(
|
||||||
|
title: Text(type.name(context)),
|
||||||
|
leading: Icon(type.icon),
|
||||||
|
onTap: () {
|
||||||
|
onClose(type);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/items/chapters_model.dart';
|
import 'package:fladder/models/items/chapters_model.dart';
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
import 'package:fladder/models/syncing/sync_item.dart';
|
import 'package:fladder/models/syncing/sync_item.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
enum PlaybackType {
|
enum PlaybackType {
|
||||||
directStream,
|
directStream,
|
||||||
|
|
@ -24,16 +25,11 @@ enum PlaybackType {
|
||||||
PlaybackType.transcode => IconsaxPlusLinear.convert,
|
PlaybackType.transcode => IconsaxPlusLinear.convert,
|
||||||
};
|
};
|
||||||
|
|
||||||
String get name {
|
String name(BuildContext context) => switch (this) {
|
||||||
switch (this) {
|
PlaybackType.directStream => context.localized.playbackTypeDirect,
|
||||||
case PlaybackType.directStream:
|
PlaybackType.offline => context.localized.playbackTypeOffline,
|
||||||
return "Direct";
|
PlaybackType.transcode => context.localized.playbackTypeTranscode
|
||||||
case PlaybackType.offline:
|
};
|
||||||
return "Offline";
|
|
||||||
case PlaybackType.transcode:
|
|
||||||
return "Transcoding";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoPlayback {
|
class VideoPlayback {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@ Future<void> showDialogAdaptive(
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useSafeArea: false,
|
useSafeArea: false,
|
||||||
builder: (context) => Dialog.fullscreen(
|
builder: (context) => Padding(
|
||||||
child: builder(context),
|
padding: MediaQuery.paddingOf(context),
|
||||||
|
child: Dialog.fullscreen(
|
||||||
|
child: builder(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
children: [const Text('type: '), Text(playbackModel.label(context) ?? "")],
|
||||||
),
|
),
|
||||||
if (sessionInfo.transCodeInfo != null) ...[
|
if (sessionInfo.transCodeInfo != null) ...[
|
||||||
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
|
|
||||||
|
|
@ -461,14 +461,14 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (playbackModel.label != null)
|
if (playbackModel != null)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => showVideoPlaybackInformation(context),
|
onTap: () => showVideoPlaybackInformation(context),
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
playbackModel?.label ?? "",
|
playbackModel.label(context) ?? "",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,12 @@ import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/models/items/photos_model.dart';
|
import 'package:fladder/models/items/photos_model.dart';
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/models/playback/playback_model.dart';
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
import 'package:fladder/models/syncing/sync_item.dart';
|
|
||||||
import 'package:fladder/models/video_stream_model.dart';
|
|
||||||
import 'package:fladder/providers/api_provider.dart';
|
import 'package:fladder/providers/api_provider.dart';
|
||||||
import 'package:fladder/providers/book_viewer_provider.dart';
|
import 'package:fladder/providers/book_viewer_provider.dart';
|
||||||
import 'package:fladder/providers/items/book_details_provider.dart';
|
import 'package:fladder/providers/items/book_details_provider.dart';
|
||||||
import 'package:fladder/providers/sync_provider.dart';
|
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/screens/book_viewer/book_viewer_screen.dart';
|
import 'package:fladder/screens/book_viewer/book_viewer_screen.dart';
|
||||||
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
||||||
import 'package:fladder/screens/shared/adaptive_dialog.dart';
|
|
||||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||||
import 'package:fladder/screens/video_player/video_player.dart';
|
import 'package:fladder/screens/video_player/video_player.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
|
|
@ -210,33 +206,12 @@ extension ItemBaseModelExtensions on ItemBaseModel? {
|
||||||
|
|
||||||
_showLoadingIndicator(context);
|
_showLoadingIndicator(context);
|
||||||
|
|
||||||
SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this);
|
PlaybackModel? model = await ref.read(playbackModelHelper).createPlaybackModel(
|
||||||
|
context,
|
||||||
final options = {
|
itemModel,
|
||||||
PlaybackType.directStream,
|
showPlaybackOptions: showPlaybackOption,
|
||||||
PlaybackType.transcode,
|
startPosition: startPosition,
|
||||||
if (syncedItem != null && syncedItem.status == SyncStatus.complete) PlaybackType.offline,
|
);
|
||||||
};
|
|
||||||
|
|
||||||
PlaybackModel? model;
|
|
||||||
|
|
||||||
if (showPlaybackOption) {
|
|
||||||
final playbackType = await _showPlaybackTypeSelection(
|
|
||||||
context: context,
|
|
||||||
options: options,
|
|
||||||
);
|
|
||||||
|
|
||||||
model = switch (playbackType) {
|
|
||||||
PlaybackType.directStream || PlaybackType.transcode => await ref
|
|
||||||
.read(playbackModelHelper)
|
|
||||||
.createServerPlaybackModel(itemModel, playbackType, startPosition: startPosition),
|
|
||||||
PlaybackType.offline => await ref.read(playbackModelHelper).createOfflinePlaybackModel(itemModel, syncedItem),
|
|
||||||
null => null
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
model = (await ref.read(playbackModelHelper).createServerPlaybackModel(itemModel, PlaybackType.directStream)) ??
|
|
||||||
await ref.read(playbackModelHelper).createOfflinePlaybackModel(itemModel, syncedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _playVideo(context, startPosition: startPosition, current: model, ref: ref);
|
await _playVideo(context, startPosition: startPosition, current: model, ref: ref);
|
||||||
}
|
}
|
||||||
|
|
@ -267,69 +242,17 @@ extension ItemBaseModelsBooleans on List<ItemBaseModel> {
|
||||||
expandedList.shuffle();
|
expandedList.shuffle();
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackModel? model = await ref.read(playbackModelHelper).createServerPlaybackModel(
|
PlaybackModel? model = await ref.read(playbackModelHelper).createPlaybackModel(
|
||||||
|
context,
|
||||||
expandedList.firstOrNull,
|
expandedList.firstOrNull,
|
||||||
PlaybackType.directStream,
|
|
||||||
libraryQueue: expandedList,
|
libraryQueue: expandedList,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await _playVideo(context, ref: ref, queue: expandedList, current: model);
|
await _playVideo(context, ref: ref, queue: expandedList, current: model);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
RefreshState.of(context).refresh();
|
RefreshState.maybeOf(context)?.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PlaybackType?> _showPlaybackTypeSelection({
|
|
||||||
required BuildContext context,
|
|
||||||
required Set<PlaybackType> options,
|
|
||||||
}) async {
|
|
||||||
PlaybackType? playbackType;
|
|
||||||
|
|
||||||
await showDialogAdaptive(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return PlaybackDialogue(
|
|
||||||
options: options,
|
|
||||||
onClose: (type) {
|
|
||||||
playbackType = type;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return playbackType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaybackDialogue extends StatelessWidget {
|
|
||||||
final Set<PlaybackType> options;
|
|
||||||
final Function(PlaybackType type) onClose;
|
|
||||||
const PlaybackDialogue({required this.options, required this.onClose, super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16).add(const EdgeInsets.only(top: 16, bottom: 8)),
|
|
||||||
child: Text(
|
|
||||||
"Playback type",
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
...options.map((type) => ListTile(
|
|
||||||
title: Text(type.name),
|
|
||||||
leading: Icon(type.icon),
|
|
||||||
onTap: () {
|
|
||||||
onClose(type);
|
|
||||||
},
|
|
||||||
))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue