Files
Aq-Accounting-Flutter/lib/models/watermark.dart
T
2026-09-28 09:59:43 +08:00

209 lines
6.4 KiB
Dart
Raw 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.
/// 水印相机模块的数据模型
///
/// 拆成两个类是为了区分「**怎么显示**」和「**显示什么**」:
/// · [WatermarkSettings] —— 全局设置,持久化,很久才改一次(开关/颜色/位置)
/// · [WatermarkData] —— 每张照片都不一样(拍摄时间/地址/天气/备注)
///
/// 绘制入口见 `pages/watermark/watermark_painter.dart`。
library;
/// 水印设置(全局共享,存 LocalStorage)
class WatermarkSettings {
const WatermarkSettings({
this.showTime = true,
this.showAddress = true,
this.showWeather = false,
this.showRemark = false,
this.showBrand = true,
this.colorValue = 0xFFFFFFFF,
this.posX,
this.posY,
});
/// 默认:时间 + 地点 + 品牌角标(和原「水印相机」的默认观感一致)
static const defaults = WatermarkSettings();
final bool showTime;
final bool showAddress;
final bool showWeather;
final bool showRemark;
/// 右下角「相机图标 + 水印相机」角标
final bool showBrand;
/// 文字颜色(ARGB)。默认白色 —— 深色照片上最通用。
final int colorValue;
/// 水印块的**归一化左上角坐标**(0~1,相对于画布宽高)。
///
/// 为 null 表示用默认位置(左下角)。存归一化而不是像素,是为了让预览和导出
/// 天然一致,也让同一份设置在不同宽高比的照片上都能用。
final double? posX;
final double? posY;
/// 有没有任何一行正文要显示(不算品牌角标)
bool get hasTextContent => showTime || showAddress || showWeather || showRemark;
/// 是否已经手动拖过位置
bool get hasCustomPosition => posX != null && posY != null;
static const _sentinel = Object();
WatermarkSettings copyWith({
bool? showTime,
bool? showAddress,
bool? showWeather,
bool? showRemark,
bool? showBrand,
int? colorValue,
// 用 sentinel 兜住「显式置回 null」(重置位置要用)
Object? posX = _sentinel,
Object? posY = _sentinel,
}) {
return WatermarkSettings(
showTime: showTime ?? this.showTime,
showAddress: showAddress ?? this.showAddress,
showWeather: showWeather ?? this.showWeather,
showRemark: showRemark ?? this.showRemark,
showBrand: showBrand ?? this.showBrand,
colorValue: colorValue ?? this.colorValue,
posX: posX == _sentinel ? this.posX : posX as double?,
posY: posY == _sentinel ? this.posY : posY as double?,
);
}
Map<String, dynamic> toJson() => {
'showTime': showTime,
'showAddress': showAddress,
'showWeather': showWeather,
'showRemark': showRemark,
'showBrand': showBrand,
'colorValue': colorValue,
'posX': posX,
'posY': posY,
};
factory WatermarkSettings.fromJson(Map<String, dynamic> json) {
return WatermarkSettings(
showTime: _asBool(json['showTime'], true),
showAddress: _asBool(json['showAddress'], true),
showWeather: _asBool(json['showWeather'], false),
showRemark: _asBool(json['showRemark'], false),
showBrand: _asBool(json['showBrand'], true),
colorValue: _asInt(json['colorValue'], 0xFFFFFFFF),
posX: _asDoubleOrNull(json['posX']),
posY: _asDoubleOrNull(json['posY']),
);
}
static bool _asBool(Object? v, bool fallback) => v is bool ? v : fallback;
static int _asInt(Object? v, int fallback) {
if (v is int) return v;
if (v is num) return v.toInt();
return fallback;
}
static double? _asDoubleOrNull(Object? v) {
if (v is num) return v.toDouble();
return null;
}
}
/// 这一张照片要显示的内容
class WatermarkData {
const WatermarkData({
required this.time,
this.address = '',
this.weather = '',
this.temperature = '',
this.remark = '',
});
/// 拍摄时间(水印上只用到「分」)
final DateTime time;
final String address;
/// 天气描述,如「晴」
final String weather;
/// 温度,如「25℃」
final String temperature;
final String remark;
/// 天气行是否可显示(天气和温度都拿不到时不显示这一行)
bool get hasWeather => weather.isNotEmpty || temperature.isNotEmpty;
WatermarkData copyWith({
DateTime? time,
String? address,
String? weather,
String? temperature,
String? remark,
}) {
return WatermarkData(
time: time ?? this.time,
address: address ?? this.address,
weather: weather ?? this.weather,
temperature: temperature ?? this.temperature,
remark: remark ?? this.remark,
);
}
}
/// 时间格式化成水印上的样子:`2026.09.28 14:30`
///
/// 纯函数,方便单测。用两位补零,月/日/时/分都对齐。
String formatWatermarkTime(DateTime time) {
String two(int n) => n.toString().padLeft(2, '0');
return '${time.year}.${two(time.month)}.${two(time.day)} '
'${two(time.hour)}:${two(time.minute)}';
}
/// 水印正文的一行
class WatermarkLine {
const WatermarkLine(this.text, {this.wrap = true});
final String text;
/// 是否允许折行。
///
/// 时间行**不折**:它是定长字符串(「时间:2026.09.28 14:30」正好 19 个字),
/// 按字数硬折会把最后一个数字甩到单独一行。
/// 这不是猜的 —— 是在导出预览图上真看到了一行只有一个「0」才发现的。
final bool wrap;
}
/// 把设置和数据拼成水印正文的每一行
///
/// 顺序固定为 时间 → 天气 → 地点 → 备注(和原项目一致)。
/// 关掉的项目直接不产生行;天气/备注没内容时也不占一行。
List<WatermarkLine> buildWatermarkLines(WatermarkSettings settings, WatermarkData data) {
final lines = <WatermarkLine>[];
if (settings.showTime) {
lines.add(WatermarkLine(
'时间:${formatWatermarkTime(data.time)}',
wrap: false,
));
}
if (settings.showWeather && data.hasWeather) {
lines.add(WatermarkLine('天气:${'${data.weather} ${data.temperature}'.trim()}'));
}
if (settings.showAddress) {
// 地址拿不到时给个占位,而不是留个「地点:」的空壳
final address = data.address.isEmpty ? '未知地点' : data.address;
lines.add(WatermarkLine('地点:$address'));
}
if (settings.showRemark && data.remark.trim().isNotEmpty) {
lines.add(WatermarkLine('备注:${data.remark.trim()}'));
}
return lines;
}