diff --git a/lib/pages/book/book_form_page.dart b/lib/pages/book/book_form_page.dart index 6446df3..cfa58f4 100644 --- a/lib/pages/book/book_form_page.dart +++ b/lib/pages/book/book_form_page.dart @@ -14,6 +14,7 @@ import '../../utils/toast_util.dart'; import '../../utils/image_path_helper.dart'; import '../../widgets/genre_selector_page.dart'; import '../../widgets/text_input_panel.dart'; +import '../../widgets/alternate_titles_dialog.dart'; /// 从多值字段列表中提取去重排序的唯一值(供 compute 使用) List _collectUnique(List> lists) { @@ -158,18 +159,10 @@ class _BookFormPageState extends State { children: [ // 第一行:书名 + 别名 _halfCard('书名', _titleController.text, Icons.book_outlined, required: true, - onTap: () async { - final r = await TextInputPanel.show(context: context, title: '书名', initialValue: _titleController.text, hint: '请输入书名'); - if (!mounted) return; - if (r != null) setState(() => _titleController.text = r); - }, + onTap: () => _editTitle(), ), _halfCard('别名', _alternateTitles.isEmpty ? '' : '${_alternateTitles.length}个:${_alternateTitles.join('、')}', Icons.alternate_email_outlined, - onTap: () async { - final r = await GenreSelectorPage.show(context: context, title: '添加别名', existingTags: [], initialSelected: _alternateTitles, hint: '输入别名'); - if (!mounted) return; - if (r != null) setState(() => _alternateTitles = r); - }, + onTap: () => _editAlternateTitles(), ), // 第二行:作者 + 译者 @@ -571,6 +564,65 @@ class _BookFormPageState extends State { // ─── 数据操作 ─── + /// 编辑书名(弹窗) + Future _editTitle() async { + final controller = TextEditingController(text: _titleController.text); + final result = await showDialog( + context: context, + builder: (ctx) { + final colors = Theme.of(ctx).colorScheme; + return AlertDialog( + backgroundColor: colors.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('书名', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: TextField( + controller: controller, + autofocus: true, + style: TextStyle(fontSize: 15, color: colors.onSurface), + decoration: InputDecoration( + hintText: '请输入书名', + hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3)), + filled: true, + fillColor: colors.surfaceContainerHigh, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: colors.primary, width: 1)), + ), + onSubmitted: (v) => Navigator.pop(ctx, v), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, controller.text), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('确定'), + ), + ], + ); + }, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.dispose(); + }); + if (!mounted) return; + if (result != null) setState(() => _titleController.text = result.trim()); + } + + /// 编辑别名(弹窗 + 标签式输入) + Future _editAlternateTitles() async { + final result = await showDialog>( + context: context, + builder: (ctx) => AlternateTitlesDialog(initial: _alternateTitles), + ); + if (!mounted) return; + if (result != null) setState(() => _alternateTitles = result); + } + Future _selectPublishDate() async { final picked = await showDatePicker(context: context, initialDate: _publishDate ?? DateTime.now(), firstDate: DateTime(1900), lastDate: DateTime.now().add(const Duration(days: 365 * 5))); if (!mounted) return; diff --git a/lib/pages/epub_reader/epub_library_page.dart b/lib/pages/epub_reader/epub_library_page.dart index 29f0978..5b99d53 100644 --- a/lib/pages/epub_reader/epub_library_page.dart +++ b/lib/pages/epub_reader/epub_library_page.dart @@ -5,11 +5,9 @@ import '../../data/epub/reader_dao.dart'; import '../../services/epub/epub_service.dart'; import '../../utils/user_prefs.dart'; import '../../utils/toast_util.dart'; -import '../../utils/responsive.dart'; import '../../widgets/fade_in_local_image.dart'; import '../../widgets/shimmer_skeleton.dart'; import 'epub_detail_page.dart'; -import 'widgets/book_grid_item.dart'; /// EPUB 书架页面 class EpubLibraryPage extends StatefulWidget { @@ -28,7 +26,6 @@ class _EpubLibraryPageState extends State { bool _isSearching = false; final TextEditingController _searchCtrl = TextEditingController(); int _sortMode = UserPrefs().epubSortMode; - int _viewMode = UserPrefs().epubViewMode; // 0=列表 1=网格 @override void initState() { @@ -236,7 +233,7 @@ class _EpubLibraryPageState extends State { ), onChanged: (_) => _onSearchChanged(), ) - : Text('EPUB 阅读', + : Text('阅读', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)), leading: IconButton( icon: Icon(_isSearching ? Icons.close : Icons.arrow_back, size: 20), @@ -262,14 +259,14 @@ class _EpubLibraryPageState extends State { hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)), border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero), onChanged: (_) => _onSearchChanged()) - : Text('EPUB 阅读', + : Text('阅读', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)))), ..._buildActions(colors), ]), ), // 主体 Expanded(child: _isLoading && _books.isEmpty - ? const BookSkeletonGrid() + ? _buildSkeletonList(colors) : _books.isEmpty ? _buildEmpty(colors) : _filteredBooks.isEmpty @@ -283,14 +280,14 @@ class _EpubLibraryPageState extends State { ); } - /// 主体内容:书架(列表/网格) + /// 主体内容:书架列表 Widget _buildContent(ColorScheme colors) { return CustomScrollView( slivers: [ // 书架分隔标题 SliverToBoxAdapter(child: _buildSectionHeader(colors)), - // 书架列表/网格 - _viewMode == 0 ? _buildSliverListView(colors) : _buildSliverGrid(colors), + // 书架列表 + _buildSliverListView(colors), ], ); } @@ -312,15 +309,6 @@ class _EpubLibraryPageState extends State { List _buildActions(ColorScheme colors) { return [ - if (!_isSearching) - IconButton( - icon: Icon(_viewMode == 0 ? Icons.grid_view_outlined : Icons.view_list_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), - tooltip: _viewMode == 0 ? '网格视图' : '列表视图', - onPressed: () { - setState(() => _viewMode = _viewMode == 0 ? 1 : 0); - UserPrefs().setEpubViewMode(_viewMode); - }, - ), if (!_isSearching) IconButton( icon: Icon(Icons.search, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), @@ -355,7 +343,7 @@ class _EpubLibraryPageState extends State { size: 40, color: colors.onSurface.withValues(alpha: 0.25)), ), const SizedBox(height: 24), - Text('EPUB 阅读', + Text('阅读', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: colors.onSurface)), const SizedBox(height: 8), Text('点击右上角导入 .epub 文件', @@ -393,31 +381,36 @@ class _EpubLibraryPageState extends State { ); } - Widget _buildSliverGrid(ColorScheme colors) { - return SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 100), - sliver: SliverLayoutBuilder( - builder: (context, constraints) { - final crossAxisCount = - responsiveCrossAxisCount(constraints.crossAxisExtent, minItemWidth: 110); - return SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - childAspectRatio: 0.55, - crossAxisSpacing: 12, - mainAxisSpacing: 16, - ), - delegate: SliverChildBuilderDelegate( - (context, index) => BookGridItem( - book: _filteredBooks[index], - viewMode: ViewMode.relaxed, - onTap: () => _openBook(_filteredBooks[index]), - onLongPress: () => _deleteBook(_filteredBooks[index]), + /// 加载骨架列表 + Widget _buildSkeletonList(ColorScheme colors) { + return ListView.builder( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 100), + itemCount: 8, + itemBuilder: (_, __) => Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const ShimmerSkeleton(width: 48, height: 64, borderRadius: 6), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerSkeleton(width: double.infinity, height: 15), + SizedBox(height: 8), + ShimmerSkeleton(width: 120, height: 12), + SizedBox(height: 10), + ShimmerSkeleton(width: 60, height: 11), + ], ), - childCount: _filteredBooks.length, ), - ); - }, + ], + ), ), ); } diff --git a/lib/pages/explore/gallery_page.dart b/lib/pages/explore/gallery_page.dart index 362182c..a49456b 100644 --- a/lib/pages/explore/gallery_page.dart +++ b/lib/pages/explore/gallery_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import '../../data/gallery/gallery_dao.dart'; @@ -180,17 +182,11 @@ class _GalleryPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 图片 - AspectRatio( - aspectRatio: _aspectRatioFor(item.category), - child: FadeInLocalImage( - path: item.path, - fit: BoxFit.cover, - errorWidget: Container( - color: colors.surfaceContainerHighest, - child: Icon(Icons.broken_image_outlined, color: colors.onSurface.withValues(alpha: 0.3)), - ), - ), + // 图片(按真实比例显示) + _GalleryImageCard( + path: item.path, + fallbackAspectRatio: _aspectRatioFor(item.category), + colors: colors, ), const SizedBox(height: 8), // 类别标签 @@ -228,7 +224,7 @@ class _GalleryPageState extends State { ); } - /// 不同类别用不同宽高比,让瀑布流有错落感 + /// 不同类别用作加载占位的默认宽高比;图片真实尺寸读出后会被覆盖 double _aspectRatioFor(String category) { switch (category) { case 'movie_poster': @@ -265,3 +261,79 @@ class _GalleryPageState extends State { ); } } + +/// 读取本地图片真实宽高,按真实比例显示;未读出前用 fallback 占位。 +class _GalleryImageCard extends StatefulWidget { + final String path; + final double fallbackAspectRatio; + final ColorScheme colors; + + const _GalleryImageCard({ + required this.path, + required this.fallbackAspectRatio, + required this.colors, + }); + + @override + State<_GalleryImageCard> createState() => _GalleryImageCardState(); +} + +class _GalleryImageCardState extends State<_GalleryImageCard> { + double? _aspectRatio; + + @override + void initState() { + super.initState(); + _resolveAspectRatio(); + } + + @override + void didUpdateWidget(_GalleryImageCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.path != widget.path) { + _aspectRatio = null; + _resolveAspectRatio(); + } + } + + Future _resolveAspectRatio() async { + final path = widget.path; + try { + if (path.startsWith('http')) { + // 网络图片不解析尺寸,直接用占位比例 + return; + } + final file = File(path); + if (!await file.exists()) return; + final bytes = await file.readAsBytes(); + if (!mounted) return; + final data = Uint8List.fromList(bytes); + final decoded = await decodeImageFromList(data); + if (!mounted) return; + if (decoded.width > 0 && decoded.height > 0) { + setState(() => _aspectRatio = decoded.width / decoded.height); + } + } catch (_) { + // 解析失败则保留占位比例 + } + } + + @override + Widget build(BuildContext context) { + final ratio = _aspectRatio ?? widget.fallbackAspectRatio; + return AspectRatio( + aspectRatio: ratio, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: FadeInLocalImage( + path: widget.path, + fit: BoxFit.cover, + errorWidget: Container( + color: widget.colors.surfaceContainerHighest, + child: Icon(Icons.broken_image_outlined, color: widget.colors.onSurface.withValues(alpha: 0.3)), + ), + ), + ), + ); + } +} diff --git a/lib/pages/game/game_form_page.dart b/lib/pages/game/game_form_page.dart index eace13a..136e8f3 100644 --- a/lib/pages/game/game_form_page.dart +++ b/lib/pages/game/game_form_page.dart @@ -185,16 +185,7 @@ class _GameFormPageState extends State { value: _titleController.text, required: true, icon: Icons.sports_esports_outlined, - onTap: () async { - final result = await TextInputPanel.show( - context: context, - title: '游戏名称', - initialValue: _titleController.text, - hint: '请输入游戏名称', - ); - if (!mounted) return; - if (result != null) setState(() => _titleController.text = result); - }, + onTap: _editTitle, ), ), // 平台 @@ -1007,6 +998,55 @@ class _GameFormPageState extends State { } } + /// 编辑游戏名称(弹窗) + Future _editTitle() async { + final controller = TextEditingController(text: _titleController.text); + final result = await showDialog( + context: context, + builder: (ctx) { + final colors = Theme.of(ctx).colorScheme; + return AlertDialog( + backgroundColor: colors.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('游戏名称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: TextField( + controller: controller, + autofocus: true, + style: TextStyle(fontSize: 15, color: colors.onSurface), + decoration: InputDecoration( + hintText: '请输入游戏名称', + hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3)), + filled: true, + fillColor: colors.surfaceContainerHigh, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: colors.primary, width: 1)), + ), + onSubmitted: (v) => Navigator.pop(ctx, v), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, controller.text), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('确定'), + ), + ], + ); + }, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.dispose(); + }); + if (!mounted) return; + if (result != null) setState(() => _titleController.text = result.trim()); + } + Future _editPlayCount() async { final controller = TextEditingController(text: _playCount > 0 ? '$_playCount' : ''); final result = await showDialog( diff --git a/lib/pages/movies/movie_form_page.dart b/lib/pages/movies/movie_form_page.dart index a086438..fff6b1b 100644 --- a/lib/pages/movies/movie_form_page.dart +++ b/lib/pages/movies/movie_form_page.dart @@ -15,6 +15,7 @@ import '../../utils/image_path_helper.dart'; import '../../widgets/genre_selector_page.dart'; import '../../widgets/text_input_panel.dart'; import '../../widgets/duration_picker.dart'; +import '../../widgets/alternate_titles_dialog.dart'; /// 从多值字段列表中提取去重排序的唯一值(供 compute 使用) List _collectUnique(List> lists) { @@ -436,16 +437,7 @@ class _MovieFormPageState extends State { value: _titleController.text, required: true, icon: Icons.movie_outlined, - onTap: () async { - final result = await TextInputPanel.show( - context: context, - title: '影视名称', - initialValue: _titleController.text, - hint: '请输入影视名称', - ); - if (!mounted) return; - if (result != null) setState(() => _titleController.text = result); - }, + onTap: () => _editTitle(), ), ), SizedBox( @@ -458,17 +450,7 @@ class _MovieFormPageState extends State { : '${_alternateTitles.length}个:${_alternateTitles.join('、')}', icon: Icons.alternate_email_outlined, scrollable: true, - onTap: () async { - final result = await GenreSelectorPage.show( - context: context, - title: '添加别名', - existingTags: [], - initialSelected: _alternateTitles, - hint: '输入别名', - ); - if (!mounted) return; - if (result != null) setState(() => _alternateTitles = result); - }, + onTap: () => _editAlternateTitles(), ), ), @@ -763,6 +745,65 @@ class _MovieFormPageState extends State { ); } + /// 编辑名称(弹窗) + Future _editTitle() async { + final controller = TextEditingController(text: _titleController.text); + final result = await showDialog( + context: context, + builder: (ctx) { + final colors = Theme.of(ctx).colorScheme; + return AlertDialog( + backgroundColor: colors.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('影视名称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: TextField( + controller: controller, + autofocus: true, + style: TextStyle(fontSize: 15, color: colors.onSurface), + decoration: InputDecoration( + hintText: '请输入影视名称', + hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3)), + filled: true, + fillColor: colors.surfaceContainerHigh, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: colors.primary, width: 1)), + ), + onSubmitted: (v) => Navigator.pop(ctx, v), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, controller.text), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('确定'), + ), + ], + ); + }, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.dispose(); + }); + if (!mounted) return; + if (result != null) setState(() => _titleController.text = result.trim()); + } + + /// 编辑别名(弹窗 + 标签式输入) + Future _editAlternateTitles() async { + final result = await showDialog>( + context: context, + builder: (ctx) => AlternateTitlesDialog(initial: _alternateTitles), + ); + if (!mounted) return; + if (result != null) setState(() => _alternateTitles = result); + } + /// 编辑观看次数 Future _editWatchCount() async { final controller = TextEditingController(text: _watchCount > 0 ? '$_watchCount' : ''); diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 6c9a83b..d5b7098 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -1457,6 +1457,11 @@ class AppProvider extends ChangeNotifier { final trimmed = name.trim(); if (trimmed.isEmpty || !seen.add(trimmed)) continue; final person = await findOrCreate(trimmed, occupationLabel); + // 演员角色:若该影视已存在该人物的配音关联,则跳过添加演员关联 + if (roleType == 'actor' && + await _moviePersonDao.existsRelation(movie.id, person.id, 'voiceActor')) { + continue; + } if (!await _moviePersonDao.existsRelation(movie.id, person.id, roleType)) { await _moviePersonDao.insert(MoviePerson( id: uuid.v4(), diff --git a/lib/utils/user_prefs.dart b/lib/utils/user_prefs.dart index dbc1d6a..230d95e 100644 --- a/lib/utils/user_prefs.dart +++ b/lib/utils/user_prefs.dart @@ -359,10 +359,6 @@ class UserPrefs { double get epubFontSize => prefs.getDouble('epubFontSize') ?? 18.0; Future setEpubFontSize(double value) => prefs.setDouble('epubFontSize', value); - /// EPUB 书架视图模式: 0=宽松, 1=紧凑 - int get epubViewMode => prefs.getInt('epubViewMode') ?? 0; - Future setEpubViewMode(int value) => prefs.setInt('epubViewMode', value); - /// EPUB 书架排序模式: 0=更新时间, 1=创建时间, 2=阅读进度, 3=书名 int get epubSortMode => prefs.getInt('epubSortMode') ?? 0; Future setEpubSortMode(int value) => prefs.setInt('epubSortMode', value); diff --git a/lib/widgets/alternate_titles_dialog.dart b/lib/widgets/alternate_titles_dialog.dart new file mode 100644 index 0000000..4582946 --- /dev/null +++ b/lib/widgets/alternate_titles_dialog.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +/// 别名标签式输入弹窗(影视/书籍共用) +class AlternateTitlesDialog extends StatefulWidget { + final List initial; + const AlternateTitlesDialog({super.key, required this.initial}); + + @override + State createState() => _AlternateTitlesDialogState(); +} + +class _AlternateTitlesDialogState extends State { + late final TextEditingController _controller; + late List _items; + final _focus = FocusNode(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _items = List.from(widget.initial); + } + + @override + void dispose() { + _controller.dispose(); + _focus.dispose(); + super.dispose(); + } + + void _add() { + final v = _controller.text.trim(); + if (v.isEmpty) return; + if (!_items.contains(v)) { + setState(() => _items.add(v)); + } + _controller.clear(); + _focus.requestFocus(); + } + + void _remove(int i) => setState(() => _items.removeAt(i)); + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return AlertDialog( + backgroundColor: colors.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Text('添加别名', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 输入框 + 添加按钮 + Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + focusNode: _focus, + style: TextStyle(fontSize: 14, color: colors.onSurface), + decoration: InputDecoration( + hintText: '输入别名', + hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3)), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + filled: true, + fillColor: colors.surfaceContainerHigh, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)), + ), + onSubmitted: (_) => _add(), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: _add, + icon: Icon(Icons.add_circle, color: colors.primary, size: 28), + ), + ], + ), + const SizedBox(height: 12), + // 标签列表 + if (_items.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text('暂无别名', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3))), + ) + else + Flexible( + child: SingleChildScrollView( + child: Wrap( + spacing: 6, + runSpacing: 6, + children: List.generate(_items.length, (i) { + return Chip( + label: Text(_items[i], style: TextStyle(fontSize: 13, color: colors.onSurface)), + backgroundColor: colors.surfaceContainerHighest, + side: BorderSide.none, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + deleteIcon: Icon(Icons.close, size: 16, color: colors.onSurface.withValues(alpha: 0.4)), + onDeleted: () => _remove(i), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ); + }), + ), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, _items), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('确定'), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/custom_drawer.dart b/lib/widgets/custom_drawer.dart index bbc61c0..46f0893 100644 --- a/lib/widgets/custom_drawer.dart +++ b/lib/widgets/custom_drawer.dart @@ -352,7 +352,7 @@ class _CustomDrawerState extends State { if (userPrefs.showSidebarGallery) toolItems.add((SvgPicture.string('', color: colors.onSurface), '图库', const GalleryPage())); if (userPrefs.showSidebarTags) toolItems.add((SvgPicture.string('', color: colors.onSurface), '标签', const TagManagementPage())); if (userPrefs.showSidebarMdReader) toolItems.add((Icon(Icons.description_outlined, size: 20, color: colors.onSurface), 'MD阅读', const MdReaderTabPage())); - if (userPrefs.showSidebarEpub) toolItems.add((SvgPicture.string('', color: colors.onSurface), 'EPUB阅读', const EpubLibraryPage())); + if (userPrefs.showSidebarEpub) toolItems.add((SvgPicture.string('', color: colors.onSurface), '阅读', const EpubLibraryPage())); if (exploreItems.isEmpty && toolItems.isEmpty) return const SizedBox.shrink();