Initial commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user