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)
|
* [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)
|
* [Per-page Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pageOverlaysBuilder.html)
|
||||||
* `pageOverlaysBuilder`
|
* `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: (_) {
|
data: (_) {
|
||||||
final themeMode = ref.watch(themeModeProvider);
|
final themeMode = ref.watch(themeModeProvider);
|
||||||
|
final seed = ref.watch(themeSeedColorProvider);
|
||||||
final appLocale = ref.watch(localeProvider);
|
final appLocale = ref.watch(localeProvider);
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
|
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: Colors.indigo,
|
seedColor: seed,
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: Colors.indigo,
|
seedColor: seed,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ Set<String> _supportedTags() {
|
||||||
|
|
||||||
// Keys
|
// Keys
|
||||||
const _kTheme = 'theme'; // 'light'|'dark'|'system'
|
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 _kLanguage = 'language'; // BCP-47 tag like 'en', 'zh-TW', 'es'
|
||||||
const _kPageView = 'page_view'; // now only 'continuous'
|
const _kPageView = 'page_view'; // now only 'continuous'
|
||||||
const _kExportDpi = 'export_dpi'; // double, allowed: 96,144,200,300
|
const _kExportDpi = 'export_dpi'; // double, allowed: 96,144,200,300
|
||||||
|
|
@ -67,6 +69,54 @@ String _normalizeLanguageTag(String tag) {
|
||||||
|
|
||||||
class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
||||||
final SharedPreferences prefs;
|
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)
|
PreferencesStateNotifier(this.prefs)
|
||||||
: super(
|
: super(
|
||||||
PreferencesState(
|
PreferencesState(
|
||||||
|
|
@ -77,7 +127,7 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
||||||
.toLanguageTag(),
|
.toLanguageTag(),
|
||||||
),
|
),
|
||||||
exportDpi: _readDpi(prefs),
|
exportDpi: _readDpi(prefs),
|
||||||
theme_color: prefs.getString(_kThemeColor) ?? 'blue',
|
theme_color: prefs.getString(_kThemeColor) ?? '#FF2196F3', // blue
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
// normalize language to supported/fallback
|
// normalize language to supported/fallback
|
||||||
|
|
@ -108,6 +158,20 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
||||||
state = state.copyWith(exportDpi: 144.0);
|
state = state.copyWith(exportDpi: 144.0);
|
||||||
prefs.setDouble(_kExportDpi, 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 {
|
Future<void> setTheme(String theme) async {
|
||||||
|
|
@ -123,6 +187,14 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
||||||
await prefs.setString(_kLanguage, normalized);
|
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 {
|
Future<void> resetToDefaults() async {
|
||||||
final device =
|
final device =
|
||||||
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag();
|
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag();
|
||||||
|
|
@ -131,12 +203,13 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
language: normalized,
|
language: normalized,
|
||||||
exportDpi: 144.0,
|
exportDpi: 144.0,
|
||||||
theme_color: '',
|
theme_color: '#FF2196F3',
|
||||||
);
|
);
|
||||||
await prefs.setString(_kTheme, 'system');
|
await prefs.setString(_kTheme, 'system');
|
||||||
await prefs.setString(_kLanguage, normalized);
|
await prefs.setString(_kLanguage, normalized);
|
||||||
await prefs.setString(_kPageView, 'continuous');
|
await prefs.setString(_kPageView, 'continuous');
|
||||||
await prefs.setDouble(_kExportDpi, 144.0);
|
await prefs.setDouble(_kExportDpi, 144.0);
|
||||||
|
await prefs.setString(_kThemeColor, '#FF2196F3');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setExportDpi(double dpi) async {
|
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 localeProvider = Provider<Locale?>((ref) {
|
||||||
final prefs = ref.watch(preferencesRepositoryProvider);
|
final prefs = ref.watch(preferencesRepositoryProvider);
|
||||||
final supported = _supportedTags();
|
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.
|
/// Invalidate the cached processed image, forcing recompute next time.
|
||||||
void invalidateCache() {
|
void invalidateCache() {
|
||||||
_cachedProcessedImage = null;
|
_cachedProcessedImage = null;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "Anzeige",
|
"display": "Anzeige",
|
||||||
"downloadStarted": "Download gestartet",
|
"downloadStarted": "Download gestartet",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "Auf Dokument ziehen",
|
||||||
"drawSignature": "Signatur zeichnen",
|
"drawSignature": "Signatur zeichnen",
|
||||||
"errorWithMessage": "Fehler: {message}",
|
"errorWithMessage": "Fehler: {message}",
|
||||||
"exportingPleaseWait": "Exportiere… Bitte warten",
|
"exportingPleaseWait": "Exportiere… Bitte warten",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "Dunkel",
|
"themeDark": "Dunkel",
|
||||||
"themeLight": "Hell",
|
"themeLight": "Hell",
|
||||||
"themeSystem": "System",
|
"themeSystem": "System",
|
||||||
|
"themeColor": "Themenfarbe",
|
||||||
|
"themeColorBlue": "Blau",
|
||||||
|
"themeColorGreen": "Grün",
|
||||||
|
"themeColorRed": "Rot",
|
||||||
|
"themeColorPurple": "Lila",
|
||||||
"undo": "Rückgängig",
|
"undo": "Rückgängig",
|
||||||
"unlock": "Entsperren"
|
"unlock": "Entsperren"
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
"@downloadStarted": {},
|
"@downloadStarted": {},
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
"@dpi": {},
|
"@dpi": {},
|
||||||
|
"dragOntoDocument": "Drag onto document",
|
||||||
|
"@dragOntoDocument": {
|
||||||
|
"description": "Tooltip message for dragging signature card onto PDF document"
|
||||||
|
},
|
||||||
"drawSignature": "Draw Signature",
|
"drawSignature": "Draw Signature",
|
||||||
"@drawSignature": {},
|
"@drawSignature": {},
|
||||||
"errorWithMessage": "Error: {message}",
|
"errorWithMessage": "Error: {message}",
|
||||||
|
|
@ -120,6 +124,16 @@
|
||||||
"@themeLight": {},
|
"@themeLight": {},
|
||||||
"themeSystem": "System",
|
"themeSystem": "System",
|
||||||
"@themeSystem": {},
|
"@themeSystem": {},
|
||||||
|
"themeColor": "Theme color",
|
||||||
|
"@themeColor": {},
|
||||||
|
"themeColorBlue": "Blue",
|
||||||
|
"@themeColorBlue": {},
|
||||||
|
"themeColorGreen": "Green",
|
||||||
|
"@themeColorGreen": {},
|
||||||
|
"themeColorRed": "Red",
|
||||||
|
"@themeColorRed": {},
|
||||||
|
"themeColorPurple": "Purple",
|
||||||
|
"@themeColorPurple": {},
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"@undo": {},
|
"@undo": {},
|
||||||
"unlock": "Unlock",
|
"unlock": "Unlock",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "Pantalla",
|
"display": "Pantalla",
|
||||||
"downloadStarted": "Descarga iniciada",
|
"downloadStarted": "Descarga iniciada",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "Arrastra sobre el documento",
|
||||||
"drawSignature": "Dibujar firma",
|
"drawSignature": "Dibujar firma",
|
||||||
"errorWithMessage": "Error: {message}",
|
"errorWithMessage": "Error: {message}",
|
||||||
"exportingPleaseWait": "Exportando... Por favor, espere",
|
"exportingPleaseWait": "Exportando... Por favor, espere",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "Oscuro",
|
"themeDark": "Oscuro",
|
||||||
"themeLight": "Claro",
|
"themeLight": "Claro",
|
||||||
"themeSystem": "Sistema",
|
"themeSystem": "Sistema",
|
||||||
|
"themeColor": "Color del tema",
|
||||||
|
"themeColorBlue": "Azul",
|
||||||
|
"themeColorGreen": "Verde",
|
||||||
|
"themeColorRed": "Rojo",
|
||||||
|
"themeColorPurple": "Púrpura",
|
||||||
"undo": "Deshacer",
|
"undo": "Deshacer",
|
||||||
"unlock": "Desbloquear"
|
"unlock": "Desbloquear"
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "Affichage",
|
"display": "Affichage",
|
||||||
"downloadStarted": "Téléchargement commencé",
|
"downloadStarted": "Téléchargement commencé",
|
||||||
"dpi": "DPI :",
|
"dpi": "DPI :",
|
||||||
|
"dragOntoDocument": "Faites glisser sur le document",
|
||||||
"drawSignature": "Dessiner une signature",
|
"drawSignature": "Dessiner une signature",
|
||||||
"errorWithMessage": "Erreur : {message}",
|
"errorWithMessage": "Erreur : {message}",
|
||||||
"exportingPleaseWait": "Exportation… Veuillez patienter",
|
"exportingPleaseWait": "Exportation… Veuillez patienter",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "Sombre",
|
"themeDark": "Sombre",
|
||||||
"themeLight": "Clair",
|
"themeLight": "Clair",
|
||||||
"themeSystem": "Système",
|
"themeSystem": "Système",
|
||||||
|
"themeColor": "Couleur du thème",
|
||||||
|
"themeColorBlue": "Bleu",
|
||||||
|
"themeColorGreen": "Vert",
|
||||||
|
"themeColorRed": "Rouge",
|
||||||
|
"themeColorPurple": "Violet",
|
||||||
"undo": "Annuler",
|
"undo": "Annuler",
|
||||||
"unlock": "Déverrouiller"
|
"unlock": "Déverrouiller"
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "表示",
|
"display": "表示",
|
||||||
"downloadStarted": "ダウンロード開始",
|
"downloadStarted": "ダウンロード開始",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "ドキュメントにドラッグします",
|
||||||
"drawSignature": "署名をかく",
|
"drawSignature": "署名をかく",
|
||||||
"errorWithMessage": "エラー:{message}",
|
"errorWithMessage": "エラー:{message}",
|
||||||
"exportingPleaseWait": "エクスポート中…お待ちください",
|
"exportingPleaseWait": "エクスポート中…お待ちください",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "ダーク",
|
"themeDark": "ダーク",
|
||||||
"themeLight": "ライト",
|
"themeLight": "ライト",
|
||||||
"themeSystem": "システム",
|
"themeSystem": "システム",
|
||||||
|
"themeColor": "テーマカラー",
|
||||||
|
"themeColorBlue": "青",
|
||||||
|
"themeColorGreen": "緑",
|
||||||
|
"themeColorRed": "赤",
|
||||||
|
"themeColorPurple": "紫",
|
||||||
"undo": "元に戻す",
|
"undo": "元に戻す",
|
||||||
"unlock": "ロック解除"
|
"unlock": "ロック解除"
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "표시",
|
"display": "표시",
|
||||||
"downloadStarted": "다운로드 시작됨",
|
"downloadStarted": "다운로드 시작됨",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "문서로 끌어다 놓습니다",
|
||||||
"drawSignature": "서명 그리기",
|
"drawSignature": "서명 그리기",
|
||||||
"errorWithMessage": "오류: {message}",
|
"errorWithMessage": "오류: {message}",
|
||||||
"exportingPleaseWait": "내보내는 중... 잠시 기다려주세요",
|
"exportingPleaseWait": "내보내는 중... 잠시 기다려주세요",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "다크",
|
"themeDark": "다크",
|
||||||
"themeLight": "라이트",
|
"themeLight": "라이트",
|
||||||
"themeSystem": "시스템",
|
"themeSystem": "시스템",
|
||||||
|
"themeColor": "테마 색상",
|
||||||
|
"themeColorBlue": "파란색",
|
||||||
|
"themeColorGreen": "녹색",
|
||||||
|
"themeColorRed": "빨간색",
|
||||||
|
"themeColorPurple": "보라색",
|
||||||
"undo": "실행 취소",
|
"undo": "실행 취소",
|
||||||
"unlock": "잠금 해제"
|
"unlock": "잠금 해제"
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "Відображення",
|
"display": "Відображення",
|
||||||
"downloadStarted": "Завантаження розпочато",
|
"downloadStarted": "Завантаження розпочато",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "Перетягніть на документ",
|
||||||
"drawSignature": "Намалювати підпис",
|
"drawSignature": "Намалювати підпис",
|
||||||
"errorWithMessage": "Помилка: {message}",
|
"errorWithMessage": "Помилка: {message}",
|
||||||
"exportingPleaseWait": "Експортування... Зачекайте",
|
"exportingPleaseWait": "Експортування... Зачекайте",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "Темна",
|
"themeDark": "Темна",
|
||||||
"themeLight": "Світла",
|
"themeLight": "Світла",
|
||||||
"themeSystem": "Системна",
|
"themeSystem": "Системна",
|
||||||
|
"themeColor": "Колір теми",
|
||||||
|
"themeColorBlue": "Синій",
|
||||||
|
"themeColorGreen": "Зелений",
|
||||||
|
"themeColorRed": "Червоний",
|
||||||
|
"themeColorPurple": "Фіолетовий",
|
||||||
"undo": "Відмінити",
|
"undo": "Відмінити",
|
||||||
"unlock": "Відмкнути"
|
"unlock": "Відмкнути"
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"display": "顯示",
|
"display": "顯示",
|
||||||
"downloadStarted": "已開始下載",
|
"downloadStarted": "已開始下載",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "拖到文档上",
|
||||||
"drawSignature": "手寫簽名",
|
"drawSignature": "手寫簽名",
|
||||||
"errorWithMessage": "錯誤:{message}",
|
"errorWithMessage": "錯誤:{message}",
|
||||||
"exportingPleaseWait": "匯出中…請稍候",
|
"exportingPleaseWait": "匯出中…請稍候",
|
||||||
|
|
@ -48,6 +49,11 @@
|
||||||
"themeDark": "深色",
|
"themeDark": "深色",
|
||||||
"themeLight": "淺色",
|
"themeLight": "淺色",
|
||||||
"themeSystem": "系統",
|
"themeSystem": "系統",
|
||||||
|
"themeColor": "主题颜色",
|
||||||
|
"themeColorBlue": "蓝色",
|
||||||
|
"themeColorGreen": "绿色",
|
||||||
|
"themeColorRed": "红色",
|
||||||
|
"themeColorPurple": "紫色",
|
||||||
"undo": "復原",
|
"undo": "復原",
|
||||||
"unlock": "解锁"
|
"unlock": "解锁"
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"display": "显示",
|
"display": "显示",
|
||||||
"downloadStarted": "下载已开始",
|
"downloadStarted": "下载已开始",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "拖到文档上",
|
||||||
"drawSignature": "绘制签名",
|
"drawSignature": "绘制签名",
|
||||||
"errorWithMessage": "错误:{message}",
|
"errorWithMessage": "错误:{message}",
|
||||||
"exportingPleaseWait": "正在导出... 请稍候",
|
"exportingPleaseWait": "正在导出... 请稍候",
|
||||||
|
|
@ -47,6 +48,11 @@
|
||||||
"themeDark": "深色",
|
"themeDark": "深色",
|
||||||
"themeLight": "浅色",
|
"themeLight": "浅色",
|
||||||
"themeSystem": "系统",
|
"themeSystem": "系统",
|
||||||
|
"themeColor": "主题颜色",
|
||||||
|
"themeColorBlue": "蓝色",
|
||||||
|
"themeColorGreen": "绿色",
|
||||||
|
"themeColorRed": "红色",
|
||||||
|
"themeColorPurple": "紫色",
|
||||||
"undo": "撤销",
|
"undo": "撤销",
|
||||||
"unlock": "解锁"
|
"unlock": "解锁"
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"display": "顯示",
|
"display": "顯示",
|
||||||
"downloadStarted": "已開始下載",
|
"downloadStarted": "已開始下載",
|
||||||
"dpi": "DPI",
|
"dpi": "DPI",
|
||||||
|
"dragOntoDocument": "拖曳到文件",
|
||||||
"drawSignature": "手寫簽名",
|
"drawSignature": "手寫簽名",
|
||||||
"errorWithMessage": "錯誤:{message}",
|
"errorWithMessage": "錯誤:{message}",
|
||||||
"exportingPleaseWait": "匯出中…請稍候",
|
"exportingPleaseWait": "匯出中…請稍候",
|
||||||
|
|
@ -48,6 +49,11 @@
|
||||||
"themeDark": "深色",
|
"themeDark": "深色",
|
||||||
"themeLight": "淺色",
|
"themeLight": "淺色",
|
||||||
"themeSystem": "系統",
|
"themeSystem": "系統",
|
||||||
|
"themeColor": "主題顏色",
|
||||||
|
"themeColorBlue": "藍色",
|
||||||
|
"themeColorGreen": "綠色",
|
||||||
|
"themeColorRed": "紅色",
|
||||||
|
"themeColorPurple": "紫色",
|
||||||
"undo": "復原",
|
"undo": "復原",
|
||||||
"unlock": "解鎖"
|
"unlock": "解鎖"
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,6 @@ class DrawCanvas extends StatefulWidget {
|
||||||
this.control,
|
this.control,
|
||||||
this.onConfirm,
|
this.onConfirm,
|
||||||
this.debugBytesSink,
|
this.debugBytesSink,
|
||||||
this.closeOnConfirmImmediately = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final hand.HandSignatureControl? control;
|
final hand.HandSignatureControl? control;
|
||||||
|
|
@ -18,9 +17,6 @@ class DrawCanvas extends StatefulWidget {
|
||||||
// For tests: allows observing exported bytes without relying on Navigator
|
// For tests: allows observing exported bytes without relying on Navigator
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
final ValueNotifier<Uint8List?>? debugBytesSink;
|
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
|
@override
|
||||||
State<DrawCanvas> createState() => _DrawCanvasState();
|
State<DrawCanvas> createState() => _DrawCanvasState();
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
if (controller.isReady) controller.goToPage(pageNumber: target);
|
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 {
|
Future<img.Image?> _loadSignatureFromFile() async {
|
||||||
final typeGroup = fs.XTypeGroup(
|
final typeGroup = fs.XTypeGroup(
|
||||||
label:
|
label:
|
||||||
|
|
@ -109,8 +120,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
try {
|
try {
|
||||||
var sigImage = img.decodeImage(bytes);
|
var sigImage = img.decodeImage(bytes);
|
||||||
sigImage?.convert(numChannels: 4);
|
return _toStdSignatureImage(sigImage);
|
||||||
return sigImage;
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -121,14 +131,13 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
enableDrag: false,
|
enableDrag: false,
|
||||||
builder: (_) => const DrawCanvas(closeOnConfirmImmediately: false),
|
builder: (_) => const DrawCanvas(),
|
||||||
);
|
);
|
||||||
if (result == null || result.isEmpty) return null;
|
if (result == null || result.isEmpty) return null;
|
||||||
// In simplified UI, adding to library isn't implemented
|
// In simplified UI, adding to library isn't implemented
|
||||||
try {
|
try {
|
||||||
var sigImage = img.decodeImage(result);
|
var sigImage = img.decodeImage(result);
|
||||||
sigImage?.convert(numChannels: 4);
|
return _toStdSignatureImage(sigImage);
|
||||||
return sigImage;
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class SettingsDialog extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
String? _theme;
|
String? _theme;
|
||||||
|
String? _themeColor;
|
||||||
String? _language;
|
String? _language;
|
||||||
// Page view removed; continuous-only
|
// Page view removed; continuous-only
|
||||||
double? _exportDpi;
|
double? _exportDpi;
|
||||||
|
|
@ -21,6 +22,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
super.initState();
|
super.initState();
|
||||||
final prefs = ref.read(preferencesRepositoryProvider);
|
final prefs = ref.read(preferencesRepositoryProvider);
|
||||||
_theme = prefs.theme;
|
_theme = prefs.theme;
|
||||||
|
_themeColor = prefs.theme_color;
|
||||||
_language = prefs.language;
|
_language = prefs.language;
|
||||||
_exportDpi = prefs.exportDpi;
|
_exportDpi = prefs.exportDpi;
|
||||||
// pageView no longer configurable (continuous-only)
|
// 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),
|
const SizedBox(height: 24),
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -190,6 +208,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
preferencesRepositoryProvider.notifier,
|
preferencesRepositoryProvider.notifier,
|
||||||
);
|
);
|
||||||
if (_theme != null) await n.setTheme(_theme!);
|
if (_theme != null) await n.setTheme(_theme!);
|
||||||
|
if (_themeColor != null)
|
||||||
|
await n.setThemeColor(_themeColor!);
|
||||||
if (_language != null) await n.setLanguage(_language!);
|
if (_language != null) await n.setLanguage(_language!);
|
||||||
if (_exportDpi != null) await n.setExportDpi(_exportDpi!);
|
if (_exportDpi != null) await n.setExportDpi(_exportDpi!);
|
||||||
// pageView not configurable anymore
|
// 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(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
@ -137,7 +147,9 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
if (widget.disabled) return 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(
|
data: SignatureDragData(
|
||||||
card: domain.SignatureCard(
|
card: domain.SignatureCard(
|
||||||
asset: widget.asset,
|
asset: widget.asset,
|
||||||
|
|
@ -187,5 +199,17 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
|
||||||
childWhenDragging: Opacity(opacity: 0.5, child: child),
|
childWhenDragging: Opacity(opacity: 0.5, child: child),
|
||||||
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 'package:image/image.dart' as img;
|
||||||
import 'image_editor_dialog.dart';
|
import 'image_editor_dialog.dart';
|
||||||
import 'signature_card_view.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
|
/// 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);
|
.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.
|
/// - Ensures the image has an alpha channel (RGBA) before modification.
|
||||||
/// - Returns a new img.Image instance; does not mutate the input reference.
|
/// - Returns a new img.Image instance; does not mutate the input reference.
|
||||||
/// - threshold: 0..255; pixels with r,g,b >= threshold become fully transparent.
|
/// - 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}) {
|
img.Image removeNearWhiteBackground(img.Image image, {int threshold = 240}) {
|
||||||
// Ensure truecolor RGBA; paletted images won't apply per-pixel alpha properly.
|
// Ensure truecolor RGBA; paletted images won't apply per-pixel alpha properly.
|
||||||
final hadAlpha = image.hasAlpha;
|
final hadAlpha = image.hasAlpha;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue