feat: add theme color selection
feat: drag-and-drop hints for signature cards
This commit is contained in:
parent
bc524e958f
commit
8197a352aa
|
|
@ -90,3 +90,7 @@ Some rule of thumb:
|
|||
* [Viewer Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html)
|
||||
* [Per-page Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pageOverlaysBuilder.html)
|
||||
* `pageOverlaysBuilder`
|
||||
* [image](https://pub.dev/packages/image)
|
||||
* whole app use its image object as image representation.
|
||||
* aware that minimize, encode/decode usage, because its has poor performance on web
|
||||
* `ColorFilterGenerator` can not replace `adjustColor` due to custom background removal algorithm need at last stage. It is GPU based, and offscreen-render then readback is not ideal.
|
||||
|
|
|
|||
|
|
@ -39,18 +39,19 @@ class MyApp extends StatelessWidget {
|
|||
),
|
||||
data: (_) {
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
final seed = ref.watch(themeSeedColorProvider);
|
||||
final appLocale = ref.watch(localeProvider);
|
||||
return MaterialApp.router(
|
||||
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
seedColor: seed,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
seedColor: seed,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ Set<String> _supportedTags() {
|
|||
|
||||
// Keys
|
||||
const _kTheme = 'theme'; // 'light'|'dark'|'system'
|
||||
const _kThemeColor = 'theme_color'; // 'blue'|'green'|'red'|'purple'
|
||||
// Theme color persisted as hex ARGB string (e.g., '#FF2196F3').
|
||||
// Backward compatible with historical names like 'blue', 'indigo', etc.
|
||||
const _kThemeColor = 'theme_color';
|
||||
const _kLanguage = 'language'; // BCP-47 tag like 'en', 'zh-TW', 'es'
|
||||
const _kPageView = 'page_view'; // now only 'continuous'
|
||||
const _kExportDpi = 'export_dpi'; // double, allowed: 96,144,200,300
|
||||
|
|
@ -67,6 +69,54 @@ String _normalizeLanguageTag(String tag) {
|
|||
|
||||
class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
||||
final SharedPreferences prefs;
|
||||
static Color? _tryParseColor(String? s) {
|
||||
if (s == null || s.isEmpty) return null;
|
||||
final v = s.trim();
|
||||
// 1) Direct hex formats: #AARRGGBB, #RRGGBB, AARRGGBB, RRGGBB
|
||||
String hex = v.startsWith('#') ? v.substring(1) : v;
|
||||
// Accept 0xAARRGGBB / 0xRRGGBB as well
|
||||
if (hex.toLowerCase().startsWith('0x')) hex = hex.substring(2);
|
||||
if (hex.length == 6) {
|
||||
final intVal = int.tryParse('FF$hex', radix: 16);
|
||||
if (intVal != null) return Color(intVal);
|
||||
} else if (hex.length == 8) {
|
||||
final intVal = int.tryParse(hex, radix: 16);
|
||||
if (intVal != null) return Color(intVal);
|
||||
}
|
||||
|
||||
// 2) Parse from Color(...) or MaterialColor(...) toString outputs
|
||||
// e.g., 'Color(0xff2196f3)' or 'MaterialColor(primary value: Color(0xff2196f3))'
|
||||
final lower = v.toLowerCase();
|
||||
final idx = lower.indexOf('0x');
|
||||
if (idx != -1) {
|
||||
var sub = lower.substring(idx);
|
||||
// Trim trailing non-hex chars
|
||||
final hexChars = RegExp(r'^[0-9a-fx]+');
|
||||
final m = hexChars.firstMatch(sub);
|
||||
if (m != null) {
|
||||
sub = m.group(0) ?? sub;
|
||||
if (sub.startsWith('0x')) sub = sub.substring(2);
|
||||
if (sub.length == 6) sub = 'FF$sub';
|
||||
if (sub.length >= 8) {
|
||||
final intVal = int.tryParse(sub.substring(0, 8), radix: 16);
|
||||
if (intVal != null) return Color(intVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) As a last resort, try to match any MaterialColor primary by toString equality
|
||||
// (useful if some code persisted mat.toString()).
|
||||
for (final mc in Colors.primaries) {
|
||||
if (mc.toString() == v) {
|
||||
return Color(mc.value);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String _toHex(Color c) =>
|
||||
'#${c.value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
|
||||
PreferencesStateNotifier(this.prefs)
|
||||
: super(
|
||||
PreferencesState(
|
||||
|
|
@ -77,7 +127,7 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
|||
.toLanguageTag(),
|
||||
),
|
||||
exportDpi: _readDpi(prefs),
|
||||
theme_color: prefs.getString(_kThemeColor) ?? 'blue',
|
||||
theme_color: prefs.getString(_kThemeColor) ?? '#FF2196F3', // blue
|
||||
),
|
||||
) {
|
||||
// normalize language to supported/fallback
|
||||
|
|
@ -108,6 +158,20 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
|||
state = state.copyWith(exportDpi: 144.0);
|
||||
prefs.setDouble(_kExportDpi, 144.0);
|
||||
}
|
||||
// Ensure theme color is a valid hex or known name; normalize to hex
|
||||
final parsed = _tryParseColor(state.theme_color);
|
||||
if (parsed == null) {
|
||||
final fallback = Colors.blue;
|
||||
final hex = _toHex(fallback);
|
||||
state = state.copyWith(theme_color: hex);
|
||||
prefs.setString(_kThemeColor, hex);
|
||||
} else {
|
||||
final hex = _toHex(parsed);
|
||||
if (state.theme_color != hex) {
|
||||
state = state.copyWith(theme_color: hex);
|
||||
prefs.setString(_kThemeColor, hex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setTheme(String theme) async {
|
||||
|
|
@ -123,6 +187,14 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
|||
await prefs.setString(_kLanguage, normalized);
|
||||
}
|
||||
|
||||
Future<void> setThemeColor(String themeColor) async {
|
||||
// Accept hex like '#FF2196F3', '#2196F3', or known names like 'blue'. Normalize to hex.
|
||||
final c = _tryParseColor(themeColor) ?? Colors.blue;
|
||||
final hex = _toHex(c);
|
||||
state = state.copyWith(theme_color: hex);
|
||||
await prefs.setString(_kThemeColor, hex);
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
final device =
|
||||
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag();
|
||||
|
|
@ -131,12 +203,13 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
|||
theme: 'system',
|
||||
language: normalized,
|
||||
exportDpi: 144.0,
|
||||
theme_color: '',
|
||||
theme_color: '#FF2196F3',
|
||||
);
|
||||
await prefs.setString(_kTheme, 'system');
|
||||
await prefs.setString(_kLanguage, normalized);
|
||||
await prefs.setString(_kPageView, 'continuous');
|
||||
await prefs.setDouble(_kExportDpi, 144.0);
|
||||
await prefs.setString(_kThemeColor, '#FF2196F3');
|
||||
}
|
||||
|
||||
Future<void> setExportDpi(double dpi) async {
|
||||
|
|
@ -182,6 +255,13 @@ final themeModeProvider = Provider<ThemeMode>((ref) {
|
|||
}
|
||||
});
|
||||
|
||||
/// Maps the selected theme color name to an actual Color for theming.
|
||||
final themeSeedColorProvider = Provider<Color>((ref) {
|
||||
final prefs = ref.watch(preferencesRepositoryProvider);
|
||||
final c = PreferencesStateNotifier._tryParseColor(prefs.theme_color);
|
||||
return c ?? Colors.blue;
|
||||
});
|
||||
|
||||
final localeProvider = Provider<Locale?>((ref) {
|
||||
final prefs = ref.watch(preferencesRepositoryProvider);
|
||||
final supported = _supportedTags();
|
||||
|
|
|
|||
|
|
@ -25,23 +25,6 @@ class CachedSignatureCard extends SignatureCard {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns cached processed image for the current [graphicAdjust], computing
|
||||
/// via [service] if not cached yet.
|
||||
img.Image getOrComputeProcessedImage(
|
||||
SignatureImageProcessingService service,
|
||||
) {
|
||||
final existing = _cachedProcessedImage;
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
final computedImage = service.processImageToImage(
|
||||
asset.sigImage,
|
||||
graphicAdjust,
|
||||
);
|
||||
_cachedProcessedImage = computedImage;
|
||||
return computedImage;
|
||||
}
|
||||
|
||||
/// Invalidate the cached processed image, forcing recompute next time.
|
||||
void invalidateCache() {
|
||||
_cachedProcessedImage = null;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "Anzeige",
|
||||
"downloadStarted": "Download gestartet",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "Auf Dokument ziehen",
|
||||
"drawSignature": "Signatur zeichnen",
|
||||
"errorWithMessage": "Fehler: {message}",
|
||||
"exportingPleaseWait": "Exportiere… Bitte warten",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "Dunkel",
|
||||
"themeLight": "Hell",
|
||||
"themeSystem": "System",
|
||||
"themeColor": "Themenfarbe",
|
||||
"themeColorBlue": "Blau",
|
||||
"themeColorGreen": "Grün",
|
||||
"themeColorRed": "Rot",
|
||||
"themeColorPurple": "Lila",
|
||||
"undo": "Rückgängig",
|
||||
"unlock": "Entsperren"
|
||||
}
|
||||
|
|
@ -28,6 +28,10 @@
|
|||
"@downloadStarted": {},
|
||||
"dpi": "DPI",
|
||||
"@dpi": {},
|
||||
"dragOntoDocument": "Drag onto document",
|
||||
"@dragOntoDocument": {
|
||||
"description": "Tooltip message for dragging signature card onto PDF document"
|
||||
},
|
||||
"drawSignature": "Draw Signature",
|
||||
"@drawSignature": {},
|
||||
"errorWithMessage": "Error: {message}",
|
||||
|
|
@ -120,6 +124,16 @@
|
|||
"@themeLight": {},
|
||||
"themeSystem": "System",
|
||||
"@themeSystem": {},
|
||||
"themeColor": "Theme color",
|
||||
"@themeColor": {},
|
||||
"themeColorBlue": "Blue",
|
||||
"@themeColorBlue": {},
|
||||
"themeColorGreen": "Green",
|
||||
"@themeColorGreen": {},
|
||||
"themeColorRed": "Red",
|
||||
"@themeColorRed": {},
|
||||
"themeColorPurple": "Purple",
|
||||
"@themeColorPurple": {},
|
||||
"undo": "Undo",
|
||||
"@undo": {},
|
||||
"unlock": "Unlock",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "Pantalla",
|
||||
"downloadStarted": "Descarga iniciada",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "Arrastra sobre el documento",
|
||||
"drawSignature": "Dibujar firma",
|
||||
"errorWithMessage": "Error: {message}",
|
||||
"exportingPleaseWait": "Exportando... Por favor, espere",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "Oscuro",
|
||||
"themeLight": "Claro",
|
||||
"themeSystem": "Sistema",
|
||||
"themeColor": "Color del tema",
|
||||
"themeColorBlue": "Azul",
|
||||
"themeColorGreen": "Verde",
|
||||
"themeColorRed": "Rojo",
|
||||
"themeColorPurple": "Púrpura",
|
||||
"undo": "Deshacer",
|
||||
"unlock": "Desbloquear"
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "Affichage",
|
||||
"downloadStarted": "Téléchargement commencé",
|
||||
"dpi": "DPI :",
|
||||
"dragOntoDocument": "Faites glisser sur le document",
|
||||
"drawSignature": "Dessiner une signature",
|
||||
"errorWithMessage": "Erreur : {message}",
|
||||
"exportingPleaseWait": "Exportation… Veuillez patienter",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "Sombre",
|
||||
"themeLight": "Clair",
|
||||
"themeSystem": "Système",
|
||||
"themeColor": "Couleur du thème",
|
||||
"themeColorBlue": "Bleu",
|
||||
"themeColorGreen": "Vert",
|
||||
"themeColorRed": "Rouge",
|
||||
"themeColorPurple": "Violet",
|
||||
"undo": "Annuler",
|
||||
"unlock": "Déverrouiller"
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "表示",
|
||||
"downloadStarted": "ダウンロード開始",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "ドキュメントにドラッグします",
|
||||
"drawSignature": "署名をかく",
|
||||
"errorWithMessage": "エラー:{message}",
|
||||
"exportingPleaseWait": "エクスポート中…お待ちください",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "ダーク",
|
||||
"themeLight": "ライト",
|
||||
"themeSystem": "システム",
|
||||
"themeColor": "テーマカラー",
|
||||
"themeColorBlue": "青",
|
||||
"themeColorGreen": "緑",
|
||||
"themeColorRed": "赤",
|
||||
"themeColorPurple": "紫",
|
||||
"undo": "元に戻す",
|
||||
"unlock": "ロック解除"
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "표시",
|
||||
"downloadStarted": "다운로드 시작됨",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "문서로 끌어다 놓습니다",
|
||||
"drawSignature": "서명 그리기",
|
||||
"errorWithMessage": "오류: {message}",
|
||||
"exportingPleaseWait": "내보내는 중... 잠시 기다려주세요",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "다크",
|
||||
"themeLight": "라이트",
|
||||
"themeSystem": "시스템",
|
||||
"themeColor": "테마 색상",
|
||||
"themeColorBlue": "파란색",
|
||||
"themeColorGreen": "녹색",
|
||||
"themeColorRed": "빨간색",
|
||||
"themeColorPurple": "보라색",
|
||||
"undo": "실행 취소",
|
||||
"unlock": "잠금 해제"
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "Відображення",
|
||||
"downloadStarted": "Завантаження розпочато",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "Перетягніть на документ",
|
||||
"drawSignature": "Намалювати підпис",
|
||||
"errorWithMessage": "Помилка: {message}",
|
||||
"exportingPleaseWait": "Експортування... Зачекайте",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "Темна",
|
||||
"themeLight": "Світла",
|
||||
"themeSystem": "Системна",
|
||||
"themeColor": "Колір теми",
|
||||
"themeColorBlue": "Синій",
|
||||
"themeColorGreen": "Зелений",
|
||||
"themeColorRed": "Червоний",
|
||||
"themeColorPurple": "Фіолетовий",
|
||||
"undo": "Відмінити",
|
||||
"unlock": "Відмкнути"
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
"display": "顯示",
|
||||
"downloadStarted": "已開始下載",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "拖到文档上",
|
||||
"drawSignature": "手寫簽名",
|
||||
"errorWithMessage": "錯誤:{message}",
|
||||
"exportingPleaseWait": "匯出中…請稍候",
|
||||
|
|
@ -48,6 +49,11 @@
|
|||
"themeDark": "深色",
|
||||
"themeLight": "淺色",
|
||||
"themeSystem": "系統",
|
||||
"themeColor": "主题颜色",
|
||||
"themeColorBlue": "蓝色",
|
||||
"themeColorGreen": "绿色",
|
||||
"themeColorRed": "红色",
|
||||
"themeColorPurple": "紫色",
|
||||
"undo": "復原",
|
||||
"unlock": "解锁"
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"display": "显示",
|
||||
"downloadStarted": "下载已开始",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "拖到文档上",
|
||||
"drawSignature": "绘制签名",
|
||||
"errorWithMessage": "错误:{message}",
|
||||
"exportingPleaseWait": "正在导出... 请稍候",
|
||||
|
|
@ -47,6 +48,11 @@
|
|||
"themeDark": "深色",
|
||||
"themeLight": "浅色",
|
||||
"themeSystem": "系统",
|
||||
"themeColor": "主题颜色",
|
||||
"themeColorBlue": "蓝色",
|
||||
"themeColorGreen": "绿色",
|
||||
"themeColorRed": "红色",
|
||||
"themeColorPurple": "紫色",
|
||||
"undo": "撤销",
|
||||
"unlock": "解锁"
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
"display": "顯示",
|
||||
"downloadStarted": "已開始下載",
|
||||
"dpi": "DPI",
|
||||
"dragOntoDocument": "拖曳到文件",
|
||||
"drawSignature": "手寫簽名",
|
||||
"errorWithMessage": "錯誤:{message}",
|
||||
"exportingPleaseWait": "匯出中…請稍候",
|
||||
|
|
@ -48,6 +49,11 @@
|
|||
"themeDark": "深色",
|
||||
"themeLight": "淺色",
|
||||
"themeSystem": "系統",
|
||||
"themeColor": "主題顏色",
|
||||
"themeColorBlue": "藍色",
|
||||
"themeColorGreen": "綠色",
|
||||
"themeColorRed": "紅色",
|
||||
"themeColorPurple": "紫色",
|
||||
"undo": "復原",
|
||||
"unlock": "解鎖"
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ class DrawCanvas extends StatefulWidget {
|
|||
this.control,
|
||||
this.onConfirm,
|
||||
this.debugBytesSink,
|
||||
this.closeOnConfirmImmediately = false,
|
||||
});
|
||||
|
||||
final hand.HandSignatureControl? control;
|
||||
|
|
@ -18,9 +17,6 @@ class DrawCanvas extends StatefulWidget {
|
|||
// For tests: allows observing exported bytes without relying on Navigator
|
||||
@visibleForTesting
|
||||
final ValueNotifier<Uint8List?>? debugBytesSink;
|
||||
// When true (used by bottom sheet), the sheet will be closed immediately
|
||||
// on confirm without waiting for export to finish.
|
||||
final bool closeOnConfirmImmediately;
|
||||
|
||||
@override
|
||||
State<DrawCanvas> createState() => _DrawCanvasState();
|
||||
|
|
|
|||
|
|
@ -98,6 +98,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
if (controller.isReady) controller.goToPage(pageNumber: target);
|
||||
}
|
||||
|
||||
img.Image? _toStdSignatureImage(img.Image? image) {
|
||||
if (image == null) return null;
|
||||
image.convert(numChannels: 4);
|
||||
// Scale down if height > 256 to improve performance
|
||||
if (image.height > 256) {
|
||||
final newWidth = (image.width * 256) ~/ image.height;
|
||||
image = img.copyResize(image, width: newWidth, height: 256);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
Future<img.Image?> _loadSignatureFromFile() async {
|
||||
final typeGroup = fs.XTypeGroup(
|
||||
label:
|
||||
|
|
@ -109,8 +120,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
final bytes = await file.readAsBytes();
|
||||
try {
|
||||
var sigImage = img.decodeImage(bytes);
|
||||
sigImage?.convert(numChannels: 4);
|
||||
return sigImage;
|
||||
return _toStdSignatureImage(sigImage);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -121,14 +131,13 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
builder: (_) => const DrawCanvas(closeOnConfirmImmediately: false),
|
||||
builder: (_) => const DrawCanvas(),
|
||||
);
|
||||
if (result == null || result.isEmpty) return null;
|
||||
// In simplified UI, adding to library isn't implemented
|
||||
try {
|
||||
var sigImage = img.decodeImage(result);
|
||||
sigImage?.convert(numChannels: 4);
|
||||
return sigImage;
|
||||
return _toStdSignatureImage(sigImage);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class SettingsDialog extends ConsumerStatefulWidget {
|
|||
|
||||
class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
String? _theme;
|
||||
String? _themeColor;
|
||||
String? _language;
|
||||
// Page view removed; continuous-only
|
||||
double? _exportDpi;
|
||||
|
|
@ -21,6 +22,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
super.initState();
|
||||
final prefs = ref.read(preferencesRepositoryProvider);
|
||||
_theme = prefs.theme;
|
||||
_themeColor = prefs.theme_color;
|
||||
_language = prefs.language;
|
||||
_exportDpi = prefs.exportDpi;
|
||||
// pageView no longer configurable (continuous-only)
|
||||
|
|
@ -174,6 +176,22 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 140, child: Text('${l.themeColor}:')),
|
||||
const SizedBox(width: 8),
|
||||
_ThemeColorCircle(
|
||||
onPick: (value) async {
|
||||
if (value == null) return;
|
||||
await ref
|
||||
.read(preferencesRepositoryProvider.notifier)
|
||||
.setThemeColor(value);
|
||||
setState(() => _themeColor = value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
|
|
@ -190,6 +208,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
preferencesRepositoryProvider.notifier,
|
||||
);
|
||||
if (_theme != null) await n.setTheme(_theme!);
|
||||
if (_themeColor != null)
|
||||
await n.setThemeColor(_themeColor!);
|
||||
if (_language != null) await n.setLanguage(_language!);
|
||||
if (_exportDpi != null) await n.setExportDpi(_exportDpi!);
|
||||
// pageView not configurable anymore
|
||||
|
|
@ -206,3 +226,90 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorDot extends StatelessWidget {
|
||||
final Color color;
|
||||
final double size;
|
||||
const _ColorDot({required this.color, this.size = 14});
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ThemeColorCircle extends ConsumerWidget {
|
||||
final ValueChanged<String?> onPick;
|
||||
const _ThemeColorCircle({required this.onPick});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final seed = ref.watch(themeSeedColorProvider);
|
||||
return InkWell(
|
||||
key: const Key('btn_theme_color_picker'),
|
||||
onTap: () async {
|
||||
final picked = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => _ThemeColorPickerDialog(currentColor: seed),
|
||||
);
|
||||
onPick(picked);
|
||||
},
|
||||
customBorder: const CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: _ColorDot(color: seed, size: 22),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeColorPickerDialog extends StatelessWidget {
|
||||
final Color currentColor;
|
||||
const _ThemeColorPickerDialog({required this.currentColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return AlertDialog(
|
||||
title: Text(l.themeColor),
|
||||
content: SizedBox(
|
||||
width: 320,
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: Colors.primaries.map((mat) {
|
||||
final c = Color(mat.value);
|
||||
final selected = c.value == currentColor.value;
|
||||
// Store as ARGB hex string, e.g., #FF2196F3
|
||||
String hex(Color color) =>
|
||||
'#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
|
||||
return InkWell(
|
||||
key: Key('pick_${mat.value}'),
|
||||
onTap: () => Navigator.of(context).pop(hex(c)),
|
||||
customBorder: const CircleBorder(),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
_ColorDot(color: c, size: 32),
|
||||
if (selected)
|
||||
const Icon(Icons.check, color: Colors.white, size: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
child: Text(l.cancel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,16 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// Subtle drag affordance icon
|
||||
Positioned(
|
||||
left: 4,
|
||||
bottom: 4,
|
||||
child: Icon(
|
||||
Icons.open_with,
|
||||
size: 14,
|
||||
color: Theme.of(context).hintColor.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
|
|
@ -137,7 +147,9 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
|||
child: child,
|
||||
);
|
||||
if (widget.disabled) return child;
|
||||
return Draggable<SignatureDragData>(
|
||||
final isDragging = ref.watch(isDraggingSignatureViewModelProvider);
|
||||
// Mouse cursor + tooltip + semantics to hint drag behavior
|
||||
final draggable = Draggable<SignatureDragData>(
|
||||
data: SignatureDragData(
|
||||
card: domain.SignatureCard(
|
||||
asset: widget.asset,
|
||||
|
|
@ -187,5 +199,17 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
|||
childWhenDragging: Opacity(opacity: 0.5, child: child),
|
||||
child: child,
|
||||
);
|
||||
return MouseRegion(
|
||||
cursor:
|
||||
isDragging ? SystemMouseCursors.grabbing : SystemMouseCursors.grab,
|
||||
child: Tooltip(
|
||||
message: AppLocalizations.of(context).dragOntoDocument,
|
||||
child: Semantics(
|
||||
label: 'Signature card',
|
||||
hint: 'Drag onto document to place',
|
||||
child: draggable,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:pdf_signature/domain/models/signature_asset.dart';
|
|||
import 'package:image/image.dart' as img;
|
||||
import 'image_editor_dialog.dart';
|
||||
import 'signature_card_view.dart';
|
||||
import '../../pdf/view_model/pdf_view_model.dart';
|
||||
// Removed PdfViewModel import; no direct interaction from drawer on tap
|
||||
|
||||
/// Data for drag-and-drop is in signature_drag_data.dart
|
||||
|
||||
|
|
@ -78,13 +78,6 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
|||
.update(card, result.rotation, result.graphicAdjust);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
// Activate a default overlay rectangle on the current page
|
||||
// so integration tests can find and size the active overlay.
|
||||
ref
|
||||
.read(pdfViewModelProvider.notifier)
|
||||
.activeRect = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import 'package:image/image.dart' as img;
|
|||
/// - Ensures the image has an alpha channel (RGBA) before modification.
|
||||
/// - Returns a new img.Image instance; does not mutate the input reference.
|
||||
/// - threshold: 0..255; pixels with r,g,b >= threshold become fully transparent.
|
||||
///
|
||||
/// TODO: optimize through SIMD or web-ffi openCV, sadly they are not stable yet.
|
||||
///
|
||||
img.Image removeNearWhiteBackground(img.Image image, {int threshold = 240}) {
|
||||
// Ensure truecolor RGBA; paletted images won't apply per-pixel alpha properly.
|
||||
final hadAlpha = image.hasAlpha;
|
||||
|
|
|
|||
Loading…
Reference in New Issue