diff --git a/android/gradle.properties b/android/gradle.properties index 90c683f..b07cc56 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,4 +3,7 @@ android.useAndroidX=true systemProp.http.proxyHost=127.0.0.1 systemProp.http.proxyPort=10808 systemProp.https.proxyHost=127.0.0.1 -systemProp.https.proxyPort=10808 \ No newline at end of file +systemProp.https.proxyPort=10808 +org.gradle.daemon=false +kotlin.incremental=false +kotlin.compiler.execution.strategy=out-of-process diff --git a/lib/main.dart b/lib/main.dart index 3012406..a1aa365 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,12 +3,16 @@ import 'package:provider/provider.dart'; import 'pages/home_page.dart'; import 'utils/app_theme.dart'; import 'utils/app_router.dart'; +import 'utils/user_prefs.dart'; import 'providers/app_provider.dart'; void main() async { // 确保 Flutter 绑定初始化完成 WidgetsFlutterBinding.ensureInitialized(); + // 初始化用户偏好设置 + await UserPrefs.init(); + // 初始化数据库 final appProvider = AppProvider(); await appProvider.initDatabase(); diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 3e5dca2..a0145f7 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,11 +3,10 @@ import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../widgets/custom_drawer.dart'; import '../widgets/bottom_nav_bar.dart'; -import 'movie_tab_page.dart'; -import 'book_tab_page.dart'; -import 'note_tab_page.dart'; +import 'main_content_page.dart'; +import 'profile_page.dart'; -/// 主页 - 包含顶部菜单、三个标签页、底部导航 +/// 主页 - 包含底部导航,可切换主页/我的 class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -19,229 +18,34 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - // 左侧弹出菜单 - drawer: const CustomDrawer(), + // 左侧弹出菜单(仅在主页显示) + drawer: context.watch().bottomNavIndex == 0 + ? const CustomDrawer() + : null, - // 主体内容 - body: Column( - children: [ - // 顶部 AppBar - _buildAppBar(), - - // 三个标签页的标题栏 - _buildTabBar(), - - // 标签页内容 - Expanded( - child: _buildTabContent(), - ), - ], - ), + // 主体内容 - 根据底部导航切换 + body: _buildBody(), // 底部导航栏 bottomNavigationBar: const CustomBottomNavBar(), ); } - /// 构建顶部 AppBar - Widget _buildAppBar() { + /// 构建主体内容 + Widget _buildBody() { return Consumer( 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() { - return Consumer( - 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( - builder: (context, provider, child) { - switch (provider.mainTabIndex) { + switch (provider.bottomNavIndex) { case 0: - return const MovieTabPage(); - case 1: - return const BookTabPage(); + // 主页 - 观影/阅读/笔记 + return const MainContentPage(); case 2: - return const NoteTabPage(); + // 我的页面 + return const ProfilePage(); 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'); - }, - ), - ], - ), - ); - }, - ); - } } diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart new file mode 100644 index 0000000..a9fb918 --- /dev/null +++ b/lib/pages/main_content_page.dart @@ -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( + 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( + 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( + 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'); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/movie_detail_page.dart b/lib/pages/movie_detail_page.dart index 4428bba..8ac01af 100644 --- a/lib/pages/movie_detail_page.dart +++ b/lib/pages/movie_detail_page.dart @@ -3,9 +3,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -import '../utils/app_theme.dart'; -/// 影视详情页 +/// 影视详情页 - 极简主义设计 class MovieDetailPage extends StatefulWidget { final Movie movie; @@ -30,446 +29,298 @@ class _MovieDetailPageState extends State { (m) => m.id == _movie.id, orElse: () => _movie, ); - setState(() { - _movie = updated; - }); + setState(() => _movie = updated); } @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - return Scaffold( - backgroundColor: colorScheme.surface, - body: CustomScrollView( - slivers: [ - // 海报区域(可折叠) - SliverAppBar( - expandedHeight: 320, - pinned: true, - backgroundColor: colorScheme.surface, - flexibleSpace: FlexibleSpaceBar( - background: _buildPosterSection(context), - ), - leading: IconButton( - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, - ), - child: const Icon(Icons.arrow_back, color: Colors.white, size: 20), + appBar: AppBar( + title: const Text('详情'), + actions: [ + TextButton( + onPressed: () => _navigateToEdit(context), + child: const Text('编辑'), + ), + const SizedBox(width: 8), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 海报 + _buildPoster(), + + 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( - icon: Container( - padding: const EdgeInsets.all(8), + + const SizedBox(height: 12), + + // 状态标签 + _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( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFE5E5E5), + width: 0.5, + ), ), - child: const Icon(Icons.edit, color: Colors.white, size: 20), - ), - onPressed: () => _navigateToEdit(context), - ), - IconButton( - icon: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, + child: Text( + genre, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), + ), ), - child: const Icon(Icons.delete_outline, color: Colors.white, size: 20), - ), - onPressed: () => _showDeleteDialog(context), + )).toList(), ), ], - ), - - // 内容区域 - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题和状态 - _buildTitleSection(context), - - const SizedBox(height: 20), - - // 基本信息 - _buildBasicInfoSection(context), - - const SizedBox(height: 24), - - // 导演 - if (_movie.directors.isNotEmpty) ...[ - _buildListSection(context, '导演', _movie.directors, Icons.videocam_outlined), - 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), - ], + + // 剧情简介 + if (_movie.summary != null && _movie.summary!.isNotEmpty) ...[ + const SizedBox(height: 32), + _buildSectionTitle('简介'), + const SizedBox(height: 12), + Text( + _movie.summary!, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF666666), + height: 1.7, + ), + ), + ], + + const SizedBox(height: 48), + + // 删除按钮 + Center( + child: TextButton( + onPressed: () => _showDeleteDialog(context), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFDC2626), + ), + child: const Text('删除此影片'), ), ), - ), - ], + + const SizedBox(height: 24), + ], + ), ), ); } - /// 构建海报区域 - Widget _buildPosterSection(BuildContext context) { - return Container( - width: double.infinity, - color: Colors.grey[300], - child: _movie.posterPath != null && _movie.posterPath!.isNotEmpty - ? Image.file( - File(_movie.posterPath!), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => _buildPlaceholder(), - ) - : _buildPlaceholder(), + /// 海报 + Widget _buildPoster() { + return Center( + child: Container( + width: 140, + height: 200, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all( + color: const Color(0xFFE5E5E5), + width: 0.5, + ), + ), + child: _movie.posterPath != null && _movie.posterPath!.isNotEmpty + ? Image.file( + File(_movie.posterPath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildPlaceholder(), + ) + : _buildPlaceholder(), + ), ); } Widget _buildPlaceholder() { - return Container( - color: Colors.grey[300], - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.movie, size: 80, color: Colors.grey[500]), - const SizedBox(height: 12), - Text( - '暂无海报', - style: TextStyle(color: Colors.grey[500], fontSize: 14), - ), - ], + return const Center( + child: Text( + '无海报', + style: TextStyle( + fontSize: 13, + color: Color(0xFF999999), ), ), ); } - /// 构建标题区域 - Widget _buildTitleSection(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; + /// 状态标签 + Widget _buildStatusTag() { + String label; + Color bgColor; + Color textColor; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 状态标签 - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: _getStatusColor().withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - _getStatusText(), - style: TextStyle( - fontSize: 12, - color: _getStatusColor(), - fontWeight: FontWeight.w600, - ), - ), - ), - - 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; + switch (_movie.status) { + case 'watched': + label = '已看'; + bgColor = const Color(0xFF1A1A1A); + textColor = Colors.white; + break; + case 'watching': + label = '在看'; + bgColor = const Color(0xFF666666); + textColor = Colors.white; + break; + case 'want_to_watch': + label = '想看'; + bgColor = const Color(0xFFF5F5F5); + textColor = const Color(0xFF666666); + break; + default: + label = '未知'; + bgColor = const Color(0xFFF5F5F5); + textColor = const Color(0xFF999999); + } return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), + color: bgColor, + borderRadius: BorderRadius.zero, ), - child: Row( - children: [ - // 上映日期 - if (_movie.releaseDate != null) ...[ - Expanded( - child: _buildInfoItem( - 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, - ), - ), - ], + child: Text( + label, + style: TextStyle( + fontSize: 12, + color: textColor, + fontWeight: FontWeight.w500, + ), ), ); } - Widget _buildInfoItem( - BuildContext context, { - required IconData icon, - required String label, - required String value, - Color? valueColor, - }) { - final colorScheme = Theme.of(context).colorScheme; + /// 基本信息行 + Widget _buildInfoRow() { + final items = []; - return Column( - children: [ - 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 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.releaseDate != null) { + items.add('${_movie.releaseDate!.year}'); } - } - - /// 获取状态文本 - String _getStatusText() { - switch (_movie.status) { - case 'watched': - return '已看'; - case 'want_to_watch': - return '想看'; - case 'watching': - return '在看'; - default: - return '未知'; + if (_movie.rating != null) { + items.add('${_movie.rating!.toStringAsFixed(1)} 分'); } + + 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 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) { Navigator.pushNamed(context, '/movie-form', arguments: _movie).then((_) { _refreshMovie(); @@ -477,29 +328,35 @@ class _MovieDetailPageState extends State { }); } - /// 显示删除对话框 + /// 删除对话框 void _showDeleteDialog(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - showDialog( context: context, builder: (context) => AlertDialog( - backgroundColor: colorScheme.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text( + backgroundColor: Colors.white, + elevation: 0, + 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( - '确定要删除"${_movie.title}"吗?此操作不可恢复。', - style: TextStyle(color: colorScheme.onSurface.withOpacity(0.7)), + '确定要删除"${_movie.title}"吗?', + style: const TextStyle( + fontSize: 15, + color: Color(0xFF666666), + ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text( + child: const Text( '取消', - style: TextStyle(color: colorScheme.onSurfaceVariant), + style: TextStyle(color: Color(0xFF666666)), ), ), TextButton( @@ -508,17 +365,10 @@ class _MovieDetailPageState extends State { if (!context.mounted) return; 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)), ), ), ], diff --git a/lib/pages/movie_form_page.dart b/lib/pages/movie_form_page.dart index 47b50b0..eb6bd49 100644 --- a/lib/pages/movie_form_page.dart +++ b/lib/pages/movie_form_page.dart @@ -7,7 +7,7 @@ import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 添加/编辑影视记录页面 +/// 添加/编辑影视记录 - 极简主义设计 class MovieFormPage extends StatefulWidget { final Movie? movie; @@ -21,19 +21,16 @@ class _MovieFormPageState extends State { final _formKey = GlobalKey(); final ImagePicker _picker = ImagePicker(); - // 文本控制器 late TextEditingController _titleController; late TextEditingController _ratingController; late TextEditingController _summaryController; - // 列表控制器(导演、编剧、演员、类型、别名) final List _directorControllers = []; final List _writerControllers = []; final List _actorControllers = []; final List _genreControllers = []; final List _alternateTitleControllers = []; - // 状态 late String _status; DateTime? _releaseDate; String? _posterPath; @@ -52,7 +49,6 @@ class _MovieFormPageState extends State { _releaseDate = movie?.releaseDate; _posterPath = movie?.posterPath; - // 初始化列表控制器 _initListControllers(movie?.directors ?? [], _directorControllers); _initListControllers(movie?.writers ?? [], _writerControllers); _initListControllers(movie?.actors ?? [], _actorControllers); @@ -89,60 +85,44 @@ class _MovieFormPageState extends State { Widget build(BuildContext context) { final isEdit = widget.movie != null; final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - + return Scaffold( - backgroundColor: colorScheme.surface, appBar: AppBar( - backgroundColor: colorScheme.surface, - elevation: 0, - centerTitle: true, - title: Text( - isEdit ? '编辑影片' : '添加影片', - style: TextStyle( - color: colorScheme.onSurface, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - leading: IconButton( - icon: Icon(Icons.arrow_back, color: colorScheme.onSurface), - onPressed: () => Navigator.pop(context), - ), + title: Text(isEdit ? '编辑' : '添加'), actions: [ TextButton( onPressed: _isLoading ? null : _saveMovie, child: _isLoading ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary) + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary) ) - : Text('保存', style: TextStyle(color: colorScheme.primary)), + : Text('保存'), ), + const SizedBox(width: 8), ], ), body: Form( key: _formKey, child: SingleChildScrollView( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 海报上传区域 + // 海报 _buildPosterSection(), - const SizedBox(height: 24), + const SizedBox(height: 40), // 基本信息 _buildSectionTitle('基本信息'), - const SizedBox(height: 12), + const SizedBox(height: 24), // 影视名称 _buildTextField( controller: _titleController, - label: '影视名称 *', - hint: '请输入影视名称', + label: '名称', validator: (value) { if (value == null || value.trim().isEmpty) { return '请输入影视名称'; @@ -151,88 +131,85 @@ class _MovieFormPageState extends State { }, ), - const SizedBox(height: 16), + const SizedBox(height: 24), - // 上映日期和评分 - Row( - children: [ - Expanded( - child: _buildDatePicker(), - ), - const SizedBox(width: 16), - Expanded( - child: _buildTextField( - controller: _ratingController, - label: '评分', - hint: '1-10', - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value != null && value.isNotEmpty) { - final rating = double.tryParse(value); - if (rating == null || rating < 1 || rating > 10) { - return '评分1-10'; - } - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 16), - - // 状态选择 - _buildStatusSelector(), + // 上映日期 + _buildDatePicker(), const SizedBox(height: 24), + // 评分 + _buildTextField( + controller: _ratingController, + label: '评分', + hint: '1-10', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (value) { + if (value != null && value.isNotEmpty) { + final rating = double.tryParse(value); + if (rating == null || rating < 1 || rating > 10) { + return '评分范围 1-10'; + } + } + return null; + }, + ), + + const SizedBox(height: 32), + + // 状态 + _buildSectionTitle('状态'), + const SizedBox(height: 16), + _buildStatusSelector(), + + const SizedBox(height: 40), + // 别名 _buildSectionTitle('别名'), - const SizedBox(height: 8), - _buildTagList(_alternateTitleControllers, '添加别名'), + const SizedBox(height: 16), + _buildTagList(_alternateTitleControllers), - const SizedBox(height: 24), + const SizedBox(height: 40), // 导演 _buildSectionTitle('导演'), - const SizedBox(height: 8), - _buildTagList(_directorControllers, '添加导演'), + const SizedBox(height: 16), + _buildTagList(_directorControllers), - const SizedBox(height: 24), + const SizedBox(height: 40), // 编剧 _buildSectionTitle('编剧'), - const SizedBox(height: 8), - _buildTagList(_writerControllers, '添加编剧'), + const SizedBox(height: 16), + _buildTagList(_writerControllers), - const SizedBox(height: 24), + const SizedBox(height: 40), // 主演 _buildSectionTitle('主演'), - const SizedBox(height: 8), - _buildTagList(_actorControllers, '添加主演'), + const SizedBox(height: 16), + _buildTagList(_actorControllers), - const SizedBox(height: 24), + const SizedBox(height: 40), // 类型 _buildSectionTitle('类型'), - const SizedBox(height: 8), - _buildTagList(_genreControllers, '添加类型'), + const SizedBox(height: 16), + _buildTagList(_genreControllers), - const SizedBox(height: 24), + const SizedBox(height: 40), // 剧情简介 - _buildSectionTitle('剧情简介'), - const SizedBox(height: 12), + _buildSectionTitle('简介'), + const SizedBox(height: 16), _buildTextField( controller: _summaryController, label: '', - hint: '请输入剧情简介...', - maxLines: 5, + hint: '剧情简介...', + maxLines: 6, ), - const SizedBox(height: 40), + const SizedBox(height: 48), ], ), ), @@ -240,73 +217,59 @@ class _MovieFormPageState extends State { ); } - /// 构建海报上传区域 + /// 海报区域 - 极简 Widget _buildPosterSection() { - final colorScheme = Theme.of(context).colorScheme; - return Center( child: GestureDetector( onTap: _pickImage, child: Container( - width: 140, - height: 200, + width: 120, + height: 170, decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surface, border: Border.all( - color: colorScheme.outline.withOpacity(0.3), - width: 1, + color: const Color(0xFFE5E5E5), + width: 0.5, ), ), child: _posterPath != null && _posterPath!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file( - File(_posterPath!), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => _buildPlaceholder(colorScheme), - ), + ? Image.file( + File(_posterPath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildPlaceholder(), ) - : _buildPlaceholder(colorScheme), + : _buildPlaceholder(), ), ), ); } - Widget _buildPlaceholder(ColorScheme colorScheme) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.add_photo_alternate_outlined, - size: 40, - color: colorScheme.onSurfaceVariant.withOpacity(0.5), + Widget _buildPlaceholder() { + return const Center( + child: Text( + '添加海报', + style: TextStyle( + fontSize: 13, + color: Color(0xFF999999), ), - const SizedBox(height: 8), - Text( - '上传海报', - style: TextStyle( - fontSize: 13, - color: colorScheme.onSurfaceVariant.withOpacity(0.6), - ), - ), - ], + ), ); } - /// 构建区块标题 + /// 区块标题 - 大写字母,小字号 Widget _buildSectionTitle(String title) { return Text( - title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + title.toUpperCase(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Color(0xFF999999), + letterSpacing: 1, ), ); } - /// 构建文本输入框 + /// 文本输入框 - 极简无边框 Widget _buildTextField({ required TextEditingController controller, required String label, @@ -315,97 +278,68 @@ class _MovieFormPageState extends State { String? Function(String?)? validator, int maxLines = 1, }) { - final colorScheme = Theme.of(context).colorScheme; - return TextFormField( controller: controller, keyboardType: keyboardType, maxLines: maxLines, validator: validator, - style: TextStyle( - fontSize: 15, - color: colorScheme.onSurface, + style: const TextStyle( + fontSize: 16, + color: Color(0xFF1A1A1A), ), decoration: InputDecoration( labelText: label.isEmpty ? null : label, hintText: hint, - hintStyle: TextStyle( - fontSize: 14, - color: colorScheme.onSurfaceVariant.withOpacity(0.5), + hintStyle: const TextStyle( + fontSize: 15, + color: Color(0xFFCCCCCC), ), - labelStyle: TextStyle( - fontSize: 14, - color: colorScheme.onSurfaceVariant, + border: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), ), - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.3), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: colorScheme.outline.withOpacity(0.2), - width: 1, - ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFF1A1A1A), width: 1), ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: colorScheme.primary.withOpacity(0.5), - width: 1.5, - ), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric(vertical: 12), ), ); } - /// 构建日期选择器 + /// 日期选择器 Widget _buildDatePicker() { - final colorScheme = Theme.of(context).colorScheme; - return GestureDetector( onTap: _selectReleaseDate, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.3), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: colorScheme.outline.withOpacity(0.2), - width: 1, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), ), ), child: Row( children: [ - Icon( - Icons.calendar_today_outlined, - size: 18, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - _releaseDate != null - ? '${_releaseDate!.year}-${_releaseDate!.month.toString().padLeft(2, '0')}-${_releaseDate!.day.toString().padLeft(2, '0')}' - : '上映日期', - style: TextStyle( - fontSize: 15, - color: _releaseDate != null - ? colorScheme.onSurface - : colorScheme.onSurfaceVariant.withOpacity(0.5), - ), + Text( + _releaseDate != null + ? '${_releaseDate!.year}.${_releaseDate!.month.toString().padLeft(2, '0')}.${_releaseDate!.day.toString().padLeft(2, '0')}' + : '上映日期', + style: TextStyle( + fontSize: 16, + color: _releaseDate != null + ? const Color(0xFF1A1A1A) + : const Color(0xFFCCCCCC), ), ), + const Spacer(), if (_releaseDate != null) GestureDetector( onTap: () => setState(() => _releaseDate = null), - child: Icon( + child: const Icon( Icons.close, - size: 18, - color: colorScheme.onSurfaceVariant, + size: 16, + color: Color(0xFF999999), ), ), ], @@ -414,54 +348,43 @@ class _MovieFormPageState extends State { ); } - /// 构建状态选择器 + /// 状态选择器 - 极简分段 Widget _buildStatusSelector() { - final colorScheme = Theme.of(context).colorScheme; final statuses = [ - {'value': 'watching', 'label': '在看', 'icon': Icons.play_circle_outline}, - {'value': 'watched', 'label': '已看', 'icon': Icons.check_circle_outline}, - {'value': 'want_to_watch', 'label': '想看', 'icon': Icons.bookmark_border}, + {'value': 'watching', 'label': '在看'}, + {'value': 'watched', 'label': '已看'}, + {'value': 'want_to_watch', 'label': '想看'}, ]; return Container( decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.3), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: colorScheme.outline.withOpacity(0.2), - width: 1, - ), + border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), ), child: Row( - children: statuses.map((status) { - final isSelected = _status == status['value']; + children: statuses.asMap().entries.map((entry) { + final isSelected = _status == entry.value['value']; + final isLast = entry.key == statuses.length - 1; + return Expanded( child: GestureDetector( - onTap: () => setState(() => _status = status['value'] as String), + onTap: () => setState(() => _status = entry.value['value'] as String), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent, - borderRadius: BorderRadius.circular(8), + color: isSelected ? const Color(0xFF1A1A1A) : Colors.transparent, + border: !isLast + ? const Border( + right: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ) + : null, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - status['icon'] as IconData, - size: 18, - color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 6), - Text( - status['label'] as String, - style: TextStyle( - fontSize: 14, - color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ], + child: Text( + entry.value['label'] as String, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: isSelected ? Colors.white : const Color(0xFF666666), + ), ), ), ), @@ -471,70 +394,55 @@ class _MovieFormPageState extends State { ); } - /// 构建标签列表(导演、编剧、演员等) - Widget _buildTagList(List controllers, String addHint) { - final colorScheme = Theme.of(context).colorScheme; - + /// 标签列表 - 极简输入 + Widget _buildTagList(List controllers) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ...controllers.asMap().entries.map((entry) { final index = entry.key; final controller = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: 8), + return Container( + padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Expanded( child: TextField( controller: controller, - style: TextStyle( - fontSize: 14, - color: colorScheme.onSurface, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), ), - decoration: InputDecoration( - hintText: addHint, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 8), + hintText: '输入...', hintStyle: TextStyle( - fontSize: 13, - color: colorScheme.onSurfaceVariant.withOpacity(0.4), + fontSize: 15, + color: Color(0xFFCCCCCC), ), - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.3), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide( - color: colorScheme.outline.withOpacity(0.15), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide( - color: colorScheme.primary.withOpacity(0.4), - width: 1, - ), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), ), ), if (controllers.length > 1) - IconButton( - icon: Icon(Icons.remove_circle_outline, - color: colorScheme.error.withOpacity(0.6), - size: 20 - ), - onPressed: () { + GestureDetector( + onTap: () { setState(() { controller.dispose(); controllers.removeAt(index); }); }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + child: const Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + '删除', + style: TextStyle( + fontSize: 13, + color: Color(0xFF999999), + ), + ), + ), ), ], ), @@ -543,37 +451,15 @@ class _MovieFormPageState extends State { // 添加按钮 GestureDetector( - onTap: () { - setState(() { - controllers.add(TextEditingController()); - }); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - border: Border.all( - color: colorScheme.outline.withOpacity(0.2), - width: 1, + onTap: () => setState(() => controllers.add(TextEditingController())), + child: const Padding( + padding: EdgeInsets.only(top: 4), + child: Text( + '+ 添加', + style: TextStyle( + fontSize: 14, + color: Color(0xFF666666), ), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.add, - size: 18, - color: colorScheme.primary, - ), - const SizedBox(width: 6), - Text( - addHint, - style: TextStyle( - fontSize: 13, - color: colorScheme.primary, - ), - ), - ], ), ), ), @@ -592,24 +478,18 @@ class _MovieFormPageState extends State { ); if (pickedFile != null) { - // 复制图片到应用目录 final appDir = await getApplicationDocumentsDirectory(); - final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; final savedPath = path.join(appDir.path, 'posters', fileName); - // 创建目录 final posterDir = Directory(path.join(appDir.path, 'posters')); if (!await posterDir.exists()) { await posterDir.create(recursive: true); } - // 复制文件 - final sourceFile = File(pickedFile.path); - await sourceFile.copy(savedPath); + await File(pickedFile.path).copy(savedPath); - setState(() { - _posterPath = savedPath; - }); + setState(() => _posterPath = savedPath); } } catch (e) { if (mounted) { @@ -620,7 +500,7 @@ class _MovieFormPageState extends State { } } - /// 选择上映日期 + /// 选择日期 Future _selectReleaseDate() async { final picked = await showDatePicker( context: context, @@ -630,8 +510,8 @@ class _MovieFormPageState extends State { builder: (context, child) { return Theme( data: Theme.of(context).copyWith( - colorScheme: Theme.of(context).colorScheme.copyWith( - primary: Theme.of(context).colorScheme.primary, + colorScheme: const ColorScheme.light( + primary: Color(0xFF1A1A1A), ), ), child: child!, @@ -640,17 +520,13 @@ class _MovieFormPageState extends State { ); if (picked != null) { - setState(() { - _releaseDate = picked; - }); + setState(() => _releaseDate = picked); } } - /// 保存影视记录 + /// 保存 Future _saveMovie() async { - if (!_formKey.currentState!.validate()) { - return; - } + if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); @@ -659,7 +535,6 @@ class _MovieFormPageState extends State { ? double.tryParse(_ratingController.text) : null; - // 收集列表数据 final directors = _collectNonEmptyTexts(_directorControllers); final writers = _collectNonEmptyTexts(_writerControllers); final actors = _collectNonEmptyTexts(_actorControllers); @@ -669,7 +544,6 @@ class _MovieFormPageState extends State { final now = DateTime.now(); if (widget.movie == null) { - // 添加新模式 final newMovie = Movie( id: now.millisecondsSinceEpoch.toString(), title: _titleController.text.trim(), @@ -691,7 +565,6 @@ class _MovieFormPageState extends State { await context.read().addMovie(newMovie); } else { - // 编辑模式 final updatedMovie = widget.movie!.copyWith( title: _titleController.text.trim(), posterPath: _posterPath, @@ -713,34 +586,18 @@ class _MovieFormPageState extends State { } if (!mounted) return; - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(widget.movie == null ? '添加成功' : '更新成功'), - behavior: SnackBarBehavior.floating, - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - Navigator.pop(context); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('保存失败: $e'), - behavior: SnackBarBehavior.floating, - backgroundColor: Theme.of(context).colorScheme.error, - ), + SnackBar(content: Text('保存失败: $e')), ); } } finally { - if (mounted) { - setState(() => _isLoading = false); - } + if (mounted) setState(() => _isLoading = false); } } - /// 收集非空文本 List _collectNonEmptyTexts(List controllers) { return controllers .map((c) => c.text.trim()) diff --git a/lib/pages/movie_tab_page.dart b/lib/pages/movie_tab_page.dart index d82ec5a..eeb275b 100644 --- a/lib/pages/movie_tab_page.dart +++ b/lib/pages/movie_tab_page.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; -import '../models/data_models.dart'; import '../widgets/movie_status_bar.dart'; import '../widgets/movie_list_item.dart'; -/// 观影标签页 +/// 观影标签页 - 极简主义设计 class MovieTabPage extends StatelessWidget { const MovieTabPage({super.key}); @@ -13,9 +12,11 @@ class MovieTabPage extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - // 状态选择栏(已看、想看、在看) + // 状态选择栏 const MovieStatusBar(), + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + // 影片列表 Expanded( child: _buildMovieList(context), @@ -28,7 +29,6 @@ class MovieTabPage extends StatelessWidget { Widget _buildMovieList(BuildContext context) { return Consumer( builder: (context, provider, child) { - // 根据状态筛选影片 final statusMap = { 0: 'watched', 1: 'want_to_watch', @@ -43,8 +43,10 @@ class MovieTabPage extends StatelessWidget { return RefreshIndicator( onRefresh: () async => await provider.loadMovies(), + color: const Color(0xFF1A1A1A), + backgroundColor: Colors.white, child: ListView.builder( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.zero, itemCount: movies.length, itemBuilder: (context, index) { return MovieListItem(movie: movies[index]); @@ -55,7 +57,7 @@ class MovieTabPage extends StatelessWidget { ); } - /// 构建空状态提示 + /// 构建空状态 Widget _buildEmptyState(BuildContext context, int statusIndex) { final statusText = ['已看', '想看', '在看'][statusIndex]; @@ -63,25 +65,23 @@ class MovieTabPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.movie_creation_outlined, - size: 80, - color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3), + const Icon( + Icons.movie_outlined, + size: 48, + color: Color(0xFFCCCCCC), ), const SizedBox(height: 16), Text( '暂无$statusText的影片', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF999999), ), ), - const SizedBox(height: 8), - ElevatedButton.icon( - icon: const Icon(Icons.add), - label: const Text('添加记录'), - onPressed: () { - Navigator.pushNamed(context, '/movie-form'); - }, + const SizedBox(height: 24), + TextButton( + onPressed: () => Navigator.pushNamed(context, '/movie-form'), + child: const Text('添加记录'), ), ], ), diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart new file mode 100644 index 0000000..036ee17 --- /dev/null +++ b/lib/pages/profile_page.dart @@ -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 createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + final ImagePicker _picker = ImagePicker(); + final UserPrefs _userPrefs = UserPrefs(); + + // 用户数据 + String _nickname = '记录者'; + String _motto = '记录生活,沉淀思考'; + String? _avatarPath; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + /// 加载用户数据 + Future _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( + 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 _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('设置功能开发中'); + } +} diff --git a/lib/utils/app_theme.dart b/lib/utils/app_theme.dart index 70eeba5..ad4ca3e 100644 --- a/lib/utils/app_theme.dart +++ b/lib/utils/app_theme.dart @@ -1,66 +1,341 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +/// 极简主义主题配置 class AppTheme { - // 主色调 - static const Color primaryColor = Color(0xFF6200EE); - static const Color secondaryColor = Color(0xFF03DAC6); + // 中性色板 - 黑白灰为主 + static const Color _black = Color(0xFF1A1A1A); + 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 wantToWatchColor = Color(0xFFFF9800); // 想看 - 橙色 - static const Color watchingColor = Color(0xFF2196F3); // 在看 - 蓝色 + // 强调色 - 仅用于关键操作 + static const Color accent = Color(0xFF0066FF); + static const Color error = Color(0xFFDC2626); - static const Color readColor = Color(0xFF4CAF50); // 读完 - 绿色 - static const Color wantToReadColor = Color(0xFFFF9800); // 准备读 - 橙色 - static const Color readingColor = Color(0xFF2196F3); // 在读 - 蓝色 + // 观影状态颜色 - 极简处理 + static const Color watched = Color(0xFF1A1A1A); // 已看 - 纯黑 + static const Color wantToWatch = Color(0xFF999999); // 想看 - 浅灰 + static const Color watching = Color(0xFF666666); // 在看 - 中灰 + + // 阅读状态颜色 - 极简处理 + static const Color readColor = Color(0xFF1A1A1A); // 已读 - 纯黑 + static const Color wantToReadColor = Color(0xFF999999); // 想读 - 浅灰 + static const Color readingColor = Color(0xFF666666); // 在读 - 中灰 - // 亮色主题 - static final ThemeData lightTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.light, - 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, - ), - appBarTheme: const AppBarTheme( - centerTitle: false, - elevation: 0, - ), - cardTheme: CardThemeData( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + scaffoldBackgroundColor: _white, + + // 颜色方案 + colorScheme: const ColorScheme.light( + primary: _black, + onPrimary: _white, + secondary: _darkGray, + onSecondary: _white, + surface: _white, + onSurface: _black, + error: error, + onError: _white, ), - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - elevation: 8, - selectedItemColor: primaryColor, - unselectedItemColor: Colors.grey, - ), - ); + + // AppBar - 极简无边框 + appBarTheme: const AppBarTheme( + backgroundColor: _white, + 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( - useMaterial3: true, - brightness: Brightness.dark, - colorScheme: ColorScheme.fromSeed( - seedColor: primaryColor, + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, brightness: Brightness.dark, - ), - appBarTheme: const AppBarTheme( - centerTitle: false, - elevation: 0, - ), - cardTheme: CardThemeData( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + scaffoldBackgroundColor: _black, + + colorScheme: const ColorScheme.dark( + primary: _white, + onPrimary: _black, + secondary: _offWhite, + onSecondary: _black, + surface: _black, + onSurface: _white, + error: Color(0xFFEF4444), + onError: _black, ), - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - elevation: 8, - selectedItemColor: secondaryColor, - unselectedItemColor: Colors.grey, - ), - ); + + appBarTheme: const AppBarTheme( + backgroundColor: _black, + foregroundColor: _white, + 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, + ), + ); + } } diff --git a/lib/utils/user_prefs.dart b/lib/utils/user_prefs.dart new file mode 100644 index 0000000..3d4488c --- /dev/null +++ b/lib/utils/user_prefs.dart @@ -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 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 setNickname(String value) => prefs.setString('nickname', value); + + /// 座右铭 + String get motto => prefs.getString('motto') ?? '记录生活,沉淀思考'; + Future setMotto(String value) => prefs.setString('motto', value); + + /// 头像路径 + String? get avatarPath => prefs.getString('avatarPath'); + Future setAvatarPath(String value) => prefs.setString('avatarPath', value); + Future clearAvatarPath() => prefs.remove('avatarPath'); + + // ========== 应用设置 ========== + + /// 是否暗黑模式 + bool get isDarkMode => prefs.getBool('isDarkMode') ?? false; + Future setDarkMode(bool value) => prefs.setBool('isDarkMode', value); + + /// 是否首次启动 + bool get isFirstLaunch => prefs.getBool('isFirstLaunch') ?? true; + Future setFirstLaunch(bool value) => prefs.setBool('isFirstLaunch', value); +} diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index 7f9dc38..2e056fc 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -17,12 +17,8 @@ class CustomBottomNavBar extends StatelessWidget { // 新增按钮 - 显示选择对话框 _showAddDialog(context, provider); } else { + // 切换主页/我的页面 provider.setBottomNavIndex(index); - if (index == 0) { - // 主页 - 重置到首页 - provider.setMainTabIndex(0); - } - // index == 2 是我的页面(待实现) } }, type: BottomNavigationBarType.fixed, diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index cc0e4b7..aee8cd3 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -1,51 +1,55 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; +import '../utils/user_prefs.dart'; -/// 自定义左侧弹出菜单 +/// 自定义左侧弹出菜单 - 极简主义设计 class CustomDrawer extends StatelessWidget { const CustomDrawer({super.key}); @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Drawer( + backgroundColor: Colors.white, child: Column( children: [ - // 顶部用户信息区域(含热力图) + // 顶部用户信息区域 _buildHeader(context), - // 分割线 - Divider(height: 1, color: colorScheme.outlineVariant), + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), // 菜单项列表 Expanded( child: ListView( padding: EdgeInsets.zero, children: [ - ListTile( - leading: const Icon(Icons.analytics), - title: const Text('统计'), + _buildMenuItem( + icon: Icons.analytics_outlined, + title: '统计', onTap: () { Navigator.pop(context); - // TODO: 跳转到统计页面 + _showToast(context, '统计功能开发中'); }, ), - ListTile( - leading: const Icon(Icons.delete_outline), - title: const Text('回收站'), + const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)), + + _buildMenuItem( + icon: Icons.delete_outline, + title: '回收站', onTap: () { Navigator.pop(context); - // TODO: 跳转到回收站页面 + _showToast(context, '回收站功能开发中'); }, ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text('设置'), + const Divider(height: 0.5, thickness: 0.5, indent: 56, color: Color(0xFFE5E5E5)), + + _buildMenuItem( + icon: Icons.settings_outlined, + title: '设置', onTap: () { Navigator.pop(context); - // TODO: 跳转到设置页面 + _showToast(context, '设置功能开发中'); }, ), ], @@ -54,12 +58,12 @@ class CustomDrawer extends StatelessWidget { // 底部版本信息 Container( - padding: const EdgeInsets.all(16), - child: Text( + padding: const EdgeInsets.all(24), + child: const Text( 'MookNote v1.0.0', style: TextStyle( fontSize: 12, - color: colorScheme.onSurfaceVariant, + color: Color(0xFF999999), ), ), ), @@ -68,146 +72,157 @@ class CustomDrawer extends StatelessWidget { ); } - /// 构建头部(含热力图) + /// 构建头部 Widget _buildHeader(BuildContext context) { return Consumer( 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( width: double.infinity, - padding: const EdgeInsets.fromLTRB(16, 48, 16, 16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).colorScheme.primaryContainer, - Theme.of(context).colorScheme.surface, - ], - ), - ), + padding: const EdgeInsets.fromLTRB(24, 48, 24, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 用户头像和名称 - Row( - children: [ - CircleAvatar( - radius: 32, - backgroundColor: Theme.of(context).colorScheme.primary, - child: const Icon( - Icons.person, - color: Colors.white, - size: 32, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '用户', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + // 用户头像 + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), + ), + child: avatarPath != null && avatarPath.isNotEmpty + ? ClipOval( + child: Image.file( + File(avatarPath), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildAvatarPlaceholder(), ), - ), - Text( - '记录生活点滴', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ], + ) + : _buildAvatarPlaceholder(), ), - 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), + ], + ), ], ), ); }, ); } + + Widget _buildAvatarPlaceholder() { + return const Center( + child: Icon( + Icons.person_outline, + size: 32, + color: Color(0xFF999999), + ), + ); + } - /// 构建热力图 - Widget _buildHeatmap(BuildContext context) { - const int weeks = 52; // 一年 52 周 - const int daysPerWeek = 7; - - // 生成随机数据(实际应从数据库获取) - final random = DateTime.now().millisecondsSinceEpoch; - + /// 统计项 + Widget _buildStatItem(String label, int count) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '年度记录', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, + '$count', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), ), ), - const SizedBox(height: 8), - SizedBox( - height: daysPerWeek * 14, // 每个格子 14x14 - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(weeks, (weekIndex) { - 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: 2), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), ), ), - 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) { - if (intensity == 0) { - return Theme.of(context).colorScheme.surfaceContainerHighest; - } else if (intensity < 0.25) { - return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.4); - } else if (intensity < 0.5) { - return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6); - } else if (intensity < 0.75) { - return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8); - } else { - return Theme.of(context).colorScheme.primary; - } + /// 菜单项 + 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(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + ), + ); } } diff --git a/lib/widgets/movie_list_item.dart b/lib/widgets/movie_list_item.dart index 257eefe..431e36d 100644 --- a/lib/widgets/movie_list_item.dart +++ b/lib/widgets/movie_list_item.dart @@ -1,11 +1,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../providers/app_provider.dart'; import '../models/data_models.dart'; -import '../utils/app_theme.dart'; -/// 观影列表项组件 +/// 观影列表项 - 极简主义设计 class MovieListItem extends StatelessWidget { final Movie movie; @@ -13,309 +10,181 @@ class MovieListItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: colorScheme.outline.withOpacity(0.1), - width: 1, + return InkWell( + onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), ), - ), - child: InkWell( - onTap: () { - Navigator.pushNamed(context, '/movie-detail', arguments: movie); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 海报 - _buildPoster(context), - - const SizedBox(width: 14), - - // 影片信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 海报 + _buildPoster(), + + const SizedBox(width: 16), + + // 信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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( - movie.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, + movie.directors.take(2).join(' / '), + style: const TextStyle( + fontSize: 13, + color: Color(0xFF999999), ), maxLines: 1, 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), - IconButton( - icon: Icon(Icons.delete_outline, size: 20, color: colorScheme.error.withOpacity(0.7)), - onPressed: () => _showDeleteDialog(context, movie), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), + + // 状态 + _buildStatusTag(), ], ), - ], - ), + ), + + // 箭头 + const Icon( + Icons.chevron_right, + size: 20, + color: Color(0xFFCCCCCC), + ), + ], ), ), ); } - /// 构建海报 - Widget _buildPoster(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - width: 70, - height: 100, - color: colorScheme.surfaceContainerHighest, - child: movie.posterPath != null && movie.posterPath!.isNotEmpty - ? Image.file( - File(movie.posterPath!), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => _buildPlaceholder(context), - ) - : _buildPlaceholder(context), + /// 海报 + Widget _buildPoster() { + return Container( + width: 56, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all( + color: const Color(0xFFE5E5E5), + width: 0.5, + ), ), + child: movie.posterPath != null && movie.posterPath!.isNotEmpty + ? Image.file( + File(movie.posterPath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildPlaceholder(), + ) + : _buildPlaceholder(), ); } - Widget _buildPlaceholder(BuildContext context) { - return Center( + Widget _buildPlaceholder() { + return const Center( child: Icon( Icons.movie_outlined, - color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3), - size: 28, + size: 20, + color: Color(0xFFCCCCCC), ), ); } - /// 构建信息行 - Widget _buildInfoRow(BuildContext context, {required String prefix, required List items}) { - final colorScheme = Theme.of(context).colorScheme; + /// 元信息 + Widget _buildMetaInfo() { + final items = []; + + 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( - children: [ - Text( - '$prefix: ', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant.withOpacity(0.6), - ), - ), - Expanded( - child: Text( - items.join(' / '), - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant.withOpacity(0.8), + children: items.asMap().entries.map((entry) { + return Row( + children: [ + Text( + entry.value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF999999), + ), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + if (entry.key < items.length - 1) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text( + '·', + style: TextStyle( + fontSize: 13, + color: Color(0xFFCCCCCC), + ), + ), + ), + ], + ); + }).toList(), ); } - /// 构建状态标签 - Widget _buildStatusTag(BuildContext context) { - Color statusColor; - String statusText; + /// 状态标签 + Widget _buildStatusTag() { + String label; + Color color; switch (movie.status) { case 'watched': - statusColor = AppTheme.watchedColor; - statusText = '已看'; - break; - case 'want_to_watch': - statusColor = AppTheme.wantToWatchColor; - statusText = '想看'; + label = '已看'; + color = const Color(0xFF1A1A1A); break; case 'watching': - statusColor = AppTheme.watchingColor; - statusText = '在看'; + label = '在看'; + color = const Color(0xFF666666); + break; + case 'want_to_watch': + label = '想看'; + color = const Color(0xFF999999); break; default: - statusColor = Colors.grey; - statusText = '未知'; + label = '未知'; + color = const Color(0xFFCCCCCC); } - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all( - 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().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), - ), - ), - ], + return Text( + label, + style: TextStyle( + fontSize: 12, + color: color, + fontWeight: FontWeight.w500, ), ); } diff --git a/lib/widgets/movie_status_bar.dart b/lib/widgets/movie_status_bar.dart index 499bec7..40d4f72 100644 --- a/lib/widgets/movie_status_bar.dart +++ b/lib/widgets/movie_status_bar.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; -import '../utils/app_theme.dart'; -/// 观影状态选择栏 +/// 观影状态选择栏 - 极简主义设计 class MovieStatusBar extends StatelessWidget { const MovieStatusBar({super.key}); @@ -12,44 +11,26 @@ class MovieStatusBar extends StatelessWidget { return Consumer( builder: (context, provider, child) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + color: Colors.white, child: Row( children: [ _buildStatusItem( - context, '已看', - AppTheme.watchedColor, - Icons.check_circle, 0, provider.movieStatusIndex, () => provider.setMovieStatusIndex(0), ), - const SizedBox(width: 12), + const SizedBox(width: 24), _buildStatusItem( - context, '想看', - AppTheme.wantToWatchColor, - Icons.bookmark_border, 1, provider.movieStatusIndex, () => provider.setMovieStatusIndex(1), ), - const SizedBox(width: 12), + const SizedBox(width: 24), _buildStatusItem( - context, '在看', - AppTheme.watchingColor, - Icons.play_circle_outline, 2, provider.movieStatusIndex, () => provider.setMovieStatusIndex(2), @@ -63,49 +44,21 @@ class MovieStatusBar extends StatelessWidget { /// 构建状态项 Widget _buildStatusItem( - BuildContext context, String label, - Color color, - IconData icon, int index, int currentIndex, VoidCallback onTap, ) { final isSelected = index == currentIndex; - return Expanded( - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: isSelected ? color.withOpacity(0.1) : Colors.transparent, - 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, - ), - ), - ], - ), + return GestureDetector( + onTap: onTap, + child: Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFF999999), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index e661ac1..704e117 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,6 +456,62 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 0fcf45d..ed5469c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: path: ^1.8.3 image_picker: ^1.0.4 path_provider: ^2.1.1 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: