feat: display roles

This commit is contained in:
Tazio
2025-11-09 15:57:33 +01:00
parent 41194f2807
commit 1043db2fb8
5 changed files with 177 additions and 0 deletions
+7
View File
@@ -1,5 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'plex_role.dart';
part 'plex_metadata.g.dart';
@JsonSerializable()
@@ -34,6 +36,8 @@ class PlexMetadata {
final int? viewCount;
final int? leafCount; // Total number of episodes in a series/season
final int? viewedLeafCount; // Number of watched episodes in a series/season
@JsonKey(name: 'Role')
final List<PlexRole>? role; // Cast members
// Transient field for clear logo (extracted from Image array)
String? _clearLogo;
@@ -70,6 +74,7 @@ class PlexMetadata {
this.viewCount,
this.leafCount,
this.viewedLeafCount,
this.role,
});
/// Create a copy of this metadata with optional field overrides
@@ -104,6 +109,7 @@ class PlexMetadata {
int? viewCount,
int? leafCount,
int? viewedLeafCount,
List<PlexRole>? role,
}) {
final copy = PlexMetadata(
ratingKey: ratingKey ?? this.ratingKey,
@@ -136,6 +142,7 @@ class PlexMetadata {
viewCount: viewCount ?? this.viewCount,
leafCount: leafCount ?? this.leafCount,
viewedLeafCount: viewedLeafCount ?? this.viewedLeafCount,
role: role ?? this.role,
);
// Preserve clearLogo
copy._clearLogo = _clearLogo;
+4
View File
@@ -37,6 +37,9 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
viewCount: (json['viewCount'] as num?)?.toInt(),
leafCount: (json['leafCount'] as num?)?.toInt(),
viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(),
role: (json['Role'] as List<dynamic>?)
?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) =>
@@ -71,4 +74,5 @@ Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) =>
'viewCount': instance.viewCount,
'leafCount': instance.leafCount,
'viewedLeafCount': instance.viewedLeafCount,
'Role': instance.role,
};
+29
View File
@@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';
part 'plex_role.g.dart';
@JsonSerializable()
class PlexRole {
final int id;
final String? filter;
final String tag;
final String? tagKey;
final String? role;
final String? thumb;
final int? count;
PlexRole({
required this.id,
this.filter,
required this.tag,
this.tagKey,
this.role,
this.thumb,
this.count,
});
factory PlexRole.fromJson(Map<String, dynamic> json) =>
_$PlexRoleFromJson(json);
Map<String, dynamic> toJson() => _$PlexRoleToJson(this);
}
+27
View File
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_role.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexRole _$PlexRoleFromJson(Map<String, dynamic> json) => PlexRole(
id: (json['id'] as num).toInt(),
filter: json['filter'] as String?,
tag: json['tag'] as String,
tagKey: json['tagKey'] as String?,
role: json['role'] as String?,
thumb: json['thumb'] as String?,
count: (json['count'] as num?)?.toInt(),
);
Map<String, dynamic> _$PlexRoleToJson(PlexRole instance) => <String, dynamic>{
'id': instance.id,
'filter': instance.filter,
'tag': instance.tag,
'tagKey': instance.tagKey,
'role': instance.role,
'thumb': instance.thumb,
'count': instance.count,
};
+110
View File
@@ -751,6 +751,116 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
const SizedBox(height: 24),
],
// Cast
if (metadata.role != null && metadata.role!.isNotEmpty) ...[
Text(
'Cast',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
SizedBox(
height: 200,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: metadata.role!.length,
separatorBuilder: (context, index) =>
const SizedBox(width: 12),
itemBuilder: (context, index) {
final actor = metadata.role![index];
return SizedBox(
width: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: actor.thumb != null
? CachedNetworkImage(
imageUrl: actor.thumb!,
width: 120,
height: 120,
fit: BoxFit.cover,
placeholder: (context, url) =>
Container(
width: 120,
height: 120,
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.person),
),
),
errorWidget: (context, url, error) =>
Container(
width: 120,
height: 120,
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.person),
),
),
)
: Container(
width: 120,
height: 120,
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.person),
),
),
),
const SizedBox(height: 8),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
actor.tag,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (actor.role != null) ...[
const SizedBox(height: 2),
Text(
actor.role!,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
);
},
),
),
const SizedBox(height: 24),
],
// Seasons (for TV shows)
if (isShow) ...[
Text(