feat: display roles
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user