Files
MookNote/lib/widgets/work_people_section.dart
2026-08-09 01:22:35 +08:00

293 lines
8.7 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/data_models.dart';
import '../providers/app_provider.dart';
import 'fade_in_local_image.dart';
import 'person_info_sheet.dart';
/// 作品详情页使用的"关联人物"区块
/// 根据 workType + workId 加载关联的 Person 列表并展示
class WorkPeopleSection extends StatefulWidget {
final String workId;
final String workType; // 'movie' / 'book' / 'game'
const WorkPeopleSection({
super.key,
required this.workId,
required this.workType,
});
@override
State<WorkPeopleSection> createState() => _WorkPeopleSectionState();
}
class _WorkPeopleSectionState extends State<WorkPeopleSection> {
List<_PersonRole> _items = [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final provider = context.read<AppProvider>();
// 确保 people 已加载,否则 person 查找全部失败
if (provider.people.isEmpty) {
await provider.loadPeople();
}
final people = provider.people;
// 按 personId 聚合:同一个人可能有多条关联(导演/编剧/演员等)
final Map<String, _PersonRole> byPerson = {};
void addRelation(dynamic r, {bool withCharacter = false}) {
final personId = r.personId as String;
final roleType = r.roleType as String;
final person = people.where((p) => p.id == personId).firstOrNull;
if (person == null) return;
final existing = byPerson[personId];
if (existing != null) {
existing.roleTypes.add(roleType);
if (withCharacter) {
final c = r.characterName as String?;
if (c != null && c.isNotEmpty) existing.characterNames.add(c);
}
} else {
final characterNames = <String>[];
if (withCharacter) {
final c = r.characterName as String?;
if (c != null && c.isNotEmpty) characterNames.add(c);
}
byPerson[personId] = _PersonRole(
person: person,
roleTypes: [roleType],
characterNames: characterNames,
);
}
}
switch (widget.workType) {
case 'movie':
final rels = await provider.getMoviePeople(widget.workId);
for (final r in rels) {
addRelation(r, withCharacter: true);
}
break;
case 'book':
final rels = await provider.getBookPeople(widget.workId);
for (final r in rels) {
addRelation(r);
}
break;
case 'game':
final rels = await provider.getGamePeople(widget.workId);
for (final r in rels) {
addRelation(r);
}
break;
}
// 按角色权重排序:导演/编剧/作者等优先,纯演员最后
// 每个人取其所有角色中的最小权重(最高优先级)作为排序依据
final items = byPerson.values.toList()
..sort((a, b) {
final aWeight = a.roleTypes.map(_roleWeight).reduce(min);
final bWeight = b.roleTypes.map(_roleWeight).reduce(min);
return aWeight.compareTo(bWeight);
});
if (!mounted) return;
setState(() {
_items = items;
_loading = false;
});
}
String _roleLabel(String roleType) {
return switch (roleType) {
'director' => '导演',
'writer' => '编剧',
'actor' => '演员',
'author' => '作者',
'translator' => '译者',
'developer' => '开发者',
_ => roleType,
};
}
/// 角色排序权重:演员放最后
int _roleWeight(String roleType) {
return switch (roleType) {
'director' => 0,
'writer' => 1,
'author' => 2,
'translator' => 3,
'developer' => 4,
'actor' => 99,
_ => 50,
};
}
/// 拼接角色描述:
/// 「导演 / 编剧」
/// 「导演 / 演员 饰 唐僧 / 演员 饰 孙悟空」
String _buildRoleText(_PersonRole item) {
final parts = <String>[];
// 非演员角色:去重后按权重排序
final nonActor = item.roleTypes.where((r) => r != 'actor').toSet().toList()
..sort((a, b) => _roleWeight(a).compareTo(_roleWeight(b)));
parts.addAll(nonActor.map(_roleLabel));
// 演员角色:每个饰演角色名单独成段
final isActor = item.roleTypes.contains('actor');
if (isActor) {
if (item.characterNames.isEmpty) {
parts.add('演员');
} else {
for (final c in item.characterNames) {
parts.add('演员 饰 $c');
}
}
}
return parts.join(' / ');
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const SizedBox.shrink();
}
if (_items.isEmpty) return const SizedBox.shrink();
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: colors.onSurface,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'人物',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
],
),
const SizedBox(height: 20),
ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color(0x00FFFFFF),
Color(0xFFFFFFFF),
Color(0xFFFFFFFF),
Color(0x00FFFFFF),
],
stops: [0.0, 0.04, 0.96, 1.0],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _items.map((item) {
final idx = _items.indexOf(item);
return Padding(
padding: EdgeInsets.only(left: idx == 0 ? 0 : 16),
child: _buildPersonChip(item, colors),
);
}).toList(),
),
),
),
],
),
);
}
Widget _buildPersonChip(_PersonRole item, ColorScheme colors) {
final person = item.person;
final hasPhoto = person.photoPath != null && person.photoPath!.isNotEmpty;
return GestureDetector(
onTap: () => PersonInfoSheet.show(context, person),
child: SizedBox(
width: 84,
child: Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
shape: BoxShape.circle,
),
clipBehavior: Clip.antiAlias,
child: hasPhoto
? FadeInLocalImage(path: person.photoPath, fit: BoxFit.cover)
: Center(
child: Text(
person.name.isNotEmpty ? person.name[0] : '?',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: colors.onSurface.withValues(alpha: 0.3),
),
),
),
),
const SizedBox(height: 8),
Text(
person.name,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: colors.onSurface),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
_buildRoleText(item),
style: TextStyle(
fontSize: 10,
color: colors.onSurface.withValues(alpha: 0.4),
height: 1.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class _PersonRole {
final Person person;
final List<String> roleTypes;
final List<String> characterNames;
_PersonRole({
required this.person,
required this.roleTypes,
required this.characterNames,
});
}