相册功能优化

This commit is contained in:
DelLevin-Home
2026-08-09 15:29:49 +08:00
parent 6d76c33672
commit 68a8d7f6ce
15 changed files with 963 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../utils/image_saver.dart';
class ImageViewer extends StatefulWidget {
final Uint8List imageData;
@@ -168,6 +169,7 @@ class _ImageViewerState extends State<ImageViewer>
rect: currentRect,
child: GestureDetector(
onTap: _handleClose,
onLongPress: () => ImageSaver.showSaveFromBytesSheet(widget.imageData, context: context),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(

View File

@@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import '../../data/gallery/gallery_dao.dart';
import '../../models/data_models.dart';
import '../../widgets/fade_in_local_image.dart';
import 'gallery_viewer_page.dart';
/// 类别显示名映射
const _categoryLabels = {
'movie_poster': '影视海报',
'movie_posters': '海报墙',
'book_cover': '书籍封面',
'note_image': '笔记图片',
'game_cover': '游戏封面',
'game_screenshot': '游戏截图',
'person_photo': '人物照片',
'movie_character': '影视角色',
'book_character': '书籍角色',
'game_character': '游戏角色',
};
class GalleryPage extends StatefulWidget {
const GalleryPage({super.key});
@override
State<GalleryPage> createState() => _GalleryPageState();
}
class _GalleryPageState extends State<GalleryPage> {
final GalleryDao _dao = GalleryDao();
List<GalleryItem> _allItems = [];
bool _loading = true;
String? _error;
String? _selectedCategory; // null = 全部
bool _descending = true; // 按时间倒序
@override
void initState() {
super.initState();
_loadImages();
}
Future<void> _loadImages() async {
try {
final items = await _dao.getAllImages();
if (!mounted) return;
setState(() {
_allItems = items;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = '加载失败:$e';
_loading = false;
});
}
}
List<GalleryItem> get _filteredItems {
var items = _allItems;
if (_selectedCategory != null) {
items = items.where((i) => i.category == _selectedCategory).toList();
}
if (!_descending) {
items = items.reversed.toList();
}
return items;
}
List<String> get _availableCategories {
final set = _allItems.map((i) => i.category).toSet();
// 按预定义顺序排列
return _categoryLabels.keys.where((k) => set.contains(k)).toList();
}
void _openPreview(int index) {
final items = _filteredItems;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GalleryViewerPage(items: items, initialIndex: index),
),
);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('图库'),
actions: [
if (!_loading && _allItems.isNotEmpty)
IconButton(
icon: Icon(_descending ? Icons.arrow_downward : Icons.arrow_upward, size: 20),
tooltip: _descending ? '当前:最新在前' : '当前:最早在前',
onPressed: () => setState(() => _descending = !_descending),
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(_error!, textAlign: TextAlign.center, style: TextStyle(color: colors.error)),
),
)
: _allItems.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.photo_library_outlined, size: 64, color: colors.onSurface.withValues(alpha: 0.2)),
const SizedBox(height: 12),
Text('还没有保存过图片', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.5))),
],
),
)
: Column(
children: [
// 类别筛选条
if (_availableCategories.length > 1)
SizedBox(
height: 44,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
children: [
_buildChip(null, '全部', colors),
..._availableCategories.map((c) => _buildChip(c, _categoryLabels[c] ?? c, colors)),
],
),
),
// 网格
Expanded(
child: GridView.builder(
padding: const EdgeInsets.all(4),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
final item = _filteredItems[index];
return GestureDetector(
onTap: () => _openPreview(index),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
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)),
),
),
),
);
},
),
),
// 底部计数
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
'${_filteredItems.length} 张图片',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)),
),
),
),
],
),
);
}
Widget _buildChip(String? category, String label, ColorScheme colors) {
final selected = _selectedCategory == category;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(label),
selected: selected,
onSelected: (_) => setState(() => _selectedCategory = selected ? null : category),
showCheckmark: false,
padding: const EdgeInsets.symmetric(horizontal: 4),
),
);
}
}

View File

@@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../data/movie/movie_dao.dart';
import '../../data/book/book_dao.dart';
import '../../data/note/note_dao.dart';
import '../../data/game/game_dao.dart';
import '../../models/data_models.dart';
import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../utils/image_saver.dart';
import '../movies/movie_detail_page.dart';
import '../book/book_detail_page.dart';
import '../note/note_detail_page.dart';
import '../game/game_detail_page.dart';
import '../people/person_detail_page.dart';
/// 图库全屏预览页 —— 支持左右滑动、双指缩放、归属信息展示与跳转
class GalleryViewerPage extends StatefulWidget {
final List<GalleryItem> items;
final int initialIndex;
const GalleryViewerPage({
super.key,
required this.items,
required this.initialIndex,
});
@override
State<GalleryViewerPage> createState() => _GalleryViewerPageState();
}
class _GalleryViewerPageState extends State<GalleryViewerPage> {
late PageController _pageController;
late int _currentIndex;
bool _infoVisible = true;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
String _buildInfoText(GalleryItem item) {
if (item.parentTitle != null && item.parentTitle!.isNotEmpty) {
return '来自:《${item.parentTitle}》— ${item.entityTitle}';
}
return '来自:《${item.entityTitle}';
}
Future<void> _navigateToDetail(GalleryItem item) async {
dynamic target;
switch (item.entityType) {
case 'movie':
target = await MovieDao().getMovieById(item.entityId);
break;
case 'book':
target = await BookDao().getBookById(item.entityId);
break;
case 'note':
target = await NoteDao().getNoteById(item.entityId);
break;
case 'game':
target = await GameDao().getGameById(item.entityId);
break;
case 'person':
if (!mounted) return;
target = await context.read<AppProvider>().getPersonById(item.entityId);
break;
}
if (!mounted) return;
if (target == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('原记录已不存在'), duration: Duration(seconds: 2)),
);
return;
}
Widget page;
switch (item.entityType) {
case 'movie':
page = MovieDetailPage(movie: target as Movie);
break;
case 'book':
page = BookDetailPage(book: target as Book);
break;
case 'note':
page = NoteDetailPage(note: target as Note);
break;
case 'game':
page = GameDetailPage(game: target as Game);
break;
case 'person':
page = PersonDetailPage(person: target as Person);
break;
default:
return;
}
Navigator.pop(context); // 关闭全屏预览
Navigator.push(context, MaterialPageRoute(builder: (_) => page));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// 图片页面视图
PageView.builder(
controller: _pageController,
itemCount: widget.items.length,
onPageChanged: (index) => setState(() => _currentIndex = index),
itemBuilder: (context, index) {
final item = widget.items[index];
return GestureDetector(
onTap: () => setState(() => _infoVisible = !_infoVisible),
onLongPress: () => ImageSaver.showSaveFromFileSheet(item.path, context: context),
child: InteractiveViewer(
minScale: 0.5,
maxScale: 3.0,
child: Center(
child: FadeInLocalImage(
path: item.path,
fit: BoxFit.contain,
),
),
),
);
},
),
// 顶部导航栏
if (_infoVisible)
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back, color: Colors.white),
),
const Spacer(),
Text(
'${_currentIndex + 1} / ${widget.items.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
const SizedBox(width: 48),
],
),
),
),
),
// 底部归属信息条
if (_infoVisible)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: SafeArea(
top: false,
child: GestureDetector(
onTap: () => _navigateToDetail(widget.items[_currentIndex]),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
child: Row(
children: [
const Icon(Icons.link, color: Colors.white70, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
_buildInfoText(widget.items[_currentIndex]),
style: const TextStyle(color: Colors.white, fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.chevron_right, color: Colors.white70, size: 20),
],
),
),
),
),
),
// 底部圆点指示器(图片较多时不显示,避免溢出)
if (widget.items.length > 1 && widget.items.length <= 20 && _infoVisible)
Positioned(
bottom: 56,
left: 0,
right: 0,
child: SafeArea(
top: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
widget.items.length,
(index) => Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index == _currentIndex
? Colors.white
: Colors.white.withValues(alpha: 0.4),
),
),
),
),
),
),
],
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../models/data_models.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../utils/image_saver.dart';
/// 游戏截图画廊页面 - 支持左右滑动浏览
class ScreenshotGalleryPage extends StatefulWidget {
@@ -42,11 +43,14 @@ class _ScreenshotGalleryPageState extends State<ScreenshotGalleryPage> {
onPageChanged: (index) => setState(() => _currentIndex = index),
itemBuilder: (context, index) {
final screenshot = widget.screenshots[index];
return InteractiveViewer(
minScale: 0.5,
maxScale: 3.0,
child: Center(
child: FadeInLocalImage(path: screenshot.screenshotPath, fit: BoxFit.contain),
return GestureDetector(
onLongPress: () => ImageSaver.showSaveFromFileSheet(screenshot.screenshotPath, context: context),
child: InteractiveViewer(
minScale: 0.5,
maxScale: 3.0,
child: Center(
child: FadeInLocalImage(path: screenshot.screenshotPath, fit: BoxFit.contain),
),
),
);
},

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../models/data_models.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../utils/image_saver.dart';
/// 海报画廊页面 - 支持左右滑动浏览
class PosterGalleryPage extends StatefulWidget {
@@ -49,13 +50,16 @@ class _PosterGalleryPageState extends State<PosterGalleryPage> {
},
itemBuilder: (context, index) {
final poster = widget.posters[index];
return InteractiveViewer(
minScale: 0.5,
maxScale: 3.0,
child: Center(
child: FadeInLocalImage(
path: poster.posterPath,
fit: BoxFit.contain,
return GestureDetector(
onLongPress: () => ImageSaver.showSaveFromFileSheet(poster.posterPath, context: context),
child: InteractiveViewer(
minScale: 0.5,
maxScale: 3.0,
child: Center(
child: FadeInLocalImage(
path: poster.posterPath,
fit: BoxFit.contain,
),
),
),
);

View File

@@ -10,6 +10,7 @@ import '../../widgets/fade_in_local_image.dart';
import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart';
import '../../utils/image_saver.dart';
import '../../utils/responsive.dart';
import '../../widgets/vditor_editor.dart';
import '../../widgets/tag_side_panel.dart';
@@ -808,6 +809,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
barrierDismissible: true,
builder: (context) => GestureDetector(
onTap: () => Navigator.pop(context),
onLongPress: () => ImageSaver.showSaveFromFileSheet(images[initialIndex], context: context),
child: Container(
color: Colors.black.withValues(alpha: 0.9),
child: Center(

View File

@@ -9,6 +9,7 @@ import 'package:uuid/uuid.dart';
import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart';
import '../../utils/image_saver.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../widgets/tag_side_panel.dart';
import '../../widgets/vditor_editor.dart';
@@ -903,6 +904,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
barrierDismissible: true,
builder: (context) => GestureDetector(
onTap: () => Navigator.pop(context),
onLongPress: () => ImageSaver.showSaveFromFileSheet(_images[index], context: context),
child: Container(
color: Colors.black.withValues(alpha: 0.9),
child: Center(

View File

@@ -30,6 +30,7 @@ class _FeatureSettingsPageState extends State<FeatureSettingsPage> {
bool _showPlaylist = true;
bool _showCalendar = true;
bool _showPerson = true;
bool _showGallery = true;
bool _showTags = true;
bool _showMdReader = true;
bool _showEpub = true;
@@ -57,6 +58,7 @@ class _FeatureSettingsPageState extends State<FeatureSettingsPage> {
_showPlaylist = _userPrefs.showSidebarPlaylist;
_showCalendar = _userPrefs.showSidebarCalendar;
_showPerson = _userPrefs.showSidebarPerson;
_showGallery = _userPrefs.showSidebarGallery;
_showTags = _userPrefs.showSidebarTags;
_showMdReader = _userPrefs.showSidebarMdReader;
_showEpub = _userPrefs.showSidebarEpub;
@@ -297,6 +299,17 @@ class _FeatureSettingsPageState extends State<FeatureSettingsPage> {
await _userPrefs.setShowSidebarPerson(v);
setState(() => _showPerson = v);
}),
Divider(
height: 0.5,
indent: 24,
endIndent: 24,
color: colors.outlineVariant),
_buildSwitchItem(
Icons.photo_library_outlined, '图库', '浏览所有保存过的图片', _showGallery,
(v) async {
await _userPrefs.setShowSidebarGallery(v);
setState(() => _showGallery = v);
}),
Divider(
height: 0.5,
indent: 24,