主要变更: 1. 新增桌面端托盘图标支持深色/浅色主题切换 2. 重构应用锁、动画配置、小组件导航服务职责 3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题 4. 优化诗词服务、阅读进度、搜索结果空状态体验 5. 完善macOS打包配置与错误静默处理逻辑 6. 新增快速卡片多语言适配与动画退出队列管理
414 lines
14 KiB
Dart
414 lines
14 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 壁纸预览底部面板
|
||
/// 创建时间: 2026-05-01
|
||
/// 更新时间: 2026-06-23
|
||
/// 作用: 壁纸大图预览 + 信息 + 下载/设为壁纸/编辑器
|
||
/// 上次更新: 下载toast改"版权未知"+增加截图保存tips+图片点击全屏预览(可缩放/无UI干扰截图)
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:photo_view/photo_view.dart';
|
||
import 'package:cached_network_image/cached_network_image.dart';
|
||
|
||
import '../../../core/theme/app_theme.dart';
|
||
import '../../../core/theme/app_typography.dart';
|
||
import '../../../shared/widgets/feedback/app_toast.dart';
|
||
import '../../../shared/widgets/media/safe_cached_image.dart';
|
||
import 'models/template_models.dart';
|
||
|
||
class WallpaperPreviewSheet extends StatelessWidget {
|
||
const WallpaperPreviewSheet({super.key, required this.item});
|
||
|
||
final WallpaperItem item;
|
||
|
||
static Future<void> show(BuildContext context, WallpaperItem item) {
|
||
final screenHeight = MediaQuery.of(context).size.height;
|
||
final sheetHeight = screenHeight * 0.7;
|
||
|
||
return showCupertinoModalPopup<void>(
|
||
context: context,
|
||
barrierColor: Colors.black54,
|
||
builder: (_) => _WallpaperSheetContent(
|
||
item: item,
|
||
sheetHeight: sheetHeight,
|
||
screenHeight: screenHeight,
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final screenHeight = MediaQuery.of(context).size.height;
|
||
final sheetHeight = screenHeight * 0.7;
|
||
|
||
return _WallpaperSheetContent(
|
||
item: item,
|
||
sheetHeight: sheetHeight,
|
||
screenHeight: screenHeight,
|
||
);
|
||
}
|
||
}
|
||
|
||
class _WallpaperSheetContent extends StatelessWidget {
|
||
const _WallpaperSheetContent({
|
||
required this.item,
|
||
required this.sheetHeight,
|
||
required this.screenHeight,
|
||
});
|
||
|
||
final WallpaperItem item;
|
||
final double sheetHeight;
|
||
final double screenHeight;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = AppTheme.ext(context);
|
||
final dismissAreaHeight = screenHeight - sheetHeight;
|
||
|
||
return Stack(
|
||
children: [
|
||
GestureDetector(
|
||
onTap: () => Navigator.pop(context),
|
||
child: SizedBox(width: double.infinity, height: dismissAreaHeight),
|
||
),
|
||
Align(
|
||
alignment: Alignment.bottomCenter,
|
||
child: Container(
|
||
height: sheetHeight,
|
||
decoration: BoxDecoration(
|
||
color: ext.bgPrimary,
|
||
borderRadius: const BorderRadius.vertical(
|
||
top: Radius.circular(20),
|
||
),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
_buildHandle(context),
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
physics: const BouncingScrollPhysics(),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildImage(context),
|
||
_buildInfo(ext),
|
||
_buildActions(context),
|
||
const SizedBox(height: 24),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildHandle(BuildContext context) {
|
||
return GestureDetector(
|
||
onTap: () => Navigator.pop(context),
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(top: 10, bottom: 6),
|
||
child: Container(
|
||
width: 36,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: CupertinoColors.systemGrey4,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildImage(BuildContext context) {
|
||
final imageUrl = item.previewUrl.isNotEmpty
|
||
? item.previewUrl
|
||
: item.imageUrl;
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: GestureDetector(
|
||
onTap: () => _openFullScreenPreview(context, imageUrl),
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(16),
|
||
child: Stack(
|
||
children: [
|
||
ConstrainedBox(
|
||
constraints: BoxConstraints(maxHeight: sheetHeight * 0.55),
|
||
child: SafeCachedImage(
|
||
url: imageUrl,
|
||
fit: BoxFit.contain,
|
||
placeholder: (_, __) => const SizedBox(
|
||
height: 200,
|
||
child: Center(child: CupertinoActivityIndicator()),
|
||
),
|
||
errorWidget: (_, __, ___) => const SizedBox(
|
||
height: 200,
|
||
child: Center(
|
||
child: Icon(CupertinoIcons.photo, size: 48, color: Colors.grey),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// 右上角放大提示角标
|
||
Positioned(
|
||
top: 8,
|
||
right: 8,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black.withValues(alpha: 0.45),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(CupertinoIcons.zoom_in, size: 12, color: Colors.white),
|
||
SizedBox(width: 3),
|
||
Text('点击放大', style: TextStyle(fontSize: 10, color: Colors.white)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 打开全屏预览(黑色背景+可缩放+点击切换UI显隐,便于截图无干扰)
|
||
void _openFullScreenPreview(BuildContext context, String imageUrl) {
|
||
Navigator.of(context, rootNavigator: true).push(
|
||
CupertinoPageRoute<void>(
|
||
fullscreenDialog: true,
|
||
builder: (_) => _CleanFullScreenImageViewer(imageUrl: imageUrl),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildInfo(AppThemeExtension ext) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (item.title.isNotEmpty)
|
||
Text(
|
||
item.title,
|
||
style: TextStyle(
|
||
color: ext.textPrimary,
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
_buildInfoChip('📐 ${item.resolution}'),
|
||
const SizedBox(width: 8),
|
||
if (item.source.isNotEmpty) _buildInfoChip('🏷 ${item.source}'),
|
||
const SizedBox(width: 8),
|
||
if (item.views > 0) _buildInfoChip('👁 ${item.views}'),
|
||
],
|
||
),
|
||
if (item.tags.isNotEmpty) ...[
|
||
const SizedBox(height: 8),
|
||
Wrap(
|
||
spacing: 6,
|
||
runSpacing: 4,
|
||
children: item.tags
|
||
.take(5)
|
||
.map((t) => _buildTagChip(t, ext))
|
||
.toList(),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildInfoChip(String text) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: CupertinoColors.systemGrey5,
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(text, style: const TextStyle(fontSize: 11)),
|
||
);
|
||
}
|
||
|
||
Widget _buildTagChip(String tag, AppThemeExtension ext) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(tag, style: TextStyle(fontSize: 11, color: ext.accent)),
|
||
);
|
||
}
|
||
|
||
Widget _buildActions(BuildContext context) {
|
||
final ext = AppTheme.ext(context);
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: CupertinoButton(
|
||
onPressed: () {
|
||
AppToast.show('版权未知');
|
||
},
|
||
color: ext.accent,
|
||
borderRadius: BorderRadius.circular(12),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(CupertinoIcons.arrow_down_circle, size: 18, color: ext.textOnAccent),
|
||
const SizedBox(width: 6),
|
||
Text('下载', style: TextStyle(fontSize: 15, color: ext.textOnAccent)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: CupertinoButton(
|
||
onPressed: () {
|
||
AppToast.show('版权未知');
|
||
},
|
||
color: ext.accent.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(12),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(CupertinoIcons.pencil_ellipsis_rectangle, size: 18, color: ext.accent),
|
||
const SizedBox(width: 6),
|
||
Text('编辑', style: TextStyle(fontSize: 15, color: ext.accent)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
// 截图保存提示
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.08),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(CupertinoIcons.info_circle, size: 14, color: ext.accent),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: Text(
|
||
'点击图片可放大预览,自行截图保存',
|
||
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// ============================================================
|
||
/// 全屏图片查看器(纯净版)
|
||
/// 作用: 黑色背景+可缩放+点击切换UI显隐,便于截图无其他视图干扰
|
||
/// ============================================================
|
||
class _CleanFullScreenImageViewer extends StatefulWidget {
|
||
const _CleanFullScreenImageViewer({required this.imageUrl});
|
||
|
||
final String imageUrl;
|
||
|
||
@override
|
||
State<_CleanFullScreenImageViewer> createState() =>
|
||
_CleanFullScreenImageViewerState();
|
||
}
|
||
|
||
class _CleanFullScreenImageViewerState extends State<_CleanFullScreenImageViewer> {
|
||
bool _showUi = true;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return CupertinoPageScaffold(
|
||
backgroundColor: Colors.black,
|
||
child: SafeArea(
|
||
child: GestureDetector(
|
||
onTap: () => setState(() => _showUi = !_showUi),
|
||
child: Stack(
|
||
children: [
|
||
PhotoView(
|
||
imageProvider: CachedNetworkImageProvider(widget.imageUrl),
|
||
minScale: PhotoViewComputedScale.contained,
|
||
maxScale: PhotoViewComputedScale.covered * 4.0,
|
||
backgroundDecoration: const BoxDecoration(color: Colors.black),
|
||
loadingBuilder: (_, __) =>
|
||
const Center(child: CupertinoActivityIndicator(radius: 16, color: Colors.white)),
|
||
errorBuilder: (_, __, ___) => const Center(
|
||
child: Icon(CupertinoIcons.photo, size: 48, color: Colors.white54),
|
||
),
|
||
),
|
||
// 顶部关闭按钮(可切换显隐)
|
||
if (_showUi)
|
||
Positioned(
|
||
top: 8,
|
||
left: 8,
|
||
child: SafeArea(
|
||
child: CupertinoButton(
|
||
padding: const EdgeInsets.all(8),
|
||
color: Colors.black.withValues(alpha: 0.4),
|
||
borderRadius: BorderRadius.circular(20),
|
||
minimumSize: const Size(36, 36),
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Icon(CupertinoIcons.xmark, size: 18, color: Colors.white),
|
||
),
|
||
),
|
||
),
|
||
// 底部提示(可切换显隐)
|
||
if (_showUi)
|
||
Positioned(
|
||
bottom: 16,
|
||
left: 0,
|
||
right: 0,
|
||
child: Center(
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black.withValues(alpha: 0.5),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: const Text(
|
||
'双指缩放 · 单击隐藏按钮后可截图',
|
||
style: TextStyle(fontSize: 12, color: Colors.white70),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|