Files

198 lines
5.7 KiB
Dart
Raw Permalink Normal View History

2026-07-14 11:11:36 +08:00
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/contracts/auth.dart';
import '../../core/contracts/library.dart';
import '../../providers/emby_headers_provider.dart';
import '../../providers/person_items_provider.dart';
import '../../providers/session_provider.dart';
import '../../shared/constants/layout_constants.dart';
import '../../shared/mappers/media_image_url.dart';
import '../../shared/utils/media_nav.dart';
import '../../shared/widgets/app_error_state.dart';
import '../../shared/widgets/app_loading_ring.dart';
import '../../shared/widgets/app_sliver_header.dart';
import '../../shared/widgets/empty_state.dart';
import '../../shared/widgets/media_poster_card.dart';
class PersonDetailPage extends ConsumerWidget {
final String embyPersonId;
const PersonDetailPage({super.key, required this.embyPersonId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final session = ref.watch(activeSessionProvider);
if (session == null) {
return Scaffold(
extendBodyBehindAppBar: true,
body: CustomScrollView(
slivers: [
AppSliverHeader(title: '演员', leading: _buildBackButton(context)),
const SliverFillRemaining(
hasScrollBody: false,
child: _EmptyState(message: '请先登录服务器'),
),
],
),
);
}
final asyncDetail = ref.watch(personItemDetailProvider(embyPersonId));
final asyncItems = ref.watch(personItemsProvider(embyPersonId));
final imageHeaders = ref.watch(embyImageHeadersProvider).value;
final personName = asyncDetail.value?.Name;
final title = (personName != null && personName.isNotEmpty)
? '演员 $personName 的全部作品'
: '演员';
final leadingAvatar = _personAvatar(
context: context,
session: session,
person: asyncDetail.value,
headers: imageHeaders,
);
final Widget titleWidget = Row(
children: [
if (leadingAvatar != null) ...[
leadingAvatar,
const SizedBox(width: 12),
],
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
);
final Widget contentSliver = asyncItems.when(
loading: () => const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: AppLoadingRing()),
),
error: (_, _) => SliverFillRemaining(
hasScrollBody: false,
child: AppErrorState(
title: '查询失败',
onRetry: () => ref.invalidate(personItemsProvider(embyPersonId)),
),
),
data: (items) {
if (items.isEmpty) {
return const SliverFillRemaining(
hasScrollBody: false,
child: EmptyState(icon: Icons.movie_filter_outlined, title: '暂无作品'),
);
}
return SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
sliver: SliverGrid(
gridDelegate: mediaGridDelegate,
delegate: SliverChildBuilderDelegate((_, i) {
final item = items[i];
return MediaPosterCard(
item: item,
width: double.infinity,
imageUrl: EmbyImageUrl.primary(
baseUrl: session.serverUrl,
token: session.token,
item: item,
maxHeight: 360,
),
preloadImageUrl: EmbyImageUrl.detailBanner(
baseUrl: session.serverUrl,
token: session.token,
item: item,
),
imageHeaders: imageHeaders,
onTap: () => goMediaDetail(context, item, ref: ref),
);
}, childCount: items.length),
),
);
},
);
return Scaffold(
extendBodyBehindAppBar: true,
body: CustomScrollView(
slivers: [
AppSliverHeader(
titleWidget: titleWidget,
leading: _buildBackButton(context),
),
contentSliver,
],
),
);
}
Widget _buildBackButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/');
}
},
);
}
Widget? _personAvatar({
required BuildContext context,
required AuthedSession session,
required EmbyRawItem? person,
required Map<String, String>? headers,
}) {
if (person == null) return null;
final hasImage =
(person.PrimaryImageTag != null) ||
(person.ImageTags?['Primary'] != null);
if (!hasImage) return null;
final url = EmbyImageUrl.primaryById(
baseUrl: session.serverUrl,
token: session.token,
itemId: person.Id,
maxHeight: 80,
);
return CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
backgroundImage: ResizeImage.resizeIfNeeded(
96,
null,
CachedNetworkImageProvider(url, headers: headers),
),
);
}
}
class _EmptyState extends StatelessWidget {
final String message;
const _EmptyState({required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
message,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 14,
),
),
);
}
}