Initial commit
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
||||
class DetailHeroModel {
|
||||
final String title;
|
||||
final String? logoUrl;
|
||||
final String? posterUrl;
|
||||
final double? rating;
|
||||
final List<String> metaParts;
|
||||
final List<String> genres;
|
||||
final String? tagline;
|
||||
final String? overview;
|
||||
final Widget? actions;
|
||||
|
||||
|
||||
final Map<String, String>? imageHeaders;
|
||||
|
||||
const DetailHeroModel({
|
||||
required this.title,
|
||||
this.logoUrl,
|
||||
this.posterUrl,
|
||||
this.rating,
|
||||
this.metaParts = const <String>[],
|
||||
this.genres = const <String>[],
|
||||
this.tagline,
|
||||
this.overview,
|
||||
this.actions,
|
||||
this.imageHeaders,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
class DetailImageModel {
|
||||
final String? backdropUrl;
|
||||
final List<String> fallbackUrls;
|
||||
final String embyBaseUrl;
|
||||
final Map<String, String>? imageHeaders;
|
||||
|
||||
const DetailImageModel({
|
||||
this.backdropUrl,
|
||||
this.fallbackUrls = const <String>[],
|
||||
this.embyBaseUrl = '',
|
||||
this.imageHeaders,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
||||
class DetailSection {
|
||||
final String id;
|
||||
final Widget child;
|
||||
|
||||
const DetailSection({required this.id, required this.child});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'detail_hero_model.dart';
|
||||
import 'detail_image_model.dart';
|
||||
import 'detail_section_model.dart';
|
||||
|
||||
|
||||
class DetailViewState {
|
||||
|
||||
final DetailImageModel image;
|
||||
|
||||
|
||||
final String navTitle;
|
||||
|
||||
|
||||
final DetailHeroModel hero;
|
||||
|
||||
|
||||
final List<DetailSection> sections;
|
||||
|
||||
|
||||
final bool showLoadingBelowHero;
|
||||
|
||||
const DetailViewState({
|
||||
required this.image,
|
||||
required this.navTitle,
|
||||
required this.hero,
|
||||
this.sections = const <DetailSection>[],
|
||||
this.showLoadingBelowHero = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/domain/usecases/series_resume_target.dart';
|
||||
|
||||
|
||||
class EpisodeAutoSelectArgs {
|
||||
|
||||
final bool autoSelectFirstEpisode;
|
||||
|
||||
|
||||
final String? autoSelectEpisodeId;
|
||||
|
||||
|
||||
final String? autoSelectSeasonId;
|
||||
|
||||
const EpisodeAutoSelectArgs({
|
||||
required this.autoSelectFirstEpisode,
|
||||
required this.autoSelectEpisodeId,
|
||||
required this.autoSelectSeasonId,
|
||||
});
|
||||
|
||||
|
||||
factory EpisodeAutoSelectArgs.fromResume(
|
||||
AsyncValue<SeriesResumeTarget?>? resumeAsync,
|
||||
) {
|
||||
final loading = resumeAsync?.isLoading ?? false;
|
||||
final target = resumeAsync?.value;
|
||||
return EpisodeAutoSelectArgs(
|
||||
autoSelectFirstEpisode: !loading,
|
||||
autoSelectEpisodeId: target?.episodeId,
|
||||
autoSelectSeasonId: target?.seasonId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import '../../../core/contracts/library.dart';
|
||||
|
||||
|
||||
EmbyRawItem mergeSelectedEpisodeDetail({
|
||||
required EmbyRawItem detail,
|
||||
required EmbyRawItem listItem,
|
||||
}) {
|
||||
final mergedRunTime = detail.RunTimeTicks ?? listItem.RunTimeTicks;
|
||||
final mergedUserData = _mergeUserData(detail.UserData, listItem.UserData);
|
||||
if (mergedRunTime == detail.RunTimeTicks &&
|
||||
identical(mergedUserData, detail.UserData)) {
|
||||
return detail;
|
||||
}
|
||||
return detail.copyWith(RunTimeTicks: mergedRunTime, UserData: mergedUserData);
|
||||
}
|
||||
|
||||
EmbyRawUserData? _mergeUserData(
|
||||
EmbyRawUserData? detail,
|
||||
EmbyRawUserData? listItem,
|
||||
) {
|
||||
if (listItem == null) return detail;
|
||||
if (detail == null) return listItem;
|
||||
if (detail.PlaybackPositionTicks == null &&
|
||||
listItem.PlaybackPositionTicks != null) {
|
||||
return detail.copyWith(
|
||||
PlaybackPositionTicks: listItem.PlaybackPositionTicks,
|
||||
);
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
EmbyRawItem applyPlaybackReturnProgress(EmbyRawItem item, int? positionTicks) {
|
||||
if (positionTicks == null || positionTicks <= 0) return item;
|
||||
final userData = (item.UserData ?? const EmbyRawUserData()).copyWith(
|
||||
PlaybackPositionTicks: positionTicks,
|
||||
);
|
||||
return item.copyWith(UserData: userData);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import '../../../core/contracts/library.dart';
|
||||
import '../../../core/infra/emby_api/playback_resolver.dart';
|
||||
|
||||
|
||||
class StreamSelection {
|
||||
|
||||
final List<EmbyRawMediaStream> subtitles;
|
||||
|
||||
|
||||
final List<EmbyRawMediaStream> audios;
|
||||
|
||||
|
||||
final int preferredSubtitleStreamIndex;
|
||||
|
||||
|
||||
final int? preferredAudioStreamIndex;
|
||||
|
||||
const StreamSelection({
|
||||
required this.subtitles,
|
||||
required this.audios,
|
||||
required this.preferredSubtitleStreamIndex,
|
||||
required this.preferredAudioStreamIndex,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
EmbyRawMediaSource? resolveActiveMediaSource(
|
||||
CrossServerSourceCard? selectedCrossServerCard,
|
||||
List<EmbyRawMediaSource> localSources,
|
||||
int selectedSourceIdx,
|
||||
) {
|
||||
return selectedCrossServerCard?.mediaSource ??
|
||||
(selectedSourceIdx < localSources.length
|
||||
? localSources[selectedSourceIdx]
|
||||
: null);
|
||||
}
|
||||
|
||||
|
||||
StreamSelection resolveStreamSelection(
|
||||
EmbyRawMediaSource? source,
|
||||
int? selectedSubtitleIdx,
|
||||
int selectedAudioIdx,
|
||||
) {
|
||||
final subtitles = (source?.MediaStreams ?? [])
|
||||
.where((s) => s.Type == 'Subtitle')
|
||||
.where((s) {
|
||||
final isExternal =
|
||||
(s.IsExternal ?? false) ||
|
||||
s.DeliveryMethod == 'External' ||
|
||||
(s.DeliveryUrl != null && s.DeliveryUrl!.isNotEmpty);
|
||||
if (!isExternal) return true;
|
||||
return PlaybackResolver.isTextSubtitleCodec(s.Codec);
|
||||
})
|
||||
.toList();
|
||||
final audios = (source?.MediaStreams ?? [])
|
||||
.where((s) => s.Type == 'Audio')
|
||||
.toList();
|
||||
final preferredSubtitleStreamIndex =
|
||||
selectedSubtitleIdx != null && selectedSubtitleIdx < subtitles.length
|
||||
? subtitles[selectedSubtitleIdx].Index ?? -1
|
||||
: -1;
|
||||
final preferredAudioStreamIndex = selectedAudioIdx < audios.length
|
||||
? audios[selectedAudioIdx].Index
|
||||
: null;
|
||||
return StreamSelection(
|
||||
subtitles: subtitles,
|
||||
audios: audios,
|
||||
preferredSubtitleStreamIndex: preferredSubtitleStreamIndex,
|
||||
preferredAudioStreamIndex: preferredAudioStreamIndex,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
int resolveDefaultSourceIndex(
|
||||
List<EmbyRawMediaSource> sortedSources,
|
||||
String? rememberedMediaSourceId, {
|
||||
int fallback = 0,
|
||||
}) {
|
||||
if (rememberedMediaSourceId == null) return fallback;
|
||||
final idx = sortedSources.indexWhere((s) => s.Id == rememberedMediaSourceId);
|
||||
return idx >= 0 ? idx : fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user