161 lines
4.9 KiB
Dart
161 lines
4.9 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 '../services/plex_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.
|
|
///
|
|
/// Plex-only today: uses `fetchAllPersonMediaAsMediaItems` which has no
|
|
/// Jellyfin counterpart yet. Callers must guard the navigation by backend
|
|
/// (see `_navigateToActorMedia` in media_detail_screen.dart).
|
|
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 title => widget.actorName;
|
|
|
|
@override
|
|
String get emptyMessage => t.discover.noContentAvailable;
|
|
|
|
@override
|
|
bool get hasItems => items.isNotEmpty;
|
|
|
|
@override
|
|
void dispose() {
|
|
disposeFocusResources();
|
|
super.dispose();
|
|
}
|
|
|
|
PlexClient get _plexClient => context.getPlexClientForServer(widget.serverId);
|
|
|
|
@override
|
|
Future<List<MediaItem>> fetchItems() async {
|
|
// Plex-only — guarded at the call site in media_detail_screen.dart.
|
|
return _plexClient.fetchAllPersonMediaAsMediaItems(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: _plexClient,
|
|
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),
|
|
],
|
|
);
|
|
}
|
|
}
|