水印相机前端
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/// 水印相机模块的数据模型
|
||||
///
|
||||
/// 拆成两个类是为了区分「**怎么显示**」和「**显示什么**」:
|
||||
/// · [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;
|
||||
}
|
||||
Reference in New Issue
Block a user