自定义昵称和头像

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

@@ -4,3 +4,6 @@ systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=10808 systemProp.http.proxyPort=10808
systemProp.https.proxyHost=127.0.0.1 systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=10808 systemProp.https.proxyPort=10808
org.gradle.daemon=false
kotlin.incremental=false
kotlin.compiler.execution.strategy=out-of-process

View File

@@ -3,12 +3,16 @@ import 'package:provider/provider.dart';
import 'pages/home_page.dart'; import 'pages/home_page.dart';
import 'utils/app_theme.dart'; import 'utils/app_theme.dart';
import 'utils/app_router.dart'; import 'utils/app_router.dart';
import 'utils/user_prefs.dart';
import 'providers/app_provider.dart'; import 'providers/app_provider.dart';
void main() async { void main() async {
// 确保 Flutter 绑定初始化完成 // 确保 Flutter 绑定初始化完成
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 初始化用户偏好设置
await UserPrefs.init();
// 初始化数据库 // 初始化数据库
final appProvider = AppProvider(); final appProvider = AppProvider();
await appProvider.initDatabase(); await appProvider.initDatabase();

View File

@@ -3,11 +3,10 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../widgets/custom_drawer.dart'; import '../widgets/custom_drawer.dart';
import '../widgets/bottom_nav_bar.dart'; import '../widgets/bottom_nav_bar.dart';
import 'movie_tab_page.dart'; import 'main_content_page.dart';
import 'book_tab_page.dart'; import 'profile_page.dart';
import 'note_tab_page.dart';
/// 主页 - 包含顶部菜单、三个标签页、底部导航 /// 主页 - 包含底部导航,可切换主页/我的
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@@ -19,229 +18,34 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// 左侧弹出菜单 // 左侧弹出菜单(仅在主页显示)
drawer: const CustomDrawer(), drawer: context.watch<AppProvider>().bottomNavIndex == 0
? const CustomDrawer()
: null,
// 主体内容 // 主体内容 - 根据底部导航切换
body: Column( body: _buildBody(),
children: [
// 顶部 AppBar
_buildAppBar(),
// 三个标签页的标题栏
_buildTabBar(),
// 标签页内容
Expanded(
child: _buildTabContent(),
),
],
),
// 底部导航栏 // 底部导航栏
bottomNavigationBar: const CustomBottomNavBar(), bottomNavigationBar: const CustomBottomNavBar(),
); );
} }
/// 构建顶部 AppBar /// 构建主体内容
Widget _buildAppBar() { Widget _buildBody() {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
return AppBar( switch (provider.bottomNavIndex) {
title: Text(_getAppBarTitle(provider)),
actions: [
// 添加按钮
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context, provider),
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// TODO: 搜索功能
},
),
],
);
},
);
}
/// 获取 AppBar 标题
String _getAppBarTitle(AppProvider provider) {
switch (provider.mainTabIndex) {
case 0:
return '观影';
case 1:
return '阅读';
case 2:
return '笔记';
default:
return 'MookNote';
}
}
/// 构建标签栏
Widget _buildTabBar() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 观影
_buildTabItem(
context,
'观影',
Icons.movie,
0,
provider.mainTabIndex,
() => provider.setMainTabIndex(0),
),
// 阅读
_buildTabItem(
context,
'阅读',
Icons.menu_book,
1,
provider.mainTabIndex,
() => provider.setMainTabIndex(1),
),
// 笔记
_buildTabItem(
context,
'笔记',
Icons.note,
2,
provider.mainTabIndex,
() => provider.setMainTabIndex(2),
),
],
),
);
},
);
}
/// 构建单个标签项
Widget _buildTabItem(
BuildContext context,
String label,
IconData icon,
int index,
int currentIndex,
VoidCallback onTap,
) {
final isSelected = index == currentIndex;
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 8),
width: 32,
height: 3,
decoration: BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
],
),
),
),
);
}
/// 构建标签页内容
Widget _buildTabContent() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
switch (provider.mainTabIndex) {
case 0: case 0:
return const MovieTabPage(); // 主页 - 观影/阅读/笔记
case 1: return const MainContentPage();
return const BookTabPage();
case 2: case 2:
return const NoteTabPage(); // 我的页面
return const ProfilePage();
default: default:
return const MovieTabPage(); return const MainContentPage();
} }
}, },
); );
} }
/// 显示添加对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.movie, color: Colors.green),
title: const Text('添加观影'),
subtitle: const Text('记录你看过的电影'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/movie-form');
},
),
ListTile(
leading: const Icon(Icons.menu_book, color: Colors.orange),
title: const Text('添加阅读'),
subtitle: const Text('记录你读过的书'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/book-form');
},
),
ListTile(
leading: const Icon(Icons.note, color: Colors.blue),
title: const Text('添加笔记'),
subtitle: const Text('记录你的想法和笔记'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/note-form');
},
),
],
),
);
},
);
}
} }

View File

@@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import 'movie_tab_page.dart';
import 'book_tab_page.dart';
import 'note_tab_page.dart';
/// 主内容页 - 观影/阅读/笔记标签页
class MainContentPage extends StatelessWidget {
const MainContentPage({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 顶部 AppBar
_buildAppBar(context),
// 三个标签页的标题栏
_buildTabBar(context),
// 标签页内容
Expanded(
child: _buildTabContent(),
),
],
);
}
/// 构建顶部 AppBar
Widget _buildAppBar(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return AppBar(
title: Text(_getAppBarTitle(provider)),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context, provider),
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// TODO: 搜索功能
},
),
],
);
},
);
}
/// 获取 AppBar 标题
String _getAppBarTitle(AppProvider provider) {
switch (provider.mainTabIndex) {
case 0:
return '观影';
case 1:
return '阅读';
case 2:
return '笔记';
default:
return 'MookNote';
}
}
/// 构建标签栏
Widget _buildTabBar(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
child: Row(
children: [
_buildTabItem(
context,
'观影',
0,
provider.mainTabIndex,
() => provider.setMainTabIndex(0),
),
_buildTabItem(
context,
'阅读',
1,
provider.mainTabIndex,
() => provider.setMainTabIndex(1),
),
_buildTabItem(
context,
'笔记',
2,
provider.mainTabIndex,
() => provider.setMainTabIndex(2),
),
],
),
);
},
);
}
/// 构建单个标签项
Widget _buildTabItem(
BuildContext context,
String label,
int index,
int currentIndex,
VoidCallback onTap,
) {
final isSelected = index == currentIndex;
return Expanded(
child: InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? const Color(0xFF1A1A1A)
: const Color(0xFF999999),
),
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 8),
width: 20,
height: 2,
color: const Color(0xFF1A1A1A),
),
],
),
),
),
);
}
/// 构建标签页内容
Widget _buildTabContent() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
switch (provider.mainTabIndex) {
case 0:
return const MovieTabPage();
case 1:
return const BookTabPage();
case 2:
return const NoteTabPage();
default:
return const MovieTabPage();
}
},
);
}
/// 显示添加对话框
void _showAddDialog(BuildContext context, AppProvider provider) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
builder: (BuildContext context) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.movie, color: Color(0xFF1A1A1A)),
title: const Text('添加观影'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/movie-form');
},
),
const Divider(height: 0.5, indent: 56),
ListTile(
leading: const Icon(Icons.menu_book, color: Color(0xFF1A1A1A)),
title: const Text('添加阅读'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/book-form');
},
),
const Divider(height: 0.5, indent: 56),
ListTile(
leading: const Icon(Icons.note, color: Color(0xFF1A1A1A)),
title: const Text('添加笔记'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/note-form');
},
),
],
),
);
},
);
}
}

View File

@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 影视详情页 /// 影视详情页 - 极简主义设计
class MovieDetailPage extends StatefulWidget { class MovieDetailPage extends StatefulWidget {
final Movie movie; final Movie movie;
@@ -30,446 +29,298 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
(m) => m.id == _movie.id, (m) => m.id == _movie.id,
orElse: () => _movie, orElse: () => _movie,
); );
setState(() { setState(() => _movie = updated);
_movie = updated;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold( return Scaffold(
backgroundColor: colorScheme.surface, appBar: AppBar(
body: CustomScrollView( title: const Text('详情'),
slivers: [ actions: [
// 海报区域(可折叠) TextButton(
SliverAppBar( onPressed: () => _navigateToEdit(context),
expandedHeight: 320, child: const Text('编辑'),
pinned: true, ),
backgroundColor: colorScheme.surface, const SizedBox(width: 8),
flexibleSpace: FlexibleSpaceBar( ],
background: _buildPosterSection(context), ),
), body: SingleChildScrollView(
leading: IconButton( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
icon: Container( child: Column(
padding: const EdgeInsets.all(8), crossAxisAlignment: CrossAxisAlignment.start,
decoration: BoxDecoration( children: [
color: Colors.black.withOpacity(0.3), // 海报
shape: BoxShape.circle, _buildPoster(),
),
child: const Icon(Icons.arrow_back, color: Colors.white, size: 20), const SizedBox(height: 40),
// 标题
Text(
_movie.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
height: 1.3,
), ),
onPressed: () => Navigator.pop(context),
), ),
actions: [
IconButton( const SizedBox(height: 12),
icon: Container(
padding: const EdgeInsets.all(8), // 状态标签
_buildStatusTag(),
const SizedBox(height: 32),
// 基本信息
_buildInfoRow(),
// 别名
if (_movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('别名'),
const SizedBox(height: 12),
_buildTextList(_movie.alternateTitles),
],
// 导演
if (_movie.directors.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('导演'),
const SizedBox(height: 12),
_buildTextList(_movie.directors),
],
// 编剧
if (_movie.writers.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('编剧'),
const SizedBox(height: 12),
_buildTextList(_movie.writers),
],
// 主演
if (_movie.actors.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('主演'),
const SizedBox(height: 12),
_buildTextList(_movie.actors),
],
// 类型
if (_movie.genres.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('类型'),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _movie.genres.map((genre) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3), border: Border.all(
shape: BoxShape.circle, color: const Color(0xFFE5E5E5),
width: 0.5,
),
), ),
child: const Icon(Icons.edit, color: Colors.white, size: 20), child: Text(
), genre,
onPressed: () => _navigateToEdit(context), style: const TextStyle(
), fontSize: 13,
IconButton( color: Color(0xFF666666),
icon: Container( ),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
shape: BoxShape.circle,
), ),
child: const Icon(Icons.delete_outline, color: Colors.white, size: 20), )).toList(),
),
onPressed: () => _showDeleteDialog(context),
), ),
], ],
),
// 内容区域 // 剧情简介
SliverToBoxAdapter( if (_movie.summary != null && _movie.summary!.isNotEmpty) ...[
child: Padding( const SizedBox(height: 32),
padding: const EdgeInsets.all(20), _buildSectionTitle('简介'),
child: Column( const SizedBox(height: 12),
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ _movie.summary!,
// 标题和状态 style: const TextStyle(
_buildTitleSection(context), fontSize: 15,
color: Color(0xFF666666),
height: 1.7,
),
),
],
const SizedBox(height: 20), const SizedBox(height: 48),
// 基本信息 // 删除按钮
_buildBasicInfoSection(context), Center(
child: TextButton(
const SizedBox(height: 24), onPressed: () => _showDeleteDialog(context),
style: TextButton.styleFrom(
// 导演 foregroundColor: const Color(0xFFDC2626),
if (_movie.directors.isNotEmpty) ...[ ),
_buildListSection(context, '导演', _movie.directors, Icons.videocam_outlined), child: const Text('删除此影片'),
const SizedBox(height: 20),
],
// 编剧
if (_movie.writers.isNotEmpty) ...[
_buildListSection(context, '编剧', _movie.writers, Icons.edit_note_outlined),
const SizedBox(height: 20),
],
// 主演
if (_movie.actors.isNotEmpty) ...[
_buildListSection(context, '主演', _movie.actors, Icons.people_outline),
const SizedBox(height: 20),
],
// 类型
if (_movie.genres.isNotEmpty) ...[
_buildGenreSection(context),
const SizedBox(height: 20),
],
// 别名
if (_movie.alternateTitles.isNotEmpty) ...[
_buildListSection(context, '别名', _movie.alternateTitles, Icons.alternate_email_outlined),
const SizedBox(height: 20),
],
// 剧情简介
if (_movie.summary != null && _movie.summary!.isNotEmpty) ...[
_buildSummarySection(context),
],
const SizedBox(height: 40),
],
), ),
), ),
),
], const SizedBox(height: 24),
],
),
), ),
); );
} }
/// 构建海报区域 /// 海报
Widget _buildPosterSection(BuildContext context) { Widget _buildPoster() {
return Container( return Center(
width: double.infinity, child: Container(
color: Colors.grey[300], width: 140,
child: _movie.posterPath != null && _movie.posterPath!.isNotEmpty height: 200,
? Image.file( decoration: BoxDecoration(
File(_movie.posterPath!), color: const Color(0xFFF5F5F5),
fit: BoxFit.cover, border: Border.all(
errorBuilder: (context, error, stackTrace) => _buildPlaceholder(), color: const Color(0xFFE5E5E5),
) width: 0.5,
: _buildPlaceholder(), ),
),
child: _movie.posterPath != null && _movie.posterPath!.isNotEmpty
? Image.file(
File(_movie.posterPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder(),
),
); );
} }
Widget _buildPlaceholder() { Widget _buildPlaceholder() {
return Container( return const Center(
color: Colors.grey[300], child: Text(
child: Center( '无海报',
child: Column( style: TextStyle(
mainAxisAlignment: MainAxisAlignment.center, fontSize: 13,
children: [ color: Color(0xFF999999),
Icon(Icons.movie, size: 80, color: Colors.grey[500]),
const SizedBox(height: 12),
Text(
'暂无海报',
style: TextStyle(color: Colors.grey[500], fontSize: 14),
),
],
), ),
), ),
); );
} }
/// 构建标题区域 /// 状态标签
Widget _buildTitleSection(BuildContext context) { Widget _buildStatusTag() {
final colorScheme = Theme.of(context).colorScheme; String label;
Color bgColor;
Color textColor;
return Column( switch (_movie.status) {
crossAxisAlignment: CrossAxisAlignment.start, case 'watched':
children: [ label = '已看';
// 状态标签 bgColor = const Color(0xFF1A1A1A);
Container( textColor = Colors.white;
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), break;
decoration: BoxDecoration( case 'watching':
color: _getStatusColor().withOpacity(0.1), label = '在看';
borderRadius: BorderRadius.circular(4), bgColor = const Color(0xFF666666);
), textColor = Colors.white;
child: Text( break;
_getStatusText(), case 'want_to_watch':
style: TextStyle( label = '想看';
fontSize: 12, bgColor = const Color(0xFFF5F5F5);
color: _getStatusColor(), textColor = const Color(0xFF666666);
fontWeight: FontWeight.w600, break;
), default:
), label = '未知';
), bgColor = const Color(0xFFF5F5F5);
textColor = const Color(0xFF999999);
const SizedBox(height: 12), }
// 标题
Text(
_movie.title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
height: 1.3,
),
),
],
);
}
/// 构建基本信息区域
Widget _buildBasicInfoSection(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.3), color: bgColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.zero,
), ),
child: Row( child: Text(
children: [ label,
// 上映日期 style: TextStyle(
if (_movie.releaseDate != null) ...[ fontSize: 12,
Expanded( color: textColor,
child: _buildInfoItem( fontWeight: FontWeight.w500,
context, ),
icon: Icons.calendar_today_outlined,
label: '上映日期',
value: '${_movie.releaseDate!.year}.${_movie.releaseDate!.month.toString().padLeft(2, '0')}.${_movie.releaseDate!.day.toString().padLeft(2, '0')}',
),
),
Container(
width: 1,
height: 40,
color: colorScheme.outline.withOpacity(0.2),
),
],
// 评分
Expanded(
child: _buildInfoItem(
context,
icon: Icons.star_rounded,
label: '评分',
value: _movie.rating != null ? '${_movie.rating!.toStringAsFixed(1)}' : '暂无',
valueColor: _movie.rating != null ? Colors.amber[700] : null,
),
),
],
), ),
); );
} }
Widget _buildInfoItem( /// 基本信息行
BuildContext context, { Widget _buildInfoRow() {
required IconData icon, final items = <String>[];
required String label,
required String value,
Color? valueColor,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Column( if (_movie.releaseDate != null) {
children: [ items.add('${_movie.releaseDate!.year}');
Icon(icon, size: 20, color: colorScheme.onSurfaceVariant),
const SizedBox(height: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: valueColor ?? colorScheme.onSurface,
),
),
],
);
}
/// 构建列表区块(导演、编剧、演员、别名)
Widget _buildListSection(BuildContext context, String title, List<String> items, IconData icon) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 18, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: items.map((item) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: colorScheme.outline.withOpacity(0.15),
width: 1,
),
),
child: Text(
item,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurface.withOpacity(0.85),
),
),
)).toList(),
),
],
);
}
/// 构建类型区块
Widget _buildGenreSection(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.local_movies_outlined, size: 18, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
'类型',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: _movie.genres.map((genre) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: colorScheme.primary.withOpacity(0.3),
width: 1,
),
),
child: Text(
genre,
style: TextStyle(
fontSize: 13,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
)).toList(),
),
],
);
}
/// 构建剧情简介区块
Widget _buildSummarySection(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.article_outlined, size: 18, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
'剧情简介',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_movie.summary!,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface.withOpacity(0.8),
height: 1.7,
),
),
),
],
);
}
/// 获取状态颜色
Color _getStatusColor() {
switch (_movie.status) {
case 'watched':
return AppTheme.watchedColor;
case 'want_to_watch':
return AppTheme.wantToWatchColor;
case 'watching':
return AppTheme.watchingColor;
default:
return Colors.grey;
} }
} if (_movie.rating != null) {
items.add('${_movie.rating!.toStringAsFixed(1)}');
/// 获取状态文本
String _getStatusText() {
switch (_movie.status) {
case 'watched':
return '已看';
case 'want_to_watch':
return '想看';
case 'watching':
return '在看';
default:
return '未知';
} }
if (items.isEmpty) return const SizedBox.shrink();
return Row(
children: items.asMap().entries.map((entry) {
return Row(
children: [
Text(
entry.value,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
),
if (entry.key < items.length - 1)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'·',
style: TextStyle(
fontSize: 14,
color: Color(0xFFCCCCCC),
),
),
),
],
);
}).toList(),
);
} }
/// 跳转到编辑页面 /// 区块标题
Widget _buildSectionTitle(String title) {
return Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
letterSpacing: 1,
),
);
}
/// 文本列表
Widget _buildTextList(List<String> items) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: items.map((item) => Text(
item,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
)).toList(),
);
}
/// 跳转到编辑
void _navigateToEdit(BuildContext context) { void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/movie-form', arguments: _movie).then((_) { Navigator.pushNamed(context, '/movie-form', arguments: _movie).then((_) {
_refreshMovie(); _refreshMovie();
@@ -477,29 +328,35 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
}); });
} }
/// 显示删除对话框 /// 删除对话框
void _showDeleteDialog(BuildContext context) { void _showDeleteDialog(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: colorScheme.surface, backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 0,
title: Text( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'确认删除', '确认删除',
style: TextStyle(color: colorScheme.onSurface), style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
), ),
content: Text( content: Text(
'确定要删除"${_movie.title}"吗?此操作不可恢复。', '确定要删除"${_movie.title}"吗?',
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.7)), style: const TextStyle(
fontSize: 15,
color: Color(0xFF666666),
),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text( child: const Text(
'取消', '取消',
style: TextStyle(color: colorScheme.onSurfaceVariant), style: TextStyle(color: Color(0xFF666666)),
), ),
), ),
TextButton( TextButton(
@@ -508,17 +365,10 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
if (!context.mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('已删除'),
behavior: SnackBarBehavior.floating,
backgroundColor: colorScheme.primary,
),
);
}, },
child: Text( child: const Text(
'删除', '删除',
style: TextStyle(color: colorScheme.error), style: TextStyle(color: Color(0xFFDC2626)),
), ),
), ),
], ],

View File

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

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../widgets/movie_status_bar.dart'; import '../widgets/movie_status_bar.dart';
import '../widgets/movie_list_item.dart'; import '../widgets/movie_list_item.dart';
/// 观影标签页 /// 观影标签页 - 极简主义设计
class MovieTabPage extends StatelessWidget { class MovieTabPage extends StatelessWidget {
const MovieTabPage({super.key}); const MovieTabPage({super.key});
@@ -13,9 +12,11 @@ class MovieTabPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// 状态选择栏(已看、想看、在看) // 状态选择栏
const MovieStatusBar(), const MovieStatusBar(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 影片列表 // 影片列表
Expanded( Expanded(
child: _buildMovieList(context), child: _buildMovieList(context),
@@ -28,7 +29,6 @@ class MovieTabPage extends StatelessWidget {
Widget _buildMovieList(BuildContext context) { Widget _buildMovieList(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
// 根据状态筛选影片
final statusMap = { final statusMap = {
0: 'watched', 0: 'watched',
1: 'want_to_watch', 1: 'want_to_watch',
@@ -43,8 +43,10 @@ class MovieTabPage extends StatelessWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async => await provider.loadMovies(), onRefresh: () async => await provider.loadMovies(),
color: const Color(0xFF1A1A1A),
backgroundColor: Colors.white,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(16), padding: EdgeInsets.zero,
itemCount: movies.length, itemCount: movies.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return MovieListItem(movie: movies[index]); return MovieListItem(movie: movies[index]);
@@ -55,7 +57,7 @@ class MovieTabPage extends StatelessWidget {
); );
} }
/// 构建空状态提示 /// 构建空状态
Widget _buildEmptyState(BuildContext context, int statusIndex) { Widget _buildEmptyState(BuildContext context, int statusIndex) {
final statusText = ['已看', '想看', '在看'][statusIndex]; final statusText = ['已看', '想看', '在看'][statusIndex];
@@ -63,25 +65,23 @@ class MovieTabPage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( const Icon(
Icons.movie_creation_outlined, Icons.movie_outlined,
size: 80, size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3), color: Color(0xFFCCCCCC),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'暂无$statusText的影片', '暂无$statusText的影片',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: const TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 15,
color: Color(0xFF999999),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 24),
ElevatedButton.icon( TextButton(
icon: const Icon(Icons.add), onPressed: () => Navigator.pushNamed(context, '/movie-form'),
label: const Text('添加记录'), child: const Text('添加记录'),
onPressed: () {
Navigator.pushNamed(context, '/movie-form');
},
), ),
], ],
), ),

593
lib/pages/profile_page.dart Normal file
View File

@@ -0,0 +1,593 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../utils/user_prefs.dart';
/// 个人中心页面 - 极简主义设计
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
final ImagePicker _picker = ImagePicker();
final UserPrefs _userPrefs = UserPrefs();
// 用户数据
String _nickname = '记录者';
String _motto = '记录生活,沉淀思考';
String? _avatarPath;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadUserData();
}
/// 加载用户数据
Future<void> _loadUserData() async {
setState(() => _isLoading = true);
try {
await UserPrefs.init();
setState(() {
_nickname = _userPrefs.nickname;
_motto = _userPrefs.motto;
_avatarPath = _userPrefs.avatarPath;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF1A1A1A),
),
);
}
return Column(
children: [
// 标题栏
AppBar(
title: const Text('我的'),
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => _showSettings(context),
),
],
),
// 内容
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部用户信息
_buildUserHeader(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 数据统计
_buildStatsSection(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 功能菜单
_buildMenuSection(),
const SizedBox(height: 48),
// 版本信息
const Center(
child: Text(
'MookNote v1.0.0',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
),
const SizedBox(height: 24),
],
),
),
),
],
);
}
/// 用户头部信息
Widget _buildUserHeader() {
return Container(
padding: const EdgeInsets.all(24),
child: Row(
children: [
// 头像
GestureDetector(
onTap: _pickAvatar,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
shape: BoxShape.circle,
),
child: _avatarPath != null && _avatarPath!.isNotEmpty
? ClipOval(
child: Image.file(
File(_avatarPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildAvatarPlaceholder(),
),
)
: _buildAvatarPlaceholder(),
),
),
const SizedBox(width: 20),
// 昵称和座右铭
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => _editNickname(context),
child: Row(
children: [
Text(
_nickname,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 8),
const Icon(
Icons.edit_outlined,
size: 16,
color: Color(0xFF999999),
),
],
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _editMotto(context),
child: Text(
_motto,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
),
),
],
),
),
],
),
);
}
Widget _buildAvatarPlaceholder() {
return const Center(
child: Icon(
Icons.person_outline,
size: 32,
color: Color(0xFFCCCCCC),
),
);
}
/// 数据统计区域
Widget _buildStatsSection() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final movies = provider.movies;
final books = provider.books;
final notes = provider.notes;
final movieCount = movies.where((m) => !m.isDeleted).length;
final watchedCount = movies.where((m) => m.status == 'watched' && !m.isDeleted).length;
final watchingCount = movies.where((m) => m.status == 'watching' && !m.isDeleted).length;
final wantToWatchCount = movies.where((m) => m.status == 'want_to_watch' && !m.isDeleted).length;
final bookCount = books.length;
final readCount = books.where((b) => b.status == 'read').length;
final readingCount = books.where((b) => b.status == 'reading').length;
final wantToReadCount = books.where((b) => b.status == 'want_to_read').length;
final noteCount = notes.length;
final movieRatings = movies
.where((m) => m.rating != null && !m.isDeleted)
.map((m) => m.rating!);
final avgMovieRating = movieRatings.isNotEmpty
? movieRatings.reduce((a, b) => a + b) / movieRatings.length
: 0.0;
final bookRatings = books
.where((b) => b.rating != null)
.map((b) => b.rating!);
final avgBookRating = bookRatings.isNotEmpty
? bookRatings.reduce((a, b) => a + b) / bookRatings.length
: 0.0;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
_buildMainStat('观影', movieCount, ''),
const SizedBox(width: 32),
_buildMainStat('阅读', bookCount, ''),
const SizedBox(width: 32),
_buildMainStat('笔记', noteCount, ''),
],
),
),
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('观影详情'),
const SizedBox(height: 16),
Row(
children: [
_buildDetailStat('已看', watchedCount),
_buildDetailStat('在看', watchingCount),
_buildDetailStat('想看', wantToWatchCount),
_buildDetailStat('均分', avgMovieRating.toStringAsFixed(1)),
],
),
const SizedBox(height: 24),
_buildSectionTitle('阅读详情'),
const SizedBox(height: 16),
Row(
children: [
_buildDetailStat('已读', readCount),
_buildDetailStat('在读', readingCount),
_buildDetailStat('想读', wantToReadCount),
_buildDetailStat('均分', avgBookRating.toStringAsFixed(1)),
],
),
],
),
),
],
),
);
},
);
}
/// 主统计项
Widget _buildMainStat(String label, int count, String unit) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'$count',
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 4),
Text(
unit,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
),
],
),
],
),
);
}
/// 详细统计项
Widget _buildDetailStat(String label, dynamic value) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
const SizedBox(height: 4),
Text(
'$value',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
],
),
);
}
/// 区块标题
Widget _buildSectionTitle(String title) {
return Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
letterSpacing: 1,
),
);
}
/// 功能菜单
Widget _buildMenuSection() {
return Column(
children: [
_buildMenuItem(
icon: Icons.analytics_outlined,
title: '数据统计',
onTap: () => _showToast('详细统计功能开发中'),
),
const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
_buildMenuItem(
icon: Icons.calendar_today_outlined,
title: '记录日历',
onTap: () => _showToast('日历功能开发中'),
),
const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
_buildMenuItem(
icon: Icons.favorite_outline,
title: '我的收藏',
onTap: () => _showToast('收藏功能开发中'),
),
const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
_buildMenuItem(
icon: Icons.delete_outline,
title: '回收站',
onTap: () => _showToast('回收站功能开发中'),
),
const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
_buildMenuItem(
icon: Icons.backup_outlined,
title: '数据备份',
onTap: () => _showToast('备份功能开发中'),
),
const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
_buildMenuItem(
icon: Icons.settings_outlined,
title: '设置',
onTap: () => _showSettings(context),
),
],
);
}
/// 菜单项
Widget _buildMenuItem({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: Icon(icon, size: 22, color: const Color(0xFF666666)),
title: Text(
title,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
),
),
trailing: const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFCCCCCC),
),
onTap: onTap,
);
}
/// 显示提示
void _showToast(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
/// 选择头像
Future<void> _pickAvatar() async {
try {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 400,
maxHeight: 400,
imageQuality: 85,
);
if (pickedFile != null) {
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final savedPath = path.join(appDir.path, 'avatars', fileName);
final avatarDir = Directory(path.join(appDir.path, 'avatars'));
if (!await avatarDir.exists()) {
await avatarDir.create(recursive: true);
}
await File(pickedFile.path).copy(savedPath);
// 保存到本地存储
await _userPrefs.setAvatarPath(savedPath);
setState(() => _avatarPath = savedPath);
}
} catch (e) {
if (mounted) {
_showToast('选择头像失败: $e');
}
}
}
/// 编辑昵称
void _editNickname(BuildContext context) {
final controller = TextEditingController(text: _nickname);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'修改昵称',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: '输入昵称',
border: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
final newNickname = controller.text.trim();
if (newNickname.isNotEmpty) {
await _userPrefs.setNickname(newNickname);
setState(() => _nickname = newNickname);
}
Navigator.pop(context);
},
child: const Text('确定'),
),
],
),
);
}
/// 编辑座右铭
void _editMotto(BuildContext context) {
final controller = TextEditingController(text: _motto);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'修改座右铭',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: TextField(
controller: controller,
maxLines: 2,
decoration: const InputDecoration(
hintText: '输入座右铭',
border: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFFE5E5E5)),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
final newMotto = controller.text.trim();
await _userPrefs.setMotto(newMotto);
setState(() => _motto = newMotto);
Navigator.pop(context);
},
child: const Text('确定'),
),
],
),
);
}
/// 显示设置
void _showSettings(BuildContext context) {
_showToast('设置功能开发中');
}
}

View File

@@ -1,66 +1,341 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// 极简主义主题配置
class AppTheme { class AppTheme {
// 主色调 // 中性色板 - 黑白灰为
static const Color primaryColor = Color(0xFF6200EE); static const Color _black = Color(0xFF1A1A1A);
static const Color secondaryColor = Color(0xFF03DAC6); static const Color _darkGray = Color(0xFF333333);
static const Color _gray = Color(0xFF666666);
static const Color _lightGray = Color(0xFF999999);
static const Color _lighterGray = Color(0xFFE5E5E5);
static const Color _offWhite = Color(0xFFF5F5F5);
static const Color _white = Color(0xFFFFFFFF);
// 状态颜色 // 强调色 - 仅用于关键操作
static const Color watchedColor = Color(0xFF4CAF50); // 已看 - 绿色 static const Color accent = Color(0xFF0066FF);
static const Color wantToWatchColor = Color(0xFFFF9800); // 想看 - 橙色 static const Color error = Color(0xFFDC2626);
static const Color watchingColor = Color(0xFF2196F3); // 在看 - 蓝色
static const Color readColor = Color(0xFF4CAF50); // 读完 - 绿色 // 观影状态颜色 - 极简处理
static const Color wantToReadColor = Color(0xFFFF9800); // 准备读 - 橙色 static const Color watched = Color(0xFF1A1A1A); // 已看 - 纯黑
static const Color readingColor = Color(0xFF2196F3); // 在读 - 蓝色 static const Color wantToWatch = Color(0xFF999999); // 想看 - 浅灰
static const Color watching = Color(0xFF666666); // 在看 - 中灰
// 亮色主题 // 阅读状态颜色 - 极简处理
static final ThemeData lightTheme = ThemeData( static const Color readColor = Color(0xFF1A1A1A); // 已读 - 纯黑
useMaterial3: true, static const Color wantToReadColor = Color(0xFF999999); // 想读 - 浅灰
brightness: Brightness.light, static const Color readingColor = Color(0xFF666666); // 在读 - 中灰
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor, // 字体配置
static const String _fontFamily = 'Inter';
// 字重
static const FontWeight _regular = FontWeight.w400;
static const FontWeight _medium = FontWeight.w500;
static const FontWeight _semibold = FontWeight.w600;
// 亮色主题 - 极简主义
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light, brightness: Brightness.light,
), scaffoldBackgroundColor: _white,
appBarTheme: const AppBarTheme(
centerTitle: false, // 颜色方案
elevation: 0, colorScheme: const ColorScheme.light(
), primary: _black,
cardTheme: CardThemeData( onPrimary: _white,
elevation: 2, secondary: _darkGray,
shape: RoundedRectangleBorder( onSecondary: _white,
borderRadius: BorderRadius.circular(12), surface: _white,
onSurface: _black,
error: error,
onError: _white,
), ),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData( // AppBar - 极简无边框
elevation: 8, appBarTheme: const AppBarTheme(
selectedItemColor: primaryColor, backgroundColor: _white,
unselectedItemColor: Colors.grey, foregroundColor: _black,
), elevation: 0,
); centerTitle: false,
titleSpacing: 24,
systemOverlayStyle: SystemUiOverlayStyle.dark,
titleTextStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 18,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.3,
),
),
// 卡片 - 无阴影,细边框
cardTheme: CardThemeData(
color: _white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
side: BorderSide(color: _lighterGray, width: 0.5),
),
margin: EdgeInsets.zero,
),
// 列表瓦片
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
minLeadingWidth: 0,
dense: true,
),
// 分割线 - 极细
dividerTheme: DividerThemeData(
color: _lighterGray,
thickness: 0.5,
space: 0,
),
// 输入框 - 无边框,底部线
inputDecorationTheme: InputDecorationTheme(
filled: false,
border: UnderlineInputBorder(
borderSide: BorderSide(color: _lighterGray, width: 0.5),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: _lighterGray, width: 0.5),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: _black, width: 1),
),
errorBorder: UnderlineInputBorder(
borderSide: BorderSide(color: error, width: 0.5),
),
contentPadding: EdgeInsets.symmetric(vertical: 12),
hintStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 15,
fontWeight: _regular,
color: _lightGray,
),
labelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 13,
fontWeight: _medium,
color: _gray,
),
),
// 按钮 - 文字按钮为主
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: _black,
foregroundColor: _white,
elevation: 0,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero),
textStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: _medium,
letterSpacing: 0.3,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: _black,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: _medium,
),
),
),
// 底部导航
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: _white,
selectedItemColor: _black,
unselectedItemColor: _lightGray,
elevation: 0,
type: BottomNavigationBarType.fixed,
selectedLabelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: _medium,
),
unselectedLabelStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: _regular,
),
),
// 文字主题
textTheme: const TextTheme(
// 大标题
headlineLarge: TextStyle(
fontFamily: _fontFamily,
fontSize: 32,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.5,
height: 1.2,
),
headlineMedium: TextStyle(
fontFamily: _fontFamily,
fontSize: 24,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.3,
height: 1.3,
),
headlineSmall: TextStyle(
fontFamily: _fontFamily,
fontSize: 20,
fontWeight: _semibold,
color: _black,
letterSpacing: -0.2,
height: 1.4,
),
// 正文
bodyLarge: TextStyle(
fontFamily: _fontFamily,
fontSize: 16,
fontWeight: _regular,
color: _darkGray,
height: 1.6,
),
bodyMedium: TextStyle(
fontFamily: _fontFamily,
fontSize: 15,
fontWeight: _regular,
color: _darkGray,
height: 1.5,
),
bodySmall: TextStyle(
fontFamily: _fontFamily,
fontSize: 13,
fontWeight: _regular,
color: _gray,
height: 1.5,
),
// 标签
labelLarge: TextStyle(
fontFamily: _fontFamily,
fontSize: 14,
fontWeight: _medium,
color: _black,
),
labelMedium: TextStyle(
fontFamily: _fontFamily,
fontSize: 12,
fontWeight: _medium,
color: _gray,
),
labelSmall: TextStyle(
fontFamily: _fontFamily,
fontSize: 11,
fontWeight: _medium,
color: _lightGray,
letterSpacing: 0.3,
),
),
);
}
// 暗色主题 // 暗色主题
static final ThemeData darkTheme = ThemeData( static ThemeData get darkTheme {
useMaterial3: true, return ThemeData(
brightness: Brightness.dark, useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark, brightness: Brightness.dark,
), scaffoldBackgroundColor: _black,
appBarTheme: const AppBarTheme(
centerTitle: false, colorScheme: const ColorScheme.dark(
elevation: 0, primary: _white,
), onPrimary: _black,
cardTheme: CardThemeData( secondary: _offWhite,
elevation: 2, onSecondary: _black,
shape: RoundedRectangleBorder( surface: _black,
borderRadius: BorderRadius.circular(12), onSurface: _white,
error: Color(0xFFEF4444),
onError: _black,
), ),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData( appBarTheme: const AppBarTheme(
elevation: 8, backgroundColor: _black,
selectedItemColor: secondaryColor, foregroundColor: _white,
unselectedItemColor: Colors.grey, elevation: 0,
), centerTitle: false,
); titleSpacing: 24,
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 18,
fontWeight: _semibold,
color: _white,
letterSpacing: -0.3,
),
),
cardTheme: CardThemeData(
color: _darkGray,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
margin: EdgeInsets.zero,
),
dividerTheme: DividerThemeData(
color: _darkGray,
thickness: 0.5,
space: 0,
),
inputDecorationTheme: InputDecorationTheme(
filled: false,
border: UnderlineInputBorder(
borderSide: BorderSide(color: _darkGray, width: 0.5),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: _darkGray, width: 0.5),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: _white, width: 1),
),
contentPadding: EdgeInsets.symmetric(vertical: 12),
hintStyle: TextStyle(
fontFamily: _fontFamily,
fontSize: 15,
fontWeight: _regular,
color: _gray,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: _white,
foregroundColor: _black,
elevation: 0,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: _white,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: _black,
selectedItemColor: _white,
unselectedItemColor: _gray,
elevation: 0,
),
);
}
} }

48
lib/utils/user_prefs.dart Normal file
View File

@@ -0,0 +1,48 @@
import 'package:shared_preferences/shared_preferences.dart';
/// 用户偏好设置管理
class UserPrefs {
static final UserPrefs _instance = UserPrefs._internal();
static SharedPreferences? _prefs;
factory UserPrefs() => _instance;
UserPrefs._internal();
/// 初始化
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
/// 获取实例
SharedPreferences get prefs {
if (_prefs == null) {
throw Exception('UserPrefs not initialized. Call UserPrefs.init() first.');
}
return _prefs!;
}
// ========== 用户信息 ==========
/// 昵称
String get nickname => prefs.getString('nickname') ?? '记录者';
Future<bool> setNickname(String value) => prefs.setString('nickname', value);
/// 座右铭
String get motto => prefs.getString('motto') ?? '记录生活,沉淀思考';
Future<bool> setMotto(String value) => prefs.setString('motto', value);
/// 头像路径
String? get avatarPath => prefs.getString('avatarPath');
Future<bool> setAvatarPath(String value) => prefs.setString('avatarPath', value);
Future<bool> clearAvatarPath() => prefs.remove('avatarPath');
// ========== 应用设置 ==========
/// 是否暗黑模式
bool get isDarkMode => prefs.getBool('isDarkMode') ?? false;
Future<bool> setDarkMode(bool value) => prefs.setBool('isDarkMode', value);
/// 是否首次启动
bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true;
Future<bool> setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value);
}

View File

@@ -17,12 +17,8 @@ class CustomBottomNavBar extends StatelessWidget {
// 新增按钮 - 显示选择对话框 // 新增按钮 - 显示选择对话框
_showAddDialog(context, provider); _showAddDialog(context, provider);
} else { } else {
// 切换主页/我的页面
provider.setBottomNavIndex(index); provider.setBottomNavIndex(index);
if (index == 0) {
// 主页 - 重置到首页
provider.setMainTabIndex(0);
}
// index == 2 是我的页面(待实现)
} }
}, },
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,

View File

@@ -1,51 +1,55 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../utils/user_prefs.dart';
/// 自定义左侧弹出菜单 /// 自定义左侧弹出菜单 - 极简主义设计
class CustomDrawer extends StatelessWidget { class CustomDrawer extends StatelessWidget {
const CustomDrawer({super.key}); const CustomDrawer({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Drawer( return Drawer(
backgroundColor: Colors.white,
child: Column( child: Column(
children: [ children: [
// 顶部用户信息区域(含热力图) // 顶部用户信息区域
_buildHeader(context), _buildHeader(context),
// 分割线 const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
Divider(height: 1, color: colorScheme.outlineVariant),
// 菜单项列表 // 菜单项列表
Expanded( Expanded(
child: ListView( child: ListView(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: [ children: [
ListTile( _buildMenuItem(
leading: const Icon(Icons.analytics), icon: Icons.analytics_outlined,
title: const Text('统计'), title: '统计',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
// TODO: 跳转到统计页面 _showToast(context, '统计功能开发中');
}, },
), ),
ListTile( const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
leading: const Icon(Icons.delete_outline),
title: const Text('回收站'), _buildMenuItem(
icon: Icons.delete_outline,
title: '回收站',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
// TODO: 跳转到回收站页面 _showToast(context, '回收站功能开发中');
}, },
), ),
ListTile( const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)),
leading: const Icon(Icons.settings),
title: const Text('设置'), _buildMenuItem(
icon: Icons.settings_outlined,
title: '设置',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
// TODO: 跳转到设置页面 _showToast(context, '设置功能开发中');
}, },
), ),
], ],
@@ -54,12 +58,12 @@ class CustomDrawer extends StatelessWidget {
// 底部版本信息 // 底部版本信息
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(24),
child: Text( child: const Text(
'MookNote v1.0.0', 'MookNote v1.0.0',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: colorScheme.onSurfaceVariant, color: Color(0xFF999999),
), ),
), ),
), ),
@@ -68,63 +72,82 @@ class CustomDrawer extends StatelessWidget {
); );
} }
/// 构建头部(含热力图) /// 构建头部
Widget _buildHeader(BuildContext context) { Widget _buildHeader(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
// 统计数据
final movieCount = provider.movies.where((m) => !m.isDeleted).length;
final bookCount = provider.books.length;
final noteCount = provider.notes.length;
// 获取用户信息
final userPrefs = UserPrefs();
final nickname = userPrefs.nickname;
final motto = userPrefs.motto;
final avatarPath = userPrefs.avatarPath;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16), padding: const EdgeInsets.fromLTRB(24, 48, 24, 24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.surface,
],
),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 用户头像和名称 // 用户头像
Row( Container(
children: [ width: 64,
CircleAvatar( height: 64,
radius: 32, decoration: BoxDecoration(
backgroundColor: Theme.of(context).colorScheme.primary, color: const Color(0xFFF5F5F5),
child: const Icon( shape: BoxShape.circle,
Icons.person, border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
color: Colors.white, ),
size: 32, child: avatarPath != null && avatarPath.isNotEmpty
), ? ClipOval(
), child: Image.file(
const SizedBox(width: 12), File(avatarPath),
Column( fit: BoxFit.cover,
crossAxisAlignment: CrossAxisAlignment.start, errorBuilder: (_, __, ___) => _buildAvatarPlaceholder(),
children: [
Text(
'用户',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
), ),
), )
Text( : _buildAvatarPlaceholder(),
'记录生活点滴',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
), ),
const SizedBox(height: 20), const SizedBox(height: 16),
// GitHub 风格热力图 // 用户名称
_buildHeatmap(context), Text(
nickname,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 4),
// 座右铭
Text(
motto,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
),
const SizedBox(height: 24),
// 简化统计
Row(
children: [
_buildStatItem('观影', movieCount),
const SizedBox(width: 24),
_buildStatItem('阅读', bookCount),
const SizedBox(width: 24),
_buildStatItem('笔记', noteCount),
],
),
], ],
), ),
); );
@@ -132,82 +155,74 @@ class CustomDrawer extends StatelessWidget {
); );
} }
/// 构建热力图 Widget _buildAvatarPlaceholder() {
Widget _buildHeatmap(BuildContext context) { return const Center(
const int weeks = 52; // 一年 52 周 child: Icon(
const int daysPerWeek = 7; Icons.person_outline,
size: 32,
// 生成随机数据(实际应从数据库获取) color: Color(0xFF999999),
final random = DateTime.now().millisecondsSinceEpoch; ),
);
}
/// 统计项
Widget _buildStatItem(String label, int count) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'年度记录', '$count',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 2),
SizedBox( Text(
height: daysPerWeek * 14, // 每个格子 14x14 label,
child: Row( style: const TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 12,
children: List.generate(weeks, (weekIndex) { color: Color(0xFF999999),
return Padding(
padding: const EdgeInsets.only(right: 2),
child: Column(
children: List.generate(daysPerWeek, (dayIndex) {
// 根据随机值决定颜色深度
final intensity = (random + weekIndex * 7 + dayIndex) % 100 / 100;
final color = _getHeatmapColor(intensity, context);
return Container(
margin: const EdgeInsets.only(bottom: 2),
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
);
}),
),
);
}),
), ),
), ),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Less',
style: Theme.of(context).textTheme.labelSmall,
),
Text(
'More',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
], ],
); );
} }
/// 根据强度获取热力图颜色 /// 菜单项
Color _getHeatmapColor(double intensity, BuildContext context) { Widget _buildMenuItem({
if (intensity == 0) { required IconData icon,
return Theme.of(context).colorScheme.surfaceContainerHighest; required String title,
} else if (intensity < 0.25) { required VoidCallback onTap,
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.4); }) {
} else if (intensity < 0.5) { return ListTile(
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6); contentPadding: const EdgeInsets.symmetric(horizontal: 24),
} else if (intensity < 0.75) { leading: Icon(icon, size: 22, color: const Color(0xFF666666)),
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8); title: Text(
} else { title,
return Theme.of(context).colorScheme.primary; style: const TextStyle(
} fontSize: 15,
color: Color(0xFF1A1A1A),
),
),
trailing: const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFCCCCCC),
),
onTap: onTap,
);
}
/// 显示提示
void _showToast(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
} }
} }

View File

@@ -1,11 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 观影列表项组件 /// 观影列表项 - 极简主义设计
class MovieListItem extends StatelessWidget { class MovieListItem extends StatelessWidget {
final Movie movie; final Movie movie;
@@ -13,309 +10,181 @@ class MovieListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); return InkWell(
final colorScheme = theme.colorScheme; onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
child: Container(
return Card( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
margin: const EdgeInsets.only(bottom: 12), decoration: const BoxDecoration(
elevation: 0, border: Border(
shape: RoundedRectangleBorder( bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
borderRadius: BorderRadius.circular(12), ),
side: BorderSide(
color: colorScheme.outline.withOpacity(0.1),
width: 1,
), ),
), child: Row(
child: InkWell( crossAxisAlignment: CrossAxisAlignment.start,
onTap: () { children: [
Navigator.pushNamed(context, '/movie-detail', arguments: movie); // 海报
}, _buildPoster(),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报
_buildPoster(context),
const SizedBox(width: 14), const SizedBox(width: 16),
// 影片信息 // 信息
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 标题 // 标题
Text(
movie.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
// 年份和评分
_buildMetaInfo(),
const SizedBox(height: 8),
// 导演
if (movie.directors.isNotEmpty)
Text( Text(
movie.title, movie.directors.take(2).join(' / '),
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 13,
fontWeight: FontWeight.w600, color: Color(0xFF999999),
color: colorScheme.onSurface,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 6),
// 上映日期和评分
Row(
children: [
if (movie.releaseDate != null) ...[
Icon(
Icons.calendar_today_outlined,
size: 13,
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
),
const SizedBox(width: 4),
Text(
'${movie.releaseDate!.year}',
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
),
),
const SizedBox(width: 12),
],
if (movie.rating != null) ...[
Icon(
Icons.star_rounded,
size: 14,
color: Colors.amber[700],
),
const SizedBox(width: 2),
Text(
movie.rating!.toStringAsFixed(1),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.amber[700],
),
),
],
],
),
const SizedBox(height: 8),
// 导演
if (movie.directors.isNotEmpty)
_buildInfoRow(
context,
prefix: '导演',
items: movie.directors.take(2).toList(),
),
if (movie.directors.isNotEmpty && movie.genres.isNotEmpty)
const SizedBox(height: 4),
// 类型标签
if (movie.genres.isNotEmpty)
Wrap(
spacing: 6,
runSpacing: 4,
children: movie.genres.take(3).map((genre) => Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(4),
),
child: Text(
genre,
style: TextStyle(
fontSize: 11,
color: colorScheme.primary.withOpacity(0.8),
),
),
)).toList(),
),
const SizedBox(height: 8),
// 状态标签
_buildStatusTag(context),
],
),
),
// 右侧操作按钮
Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit_outlined, size: 20, color: colorScheme.primary),
onPressed: () {
Navigator.pushNamed(context, '/movie-form', arguments: movie);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(height: 8), const SizedBox(height: 8),
IconButton(
icon: Icon(Icons.delete_outline, size: 20, color: colorScheme.error.withOpacity(0.7)), // 状态
onPressed: () => _showDeleteDialog(context, movie), _buildStatusTag(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
], ],
), ),
], ),
),
// 箭头
const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFCCCCCC),
),
],
), ),
), ),
); );
} }
/// 构建海报 /// 海报
Widget _buildPoster(BuildContext context) { Widget _buildPoster() {
final colorScheme = Theme.of(context).colorScheme; return Container(
width: 56,
return ClipRRect( height: 80,
borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(
child: Container( color: const Color(0xFFF5F5F5),
width: 70, border: Border.all(
height: 100, color: const Color(0xFFE5E5E5),
color: colorScheme.surfaceContainerHighest, width: 0.5,
child: movie.posterPath != null && movie.posterPath!.isNotEmpty ),
? Image.file(
File(movie.posterPath!),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _buildPlaceholder(context),
)
: _buildPlaceholder(context),
), ),
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
? Image.file(
File(movie.posterPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder(),
); );
} }
Widget _buildPlaceholder(BuildContext context) { Widget _buildPlaceholder() {
return Center( return const Center(
child: Icon( child: Icon(
Icons.movie_outlined, Icons.movie_outlined,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3), size: 20,
size: 28, color: Color(0xFFCCCCCC),
), ),
); );
} }
/// 构建信息 /// 信息
Widget _buildInfoRow(BuildContext context, {required String prefix, required List<String> items}) { Widget _buildMetaInfo() {
final colorScheme = Theme.of(context).colorScheme; final items = <String>[];
if (movie.releaseDate != null) {
items.add('${movie.releaseDate!.year}');
}
if (movie.rating != null) {
items.add(movie.rating!.toStringAsFixed(1));
}
if (items.isEmpty) return const SizedBox.shrink();
return Row( return Row(
children: [ children: items.asMap().entries.map((entry) {
Text( return Row(
'$prefix: ', children: [
style: TextStyle( Text(
fontSize: 12, entry.value,
color: colorScheme.onSurfaceVariant.withOpacity(0.6), style: const TextStyle(
), fontSize: 13,
), color: Color(0xFF999999),
Expanded( ),
child: Text(
items.join(' / '),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant.withOpacity(0.8),
), ),
maxLines: 1, if (entry.key < items.length - 1)
overflow: TextOverflow.ellipsis, const Padding(
), padding: EdgeInsets.symmetric(horizontal: 8),
), child: Text(
], '·',
style: TextStyle(
fontSize: 13,
color: Color(0xFFCCCCCC),
),
),
),
],
);
}).toList(),
); );
} }
/// 构建状态标签 /// 状态标签
Widget _buildStatusTag(BuildContext context) { Widget _buildStatusTag() {
Color statusColor; String label;
String statusText; Color color;
switch (movie.status) { switch (movie.status) {
case 'watched': case 'watched':
statusColor = AppTheme.watchedColor; label = '已看';
statusText = '已看'; color = const Color(0xFF1A1A1A);
break;
case 'want_to_watch':
statusColor = AppTheme.wantToWatchColor;
statusText = '想看';
break; break;
case 'watching': case 'watching':
statusColor = AppTheme.watchingColor; label = '在看';
statusText = '在看'; color = const Color(0xFF666666);
break;
case 'want_to_watch':
label = '想看';
color = const Color(0xFF999999);
break; break;
default: default:
statusColor = Colors.grey; label = '未知';
statusText = '未知'; color = const Color(0xFFCCCCCC);
} }
return Container( return Text(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), label,
decoration: BoxDecoration( style: TextStyle(
color: statusColor.withOpacity(0.1), fontSize: 12,
borderRadius: BorderRadius.circular(4), color: color,
border: Border.all( fontWeight: FontWeight.w500,
color: statusColor.withOpacity(0.3),
width: 1,
),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 11,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
);
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context, Movie movie) {
final colorScheme = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(
'确认删除',
style: TextStyle(color: colorScheme.onSurface),
),
content: Text(
'确定要删除"${movie.title}"吗?',
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.7)),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMovie(movie.id);
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('已删除'),
behavior: SnackBarBehavior.floating,
backgroundColor: colorScheme.primary,
),
);
},
child: Text(
'删除',
style: TextStyle(color: colorScheme.error),
),
),
],
), ),
); );
} }

View File

@@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../utils/app_theme.dart';
/// 观影状态选择栏 /// 观影状态选择栏 - 极简主义设计
class MovieStatusBar extends StatelessWidget { class MovieStatusBar extends StatelessWidget {
const MovieStatusBar({super.key}); const MovieStatusBar({super.key});
@@ -12,44 +11,26 @@ class MovieStatusBar extends StatelessWidget {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration( color: Colors.white,
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row( child: Row(
children: [ children: [
_buildStatusItem( _buildStatusItem(
context,
'已看', '已看',
AppTheme.watchedColor,
Icons.check_circle,
0, 0,
provider.movieStatusIndex, provider.movieStatusIndex,
() => provider.setMovieStatusIndex(0), () => provider.setMovieStatusIndex(0),
), ),
const SizedBox(width: 12), const SizedBox(width: 24),
_buildStatusItem( _buildStatusItem(
context,
'想看', '想看',
AppTheme.wantToWatchColor,
Icons.bookmark_border,
1, 1,
provider.movieStatusIndex, provider.movieStatusIndex,
() => provider.setMovieStatusIndex(1), () => provider.setMovieStatusIndex(1),
), ),
const SizedBox(width: 12), const SizedBox(width: 24),
_buildStatusItem( _buildStatusItem(
context,
'在看', '在看',
AppTheme.watchingColor,
Icons.play_circle_outline,
2, 2,
provider.movieStatusIndex, provider.movieStatusIndex,
() => provider.setMovieStatusIndex(2), () => provider.setMovieStatusIndex(2),
@@ -63,49 +44,21 @@ class MovieStatusBar extends StatelessWidget {
/// 构建状态项 /// 构建状态项
Widget _buildStatusItem( Widget _buildStatusItem(
BuildContext context,
String label, String label,
Color color,
IconData icon,
int index, int index,
int currentIndex, int currentIndex,
VoidCallback onTap, VoidCallback onTap,
) { ) {
final isSelected = index == currentIndex; final isSelected = index == currentIndex;
return Expanded( return GestureDetector(
child: InkWell( onTap: onTap,
onTap: onTap, child: Text(
borderRadius: BorderRadius.circular(8), label,
child: Container( style: TextStyle(
padding: const EdgeInsets.symmetric(vertical: 10), fontSize: 15,
decoration: BoxDecoration( fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? color.withOpacity(0.1) : Colors.transparent, color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFF999999),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
), ),
), ),
); );

View File

@@ -456,6 +456,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41"
url: "https://pub.dev"
source: hosted
version: "2.4.21"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@@ -16,6 +16,7 @@ dependencies:
path: ^1.8.3 path: ^1.8.3
image_picker: ^1.0.4 image_picker: ^1.0.4
path_provider: ^2.1.1 path_provider: ^2.1.1
shared_preferences: ^2.2.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: