Initial commit
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/contracts/library.dart';
|
||||
import '../../core/contracts/tmdb.dart';
|
||||
import '../../providers/detail_palette_provider.dart';
|
||||
import '../../shared/mappers/tmdb_image_selector.dart';
|
||||
import '../../shared/mappers/tmdb_image_url.dart';
|
||||
import '../../shared/utils/format_utils.dart';
|
||||
import '../../shared/utils/tmdb_cta_model.dart';
|
||||
import 'utils/detail_helpers.dart';
|
||||
import 'android/android_detail_layout.dart';
|
||||
import 'android/android_detail_colors.dart';
|
||||
import 'android/android_external_links_section.dart';
|
||||
import 'android/android_hero_actions.dart';
|
||||
import 'android/android_hero_info.dart';
|
||||
import 'android/android_overview_section.dart';
|
||||
import 'android/android_section_divider.dart';
|
||||
import 'android/android_stream_selector_row.dart';
|
||||
import 'detail_entry.dart';
|
||||
import 'model/detail_section_model.dart';
|
||||
import 'widgets/tmdb_cast_section.dart';
|
||||
import 'widgets/tmdb_recommendation_section.dart';
|
||||
|
||||
class TmdbDetailBodyAndroid extends ConsumerWidget {
|
||||
const TmdbDetailBodyAndroid({
|
||||
super.key,
|
||||
required this.entry,
|
||||
required this.enriched,
|
||||
required this.imageBaseUrl,
|
||||
required this.language,
|
||||
required this.showSpinner,
|
||||
required this.scrollController,
|
||||
required this.scrollProgress,
|
||||
required this.primaryHit,
|
||||
required this.availabilityResolving,
|
||||
required this.isFavorite,
|
||||
required this.isPlayed,
|
||||
required this.subtitles,
|
||||
required this.audios,
|
||||
required this.selectedSubtitleIdx,
|
||||
required this.selectedAudioIdx,
|
||||
required this.onPlay,
|
||||
required this.onReplay,
|
||||
required this.onToggleFavorite,
|
||||
required this.onTogglePlayed,
|
||||
required this.onSelectSubtitleIndex,
|
||||
required this.onSelectAudioIndex,
|
||||
required this.hitCount,
|
||||
required this.hitStateSections,
|
||||
});
|
||||
|
||||
final TmdbDetailEntry entry;
|
||||
final TmdbEnrichedDetail? enriched;
|
||||
final String imageBaseUrl;
|
||||
final String language;
|
||||
final bool showSpinner;
|
||||
final ScrollController scrollController;
|
||||
final ValueNotifier<double> scrollProgress;
|
||||
final TmdbLibraryHit? primaryHit;
|
||||
final bool availabilityResolving;
|
||||
final bool isFavorite;
|
||||
final bool isPlayed;
|
||||
final List<EmbyRawMediaStream> subtitles;
|
||||
final List<EmbyRawMediaStream> audios;
|
||||
final int? selectedSubtitleIdx;
|
||||
final int selectedAudioIdx;
|
||||
final VoidCallback onPlay;
|
||||
final VoidCallback onReplay;
|
||||
final VoidCallback onToggleFavorite;
|
||||
final VoidCallback onTogglePlayed;
|
||||
final ValueChanged<int?> onSelectSubtitleIndex;
|
||||
final ValueChanged<int> onSelectAudioIndex;
|
||||
final int hitCount;
|
||||
final List<DetailSection> hitStateSections;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mediaType = entry.mediaType;
|
||||
final detail = enriched?.detail;
|
||||
final title = (detail?.title.isNotEmpty ?? false)
|
||||
? detail!.title
|
||||
: (entry.seed?.title ?? '');
|
||||
|
||||
final posterPath = detail?.posterPath ?? entry.seed?.posterPath;
|
||||
final backdropPath = detail?.backdropPath ?? entry.seed?.backdropPath;
|
||||
final posterUrl = TmdbImageUrl.poster(imageBaseUrl, posterPath);
|
||||
final backdropUrl = TmdbImageUrl.backdrop(imageBaseUrl, backdropPath);
|
||||
final fallbackUrls = TmdbImageSelector.buildFallbackUrls(
|
||||
primaryUrl: backdropUrl,
|
||||
candidates: [posterUrl],
|
||||
);
|
||||
final paletteImageUrl = posterUrl ?? backdropUrl;
|
||||
final extractedBackgroundColor = paletteImageUrl == null
|
||||
? null
|
||||
: ref
|
||||
.watch(
|
||||
detailPaletteProvider(
|
||||
DetailPaletteRequest(url: paletteImageUrl),
|
||||
),
|
||||
)
|
||||
.value;
|
||||
final detailBackgroundColor =
|
||||
extractedBackgroundColor ?? AndroidDetailColors.background;
|
||||
|
||||
final metaParts = <String>[];
|
||||
final year = yearOf(detail?.releaseDate);
|
||||
if (year != null) metaParts.add(year);
|
||||
final runtime = formatRuntimeMinutes(detail?.runtime);
|
||||
if (runtime != null) metaParts.add(runtime);
|
||||
|
||||
final genreNames = [
|
||||
for (final g in detail?.genres ?? const <TmdbGenre>[])
|
||||
if (g.name.isNotEmpty) g.name,
|
||||
];
|
||||
|
||||
final overview = (detail?.overview?.trim().isNotEmpty ?? false)
|
||||
? detail!.overview!.trim()
|
||||
: (entry.seed?.overview?.trim() ?? '');
|
||||
|
||||
final voteAverage = detail?.voteAverage ?? entry.seed?.voteAverage;
|
||||
|
||||
String? playLabel;
|
||||
bool showReplay = false;
|
||||
double? progressPercent;
|
||||
String? positionClock;
|
||||
bool canPlay = false;
|
||||
|
||||
if (!availabilityResolving) {
|
||||
final ctaLabel = buildTmdbCtaLabel(
|
||||
mediaType: mediaType,
|
||||
primaryHit: primaryHit,
|
||||
hitCount: hitCount,
|
||||
);
|
||||
if (ctaLabel != null && primaryHit != null) {
|
||||
canPlay = true;
|
||||
playLabel = ctaLabel;
|
||||
final pos = primaryHit!.playbackPositionTicks ?? 0;
|
||||
final rt = primaryHit!.runTimeTicks ?? 0;
|
||||
final hasProgress = pos > 0;
|
||||
progressPercent = (hasProgress && rt > 0)
|
||||
? (pos / rt * 100).clamp(0.0, 100.0)
|
||||
: null;
|
||||
positionClock = hasProgress ? formatTicksAsClock(pos) : null;
|
||||
showReplay = hasProgress;
|
||||
}
|
||||
}
|
||||
|
||||
final tmdbIdInt = detail?.id;
|
||||
final imdbIdStr = enriched?.imdbId;
|
||||
|
||||
final cast = enriched?.credits?.cast ?? const <TmdbCastMember>[];
|
||||
final recommendations =
|
||||
enriched?.recommendations?.results ?? const <TmdbRecommendation>[];
|
||||
|
||||
final sectionWidgets = <Widget>[
|
||||
for (final section in hitStateSections)
|
||||
if (section.id != 'tmdb_movie_divider' &&
|
||||
section.id != 'tmdb_tv_divider')
|
||||
section.child,
|
||||
];
|
||||
|
||||
sectionWidgets.add(
|
||||
TmdbCastSection(
|
||||
cast: cast,
|
||||
imageBaseUrl: imageBaseUrl,
|
||||
leading: const AndroidSectionDivider(),
|
||||
),
|
||||
);
|
||||
sectionWidgets.add(
|
||||
TmdbRecommendationSection(
|
||||
items: recommendations,
|
||||
imageBaseUrl: imageBaseUrl,
|
||||
fallbackMediaType: mediaType,
|
||||
leading: const AndroidSectionDivider(),
|
||||
),
|
||||
);
|
||||
sectionWidgets.add(
|
||||
ExternalLinksSection(
|
||||
imdbId: imdbIdStr,
|
||||
tmdbId: tmdbIdInt,
|
||||
tmdbMediaType: mediaType,
|
||||
leading: const AndroidSectionDivider(),
|
||||
),
|
||||
);
|
||||
|
||||
return AndroidDetailLayout(
|
||||
scrollController: scrollController,
|
||||
scrollProgress: scrollProgress,
|
||||
backdropUrl: backdropUrl,
|
||||
fallbackUrls: fallbackUrls,
|
||||
backgroundColor: detailBackgroundColor,
|
||||
navigationColor: createDetailNavigationColor(detailBackgroundColor),
|
||||
title: title,
|
||||
onBack: () => context.pop(),
|
||||
heroInfo: AndroidHeroInfo(
|
||||
posterUrl: posterUrl,
|
||||
title: title,
|
||||
rating: voteAverage,
|
||||
metaParts: metaParts,
|
||||
genres: genreNames,
|
||||
),
|
||||
heroActions: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AndroidHeroActions(
|
||||
playLabel: playLabel ?? '播放',
|
||||
playTrailingLabel: positionClock,
|
||||
progressPercent: progressPercent,
|
||||
isLoading: availabilityResolving,
|
||||
loadingLabel: '检测中',
|
||||
canPlay: canPlay || availabilityResolving,
|
||||
onPlay: canPlay ? onPlay : null,
|
||||
showReplay: showReplay,
|
||||
onReplay: onReplay,
|
||||
isPlayed: isPlayed,
|
||||
onTogglePlayed: onTogglePlayed,
|
||||
isFavorite: isFavorite,
|
||||
onToggleFavorite: onToggleFavorite,
|
||||
),
|
||||
if (canPlay)
|
||||
AndroidStreamSelectorRow(
|
||||
subtitles: subtitles,
|
||||
audios: audios,
|
||||
selectedSubtitleIdx: selectedSubtitleIdx,
|
||||
selectedAudioIdx: selectedAudioIdx,
|
||||
onSelectSubtitleIndex: onSelectSubtitleIndex,
|
||||
onSelectAudioIndex: onSelectAudioIndex,
|
||||
),
|
||||
],
|
||||
),
|
||||
overview: overview.isNotEmpty
|
||||
? AndroidOverviewSection(overview: overview)
|
||||
: null,
|
||||
sections: sectionWidgets,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user