水印相机前端
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/watermark.dart';
|
||||
|
||||
/// 编辑这一张照片的水印内容(地址 / 天气 / 温度 / 备注)
|
||||
///
|
||||
/// 返回 null 表示用户取消了。时间不在这里改 —— 它总是「现在」,
|
||||
/// 让用户手改反而容易把水印弄成假的时间。
|
||||
Future<WatermarkData?> showWatermarkContentSheet(
|
||||
BuildContext context, {
|
||||
required WatermarkData initial,
|
||||
}) {
|
||||
return showModalBottomSheet<WatermarkData>(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => _WatermarkContentSheet(initial: initial),
|
||||
);
|
||||
}
|
||||
|
||||
class _WatermarkContentSheet extends StatefulWidget {
|
||||
const _WatermarkContentSheet({required this.initial});
|
||||
|
||||
final WatermarkData initial;
|
||||
|
||||
@override
|
||||
State<_WatermarkContentSheet> createState() => _WatermarkContentSheetState();
|
||||
}
|
||||
|
||||
class _WatermarkContentSheetState extends State<_WatermarkContentSheet> {
|
||||
late final TextEditingController _address;
|
||||
late final TextEditingController _weather;
|
||||
late final TextEditingController _temperature;
|
||||
late final TextEditingController _remark;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_address = TextEditingController(text: widget.initial.address);
|
||||
_weather = TextEditingController(text: widget.initial.weather);
|
||||
_temperature = TextEditingController(text: widget.initial.temperature);
|
||||
_remark = TextEditingController(text: widget.initial.remark);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_address.dispose();
|
||||
_weather.dispose();
|
||||
_temperature.dispose();
|
||||
_remark.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
Navigator.of(context).pop(
|
||||
widget.initial.copyWith(
|
||||
address: _address.text.trim(),
|
||||
weather: _weather.text.trim(),
|
||||
temperature: _temperature.text.trim(),
|
||||
remark: _remark.text.trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
// 键盘弹出时把内容顶上去,否则输入框会被挡住
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Center(
|
||||
child: Text(
|
||||
'编辑水印内容',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
_field(label: '地址', controller: _address, hint: '自动定位,也可手改'),
|
||||
_field(label: '天气', controller: _weather, hint: '如:晴'),
|
||||
_field(label: '温度', controller: _temperature, hint: '如:25℃'),
|
||||
_field(label: '备注', controller: _remark, hint: '想写点什么'),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 46,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFC8956E),
|
||||
side: const BorderSide(color: Color(0xFFC8956E)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 46,
|
||||
child: FilledButton(
|
||||
onPressed: _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFC8956E),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
),
|
||||
),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required String hint,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF8D6E63)),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF7F4F1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF5D4037)),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 13),
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(fontSize: 14, color: Color(0xFFD7CCC8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../providers/watermark_provider.dart';
|
||||
|
||||
/// 水印设置(开关 + 文字颜色)
|
||||
///
|
||||
/// 用底部弹层而不是独立页面:编辑页里改完能立刻在预览上看到效果,
|
||||
/// 入口页也能从同一个地方进。
|
||||
Future<void> showWatermarkSettingsSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => const _WatermarkSettingsSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 可选文字颜色。白色是默认 —— 深色照片上最通用;浅色照片可以换黑色或暖黄。
|
||||
const List<({String label, int value})> _textColors = [
|
||||
(label: '白', value: 0xFFFFFFFF),
|
||||
(label: '黑', value: 0xFF000000),
|
||||
(label: '暖黄', value: 0xFFFFD54F),
|
||||
(label: '品牌黄', value: 0xFFFFB800),
|
||||
(label: '品牌棕', value: 0xFFC8956E),
|
||||
(label: '浅蓝', value: 0xFF4FC3F7),
|
||||
];
|
||||
|
||||
class _WatermarkSettingsSheet extends ConsumerWidget {
|
||||
const _WatermarkSettingsSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(watermarkSettingsProvider);
|
||||
final notifier = ref.read(watermarkSettingsProvider.notifier);
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Center(
|
||||
child: Text(
|
||||
'水印设置',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
_sectionTitle('显示内容'),
|
||||
_switchRow(
|
||||
'时间',
|
||||
'如 2026.09.28 14:30',
|
||||
settings.showTime,
|
||||
(v) => notifier.update(settings.copyWith(showTime: v)),
|
||||
),
|
||||
_switchRow(
|
||||
'地点',
|
||||
'自动定位,也可在编辑页手填',
|
||||
settings.showAddress,
|
||||
(v) => notifier.update(settings.copyWith(showAddress: v)),
|
||||
),
|
||||
_switchRow(
|
||||
'天气温度',
|
||||
'如 晴 25℃(拿不到时这一行不显示)',
|
||||
settings.showWeather,
|
||||
(v) => notifier.update(settings.copyWith(showWeather: v)),
|
||||
),
|
||||
_switchRow(
|
||||
'备注',
|
||||
'每张照片单独填',
|
||||
settings.showRemark,
|
||||
(v) => notifier.update(settings.copyWith(showRemark: v)),
|
||||
),
|
||||
_switchRow(
|
||||
'右下角品牌角标',
|
||||
'相机图标 + 水印相机',
|
||||
settings.showBrand,
|
||||
(v) => notifier.update(settings.copyWith(showBrand: v)),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
_sectionTitle('文字颜色'),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: _textColors.map((c) {
|
||||
final selected = settings.colorValue == c.value;
|
||||
return GestureDetector(
|
||||
onTap: () => notifier.update(settings.copyWith(colorValue: c.value)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(c.value),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? const Color(0xFFC8956E)
|
||||
: const Color(0xFFE0E0E0),
|
||||
width: selected ? 3 : 1,
|
||||
),
|
||||
),
|
||||
child: selected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: _isLight(c.value)
|
||||
? const Color(0xFF5D4037)
|
||||
: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
c.label,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF8D6E63)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
_sectionTitle('水印位置'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3E6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'位置在编辑页直接拖动水印调整,会自动记住',
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFF6D4C41), height: 1.5),
|
||||
),
|
||||
),
|
||||
if (settings.hasCustomPosition)
|
||||
TextButton(
|
||||
onPressed: notifier.resetPosition,
|
||||
child: const Text(
|
||||
'恢复默认',
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFFC8956E)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionTitle(String text) {
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF8D6E63),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _switchRow(String label, String hint, bool value, ValueChanged<bool> onChanged) {
|
||||
return InkWell(
|
||||
onTap: () => onChanged(!value),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 15, color: Color(0xFF5D4037)),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hint,
|
||||
style: const TextStyle(fontSize: 11.5, color: Color(0xFFBCAAA4)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(value: value, onChanged: onChanged),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 浅色底上的对勾要用深色,不然看不见
|
||||
static bool _isLight(int argb) {
|
||||
final r = (argb >> 16) & 0xFF;
|
||||
final g = (argb >> 8) & 0xFF;
|
||||
final b = argb & 0xFF;
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 160;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user