diff --git a/docs/meta-arch.md b/docs/meta-arch.md index 84c8846..bc49733 100644 --- a/docs/meta-arch.md +++ b/docs/meta-arch.md @@ -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. diff --git a/lib/app.dart b/lib/app.dart index d967cec..2a78a86 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -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, ), ), diff --git a/lib/data/repositories/preferences_repository.dart b/lib/data/repositories/preferences_repository.dart index da0a6bf..a6d28e6 100644 --- a/lib/data/repositories/preferences_repository.dart +++ b/lib/data/repositories/preferences_repository.dart @@ -28,7 +28,9 @@ Set _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 { 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 { .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 { 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 setTheme(String theme) async { @@ -123,6 +187,14 @@ class PreferencesStateNotifier extends StateNotifier { await prefs.setString(_kLanguage, normalized); } + Future 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 resetToDefaults() async { final device = WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(); @@ -131,12 +203,13 @@ class PreferencesStateNotifier extends StateNotifier { 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 setExportDpi(double dpi) async { @@ -182,6 +255,13 @@ final themeModeProvider = Provider((ref) { } }); +/// Maps the selected theme color name to an actual Color for theming. +final themeSeedColorProvider = Provider((ref) { + final prefs = ref.watch(preferencesRepositoryProvider); + final c = PreferencesStateNotifier._tryParseColor(prefs.theme_color); + return c ?? Colors.blue; +}); + final localeProvider = Provider((ref) { final prefs = ref.watch(preferencesRepositoryProvider); final supported = _supportedTags(); diff --git a/lib/data/repositories/signature_card_repository.dart b/lib/data/repositories/signature_card_repository.dart index 1269162..d936010 100644 --- a/lib/data/repositories/signature_card_repository.dart +++ b/lib/data/repositories/signature_card_repository.dart @@ -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; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e0df98a..e13757f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2e45911..616fa46 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 2554422..8e51f27 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f2c23cf..5099f3c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index cef5741..501802a 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -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": "ロック解除" } \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 5572d80..8b61a1f 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -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": "잠금 해제" } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 03cd914..17e5340 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -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": "Відмкнути" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 38494e3..adaac0a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -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": "解锁" } \ No newline at end of file diff --git a/lib/l10n/app_zh_CN.arb b/lib/l10n/app_zh_CN.arb index 1a611dc..a1a1c16 100644 --- a/lib/l10n/app_zh_CN.arb +++ b/lib/l10n/app_zh_CN.arb @@ -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": "解锁" } \ No newline at end of file diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb index feeb299..b5aa4e9 100644 --- a/lib/l10n/app_zh_TW.arb +++ b/lib/l10n/app_zh_TW.arb @@ -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": "解鎖" } \ No newline at end of file diff --git a/lib/ui/features/pdf/widgets/draw_canvas.dart b/lib/ui/features/pdf/widgets/draw_canvas.dart index 30ce0b1..642aca0 100644 --- a/lib/ui/features/pdf/widgets/draw_canvas.dart +++ b/lib/ui/features/pdf/widgets/draw_canvas.dart @@ -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? 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 createState() => _DrawCanvasState(); diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index 4c86cf6..c8de024 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -98,6 +98,17 @@ class _PdfSignatureHomePageState extends ConsumerState { 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 _loadSignatureFromFile() async { final typeGroup = fs.XTypeGroup( label: @@ -109,8 +120,7 @@ class _PdfSignatureHomePageState extends ConsumerState { 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 { 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; } diff --git a/lib/ui/features/preferences/widgets/settings_screen.dart b/lib/ui/features/preferences/widgets/settings_screen.dart index a3c9a87..1242dd3 100644 --- a/lib/ui/features/preferences/widgets/settings_screen.dart +++ b/lib/ui/features/preferences/widgets/settings_screen.dart @@ -12,6 +12,7 @@ class SettingsDialog extends ConsumerStatefulWidget { class _SettingsDialogState extends ConsumerState { String? _theme; + String? _themeColor; String? _language; // Page view removed; continuous-only double? _exportDpi; @@ -21,6 +22,7 @@ class _SettingsDialogState extends ConsumerState { 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 { ), ], ), + 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 { 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 { ); } } + +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 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( + 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), + ), + ], + ); + } +} diff --git a/lib/ui/features/signature/widgets/signature_card_view.dart b/lib/ui/features/signature/widgets/signature_card_view.dart index 87c8800..ffd9c63 100644 --- a/lib/ui/features/signature/widgets/signature_card_view.dart +++ b/lib/ui/features/signature/widgets/signature_card_view.dart @@ -106,6 +106,16 @@ class _SignatureCardViewState extends ConsumerState { ), ), ), + // 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 { child: child, ); if (widget.disabled) return child; - return Draggable( + final isDragging = ref.watch(isDraggingSignatureViewModelProvider); + // Mouse cursor + tooltip + semantics to hint drag behavior + final draggable = Draggable( data: SignatureDragData( card: domain.SignatureCard( asset: widget.asset, @@ -187,5 +199,17 @@ class _SignatureCardViewState extends ConsumerState { 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, + ), + ), + ); } } diff --git a/lib/ui/features/signature/widgets/signature_drawer.dart b/lib/ui/features/signature/widgets/signature_drawer.dart index edfb0cf..77fc291 100644 --- a/lib/ui/features/signature/widgets/signature_drawer.dart +++ b/lib/ui/features/signature/widgets/signature_drawer.dart @@ -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 { .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); - }, ), ), ), diff --git a/lib/utils/background_removal.dart b/lib/utils/background_removal.dart index 4a48edf..d5df141 100644 --- a/lib/utils/background_removal.dart +++ b/lib/utils/background_removal.dart @@ -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;