Files
plezy/lib/screens/actor_media_screen.dart
T

204 lines
6.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../media/library_query.dart';
import '../media/media_backend.dart';
import '../media/media_item.dart';
import '../media/media_kind.dart';
import '../media/media_server_client.dart';
import '../mixins/paginated_item_loader.dart';
import '../utils/app_logger.dart';
import '../utils/media_server_http_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
GridFocusNodeMixin<ActorMediaScreen>,
FocusableDetailScreenMixin<ActorMediaScreen>,
PaginatedItemLoader<MediaItem, ActorMediaScreen> {
static const int _pageSize = 200;
@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 => totalSize > 0;
@override
void dispose() {
disposePagination();
disposeFocusResources();
super.dispose();
}
MediaServerClient get _mediaClient => context.getMediaClientForServer(widget.serverId);
@override
Future<LibraryPage<MediaItem>> fetchPage(int start, int size, AbortController? abort) {
return _mediaClient.fetchPersonMediaPage(widget.personId, start: start, size: size, abort: abort);
}
@override
void updateItemInLists(String itemId, MediaItem updatedItem) {
for (final entry in loadedItems.entries) {
if (entry.value.id == itemId) {
loadedItems[entry.key] = updatedItem;
return;
}
}
}
@override
Future<void> loadItems() async {
setState(() {
isLoading = true;
errorMessage = null;
items = [];
resetPaginationState();
});
try {
final initialPage = await loadInitialPageWithStatus(_pageSize);
if (!initialPage.applied || !mounted) return;
setState(() {
items = loadedItems.values.toList();
isLoading = false;
});
appLogger.d('Loaded ${loadedItems.length} of $totalSize items for actor: ${widget.actorName}');
autoFocusFirstItemAfterLoad();
} catch (e, st) {
appLogger.e('Failed to load actor media', error: e, stackTrace: st);
if (!mounted) return;
setState(() {
errorMessage = t.messages.errorLoading(error: e.toString());
isLoading = false;
});
}
}
@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 (totalSize > 0) ...[
const SizedBox(height: 4),
Text(
'$totalSize ${totalSize == 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 (hasItems)
buildSparseFocusableGrid(
totalItems: totalSize,
itemAt: (index) => loadedItems[index],
onRefresh: updateItem,
onSkeletonVisible: (index) => ensureIndexLoaded(index, pageSize: _pageSize),
),
],
);
}
}