346 lines
10 KiB
Dart
346 lines
10 KiB
Dart
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),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|