水印优化+ORC优化
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../api/geo_api.dart';
|
||||
import '../../../api/photo_api.dart';
|
||||
@@ -21,10 +24,25 @@ import 'widgets/watermark_settings_sheet.dart';
|
||||
///
|
||||
/// 水印是**本地合成**的 —— 服务器那份只是原图备份。
|
||||
class WatermarkEditPage extends ConsumerStatefulWidget {
|
||||
const WatermarkEditPage({super.key, required this.imagePath});
|
||||
const WatermarkEditPage({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
this.uploadOriginal = true,
|
||||
this.initialData,
|
||||
});
|
||||
|
||||
final String imagePath;
|
||||
|
||||
/// 是否把原图上传存档。
|
||||
///
|
||||
/// 新拍的要传;从「最近拍摄」里挑出来的老照片**不能传** ——
|
||||
/// 服务器上本来就有那一张,再传一遍就是重复文件。
|
||||
final bool uploadOriginal;
|
||||
|
||||
/// 已有的水印内容。传了就不自动定位 ——
|
||||
/// 老照片要用**拍摄当时**的地点,而不是你现在站的地方。
|
||||
final WatermarkData? initialData;
|
||||
|
||||
@override
|
||||
ConsumerState<WatermarkEditPage> createState() => _WatermarkEditPageState();
|
||||
}
|
||||
@@ -40,6 +58,9 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
bool _locating = false;
|
||||
String? _locateError;
|
||||
|
||||
/// 定位成功但地址没解析出来时的原因(配额用完、两个服务都连不上…)
|
||||
String? _addressError;
|
||||
|
||||
/// 定位到的坐标(上传原图时一并存到服务器)
|
||||
double? _latitude;
|
||||
double? _longitude;
|
||||
@@ -47,14 +68,26 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
_UploadStatus _upload = _UploadStatus.waiting;
|
||||
String? _uploadError;
|
||||
bool _saving = false;
|
||||
bool _sharing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_data = WatermarkData(time: DateTime.now());
|
||||
_data = widget.initialData ?? WatermarkData(time: DateTime.now());
|
||||
_loadImageSize();
|
||||
// 先定位、再上传 —— 这样服务器那份记录能带上地址和天气
|
||||
_locateThenUpload();
|
||||
_bootstrap();
|
||||
}
|
||||
|
||||
/// 启动流程分两种:
|
||||
/// · 新拍的:先定位(拿地址/天气)、再把原图上传存档
|
||||
/// · 从「最近拍摄」挑的老照片:地址天气从服务器记录里还原,既不用定位
|
||||
/// 也不用重传 —— 那张原图服务器上本来就有
|
||||
Future<void> _bootstrap() async {
|
||||
if (widget.initialData != null || !widget.uploadOriginal) {
|
||||
return;
|
||||
}
|
||||
await _locate();
|
||||
await _uploadOriginal();
|
||||
}
|
||||
|
||||
// ---------------- 图片尺寸 ----------------
|
||||
@@ -79,11 +112,6 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
|
||||
// ---------------- 定位 ----------------
|
||||
|
||||
Future<void> _locateThenUpload() async {
|
||||
await _locate();
|
||||
await _uploadOriginal();
|
||||
}
|
||||
|
||||
Future<void> _locate() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -97,6 +125,7 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
setState(() {
|
||||
_locating = false;
|
||||
_locateError = snapshot.error;
|
||||
_addressError = snapshot.addressError;
|
||||
_latitude = snapshot.latitude;
|
||||
_longitude = snapshot.longitude;
|
||||
_data = _data.copyWith(
|
||||
@@ -148,23 +177,25 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
return joined;
|
||||
}
|
||||
|
||||
// ---------------- 保存 ----------------
|
||||
// ---------------- 合成 / 保存 / 分享 ----------------
|
||||
|
||||
/// 合成一次带水印的 JPEG。
|
||||
/// 保存和分享共用这一份 —— 两边都要同一张图,逻辑不该写两遍。
|
||||
Future<Uint8List> _composeJpeg() async {
|
||||
final bytes = await File(widget.imagePath).readAsBytes();
|
||||
return composeWatermarkedJpeg(
|
||||
originalBytes: bytes,
|
||||
settings: ref.read(watermarkSettingsProvider),
|
||||
data: _data,
|
||||
brandIcon: ref.read(brandIconProvider).value,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_saving) return;
|
||||
final settings = ref.read(watermarkSettingsProvider);
|
||||
final brandIcon = ref.read(brandIconProvider).value;
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final bytes = await File(widget.imagePath).readAsBytes();
|
||||
final jpeg = await composeWatermarkedJpeg(
|
||||
originalBytes: bytes,
|
||||
settings: settings,
|
||||
data: _data,
|
||||
brandIcon: brandIcon,
|
||||
);
|
||||
|
||||
final jpeg = await _composeJpeg();
|
||||
// gal 的约定:name 不带扩展名
|
||||
await Gal.putImageBytes(jpeg, name: 'qiangbao_${_timestamp()}');
|
||||
|
||||
@@ -173,7 +204,7 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
} on GalException catch (e) {
|
||||
if (!mounted) return;
|
||||
showToast(context, e.type.message, error: true);
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
showToast(context, '保存失败,请重试', error: true);
|
||||
} finally {
|
||||
@@ -181,6 +212,31 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 调系统分享面板(Android 走 ACTION_SEND,会列出微信/QQ/邮件等)
|
||||
Future<void> _share() async {
|
||||
if (_sharing) return;
|
||||
setState(() => _sharing = true);
|
||||
try {
|
||||
final jpeg = await _composeJpeg();
|
||||
|
||||
// 先落一个临时文件再分享。
|
||||
// 不用 XFile.fromData:cross_file 在 Android 上会忽略它的 name,
|
||||
// 对方收到会是个随机文件名。
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File('${dir.path}/qiangbao_${_timestamp()}.jpg');
|
||||
await file.writeAsBytes(jpeg, flush: true);
|
||||
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(files: [XFile(file.path, mimeType: 'image/jpeg')]),
|
||||
);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
showToast(context, '分享失败,请重试', error: true);
|
||||
} finally {
|
||||
if (mounted) setState(() => _sharing = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _timestamp() {
|
||||
final t = DateTime.now();
|
||||
String two(int n) => n.toString().padLeft(2, '0');
|
||||
@@ -320,6 +376,27 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
|
||||
/// 原图上传状态条
|
||||
Widget _uploadBar() {
|
||||
// 老照片本来就在服务器上,没有「上传」这回事,别显示「准备上传原图」误导人
|
||||
if (!widget.uploadOriginal) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: const Color(0xFFF7F4F1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.circle, size: 8, color: Color(0xFF19BE6B)),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'原图已在服务器上',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF19BE6B)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final (String text, Color color) = switch (_upload) {
|
||||
_UploadStatus.waiting => ('准备上传原图', const Color(0xFFBCAAA4)),
|
||||
_UploadStatus.uploading => ('正在上传原图…', const Color(0xFF8D6E63)),
|
||||
@@ -365,16 +442,13 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
|
||||
/// 定位/地址条
|
||||
Widget _locationBar() {
|
||||
final String text;
|
||||
if (_locating) {
|
||||
text = '正在定位…';
|
||||
} else if (_locateError != null) {
|
||||
text = _locateError!;
|
||||
} else if (_data.address.isEmpty) {
|
||||
text = '未获取到位置';
|
||||
} else {
|
||||
text = _data.address;
|
||||
}
|
||||
final (String text, Color color) = _locating
|
||||
? ('正在定位…', const Color(0xFF8D6E63))
|
||||
: _locateError != null
|
||||
? (_locateError!, const Color(0xFFE53935))
|
||||
: _data.address.isEmpty
|
||||
? ('未获取到位置', const Color(0xFFE53935))
|
||||
: (_data.address, const Color(0xFF5D4037));
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -385,17 +459,33 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
const Icon(Icons.location_on_outlined, size: 16, color: Color(0xFFC8956E)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.4,
|
||||
color: _locateError != null
|
||||
? const Color(0xFFE53935)
|
||||
: const Color(0xFF5D4037),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 13, height: 1.4, color: color),
|
||||
),
|
||||
// 定位成功但地址没解析出来时,把原因摆出来 ——
|
||||
// 只给一串坐标、不说为什么,上次就让人以为是功能坏了
|
||||
if (_addressError != null && _locateError == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Text(
|
||||
'地址没解析出来:$_addressError\n可点下面「编辑内容」手动填',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.35,
|
||||
color: Color(0xFFE53935),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
@@ -434,6 +524,12 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
label: '水印设置',
|
||||
onTap: () => showWatermarkSettingsSheet(context),
|
||||
),
|
||||
_toolButton(
|
||||
icon: Icons.share_outlined,
|
||||
label: '分享',
|
||||
busy: _sharing,
|
||||
onTap: _share,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -450,17 +546,24 @@ class _WatermarkEditPageState extends ConsumerState<WatermarkEditPage> {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
bool busy = false,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onTap: busy ? null : onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 21, color: const Color(0xFF8D6E63)),
|
||||
SizedBox(
|
||||
height: 21,
|
||||
width: 21,
|
||||
child: busy
|
||||
? const CircularProgressIndicator(strokeWidth: 2)
|
||||
: Icon(icon, size: 21, color: const Color(0xFF8D6E63)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user