Files
plezy/lib/screens/actor_media_screen.dart
T
2026-05-11 11:54:37 +02:00

157 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../media/media_backend.dart';
import '../media/media_item.dart';
import '../media/media_kind.dart';
import '../media/media_server_client.dart';
import '../utils/provider_extensions.dart';
import '../widgets/desktop_app_bar.dart';
import '../widgets/optimized_media_image.dart';
import '../utils/media_image_helper.dart';
import '../i18n/strings.g.dart';
import 'base_media_list_detail_screen.dart';
import 'focusable_detail_screen_mixin.dart';
import '../mixins/grid_focus_node_mixin.dart';
import '../focus/focusable_action_bar.dart';
/// Screen to browse all media featuring a specific actor.
class ActorMediaScreen extends StatefulWidget {
final String actorName;
final String personId;
final String? actorThumb;
final String? characterName;
final String serverId;
final String? serverName;
final MediaBackend backend;
const ActorMediaScreen({
super.key,
required this.actorName,
required this.personId,
this.actorThumb,
this.characterName,
required this.serverId,
this.serverName,
required this.backend,
});
@override
State<ActorMediaScreen> createState() => _ActorMediaScreenState();
}
class _ActorMediaScreenState extends BaseMediaListDetailScreen<ActorMediaScreen>
with
StandardItemLoader<ActorMediaScreen>,
GridFocusNodeMixin<ActorMediaScreen>,
FocusableDetailScreenMixin<ActorMediaScreen> {
@override
MediaItem get mediaItem => MediaItem(
id: '',
backend: widget.backend,
kind: MediaKind.unknown,
serverId: widget.serverId,
serverName: widget.serverName,
);
@override
String? get itemServerId => widget.serverId;
@override
String get title => widget.actorName;
@override
String get emptyMessage => t.discover.noContentAvailable;
@override
bool get hasItems => items.isNotEmpty;
@override
void dispose() {
disposeFocusResources();
super.dispose();
}
MediaServerClient get _mediaClient => context.getMediaClientForServer(widget.serverId);
@override
Future<List<MediaItem>> fetchItems() => _mediaClient.fetchPersonMedia(widget.personId);
@override
Future<void> loadItems() async {
await super.loadItems();
autoFocusFirstItemAfterLoad();
}
@override
List<FocusableAction> getAppBarActions() {
return [];
}
Widget _buildActorHeader() {
final theme = Theme.of(context);
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(40),
child: OptimizedMediaImage(
client: _mediaClient,
imagePath: widget.actorThumb,
width: 80,
height: 80,
fit: BoxFit.cover,
imageType: ImageType.avatar,
fallbackIcon: Symbols.person_rounded,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.actorName,
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (widget.characterName != null) ...[
const SizedBox(height: 4),
Text(
widget.characterName!,
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
if (items.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'${items.length} ${items.length == 1 ? 'title' : 'titles'}',
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
),
],
],
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return buildDetailScaffold(
slivers: [
CustomAppBar(title: Text(widget.actorName), pinned: true, actions: buildFocusableAppBarActions()),
_buildActorHeader(),
...buildStateSlivers(),
if (items.isNotEmpty) buildFocusableGrid(items: items, onRefresh: updateItem),
],
);
}
}