Files

240 lines
7.8 KiB
Dart
Raw Permalink Normal View History

2026-07-14 11:11:36 +08:00
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,
);
}
}