自定义昵称和头像

This commit is contained in:
DelLevin-Home
2026-03-05 13:24:46 +08:00
parent 44e5418031
commit 3ebc71da7d
16 changed files with 2037 additions and 1503 deletions

View File

@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 添加/编辑影视记录页面
/// 添加/编辑影视记录 - 极简主义设计
class MovieFormPage extends StatefulWidget {
final Movie? movie;
@@ -21,19 +21,16 @@ class _MovieFormPageState extends State<MovieFormPage> {
final _formKey = GlobalKey<FormState>();
final ImagePicker _picker = ImagePicker();
// 文本控制器
late TextEditingController _titleController;
late TextEditingController _ratingController;
late TextEditingController _summaryController;
// 列表控制器(导演、编剧、演员、类型、别名)
final List<TextEditingController> _directorControllers = [];
final List<TextEditingController> _writerControllers = [];
final List<TextEditingController> _actorControllers = [];
final List<TextEditingController> _genreControllers = [];
final List<TextEditingController> _alternateTitleControllers = [];
// 状态
late String _status;
DateTime? _releaseDate;
String? _posterPath;
@@ -52,7 +49,6 @@ class _MovieFormPageState extends State<MovieFormPage> {
_releaseDate = movie?.releaseDate;
_posterPath = movie?.posterPath;
// 初始化列表控制器
_initListControllers(movie?.directors ?? [], _directorControllers);
_initListControllers(movie?.writers ?? [], _writerControllers);
_initListControllers(movie?.actors ?? [], _actorControllers);
@@ -89,60 +85,44 @@ class _MovieFormPageState extends State<MovieFormPage> {
Widget build(BuildContext context) {
final isEdit = widget.movie != null;
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: AppBar(
backgroundColor: colorScheme.surface,
elevation: 0,
centerTitle: true,
title: Text(
isEdit ? '编辑影片' : '添加影片',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(isEdit ? '编辑' : '添加'),
actions: [
TextButton(
onPressed: _isLoading ? null : _saveMovie,
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary)
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary)
)
: Text('保存', style: TextStyle(color: colorScheme.primary)),
: Text('保存'),
),
const SizedBox(width: 8),
],
),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报上传区域
// 海报
_buildPosterSection(),
const SizedBox(height: 24),
const SizedBox(height: 40),
// 基本信息
_buildSectionTitle('基本信息'),
const SizedBox(height: 12),
const SizedBox(height: 24),
// 影视名称
_buildTextField(
controller: _titleController,
label: '影视名称 *',
hint: '请输入影视名称',
label: '名称',
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入影视名称';
@@ -151,88 +131,85 @@ class _MovieFormPageState extends State<MovieFormPage> {
},
),
const SizedBox(height: 16),
const SizedBox(height: 24),
// 上映日期和评分
Row(
children: [
Expanded(
child: _buildDatePicker(),
),
const SizedBox(width: 16),
Expanded(
child: _buildTextField(
controller: _ratingController,
label: '评分',
hint: '1-10',
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value != null && value.isNotEmpty) {
final rating = double.tryParse(value);
if (rating == null || rating < 1 || rating > 10) {
return '评分1-10';
}
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// 状态选择
_buildStatusSelector(),
// 上映日期
_buildDatePicker(),
const SizedBox(height: 24),
// 评分
_buildTextField(
controller: _ratingController,
label: '评分',
hint: '1-10',
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value != null && value.isNotEmpty) {
final rating = double.tryParse(value);
if (rating == null || rating < 1 || rating > 10) {
return '评分范围 1-10';
}
}
return null;
},
),
const SizedBox(height: 32),
// 状态
_buildSectionTitle('状态'),
const SizedBox(height: 16),
_buildStatusSelector(),
const SizedBox(height: 40),
// 别名
_buildSectionTitle('别名'),
const SizedBox(height: 8),
_buildTagList(_alternateTitleControllers, '添加别名'),
const SizedBox(height: 16),
_buildTagList(_alternateTitleControllers),
const SizedBox(height: 24),
const SizedBox(height: 40),
// 导演
_buildSectionTitle('导演'),
const SizedBox(height: 8),
_buildTagList(_directorControllers, '添加导演'),
const SizedBox(height: 16),
_buildTagList(_directorControllers),
const SizedBox(height: 24),
const SizedBox(height: 40),
// 编剧
_buildSectionTitle('编剧'),
const SizedBox(height: 8),
_buildTagList(_writerControllers, '添加编剧'),
const SizedBox(height: 16),
_buildTagList(_writerControllers),
const SizedBox(height: 24),
const SizedBox(height: 40),
// 主演
_buildSectionTitle('主演'),
const SizedBox(height: 8),
_buildTagList(_actorControllers, '添加主演'),
const SizedBox(height: 16),
_buildTagList(_actorControllers),
const SizedBox(height: 24),
const SizedBox(height: 40),
// 类型
_buildSectionTitle('类型'),
const SizedBox(height: 8),
_buildTagList(_genreControllers, '添加类型'),
const SizedBox(height: 16),
_buildTagList(_genreControllers),
const SizedBox(height: 24),
const SizedBox(height: 40),
// 剧情简介
_buildSectionTitle('剧情简介'),
const SizedBox(height: 12),
_buildSectionTitle('简介'),
const SizedBox(height: 16),
_buildTextField(
controller: _summaryController,
label: '',
hint: '请输入剧情简介...',
maxLines: 5,
hint: '剧情简介...',
maxLines: 6,
),
const SizedBox(height: 40),
const SizedBox(height: 48),
],
),
),
@@ -240,73 +217,59 @@ class _MovieFormPageState extends State<MovieFormPage> {
);
}
/// 构建海报上传区域
/// 海报区域 - 极简
Widget _buildPosterSection() {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: GestureDetector(
onTap: _pickImage,
child: Container(
width: 140,
height: 200,
width: 120,
height: 170,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 1,
color: const Color(0xFFE5E5E5),
width: 0.5,
),
),
child: _posterPath != null && _posterPath!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(_posterPath!),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _buildPlaceholder(colorScheme),
),
? Image.file(
File(_posterPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder(colorScheme),
: _buildPlaceholder(),
),
),
);
}
Widget _buildPlaceholder(ColorScheme colorScheme) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate_outlined,
size: 40,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
Widget _buildPlaceholder() {
return const Center(
child: Text(
'添加海报',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
const SizedBox(height: 8),
Text(
'上传海报',
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
),
],
),
);
}
/// 构建区块标题
/// 区块标题 - 大写字母,小字号
Widget _buildSectionTitle(String title) {
return Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
letterSpacing: 1,
),
);
}
/// 构建文本输入框
/// 文本输入框 - 极简无边框
Widget _buildTextField({
required TextEditingController controller,
required String label,
@@ -315,97 +278,68 @@ class _MovieFormPageState extends State<MovieFormPage> {
String? Function(String?)? validator,
int maxLines = 1,
}) {
final colorScheme = Theme.of(context).colorScheme;
return TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
validator: validator,
style: TextStyle(
fontSize: 15,
color: colorScheme.onSurface,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
),
decoration: InputDecoration(
labelText: label.isEmpty ? null : label,
hintText: hint,
hintStyle: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
hintStyle: const TextStyle(
fontSize: 15,
color: Color(0xFFCCCCCC),
),
labelStyle: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
border: const UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: colorScheme.outline.withOpacity(0.2),
width: 1,
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFF1A1A1A), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: colorScheme.primary.withOpacity(0.5),
width: 1.5,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
);
}
/// 构建日期选择器
/// 日期选择器
Widget _buildDatePicker() {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: _selectReleaseDate,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
width: 1,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
child: Row(
children: [
Icon(
Icons.calendar_today_outlined,
size: 18,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_releaseDate != null
? '${_releaseDate!.year}-${_releaseDate!.month.toString().padLeft(2, '0')}-${_releaseDate!.day.toString().padLeft(2, '0')}'
: '上映日期',
style: TextStyle(
fontSize: 15,
color: _releaseDate != null
? colorScheme.onSurface
: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
Text(
_releaseDate != null
? '${_releaseDate!.year}.${_releaseDate!.month.toString().padLeft(2, '0')}.${_releaseDate!.day.toString().padLeft(2, '0')}'
: '上映日期',
style: TextStyle(
fontSize: 16,
color: _releaseDate != null
? const Color(0xFF1A1A1A)
: const Color(0xFFCCCCCC),
),
),
const Spacer(),
if (_releaseDate != null)
GestureDetector(
onTap: () => setState(() => _releaseDate = null),
child: Icon(
child: const Icon(
Icons.close,
size: 18,
color: colorScheme.onSurfaceVariant,
size: 16,
color: Color(0xFF999999),
),
),
],
@@ -414,54 +348,43 @@ class _MovieFormPageState extends State<MovieFormPage> {
);
}
/// 构建状态选择器
/// 状态选择器 - 极简分段
Widget _buildStatusSelector() {
final colorScheme = Theme.of(context).colorScheme;
final statuses = [
{'value': 'watching', 'label': '在看', 'icon': Icons.play_circle_outline},
{'value': 'watched', 'label': '已看', 'icon': Icons.check_circle_outline},
{'value': 'want_to_watch', 'label': '想看', 'icon': Icons.bookmark_border},
{'value': 'watching', 'label': '在看'},
{'value': 'watched', 'label': '已看'},
{'value': 'want_to_watch', 'label': '想看'},
];
return Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
width: 1,
),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
),
child: Row(
children: statuses.map((status) {
final isSelected = _status == status['value'];
children: statuses.asMap().entries.map((entry) {
final isSelected = _status == entry.value['value'];
final isLast = entry.key == statuses.length - 1;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _status = status['value'] as String),
onTap: () => setState(() => _status = entry.value['value'] as String),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
color: isSelected ? const Color(0xFF1A1A1A) : Colors.transparent,
border: !isLast
? const Border(
right: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
)
: null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
status['icon'] as IconData,
size: 18,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
const SizedBox(width: 6),
Text(
status['label'] as String,
style: TextStyle(
fontSize: 14,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
],
child: Text(
entry.value['label'] as String,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: isSelected ? Colors.white : const Color(0xFF666666),
),
),
),
),
@@ -471,70 +394,55 @@ class _MovieFormPageState extends State<MovieFormPage> {
);
}
/// 构建标签列表(导演、编剧、演员等)
Widget _buildTagList(List<TextEditingController> controllers, String addHint) {
final colorScheme = Theme.of(context).colorScheme;
/// 标签列表 - 极简输入
Widget _buildTagList(List<TextEditingController> controllers) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...controllers.asMap().entries.map((entry) {
final index = entry.key;
final controller = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
return Container(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
),
decoration: InputDecoration(
hintText: addHint,
decoration: const InputDecoration(
isDense: true,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 8),
hintText: '输入...',
hintStyle: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant.withOpacity(0.4),
fontSize: 15,
color: Color(0xFFCCCCCC),
),
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: colorScheme.outline.withOpacity(0.15),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: colorScheme.primary.withOpacity(0.4),
width: 1,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
),
if (controllers.length > 1)
IconButton(
icon: Icon(Icons.remove_circle_outline,
color: colorScheme.error.withOpacity(0.6),
size: 20
),
onPressed: () {
GestureDetector(
onTap: () {
setState(() {
controller.dispose();
controllers.removeAt(index);
});
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
child: const Padding(
padding: EdgeInsets.only(left: 12),
child: Text(
'删除',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
),
),
),
],
),
@@ -543,37 +451,15 @@ class _MovieFormPageState extends State<MovieFormPage> {
// 添加按钮
GestureDetector(
onTap: () {
setState(() {
controllers.add(TextEditingController());
});
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
width: 1,
onTap: () => setState(() => controllers.add(TextEditingController())),
child: const Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
'+ 添加',
style: TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add,
size: 18,
color: colorScheme.primary,
),
const SizedBox(width: 6),
Text(
addHint,
style: TextStyle(
fontSize: 13,
color: colorScheme.primary,
),
),
],
),
),
),
@@ -592,24 +478,18 @@ class _MovieFormPageState extends State<MovieFormPage> {
);
if (pickedFile != null) {
// 复制图片到应用目录
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, 'posters', fileName);
// 创建目录
final posterDir = Directory(path.join(appDir.path, 'posters'));
if (!await posterDir.exists()) {
await posterDir.create(recursive: true);
}
// 复制文件
final sourceFile = File(pickedFile.path);
await sourceFile.copy(savedPath);
await File(pickedFile.path).copy(savedPath);
setState(() {
_posterPath = savedPath;
});
setState(() => _posterPath = savedPath);
}
} catch (e) {
if (mounted) {
@@ -620,7 +500,7 @@ class _MovieFormPageState extends State<MovieFormPage> {
}
}
/// 选择上映日期
/// 选择日期
Future<void> _selectReleaseDate() async {
final picked = await showDatePicker(
context: context,
@@ -630,8 +510,8 @@ class _MovieFormPageState extends State<MovieFormPage> {
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(
primary: Theme.of(context).colorScheme.primary,
colorScheme: const ColorScheme.light(
primary: Color(0xFF1A1A1A),
),
),
child: child!,
@@ -640,17 +520,13 @@ class _MovieFormPageState extends State<MovieFormPage> {
);
if (picked != null) {
setState(() {
_releaseDate = picked;
});
setState(() => _releaseDate = picked);
}
}
/// 保存影视记录
/// 保存
Future<void> _saveMovie() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
@@ -659,7 +535,6 @@ class _MovieFormPageState extends State<MovieFormPage> {
? double.tryParse(_ratingController.text)
: null;
// 收集列表数据
final directors = _collectNonEmptyTexts(_directorControllers);
final writers = _collectNonEmptyTexts(_writerControllers);
final actors = _collectNonEmptyTexts(_actorControllers);
@@ -669,7 +544,6 @@ class _MovieFormPageState extends State<MovieFormPage> {
final now = DateTime.now();
if (widget.movie == null) {
// 添加新模式
final newMovie = Movie(
id: now.millisecondsSinceEpoch.toString(),
title: _titleController.text.trim(),
@@ -691,7 +565,6 @@ class _MovieFormPageState extends State<MovieFormPage> {
await context.read<AppProvider>().addMovie(newMovie);
} else {
// 编辑模式
final updatedMovie = widget.movie!.copyWith(
title: _titleController.text.trim(),
posterPath: _posterPath,
@@ -713,34 +586,18 @@ class _MovieFormPageState extends State<MovieFormPage> {
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.movie == null ? '添加成功' : '更新成功'),
behavior: SnackBarBehavior.floating,
backgroundColor: Theme.of(context).colorScheme.primary,
),
);
Navigator.pop(context);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败: $e'),
behavior: SnackBarBehavior.floating,
backgroundColor: Theme.of(context).colorScheme.error,
),
SnackBar(content: Text('保存失败: $e')),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
if (mounted) setState(() => _isLoading = false);
}
}
/// 收集非空文本
List<String> _collectNonEmptyTexts(List<TextEditingController> controllers) {
return controllers
.map((c) => c.text.trim())