MoodCanvas/lib/screens/editor/editor_preview.dart
fengshengxiong 91b7eebbf2 接入TopON
2026-01-22 16:34:55 +08:00

346 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle, ByteData;
import 'package:provider/provider.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:aesthetica_wallpaper/models/recipe.dart';
import 'package:aesthetica_wallpaper/providers/editor_provider.dart';
import 'wallpaper_painter.dart';
// -------------------------------------
// --- 2. 编辑器预览 (EditorPreview) ---
// -------------------------------------
class EditorPreview extends StatefulWidget {
final GlobalKey? repaintBoundaryKey;
final GlobalKey? pureImageKey; // 新增纯净图片的key
const EditorPreview({super.key, this.repaintBoundaryKey, this.pureImageKey});
@override
State<EditorPreview> createState() => _EditorPreviewState();
}
class _EditorPreviewState extends State<EditorPreview> {
// State 变量,用于管理异步资源和流
ui.Image? _loadedImage;
bool _isLoading = true;
String _currentImagePath = '';
// 动态效果的当前值
Color _timeOverlayColor = Colors.transparent;
double _batterySaturationMod = 1.0; // 1.0 = 正常, 0.0 = 黑白
// 流订阅
StreamSubscription? _timeSubscription;
StreamSubscription? _batterySubscription;
// 异步加载 ui.Image
Future<void> _loadImage(String path) async {
if (mounted) {
setState(() {
_isLoading = true;
});
}
try {
final ByteData data = await rootBundle.load(path);
final ui.Codec codec = await ui.instantiateImageCodec(
data.buffer.asUint8List(),
);
final ui.FrameInfo fi = await codec.getNextFrame();
if (mounted) {
setState(() {
_loadedImage = fi.image;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
}
debugPrint("Error loading image: $e");
}
}
// 在 didChangeDependencies 中加载图片和订阅流
@override
void didChangeDependencies() {
super.didChangeDependencies();
final provider = Provider.of<EditorProvider>(context);
final recipe = provider.currentRecipe;
// 检查图片路径是否已更改
if (recipe.baseImagePath != _currentImagePath &&
recipe.baseImagePath.isNotEmpty) {
setState(() {
_isLoading = true;
_currentImagePath = recipe.baseImagePath;
});
_loadImage(_currentImagePath);
}
// 订阅或取消订阅流
_subscribeToStreams(provider);
}
// 管理流订阅
void _subscribeToStreams(EditorProvider provider) {
// ---- 时间感知 ----
_timeSubscription?.cancel();
if (provider.isTimeAware) {
_timeSubscription =
Stream.periodic(
const Duration(minutes: 1),
(_) => DateTime.now(),
).listen((time) {
if (mounted) {
setState(() => _timeOverlayColor = _getTimeAwareOverlay(time));
}
});
// 立即设置一次
_timeOverlayColor = _getTimeAwareOverlay(DateTime.now());
} else {
_timeOverlayColor = Colors.transparent;
}
// ---- 电量感知 ----
_batterySubscription?.cancel();
if (provider.isBatteryAware) {
final battery = Battery();
_batterySubscription = battery.onBatteryStateChanged.listen((_) async {
_updateBatteryEffect(battery);
});
// 立即设置一次
_updateBatteryEffect(battery);
} else {
_batterySaturationMod = 1.0;
}
}
// (已修复)
Future<void> _updateBatteryEffect(Battery battery) async {
try {
// 错误 1 修复:
// 'getBatteryLevel()' 不是一个方法。
// 正确的属性是 '.batteryLevel',它是一个 Future<int>。
final level = await battery.batteryLevel;
final newState = (level < 20) ? 0.0 : 1.0; // 低于20%则变为黑白
if (mounted && newState != _batterySaturationMod) {
setState(() => _batterySaturationMod = newState);
}
} catch (e) {
debugPrint("Error getting battery level: $e");
}
}
// 在 widget 销毁时取消所有订阅
@override
void dispose() {
_timeSubscription?.cancel();
_batterySubscription?.cancel();
_loadedImage?.dispose();
super.dispose();
}
// 帮助函数: 根据时间获取动态蒙版颜色
Color _getTimeAwareOverlay(DateTime time) {
final hour = time.hour;
if (hour < 5 || hour > 20) {
// 夜晚 (8 PM - 5 AM)
return Colors.blue.withValues(alpha: 0.3);
} else if (hour < 10) {
// 早晨 (5 AM - 10 AM)
return Colors.yellow.withValues(alpha: 0.15);
}
return Colors.transparent; // 白天
}
@override
Widget build(BuildContext context) {
// 监听 EditorProvider 的变化以触发重建
final provider = context.watch<EditorProvider>();
final recipe = provider.currentRecipe;
return Stack(
children: [
// 纯净图片渲染区域用于保存使用Offstage隐藏
Offstage(
offstage: true, // 隐藏但仍然渲染
child: RepaintBoundary(
key: widget.pureImageKey,
child: (_isLoading || _loadedImage == null)
? const SizedBox(width: 100, height: 100) // 占位符
: _buildPureImageRenderer(recipe),
),
),
// 模拟手机屏幕预览
_buildPhonePreview(recipe),
],
);
}
// 构建纯净的图片渲染器(用于保存)
Widget _buildPureImageRenderer(Recipe recipe) {
// 获取原始图片尺寸
final imageWidth = _loadedImage!.width.toDouble();
final imageHeight = _loadedImage!.height.toDouble();
Widget canvasWidget = CustomPaint(
size: Size(imageWidth, imageHeight),
painter: WallpaperPainter(
image: _loadedImage!,
recipe: recipe,
timeOverlay: _timeOverlayColor,
batterySaturation: _batterySaturationMod,
),
);
// 应用像素化效果
if (recipe.pixelate > 1.0) {
final Matrix4 pixelMatrix = Matrix4.identity();
final double scale = 1.0 / recipe.pixelate;
pixelMatrix.scaleByDouble(scale, scale, 1.0, 1.0);
return ImageFiltered(
imageFilter: ui.ImageFilter.matrix(
pixelMatrix.storage,
filterQuality: ui.FilterQuality.none,
),
child: canvasWidget,
);
}
return canvasWidget;
}
// 构建手机预览界面
Widget _buildPhonePreview(Recipe recipe) {
return Container(
margin: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(color: Colors.grey[700]!, width: 4),
borderRadius: BorderRadius.circular(40),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(36),
child: RepaintBoundary(
key: widget.repaintBoundaryKey,
child: Stack(
fit: StackFit.expand,
children: [
// --- 核心渲染区 ---
(_isLoading || _loadedImage == null)
? const Center(child: CircularProgressIndicator())
: _buildCanvasRenderer(recipe),
// --- 模拟手机 UI (保持在顶部) ---
_buildMockUI(context),
],
),
),
),
);
}
// (已修复)
Widget _buildCanvasRenderer(Recipe recipe) {
// 我们的 "像素化" 效果是一个特例
// 它通过 ImageFiltered hack 实现,所以我们把它放在 CustomPaint 的 *外部*
Widget canvasWidget = CustomPaint(
// 错误 1, 2, 3 修复:
// 确保所有参数都在 CustomPaint 构造函数内部
painter: WallpaperPainter(
image: _loadedImage!,
recipe: recipe,
timeOverlay: _timeOverlayColor,
batterySaturation: _batterySaturationMod,
),
// 必须有一个 child 才能让 CustomPaint 获得大小
child: const SizedBox.expand(),
);
// 应用像素化 Hack
if (recipe.pixelate > 1.0) {
// 错误 4, 5, 6 修复:
// 确保 `Matrix4` 逻辑在 CustomPaint *外部*
final Matrix4 pixelMatrix = Matrix4.identity();
final double scale = 1.0 / recipe.pixelate;
pixelMatrix.scaleByDouble(scale, scale, 1.0, 1.0);
return ImageFiltered(
imageFilter: ui.ImageFilter.matrix(
pixelMatrix.storage, // 明确传递 .storage (一个 Float64List)
filterQuality: ui.FilterQuality.none, // 关键:使用最近邻插值
),
child: canvasWidget,
);
}
return canvasWidget;
}
// 模拟手机UI
Widget _buildMockUI(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 模拟状态栏
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'9:41',
style: GoogleFonts.lato(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const Row(
children: [
Icon(
Icons.signal_cellular_alt,
color: Colors.white,
size: 16,
),
SizedBox(width: 4),
Icon(Icons.wifi, color: Colors.white, size: 16),
SizedBox(width: 4),
Icon(Icons.battery_full, color: Colors.white, size: 16),
],
),
],
),
),
// 模拟 DOCK 栏
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(24),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.phone, color: Colors.white, size: 32),
Icon(Icons.message, color: Colors.white, size: 32),
Icon(Icons.camera_alt, color: Colors.white, size: 32),
Icon(Icons.music_note, color: Colors.white, size: 32),
],
),
),
],
);
}
}