refactor: adjust some text layout in pdf toolbar

This commit is contained in:
insleker 2025-09-04 16:59:43 +08:00
parent a4890b6ea0
commit dc1304f973
19 changed files with 167 additions and 80 deletions

View File

@ -21,13 +21,13 @@ flutter analyze
# > run unit tests and widget tests # > run unit tests and widget tests
flutter test flutter test
# > run integration tests # > run integration tests
flutter test integration_test/ -d linux flutter test integration_test/ -d <device_id>
# dart run tool/gen_view_wireframe_md.dart # dart run tool/gen_view_wireframe_md.dart
# flutter pub run dead_code_analyzer # flutter pub run dead_code_analyzer
# run the app # run the app
flutter run flutter run -d <device_id>
``` ```
### build ### build

View File

@ -45,6 +45,7 @@ void main() {
child: const MaterialApp( child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(),
), ),
), ),
@ -96,6 +97,7 @@ void main() {
child: const MaterialApp( child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(),
), ),
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart' as pp; import 'package:path_provider/path_provider.dart' as pp;
import 'package:file_selector/file_selector.dart' as fs; import 'package:file_selector/file_selector.dart' as fs;
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/data/services/preferences_providers.dart';
// Feature-scoped DI and configuration providers // Feature-scoped DI and configuration providers
@ -11,8 +12,19 @@ final useMockViewerProvider = Provider<bool>((_) => false);
// Export service injection for testability // Export service injection for testability
final exportServiceProvider = Provider<ExportService>((_) => ExportService()); final exportServiceProvider = Provider<ExportService>((_) => ExportService());
// Export DPI setting (points per inch mapping), default 144 DPI // Export DPI setting (points per inch mapping). Reads from SharedPreferences when available,
final exportDpiProvider = StateProvider<double>((_) => 144.0); // otherwise falls back to 144.0 to keep tests deterministic without bootstrapping prefs.
final exportDpiProvider = Provider<double>((ref) {
final sp = ref.watch(sharedPreferencesProvider);
return sp.maybeWhen(
data: (prefs) {
const allowed = [96.0, 144.0, 200.0, 300.0];
final v = prefs.getDouble('export_dpi');
return (v != null && allowed.contains(v)) ? v : 144.0;
},
orElse: () => 144.0,
);
});
// Controls whether signature overlay is visible (used to hide on non-stamped pages during export) // Controls whether signature overlay is visible (used to hide on non-stamped pages during export)
final signatureVisibilityProvider = StateProvider<bool>((_) => true); final signatureVisibilityProvider = StateProvider<bool>((_) => true);

View File

@ -29,6 +29,7 @@ Set<String> _supportedTags() {
const _kTheme = 'theme'; // 'light'|'dark'|'system' const _kTheme = 'theme'; // 'light'|'dark'|'system'
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
String _normalizeLanguageTag(String tag) { String _normalizeLanguageTag(String tag) {
final tags = _supportedTags(); final tags = _supportedTags();
@ -66,20 +67,24 @@ class PreferencesState {
final String theme; // 'light' | 'dark' | 'system' final String theme; // 'light' | 'dark' | 'system'
final String language; // 'en' | 'zh-TW' | 'es' final String language; // 'en' | 'zh-TW' | 'es'
final String pageView; // only 'continuous' final String pageView; // only 'continuous'
final double exportDpi; // 96.0 | 144.0 | 200.0 | 300.0
const PreferencesState({ const PreferencesState({
required this.theme, required this.theme,
required this.language, required this.language,
required this.pageView, required this.pageView,
required this.exportDpi,
}); });
PreferencesState copyWith({ PreferencesState copyWith({
String? theme, String? theme,
String? language, String? language,
String? pageView, String? pageView,
double? exportDpi,
}) => PreferencesState( }) => PreferencesState(
theme: theme ?? this.theme, theme: theme ?? this.theme,
language: language ?? this.language, language: language ?? this.language,
pageView: pageView ?? this.pageView, pageView: pageView ?? this.pageView,
exportDpi: exportDpi ?? this.exportDpi,
); );
} }
@ -95,12 +100,20 @@ class PreferencesNotifier extends StateNotifier<PreferencesState> {
.toLanguageTag(), .toLanguageTag(),
), ),
pageView: prefs.getString(_kPageView) ?? 'continuous', pageView: prefs.getString(_kPageView) ?? 'continuous',
exportDpi: _readDpi(prefs),
), ),
) { ) {
// normalize language to supported/fallback // normalize language to supported/fallback
_ensureValid(); _ensureValid();
} }
static double _readDpi(SharedPreferences prefs) {
final d = prefs.getDouble(_kExportDpi);
if (d == null) return 144.0;
const allowed = [96.0, 144.0, 200.0, 300.0];
return allowed.contains(d) ? d : 144.0;
}
void _ensureValid() { void _ensureValid() {
final themeValid = {'light', 'dark', 'system'}; final themeValid = {'light', 'dark', 'system'};
if (!themeValid.contains(state.theme)) { if (!themeValid.contains(state.theme)) {
@ -117,6 +130,12 @@ class PreferencesNotifier extends StateNotifier<PreferencesState> {
state = state.copyWith(pageView: 'continuous'); state = state.copyWith(pageView: 'continuous');
prefs.setString(_kPageView, 'continuous'); prefs.setString(_kPageView, 'continuous');
} }
// Ensure DPI is one of allowed values
const allowed = [96.0, 144.0, 200.0, 300.0];
if (!allowed.contains(state.exportDpi)) {
state = state.copyWith(exportDpi: 144.0);
prefs.setDouble(_kExportDpi, 144.0);
}
} }
Future<void> setTheme(String theme) async { Future<void> setTheme(String theme) async {
@ -140,10 +159,12 @@ class PreferencesNotifier extends StateNotifier<PreferencesState> {
theme: 'system', theme: 'system',
language: normalized, language: normalized,
pageView: 'continuous', pageView: 'continuous',
exportDpi: 144.0,
); );
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);
} }
Future<void> setPageView(String pageView) async { Future<void> setPageView(String pageView) async {
@ -152,6 +173,13 @@ class PreferencesNotifier extends StateNotifier<PreferencesState> {
state = state.copyWith(pageView: pageView); state = state.copyWith(pageView: pageView);
await prefs.setString(_kPageView, pageView); await prefs.setString(_kPageView, pageView);
} }
Future<void> setExportDpi(double dpi) async {
const allowed = [96.0, 144.0, 200.0, 300.0];
if (!allowed.contains(dpi)) return;
state = state.copyWith(exportDpi: dpi);
await prefs.setDouble(_kExportDpi, dpi);
}
} }
final sharedPreferencesProvider = FutureProvider<SharedPreferences>(( final sharedPreferencesProvider = FutureProvider<SharedPreferences>((

View File

@ -11,7 +11,7 @@
"delete": "Löschen", "delete": "Löschen",
"display": "Anzeige", "display": "Anzeige",
"downloadStarted": "Download gestartet", "downloadStarted": "Download gestartet",
"dpi": "DPI:", "dpi": "DPI",
"drawSignature": "Signatur zeichnen", "drawSignature": "Signatur zeichnen",
"errorWithMessage": "Fehler: {message}", "errorWithMessage": "Fehler: {message}",
"exportingPleaseWait": "Exportiere… Bitte warten", "exportingPleaseWait": "Exportiere… Bitte warten",
@ -19,6 +19,7 @@
"failedToSavePdf": "PDF konnte nicht gespeichert werden", "failedToSavePdf": "PDF konnte nicht gespeichert werden",
"general": "Allgemein", "general": "Allgemein",
"goTo": "Gehe zu:", "goTo": "Gehe zu:",
"image": "Bild",
"invalidOrUnsupportedFile": "Ungültige oder nicht unterstützte Datei", "invalidOrUnsupportedFile": "Ungültige oder nicht unterstützte Datei",
"language": "Sprache", "language": "Sprache",
"loadSignatureFromFile": "Signatur aus Datei laden", "loadSignatureFromFile": "Signatur aus Datei laden",
@ -34,6 +35,7 @@
"pageViewContinuous": "Kontinuierlich", "pageViewContinuous": "Kontinuierlich",
"prev": "Vorherige", "prev": "Vorherige",
"resetToDefaults": "Auf Standardwerte zurücksetzen", "resetToDefaults": "Auf Standardwerte zurücksetzen",
"rotate": "Drehen",
"save": "Speichern", "save": "Speichern",
"savedWithPath": "Gespeichert: {path}", "savedWithPath": "Gespeichert: {path}",
"saveSignedPdf": "Signiertes PDF speichern", "saveSignedPdf": "Signiertes PDF speichern",

View File

@ -24,7 +24,7 @@
"@display": {}, "@display": {},
"downloadStarted": "Download started", "downloadStarted": "Download started",
"@downloadStarted": {}, "@downloadStarted": {},
"dpi": "DPI:", "dpi": "DPI",
"@dpi": {}, "@dpi": {},
"drawSignature": "Draw Signature", "drawSignature": "Draw Signature",
"@drawSignature": {}, "@drawSignature": {},
@ -47,6 +47,8 @@
"@general": {}, "@general": {},
"goTo": "Go to:", "goTo": "Go to:",
"@goTo": {}, "@goTo": {},
"image": "Image",
"@image": {},
"invalidOrUnsupportedFile": "Invalid or unsupported file", "invalidOrUnsupportedFile": "Invalid or unsupported file",
"@invalidOrUnsupportedFile": {}, "@invalidOrUnsupportedFile": {},
"language": "Language", "language": "Language",
@ -87,6 +89,8 @@
"@prev": {}, "@prev": {},
"resetToDefaults": "Reset to defaults", "resetToDefaults": "Reset to defaults",
"@resetToDefaults": {}, "@resetToDefaults": {},
"rotate": "Rotate",
"@rotate": {},
"save": "Save", "save": "Save",
"@save": {}, "@save": {},
"savedWithPath": "Saved: {path}", "savedWithPath": "Saved: {path}",

View File

@ -11,7 +11,7 @@
"delete": "Eliminar", "delete": "Eliminar",
"display": "Pantalla", "display": "Pantalla",
"downloadStarted": "Descarga iniciada", "downloadStarted": "Descarga iniciada",
"dpi": "DPI:", "dpi": "DPI",
"drawSignature": "Dibujar firma", "drawSignature": "Dibujar firma",
"errorWithMessage": "Error: {message}", "errorWithMessage": "Error: {message}",
"exportingPleaseWait": "Exportando... Por favor, espere", "exportingPleaseWait": "Exportando... Por favor, espere",
@ -19,6 +19,7 @@
"failedToSavePdf": "No se pudo guardar el PDF", "failedToSavePdf": "No se pudo guardar el PDF",
"general": "General", "general": "General",
"goTo": "Ir a:", "goTo": "Ir a:",
"image": "Imagen",
"invalidOrUnsupportedFile": "Archivo inválido o no compatible", "invalidOrUnsupportedFile": "Archivo inválido o no compatible",
"language": "Idioma", "language": "Idioma",
"loadSignatureFromFile": "Cargar firma desde archivo", "loadSignatureFromFile": "Cargar firma desde archivo",
@ -34,6 +35,7 @@
"pageViewContinuous": "Continuo", "pageViewContinuous": "Continuo",
"prev": "Anterior", "prev": "Anterior",
"resetToDefaults": "Restablecer valores predeterminados", "resetToDefaults": "Restablecer valores predeterminados",
"rotate": "Rotar",
"save": "Guardar", "save": "Guardar",
"savedWithPath": "Guardado: {path}", "savedWithPath": "Guardado: {path}",
"saveSignedPdf": "Guardar PDF firmado", "saveSignedPdf": "Guardar PDF firmado",

View File

@ -19,6 +19,7 @@
"failedToSavePdf": "Échec de l'enregistrement du PDF", "failedToSavePdf": "Échec de l'enregistrement du PDF",
"general": "Général", "general": "Général",
"goTo": "Aller à :", "goTo": "Aller à :",
"image": "Image",
"invalidOrUnsupportedFile": "Fichier invalide ou non pris en charge", "invalidOrUnsupportedFile": "Fichier invalide ou non pris en charge",
"language": "Langue", "language": "Langue",
"loadSignatureFromFile": "Charger une signature depuis un fichier", "loadSignatureFromFile": "Charger une signature depuis un fichier",
@ -34,6 +35,7 @@
"pageViewContinuous": "Continu", "pageViewContinuous": "Continu",
"prev": "Précédent", "prev": "Précédent",
"resetToDefaults": "Rétablir les valeurs par défaut", "resetToDefaults": "Rétablir les valeurs par défaut",
"rotate": "Rotation",
"save": "Enregistrer", "save": "Enregistrer",
"savedWithPath": "Enregistré : {path}", "savedWithPath": "Enregistré : {path}",
"saveSignedPdf": "Enregistrer le PDF signé", "saveSignedPdf": "Enregistrer le PDF signé",

View File

@ -11,7 +11,7 @@
"delete": "削除", "delete": "削除",
"display": "表示", "display": "表示",
"downloadStarted": "ダウンロード開始", "downloadStarted": "ダウンロード開始",
"dpi": "DPI", "dpi": "DPI",
"drawSignature": "署名をかく", "drawSignature": "署名をかく",
"errorWithMessage": "エラー:{message}", "errorWithMessage": "エラー:{message}",
"exportingPleaseWait": "エクスポート中…お待ちください", "exportingPleaseWait": "エクスポート中…お待ちください",
@ -19,6 +19,7 @@
"failedToSavePdf": "PDFの保存に失敗しました", "failedToSavePdf": "PDFの保存に失敗しました",
"general": "一般", "general": "一般",
"goTo": "移動:", "goTo": "移動:",
"image": "画像",
"invalidOrUnsupportedFile": "無効なファイルまたはサポートされていないファイル", "invalidOrUnsupportedFile": "無効なファイルまたはサポートされていないファイル",
"language": "言語", "language": "言語",
"loadSignatureFromFile": "ファイルから署名を読み込む", "loadSignatureFromFile": "ファイルから署名を読み込む",
@ -34,6 +35,7 @@
"pageViewContinuous": "連続", "pageViewContinuous": "連続",
"prev": "前へ", "prev": "前へ",
"resetToDefaults": "デフォルトに戻す", "resetToDefaults": "デフォルトに戻す",
"rotate": "回転",
"save": "保存", "save": "保存",
"savedWithPath": "保存しました:{path}", "savedWithPath": "保存しました:{path}",
"saveSignedPdf": "署名済みPDFを保存", "saveSignedPdf": "署名済みPDFを保存",

View File

@ -11,7 +11,7 @@
"delete": "삭제", "delete": "삭제",
"display": "표시", "display": "표시",
"downloadStarted": "다운로드 시작됨", "downloadStarted": "다운로드 시작됨",
"dpi": "DPI:", "dpi": "DPI",
"drawSignature": "서명 그리기", "drawSignature": "서명 그리기",
"errorWithMessage": "오류: {message}", "errorWithMessage": "오류: {message}",
"exportingPleaseWait": "내보내는 중... 잠시 기다려주세요", "exportingPleaseWait": "내보내는 중... 잠시 기다려주세요",
@ -19,6 +19,7 @@
"failedToSavePdf": "PDF 저장 실패", "failedToSavePdf": "PDF 저장 실패",
"general": "일반", "general": "일반",
"goTo": "이동:", "goTo": "이동:",
"image": "이미지",
"invalidOrUnsupportedFile": "잘못된 파일이거나 지원되지 않는 파일입니다.", "invalidOrUnsupportedFile": "잘못된 파일이거나 지원되지 않는 파일입니다.",
"language": "언어", "language": "언어",
"loadSignatureFromFile": "파일에서 서명 불러오기", "loadSignatureFromFile": "파일에서 서명 불러오기",
@ -34,6 +35,7 @@
"pageViewContinuous": "연속", "pageViewContinuous": "연속",
"prev": "이전", "prev": "이전",
"resetToDefaults": "기본값으로 재설정", "resetToDefaults": "기본값으로 재설정",
"rotate": "회전",
"save": "저장", "save": "저장",
"savedWithPath": "{path}에 저장됨", "savedWithPath": "{path}에 저장됨",
"saveSignedPdf": "서명된 PDF 저장", "saveSignedPdf": "서명된 PDF 저장",

View File

@ -11,7 +11,7 @@
"delete": "Видалити", "delete": "Видалити",
"display": "Відображення", "display": "Відображення",
"downloadStarted": "Завантаження розпочато", "downloadStarted": "Завантаження розпочато",
"dpi": "DPI:", "dpi": "DPI",
"drawSignature": "Намалювати підпис", "drawSignature": "Намалювати підпис",
"errorWithMessage": "Помилка: {message}", "errorWithMessage": "Помилка: {message}",
"exportingPleaseWait": "Експортування... Зачекайте", "exportingPleaseWait": "Експортування... Зачекайте",
@ -19,6 +19,7 @@
"failedToSavePdf": "Не вдалося зберегти PDF", "failedToSavePdf": "Не вдалося зберегти PDF",
"general": "Загальні", "general": "Загальні",
"goTo": "Перейти до:", "goTo": "Перейти до:",
"image": "Зображення",
"invalidOrUnsupportedFile": "Недійсний або непідтримуваний файл", "invalidOrUnsupportedFile": "Недійсний або непідтримуваний файл",
"language": "Мова", "language": "Мова",
"loadSignatureFromFile": "Завантажити підпис з файлу", "loadSignatureFromFile": "Завантажити підпис з файлу",
@ -34,6 +35,7 @@
"pageViewContinuous": "Безперервний", "pageViewContinuous": "Безперервний",
"prev": "Попередня", "prev": "Попередня",
"resetToDefaults": "Скинути до значень за замовчуванням", "resetToDefaults": "Скинути до значень за замовчуванням",
"rotate": "Повернути",
"save": "Зберегти", "save": "Зберегти",
"savedWithPath": "Збережено: {path}", "savedWithPath": "Збережено: {path}",
"saveSignedPdf": "Зберегти підписаний PDF", "saveSignedPdf": "Зберегти підписаний PDF",

View File

@ -12,7 +12,7 @@
"delete": "刪除", "delete": "刪除",
"display": "顯示", "display": "顯示",
"downloadStarted": "已開始下載", "downloadStarted": "已開始下載",
"dpi": "DPI", "dpi": "DPI",
"drawSignature": "手寫簽名", "drawSignature": "手寫簽名",
"errorWithMessage": "錯誤:{message}", "errorWithMessage": "錯誤:{message}",
"exportingPleaseWait": "匯出中…請稍候", "exportingPleaseWait": "匯出中…請稍候",
@ -20,6 +20,7 @@
"failedToSavePdf": "儲存 PDF 失敗", "failedToSavePdf": "儲存 PDF 失敗",
"general": "一般", "general": "一般",
"goTo": "前往:", "goTo": "前往:",
"image": "圖片",
"invalidOrUnsupportedFile": "無效或不支援的檔案", "invalidOrUnsupportedFile": "無效或不支援的檔案",
"language": "語言", "language": "語言",
"loadSignatureFromFile": "從檔案載入簽名", "loadSignatureFromFile": "從檔案載入簽名",
@ -27,7 +28,7 @@
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。", "longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
"next": "下一頁", "next": "下一頁",
"noPdfLoaded": "尚未載入 PDF", "noPdfLoaded": "尚未載入 PDF",
"noSignatureLoaded": "没有加载签名", "noSignatureLoaded": "沒有加載簽名",
"nothingToSaveYet": "尚無可儲存的內容", "nothingToSaveYet": "尚無可儲存的內容",
"openPdf": "開啟 PDF…", "openPdf": "開啟 PDF…",
"pageInfo": "第 {current}/{total} 頁", "pageInfo": "第 {current}/{total} 頁",
@ -35,6 +36,7 @@
"pageViewContinuous": "連續", "pageViewContinuous": "連續",
"prev": "上一頁", "prev": "上一頁",
"resetToDefaults": "重設為預設值", "resetToDefaults": "重設為預設值",
"rotate": "旋轉",
"save": "儲存", "save": "儲存",
"savedWithPath": "已儲存:{path}", "savedWithPath": "已儲存:{path}",
"saveSignedPdf": "儲存已簽名 PDF", "saveSignedPdf": "儲存已簽名 PDF",

View File

@ -11,7 +11,7 @@
"delete": "删除", "delete": "删除",
"display": "显示", "display": "显示",
"downloadStarted": "下载已开始", "downloadStarted": "下载已开始",
"dpi": "DPI", "dpi": "DPI",
"drawSignature": "绘制签名", "drawSignature": "绘制签名",
"errorWithMessage": "错误:{message}", "errorWithMessage": "错误:{message}",
"exportingPleaseWait": "正在导出... 请稍候", "exportingPleaseWait": "正在导出... 请稍候",
@ -19,6 +19,7 @@
"failedToSavePdf": "PDF 保存失败", "failedToSavePdf": "PDF 保存失败",
"general": "常规", "general": "常规",
"goTo": "跳转到:", "goTo": "跳转到:",
"image": "图片",
"invalidOrUnsupportedFile": "无效或不支持的文件", "invalidOrUnsupportedFile": "无效或不支持的文件",
"language": "语言", "language": "语言",
"loadSignatureFromFile": "从文件加载签名", "loadSignatureFromFile": "从文件加载签名",
@ -34,6 +35,7 @@
"pageViewContinuous": "连续", "pageViewContinuous": "连续",
"prev": "上一页", "prev": "上一页",
"resetToDefaults": "恢复默认值", "resetToDefaults": "恢复默认值",
"rotate": "旋转",
"save": "保存", "save": "保存",
"savedWithPath": "已保存:{path}", "savedWithPath": "已保存:{path}",
"saveSignedPdf": "保存已签名的 PDF", "saveSignedPdf": "保存已签名的 PDF",

View File

@ -12,7 +12,7 @@
"delete": "刪除", "delete": "刪除",
"display": "顯示", "display": "顯示",
"downloadStarted": "已開始下載", "downloadStarted": "已開始下載",
"dpi": "DPI", "dpi": "DPI",
"drawSignature": "手寫簽名", "drawSignature": "手寫簽名",
"errorWithMessage": "錯誤:{message}", "errorWithMessage": "錯誤:{message}",
"exportingPleaseWait": "匯出中…請稍候", "exportingPleaseWait": "匯出中…請稍候",
@ -20,6 +20,7 @@
"failedToSavePdf": "儲存 PDF 失敗", "failedToSavePdf": "儲存 PDF 失敗",
"general": "一般", "general": "一般",
"goTo": "前往:", "goTo": "前往:",
"image": "圖片",
"invalidOrUnsupportedFile": "無效或不支援的檔案", "invalidOrUnsupportedFile": "無效或不支援的檔案",
"language": "語言", "language": "語言",
"loadSignatureFromFile": "從檔案載入簽名", "loadSignatureFromFile": "從檔案載入簽名",
@ -35,6 +36,7 @@
"pageViewContinuous": "連續", "pageViewContinuous": "連續",
"prev": "上一頁", "prev": "上一頁",
"resetToDefaults": "重設為預設值", "resetToDefaults": "重設為預設值",
"rotate": "旋轉",
"save": "儲存", "save": "儲存",
"savedWithPath": "已儲存:{path}", "savedWithPath": "已儲存:{path}",
"saveSignedPdf": "儲存已簽名 PDF", "saveSignedPdf": "儲存已簽名 PDF",

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/model/model.dart'; import '../../../../data/model/model.dart';
@ -251,7 +252,14 @@ class SignatureController extends StateNotifier<SignatureState> {
void setInvalidSelected(BuildContext context) { void setInvalidSelected(BuildContext context) {
// Fallback message without localization to keep core logic testable // Fallback message without localization to keep core logic testable
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid or unsupported file')), SnackBar(
content: Text(
Localizations.of<AppLocalizations>(
context,
AppLocalizations,
)!.invalidOrUnsupportedFile,
),
),
); );
} }

View File

@ -10,6 +10,8 @@ class ImageEditorDialog extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = Localizations.of<AppLocalizations>(context, AppLocalizations)!;
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
final sig = ref.watch(signatureProvider); final sig = ref.watch(signatureProvider);
return Dialog( return Dialog(
@ -57,7 +59,7 @@ class ImageEditorDialog extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
Text('Rotate'), Text(l10n.rotate),
Expanded( Expanded(
child: Slider( child: Slider(
key: const Key('sld_rotation'), key: const Key('sld_rotation'),

View File

@ -68,8 +68,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
} }
Future<Uint8List?> _loadSignatureFromFile() async { Future<Uint8List?> _loadSignatureFromFile() async {
final typeGroup = const fs.XTypeGroup( final typeGroup = fs.XTypeGroup(
label: 'Image', label:
Localizations.of<AppLocalizations>(context, AppLocalizations)?.image,
extensions: ['png', 'jpg', 'jpeg', 'webp'], extensions: ['png', 'jpg', 'jpeg', 'webp'],
); );
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]); final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);

View File

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../../../data/services/export_providers.dart';
import '../view_model/view_model.dart'; import '../view_model/view_model.dart';
class PdfToolbar extends ConsumerStatefulWidget { class PdfToolbar extends ConsumerStatefulWidget {
@ -57,7 +56,6 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pdf = ref.watch(pdfProvider); final pdf = ref.watch(pdfProvider);
final dpi = ref.watch(exportDpiProvider);
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
final pageInfo = l.pageInfo(pdf.currentPage, pdf.pageCount); final pageInfo = l.pageInfo(pdf.currentPage, pdf.pageCount);
@ -97,25 +95,32 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
Wrap( Wrap(
spacing: 8, spacing: 8,
children: [ children: [
IconButton( Wrap(
key: const Key('btn_prev'), crossAxisAlignment: WrapCrossAlignment.center,
onPressed: children: [
widget.disabled IconButton(
? null key: const Key('btn_prev'),
: () => widget.onJumpToPage(pdf.currentPage - 1), onPressed:
icon: const Icon(Icons.chevron_left), widget.disabled
tooltip: l.prev, ? null
), : () =>
// Current page label widget.onJumpToPage(pdf.currentPage - 1),
Text(pageInfo, key: const Key('lbl_page_info')), icon: const Icon(Icons.chevron_left),
IconButton( tooltip: l.prev,
key: const Key('btn_next'), ),
onPressed: // Current page label
widget.disabled Text(pageInfo, key: const Key('lbl_page_info')),
? null IconButton(
: () => widget.onJumpToPage(pdf.currentPage + 1), key: const Key('btn_next'),
icon: const Icon(Icons.chevron_right), onPressed:
tooltip: l.next, widget.disabled
? null
: () =>
widget.onJumpToPage(pdf.currentPage + 1),
icon: const Icon(Icons.chevron_right),
tooltip: l.next,
),
],
), ),
Wrap( Wrap(
spacing: 6, spacing: 6,
@ -150,48 +155,29 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
], ],
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton( Wrap(
key: const Key('btn_zoom_out'), crossAxisAlignment: WrapCrossAlignment.center,
tooltip: 'Zoom out', children: [
onPressed: widget.disabled ? null : widget.onZoomOut, IconButton(
icon: const Icon(Icons.zoom_out), key: const Key('btn_zoom_out'),
), tooltip: 'Zoom out',
Text( onPressed: widget.disabled ? null : widget.onZoomOut,
//if not null icon: const Icon(Icons.zoom_out),
widget.zoomLevel != null ? '${widget.zoomLevel}%' : '', ),
style: const TextStyle(fontSize: 12), Text(
), //if not null
IconButton( widget.zoomLevel != null ? '${widget.zoomLevel}%' : '',
key: const Key('btn_zoom_in'), style: const TextStyle(fontSize: 12),
tooltip: 'Zoom in', ),
onPressed: widget.disabled ? null : widget.onZoomIn, IconButton(
icon: const Icon(Icons.zoom_in), key: const Key('btn_zoom_in'),
tooltip: 'Zoom in',
onPressed: widget.disabled ? null : widget.onZoomIn,
icon: const Icon(Icons.zoom_in),
),
],
), ),
SizedBox(width: 6), SizedBox(width: 6),
// show zoom ratio
Text(l.dpi),
const SizedBox(width: 8),
DropdownButton<double>(
key: const Key('ddl_export_dpi'),
value: dpi,
items:
const [96.0, 144.0, 200.0, 300.0]
.map(
(v) => DropdownMenuItem(
value: v,
child: Text(v.toStringAsFixed(0)),
),
)
.toList(),
onChanged:
widget.disabled
? null
: (v) {
if (v != null) {
ref.read(exportDpiProvider.notifier).state = v;
}
},
),
], ],
), ),
], ],

View File

@ -14,6 +14,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
String? _theme; String? _theme;
String? _language; String? _language;
// Page view removed; continuous-only // Page view removed; continuous-only
double? _exportDpi;
@override @override
void initState() { void initState() {
@ -21,6 +22,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
final prefs = ref.read(preferencesProvider); final prefs = ref.read(preferencesProvider);
_theme = prefs.theme; _theme = prefs.theme;
_language = prefs.language; _language = prefs.language;
_exportDpi = prefs.exportDpi;
// pageView no longer configurable (continuous-only) // pageView no longer configurable (continuous-only)
} }
@ -118,6 +120,29 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
), ),
], ],
), ),
Row(
children: [
SizedBox(width: 140, child: Text('${l.dpi}:')),
const SizedBox(width: 8),
Expanded(
child: DropdownButton<double>(
key: const Key('ddl_export_dpi'),
isExpanded: true,
value: _exportDpi,
items:
const [96.0, 144.0, 200.0, 300.0]
.map(
(v) => DropdownMenuItem<double>(
value: v,
child: Text(v.toStringAsFixed(0)),
),
)
.toList(),
onChanged: (v) => setState(() => _exportDpi = v),
),
),
],
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text(l.display, style: Theme.of(context).textTheme.titleMedium), Text(l.display, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -149,7 +174,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
), ),
], ],
), ),
// Page view setting removed (continuous-only)
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -164,6 +189,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
final n = ref.read(preferencesProvider.notifier); final n = ref.read(preferencesProvider.notifier);
if (_theme != null) await n.setTheme(_theme!); if (_theme != null) await n.setTheme(_theme!);
if (_language != null) await n.setLanguage(_language!); if (_language != null) await n.setLanguage(_language!);
if (_exportDpi != null) await n.setExportDpi(_exportDpi!);
// pageView not configurable anymore // pageView not configurable anymore
if (mounted) Navigator.of(context).pop(true); if (mounted) Navigator.of(context).pop(true);
}, },