Compare commits
7 Commits
60630c6ddd
...
51af255ea7
Author | SHA1 | Date |
---|---|---|
|
51af255ea7 | |
|
52a278e969 | |
|
b2ac63d22b | |
|
bc0444f873 | |
|
828eee49e2 | |
|
5990f6fb01 | |
|
b8918717b5 |
|
@ -122,3 +122,4 @@ docs/.*
|
|||
.vscode/tasks.json
|
||||
.vscode/launch.json
|
||||
devtools_options.yaml
|
||||
test/features/*_test.dart
|
||||
|
|
|
@ -9,8 +9,11 @@ checkout [`docs/FRs.md`](docs/FRs.md)
|
|||
## Build
|
||||
|
||||
```bash
|
||||
# flutter clean
|
||||
flutter pub get
|
||||
# flutter run build_runner build --delete-conflicting-outputs
|
||||
# generate gherkin test
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# dart run tool/prune_unused_steps.dart --delete
|
||||
|
||||
# run the app
|
||||
flutter run
|
||||
|
@ -19,4 +22,6 @@ flutter run
|
|||
flutter test
|
||||
|
||||
flutter build
|
||||
# create windows installer
|
||||
flutter pub run msix:create
|
||||
```
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
targets:
|
||||
$default:
|
||||
sources:
|
||||
- integration_test/**
|
||||
- test/**
|
||||
- lib/**
|
||||
- $package$
|
||||
builders:
|
|
@ -26,3 +26,11 @@
|
|||
* role: user
|
||||
* functionality: save/export the signed PDF document
|
||||
* benefit: easily keep a copy of the signed document for records.
|
||||
* name: preferences for app
|
||||
* role: user
|
||||
* functionality: configure app preferences such as `theme`, `language`.
|
||||
* benefit: customize the app experience to better fit user needs
|
||||
* name: remember preferences
|
||||
* role: user
|
||||
* functionality: remember user preferences for future sessions
|
||||
* benefit: provide a consistent and personalized experience
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Non-Functional Requirements
|
||||
|
||||
* Package structure
|
||||
* plz follow official [Package structure](https://docs.flutter.dev/app-architecture/case-study#package-structure) with a slight modification, put each `<FEATURE NAME>/`s in `features/` sub-directory under `ui/`.
|
|
@ -0,0 +1,6 @@
|
|||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-class: AppLocalizations
|
||||
output-localization-file: app_localizations.dart
|
||||
nullable-getter: false
|
||||
untranslated-messages-file: build/l10n_missing.txt
|
61
lib/app.dart
61
lib/app.dart
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
||||
import 'ui/features/preferences/providers.dart';
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
@ -8,12 +10,57 @@ class MyApp extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ProviderScope(
|
||||
child: MaterialApp(
|
||||
title: 'PDF Signature',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
||||
),
|
||||
home: const PdfSignatureHomePage(),
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
// Ensure SharedPreferences loaded before building MaterialApp
|
||||
final sp = ref.watch(sharedPreferencesProvider);
|
||||
return sp.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error:
|
||||
(e, st) => MaterialApp(
|
||||
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates:
|
||||
AppLocalizations.localizationsDelegates,
|
||||
home: Builder(
|
||||
builder:
|
||||
(ctx) => Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
ctx,
|
||||
).errorWithMessage(e.toString()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (_) {
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
final appLocale = ref.watch(localeProvider);
|
||||
return MaterialApp(
|
||||
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
themeMode: themeMode,
|
||||
locale: appLocale,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
home: const PdfSignatureHomePage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class PdfState {
|
||||
final bool loaded;
|
||||
final int pageCount;
|
||||
final int currentPage;
|
||||
final bool markedForSigning;
|
||||
final String? pickedPdfPath;
|
||||
final Uint8List? pickedPdfBytes;
|
||||
final int? signedPage;
|
||||
const PdfState({
|
||||
required this.loaded,
|
||||
required this.pageCount,
|
||||
required this.currentPage,
|
||||
required this.markedForSigning,
|
||||
this.pickedPdfPath,
|
||||
this.pickedPdfBytes,
|
||||
this.signedPage,
|
||||
});
|
||||
factory PdfState.initial() => const PdfState(
|
||||
loaded: false,
|
||||
pageCount: 0,
|
||||
currentPage: 1,
|
||||
markedForSigning: false,
|
||||
pickedPdfBytes: null,
|
||||
signedPage: null,
|
||||
);
|
||||
PdfState copyWith({
|
||||
bool? loaded,
|
||||
int? pageCount,
|
||||
int? currentPage,
|
||||
bool? markedForSigning,
|
||||
String? pickedPdfPath,
|
||||
Uint8List? pickedPdfBytes,
|
||||
int? signedPage,
|
||||
}) => PdfState(
|
||||
loaded: loaded ?? this.loaded,
|
||||
pageCount: pageCount ?? this.pageCount,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
markedForSigning: markedForSigning ?? this.markedForSigning,
|
||||
pickedPdfPath: pickedPdfPath ?? this.pickedPdfPath,
|
||||
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
||||
signedPage: signedPage ?? this.signedPage,
|
||||
);
|
||||
}
|
||||
|
||||
class SignatureState {
|
||||
final Rect? rect;
|
||||
final bool aspectLocked;
|
||||
final bool bgRemoval;
|
||||
final double contrast;
|
||||
final double brightness;
|
||||
final List<List<Offset>> strokes;
|
||||
final Uint8List? imageBytes;
|
||||
const SignatureState({
|
||||
required this.rect,
|
||||
required this.aspectLocked,
|
||||
required this.bgRemoval,
|
||||
required this.contrast,
|
||||
required this.brightness,
|
||||
required this.strokes,
|
||||
this.imageBytes,
|
||||
});
|
||||
factory SignatureState.initial() => const SignatureState(
|
||||
rect: null,
|
||||
aspectLocked: false,
|
||||
bgRemoval: false,
|
||||
contrast: 1.0,
|
||||
brightness: 0.0,
|
||||
strokes: [],
|
||||
imageBytes: null,
|
||||
);
|
||||
SignatureState copyWith({
|
||||
Rect? rect,
|
||||
bool? aspectLocked,
|
||||
bool? bgRemoval,
|
||||
double? contrast,
|
||||
double? brightness,
|
||||
List<List<Offset>>? strokes,
|
||||
Uint8List? imageBytes,
|
||||
}) => SignatureState(
|
||||
rect: rect ?? this.rect,
|
||||
aspectLocked: aspectLocked ?? this.aspectLocked,
|
||||
bgRemoval: bgRemoval ?? this.bgRemoval,
|
||||
contrast: contrast ?? this.contrast,
|
||||
brightness: brightness ?? this.brightness,
|
||||
strokes: strokes ?? this.strokes,
|
||||
imageBytes: imageBytes ?? this.imageBytes,
|
||||
);
|
||||
}
|
|
@ -34,9 +34,9 @@ class ExportService {
|
|||
required Uint8List? signatureImageBytes,
|
||||
double targetDpi = 144.0,
|
||||
}) async {
|
||||
print(
|
||||
'exportSignedPdfFromFile: enter signedPage=$signedPage outputPath=$outputPath',
|
||||
);
|
||||
// print(
|
||||
// 'exportSignedPdfFromFile: enter signedPage=$signedPage outputPath=$outputPath',
|
||||
// );
|
||||
// Read source bytes and delegate to bytes-based exporter
|
||||
Uint8List? srcBytes;
|
||||
try {
|
||||
|
@ -160,10 +160,20 @@ class ExportService {
|
|||
signatureImageBytes.isNotEmpty;
|
||||
if (shouldStamp) {
|
||||
try {
|
||||
final decoded = img.decodeImage(signatureImageBytes);
|
||||
if (decoded != null) {
|
||||
final jpg = img.encodeJpg(decoded, quality: 90);
|
||||
sigImgObj = pw.MemoryImage(Uint8List.fromList(jpg));
|
||||
// If it's already PNG, keep as-is to preserve alpha; otherwise decode/encode PNG
|
||||
final asStr = String.fromCharCodes(signatureImageBytes.take(8));
|
||||
final isPng =
|
||||
signatureImageBytes.length > 8 &&
|
||||
signatureImageBytes[0] == 0x89 &&
|
||||
asStr.startsWith('\u0089PNG');
|
||||
if (isPng) {
|
||||
sigImgObj = pw.MemoryImage(signatureImageBytes);
|
||||
} else {
|
||||
final decoded = img.decodeImage(signatureImageBytes);
|
||||
if (decoded != null) {
|
||||
final png = img.encodePng(decoded, level: 6);
|
||||
sigImgObj = pw.MemoryImage(Uint8List.fromList(png));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart' as pp;
|
||||
import 'package:file_selector/file_selector.dart' as fs;
|
||||
import 'package:pdf_signature/data/services/export_service.dart';
|
||||
|
||||
// Feature-scoped DI and configuration providers
|
||||
|
||||
// Toggle mock viewer (used by tests to show a gray placeholder instead of real PDF pages)
|
||||
final useMockViewerProvider = Provider<bool>((_) => false);
|
||||
|
||||
// Export service injection for testability
|
||||
final exportServiceProvider = Provider<ExportService>((_) => ExportService());
|
||||
|
||||
// Export DPI setting (points per inch mapping), default 144 DPI
|
||||
final exportDpiProvider = StateProvider<double>((_) => 144.0);
|
||||
|
||||
// Controls whether signature overlay is visible (used to hide on non-stamped pages during export)
|
||||
final signatureVisibilityProvider = StateProvider<bool>((_) => true);
|
||||
|
||||
// Global exporting state to show loading UI and block interactions while saving/exporting
|
||||
final exportingProvider = StateProvider<bool>((_) => false);
|
||||
|
||||
// Save path picker (injected for tests)
|
||||
final savePathPickerProvider = Provider<Future<String?> Function()>((ref) {
|
||||
return () async {
|
||||
String? initialDir;
|
||||
try {
|
||||
final d = await pp.getDownloadsDirectory();
|
||||
initialDir = d?.path;
|
||||
} catch (_) {}
|
||||
if (initialDir == null) {
|
||||
try {
|
||||
final d = await pp.getApplicationDocumentsDirectory();
|
||||
initialDir = d.path;
|
||||
} catch (_) {}
|
||||
}
|
||||
final location = await fs.getSaveLocation(
|
||||
suggestedName: 'signed.pdf',
|
||||
acceptedTypeGroups: [
|
||||
const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']),
|
||||
],
|
||||
initialDirectory: initialDir,
|
||||
);
|
||||
if (location == null) return null;
|
||||
final path = location.path;
|
||||
return path.toLowerCase().endsWith('.pdf') ? path : '$path.pdf';
|
||||
};
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "PDF Signature",
|
||||
"errorWithMessage": "Error: {message}",
|
||||
"@errorWithMessage": {
|
||||
"description": "Generic error text with message",
|
||||
"placeholders": {"message": {"type": "String"}}
|
||||
},
|
||||
|
||||
"settings": "Settings",
|
||||
"theme": "Theme",
|
||||
"themeLight": "Light",
|
||||
"themeDark": "Dark",
|
||||
"themeSystem": "System",
|
||||
"language": "Language",
|
||||
"languageEnglish": "English",
|
||||
"languageChineseTraditional": "Traditional Chinese",
|
||||
"languageSpanish": "Spanish",
|
||||
"resetToDefaults": "Reset to defaults",
|
||||
|
||||
"openPdf": "Open PDF...",
|
||||
"prev": "Prev",
|
||||
"next": "Next",
|
||||
"pageInfo": "Page {current}/{total}",
|
||||
"@pageInfo": {
|
||||
"description": "Label showing current page and total",
|
||||
"placeholders": {
|
||||
"current": {"type": "int"},
|
||||
"total": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"goTo": "Go to:",
|
||||
"dpi": "DPI:",
|
||||
"markForSigning": "Mark for Signing",
|
||||
"unmarkSigning": "Unmark Signing",
|
||||
"saveSignedPdf": "Save Signed PDF",
|
||||
"loadSignatureFromFile": "Load Signature from file",
|
||||
"drawSignature": "Draw Signature",
|
||||
"noPdfLoaded": "No PDF loaded",
|
||||
"signature": "Signature",
|
||||
"lockAspectRatio": "Lock aspect ratio",
|
||||
"backgroundRemoval": "Background removal",
|
||||
"contrast": "Contrast",
|
||||
"brightness": "Brightness",
|
||||
"exportingPleaseWait": "Exporting... Please wait",
|
||||
|
||||
"nothingToSaveYet": "Nothing to save yet",
|
||||
"savedWithPath": "Saved: {path}",
|
||||
"@savedWithPath": {
|
||||
"description": "Snackbar text showing where file saved",
|
||||
"placeholders": {"path": {"type": "String"}}
|
||||
},
|
||||
"failedToSavePdf": "Failed to save PDF",
|
||||
"downloadStarted": "Download started",
|
||||
"failedToGeneratePdf": "Failed to generate PDF",
|
||||
"invalidOrUnsupportedFile": "Invalid or unsupported file",
|
||||
|
||||
"confirm": "Confirm",
|
||||
"undo": "Undo",
|
||||
"clear": "Clear"
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"@@locale": "es",
|
||||
"appTitle": "Firma PDF",
|
||||
"errorWithMessage": "Error: {message}",
|
||||
|
||||
"settings": "Ajustes",
|
||||
"theme": "Tema",
|
||||
"themeLight": "Claro",
|
||||
"themeDark": "Oscuro",
|
||||
"themeSystem": "Del sistema",
|
||||
"language": "Idioma",
|
||||
"languageEnglish": "Inglés",
|
||||
"languageChineseTraditional": "Chino tradicional",
|
||||
"languageSpanish": "Español",
|
||||
"resetToDefaults": "Restablecer valores",
|
||||
|
||||
"openPdf": "Abrir PDF…",
|
||||
"prev": "Anterior",
|
||||
"next": "Siguiente",
|
||||
"pageInfo": "Página {current}/{total}",
|
||||
"goTo": "Ir a:",
|
||||
"dpi": "DPI:",
|
||||
"markForSigning": "Marcar para firmar",
|
||||
"unmarkSigning": "Quitar marca",
|
||||
"saveSignedPdf": "Guardar PDF firmado",
|
||||
"loadSignatureFromFile": "Cargar firma desde archivo",
|
||||
"drawSignature": "Dibujar firma",
|
||||
"noPdfLoaded": "No hay PDF cargado",
|
||||
"signature": "Firma",
|
||||
"lockAspectRatio": "Bloquear relación de aspecto",
|
||||
"backgroundRemoval": "Eliminación de fondo",
|
||||
"contrast": "Contraste",
|
||||
"brightness": "Brillo",
|
||||
"exportingPleaseWait": "Exportando... Por favor espera",
|
||||
|
||||
"nothingToSaveYet": "Nada que guardar todavía",
|
||||
"savedWithPath": "Guardado: {path}",
|
||||
"failedToSavePdf": "Error al guardar el PDF",
|
||||
"downloadStarted": "Descarga iniciada",
|
||||
"failedToGeneratePdf": "Error al generar el PDF",
|
||||
"invalidOrUnsupportedFile": "Archivo no válido o no compatible",
|
||||
|
||||
"confirm": "Confirmar",
|
||||
"undo": "Deshacer",
|
||||
"clear": "Limpiar"
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_es.dart';
|
||||
import 'app_localizations_zh.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('zh'),
|
||||
Locale('zh', 'TW'),
|
||||
];
|
||||
|
||||
/// No description provided for @appTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PDF Signature'**
|
||||
String get appTitle;
|
||||
|
||||
/// Generic error text with message
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Error: {message}'**
|
||||
String errorWithMessage(String message);
|
||||
|
||||
/// No description provided for @settings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settings;
|
||||
|
||||
/// No description provided for @theme.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Theme'**
|
||||
String get theme;
|
||||
|
||||
/// No description provided for @themeLight.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Light'**
|
||||
String get themeLight;
|
||||
|
||||
/// No description provided for @themeDark.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Dark'**
|
||||
String get themeDark;
|
||||
|
||||
/// No description provided for @themeSystem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'System'**
|
||||
String get themeSystem;
|
||||
|
||||
/// No description provided for @language.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Language'**
|
||||
String get language;
|
||||
|
||||
/// No description provided for @languageEnglish.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'English'**
|
||||
String get languageEnglish;
|
||||
|
||||
/// No description provided for @languageChineseTraditional.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Traditional Chinese'**
|
||||
String get languageChineseTraditional;
|
||||
|
||||
/// No description provided for @languageSpanish.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Spanish'**
|
||||
String get languageSpanish;
|
||||
|
||||
/// No description provided for @resetToDefaults.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset to defaults'**
|
||||
String get resetToDefaults;
|
||||
|
||||
/// No description provided for @openPdf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open PDF...'**
|
||||
String get openPdf;
|
||||
|
||||
/// No description provided for @prev.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Prev'**
|
||||
String get prev;
|
||||
|
||||
/// No description provided for @next.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Next'**
|
||||
String get next;
|
||||
|
||||
/// Label showing current page and total
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Page {current}/{total}'**
|
||||
String pageInfo(int current, int total);
|
||||
|
||||
/// No description provided for @goTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Go to:'**
|
||||
String get goTo;
|
||||
|
||||
/// No description provided for @dpi.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'DPI:'**
|
||||
String get dpi;
|
||||
|
||||
/// No description provided for @markForSigning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mark for Signing'**
|
||||
String get markForSigning;
|
||||
|
||||
/// No description provided for @unmarkSigning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unmark Signing'**
|
||||
String get unmarkSigning;
|
||||
|
||||
/// No description provided for @saveSignedPdf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save Signed PDF'**
|
||||
String get saveSignedPdf;
|
||||
|
||||
/// No description provided for @loadSignatureFromFile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Load Signature from file'**
|
||||
String get loadSignatureFromFile;
|
||||
|
||||
/// No description provided for @drawSignature.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Draw Signature'**
|
||||
String get drawSignature;
|
||||
|
||||
/// No description provided for @noPdfLoaded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No PDF loaded'**
|
||||
String get noPdfLoaded;
|
||||
|
||||
/// No description provided for @signature.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signature'**
|
||||
String get signature;
|
||||
|
||||
/// No description provided for @lockAspectRatio.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lock aspect ratio'**
|
||||
String get lockAspectRatio;
|
||||
|
||||
/// No description provided for @backgroundRemoval.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Background removal'**
|
||||
String get backgroundRemoval;
|
||||
|
||||
/// No description provided for @contrast.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contrast'**
|
||||
String get contrast;
|
||||
|
||||
/// No description provided for @brightness.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Brightness'**
|
||||
String get brightness;
|
||||
|
||||
/// No description provided for @exportingPleaseWait.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Exporting... Please wait'**
|
||||
String get exportingPleaseWait;
|
||||
|
||||
/// No description provided for @nothingToSaveYet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nothing to save yet'**
|
||||
String get nothingToSaveYet;
|
||||
|
||||
/// Snackbar text showing where file saved
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Saved: {path}'**
|
||||
String savedWithPath(String path);
|
||||
|
||||
/// No description provided for @failedToSavePdf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to save PDF'**
|
||||
String get failedToSavePdf;
|
||||
|
||||
/// No description provided for @downloadStarted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download started'**
|
||||
String get downloadStarted;
|
||||
|
||||
/// No description provided for @failedToGeneratePdf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to generate PDF'**
|
||||
String get failedToGeneratePdf;
|
||||
|
||||
/// No description provided for @invalidOrUnsupportedFile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid or unsupported file'**
|
||||
String get invalidOrUnsupportedFile;
|
||||
|
||||
/// No description provided for @confirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Confirm'**
|
||||
String get confirm;
|
||||
|
||||
/// No description provided for @undo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Undo'**
|
||||
String get undo;
|
||||
|
||||
/// No description provided for @clear.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear'**
|
||||
String get clear;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'es', 'zh'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when language+country codes are specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'zh':
|
||||
{
|
||||
switch (locale.countryCode) {
|
||||
case 'TW':
|
||||
return AppLocalizationsZhTw();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'es':
|
||||
return AppLocalizationsEs();
|
||||
case 'zh':
|
||||
return AppLocalizationsZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'PDF Signature';
|
||||
|
||||
@override
|
||||
String errorWithMessage(String message) {
|
||||
return 'Error: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get theme => 'Theme';
|
||||
|
||||
@override
|
||||
String get themeLight => 'Light';
|
||||
|
||||
@override
|
||||
String get themeDark => 'Dark';
|
||||
|
||||
@override
|
||||
String get themeSystem => 'System';
|
||||
|
||||
@override
|
||||
String get language => 'Language';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'English';
|
||||
|
||||
@override
|
||||
String get languageChineseTraditional => 'Traditional Chinese';
|
||||
|
||||
@override
|
||||
String get languageSpanish => 'Spanish';
|
||||
|
||||
@override
|
||||
String get resetToDefaults => 'Reset to defaults';
|
||||
|
||||
@override
|
||||
String get openPdf => 'Open PDF...';
|
||||
|
||||
@override
|
||||
String get prev => 'Prev';
|
||||
|
||||
@override
|
||||
String get next => 'Next';
|
||||
|
||||
@override
|
||||
String pageInfo(int current, int total) {
|
||||
return 'Page $current/$total';
|
||||
}
|
||||
|
||||
@override
|
||||
String get goTo => 'Go to:';
|
||||
|
||||
@override
|
||||
String get dpi => 'DPI:';
|
||||
|
||||
@override
|
||||
String get markForSigning => 'Mark for Signing';
|
||||
|
||||
@override
|
||||
String get unmarkSigning => 'Unmark Signing';
|
||||
|
||||
@override
|
||||
String get saveSignedPdf => 'Save Signed PDF';
|
||||
|
||||
@override
|
||||
String get loadSignatureFromFile => 'Load Signature from file';
|
||||
|
||||
@override
|
||||
String get drawSignature => 'Draw Signature';
|
||||
|
||||
@override
|
||||
String get noPdfLoaded => 'No PDF loaded';
|
||||
|
||||
@override
|
||||
String get signature => 'Signature';
|
||||
|
||||
@override
|
||||
String get lockAspectRatio => 'Lock aspect ratio';
|
||||
|
||||
@override
|
||||
String get backgroundRemoval => 'Background removal';
|
||||
|
||||
@override
|
||||
String get contrast => 'Contrast';
|
||||
|
||||
@override
|
||||
String get brightness => 'Brightness';
|
||||
|
||||
@override
|
||||
String get exportingPleaseWait => 'Exporting... Please wait';
|
||||
|
||||
@override
|
||||
String get nothingToSaveYet => 'Nothing to save yet';
|
||||
|
||||
@override
|
||||
String savedWithPath(String path) {
|
||||
return 'Saved: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToSavePdf => 'Failed to save PDF';
|
||||
|
||||
@override
|
||||
String get downloadStarted => 'Download started';
|
||||
|
||||
@override
|
||||
String get failedToGeneratePdf => 'Failed to generate PDF';
|
||||
|
||||
@override
|
||||
String get invalidOrUnsupportedFile => 'Invalid or unsupported file';
|
||||
|
||||
@override
|
||||
String get confirm => 'Confirm';
|
||||
|
||||
@override
|
||||
String get undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get clear => 'Clear';
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Spanish Castilian (`es`).
|
||||
class AppLocalizationsEs extends AppLocalizations {
|
||||
AppLocalizationsEs([String locale = 'es']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Firma PDF';
|
||||
|
||||
@override
|
||||
String errorWithMessage(String message) {
|
||||
return 'Error: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings => 'Ajustes';
|
||||
|
||||
@override
|
||||
String get theme => 'Tema';
|
||||
|
||||
@override
|
||||
String get themeLight => 'Claro';
|
||||
|
||||
@override
|
||||
String get themeDark => 'Oscuro';
|
||||
|
||||
@override
|
||||
String get themeSystem => 'Del sistema';
|
||||
|
||||
@override
|
||||
String get language => 'Idioma';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'Inglés';
|
||||
|
||||
@override
|
||||
String get languageChineseTraditional => 'Chino tradicional';
|
||||
|
||||
@override
|
||||
String get languageSpanish => 'Español';
|
||||
|
||||
@override
|
||||
String get resetToDefaults => 'Restablecer valores';
|
||||
|
||||
@override
|
||||
String get openPdf => 'Abrir PDF…';
|
||||
|
||||
@override
|
||||
String get prev => 'Anterior';
|
||||
|
||||
@override
|
||||
String get next => 'Siguiente';
|
||||
|
||||
@override
|
||||
String pageInfo(int current, int total) {
|
||||
return 'Página $current/$total';
|
||||
}
|
||||
|
||||
@override
|
||||
String get goTo => 'Ir a:';
|
||||
|
||||
@override
|
||||
String get dpi => 'DPI:';
|
||||
|
||||
@override
|
||||
String get markForSigning => 'Marcar para firmar';
|
||||
|
||||
@override
|
||||
String get unmarkSigning => 'Quitar marca';
|
||||
|
||||
@override
|
||||
String get saveSignedPdf => 'Guardar PDF firmado';
|
||||
|
||||
@override
|
||||
String get loadSignatureFromFile => 'Cargar firma desde archivo';
|
||||
|
||||
@override
|
||||
String get drawSignature => 'Dibujar firma';
|
||||
|
||||
@override
|
||||
String get noPdfLoaded => 'No hay PDF cargado';
|
||||
|
||||
@override
|
||||
String get signature => 'Firma';
|
||||
|
||||
@override
|
||||
String get lockAspectRatio => 'Bloquear relación de aspecto';
|
||||
|
||||
@override
|
||||
String get backgroundRemoval => 'Eliminación de fondo';
|
||||
|
||||
@override
|
||||
String get contrast => 'Contraste';
|
||||
|
||||
@override
|
||||
String get brightness => 'Brillo';
|
||||
|
||||
@override
|
||||
String get exportingPleaseWait => 'Exportando... Por favor espera';
|
||||
|
||||
@override
|
||||
String get nothingToSaveYet => 'Nada que guardar todavía';
|
||||
|
||||
@override
|
||||
String savedWithPath(String path) {
|
||||
return 'Guardado: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToSavePdf => 'Error al guardar el PDF';
|
||||
|
||||
@override
|
||||
String get downloadStarted => 'Descarga iniciada';
|
||||
|
||||
@override
|
||||
String get failedToGeneratePdf => 'Error al generar el PDF';
|
||||
|
||||
@override
|
||||
String get invalidOrUnsupportedFile => 'Archivo no válido o no compatible';
|
||||
|
||||
@override
|
||||
String get confirm => 'Confirmar';
|
||||
|
||||
@override
|
||||
String get undo => 'Deshacer';
|
||||
|
||||
@override
|
||||
String get clear => 'Limpiar';
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Chinese (`zh`).
|
||||
class AppLocalizationsZh extends AppLocalizations {
|
||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'PDF 簽名';
|
||||
|
||||
@override
|
||||
String errorWithMessage(String message) {
|
||||
return '錯誤:$message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings => '設定';
|
||||
|
||||
@override
|
||||
String get theme => '主題';
|
||||
|
||||
@override
|
||||
String get themeLight => '淺色';
|
||||
|
||||
@override
|
||||
String get themeDark => '深色';
|
||||
|
||||
@override
|
||||
String get themeSystem => '系統';
|
||||
|
||||
@override
|
||||
String get language => '語言';
|
||||
|
||||
@override
|
||||
String get languageEnglish => '英文';
|
||||
|
||||
@override
|
||||
String get languageChineseTraditional => '繁體中文';
|
||||
|
||||
@override
|
||||
String get languageSpanish => '西班牙文';
|
||||
|
||||
@override
|
||||
String get resetToDefaults => '重設為預設值';
|
||||
|
||||
@override
|
||||
String get openPdf => '開啟 PDF…';
|
||||
|
||||
@override
|
||||
String get prev => '上一頁';
|
||||
|
||||
@override
|
||||
String get next => '下一頁';
|
||||
|
||||
@override
|
||||
String pageInfo(int current, int total) {
|
||||
return '第 $current/$total 頁';
|
||||
}
|
||||
|
||||
@override
|
||||
String get goTo => '前往:';
|
||||
|
||||
@override
|
||||
String get dpi => 'DPI:';
|
||||
|
||||
@override
|
||||
String get markForSigning => '標記簽署';
|
||||
|
||||
@override
|
||||
String get unmarkSigning => '取消標記';
|
||||
|
||||
@override
|
||||
String get saveSignedPdf => '儲存已簽名 PDF';
|
||||
|
||||
@override
|
||||
String get loadSignatureFromFile => '從檔案載入簽名';
|
||||
|
||||
@override
|
||||
String get drawSignature => '手寫簽名';
|
||||
|
||||
@override
|
||||
String get noPdfLoaded => '尚未載入 PDF';
|
||||
|
||||
@override
|
||||
String get signature => '簽名';
|
||||
|
||||
@override
|
||||
String get lockAspectRatio => '鎖定長寬比';
|
||||
|
||||
@override
|
||||
String get backgroundRemoval => '去除背景';
|
||||
|
||||
@override
|
||||
String get contrast => '對比';
|
||||
|
||||
@override
|
||||
String get brightness => '亮度';
|
||||
|
||||
@override
|
||||
String get exportingPleaseWait => '匯出中…請稍候';
|
||||
|
||||
@override
|
||||
String get nothingToSaveYet => '尚無可儲存的內容';
|
||||
|
||||
@override
|
||||
String savedWithPath(String path) {
|
||||
return '已儲存:$path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToSavePdf => '儲存 PDF 失敗';
|
||||
|
||||
@override
|
||||
String get downloadStarted => '已開始下載';
|
||||
|
||||
@override
|
||||
String get failedToGeneratePdf => '產生 PDF 失敗';
|
||||
|
||||
@override
|
||||
String get invalidOrUnsupportedFile => '無效或不支援的檔案';
|
||||
|
||||
@override
|
||||
String get confirm => '確認';
|
||||
|
||||
@override
|
||||
String get undo => '復原';
|
||||
|
||||
@override
|
||||
String get clear => '清除';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||
class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
AppLocalizationsZhTw() : super('zh_TW');
|
||||
|
||||
@override
|
||||
String get appTitle => 'PDF 簽名';
|
||||
|
||||
@override
|
||||
String errorWithMessage(String message) {
|
||||
return '錯誤:$message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings => '設定';
|
||||
|
||||
@override
|
||||
String get theme => '主題';
|
||||
|
||||
@override
|
||||
String get themeLight => '淺色';
|
||||
|
||||
@override
|
||||
String get themeDark => '深色';
|
||||
|
||||
@override
|
||||
String get themeSystem => '系統';
|
||||
|
||||
@override
|
||||
String get language => '語言';
|
||||
|
||||
@override
|
||||
String get languageEnglish => '英文';
|
||||
|
||||
@override
|
||||
String get languageChineseTraditional => '繁體中文';
|
||||
|
||||
@override
|
||||
String get languageSpanish => '西班牙文';
|
||||
|
||||
@override
|
||||
String get resetToDefaults => '重設為預設值';
|
||||
|
||||
@override
|
||||
String get openPdf => '開啟 PDF…';
|
||||
|
||||
@override
|
||||
String get prev => '上一頁';
|
||||
|
||||
@override
|
||||
String get next => '下一頁';
|
||||
|
||||
@override
|
||||
String pageInfo(int current, int total) {
|
||||
return '第 $current/$total 頁';
|
||||
}
|
||||
|
||||
@override
|
||||
String get goTo => '前往:';
|
||||
|
||||
@override
|
||||
String get dpi => 'DPI:';
|
||||
|
||||
@override
|
||||
String get markForSigning => '標記簽署';
|
||||
|
||||
@override
|
||||
String get unmarkSigning => '取消標記';
|
||||
|
||||
@override
|
||||
String get saveSignedPdf => '儲存已簽名 PDF';
|
||||
|
||||
@override
|
||||
String get loadSignatureFromFile => '從檔案載入簽名';
|
||||
|
||||
@override
|
||||
String get drawSignature => '手寫簽名';
|
||||
|
||||
@override
|
||||
String get noPdfLoaded => '尚未載入 PDF';
|
||||
|
||||
@override
|
||||
String get signature => '簽名';
|
||||
|
||||
@override
|
||||
String get lockAspectRatio => '鎖定長寬比';
|
||||
|
||||
@override
|
||||
String get backgroundRemoval => '去除背景';
|
||||
|
||||
@override
|
||||
String get contrast => '對比';
|
||||
|
||||
@override
|
||||
String get brightness => '亮度';
|
||||
|
||||
@override
|
||||
String get exportingPleaseWait => '匯出中…請稍候';
|
||||
|
||||
@override
|
||||
String get nothingToSaveYet => '尚無可儲存的內容';
|
||||
|
||||
@override
|
||||
String savedWithPath(String path) {
|
||||
return '已儲存:$path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToSavePdf => '儲存 PDF 失敗';
|
||||
|
||||
@override
|
||||
String get downloadStarted => '已開始下載';
|
||||
|
||||
@override
|
||||
String get failedToGeneratePdf => '產生 PDF 失敗';
|
||||
|
||||
@override
|
||||
String get invalidOrUnsupportedFile => '無效或不支援的檔案';
|
||||
|
||||
@override
|
||||
String get confirm => '確認';
|
||||
|
||||
@override
|
||||
String get undo => '復原';
|
||||
|
||||
@override
|
||||
String get clear => '清除';
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"@@locale": "zh",
|
||||
"appTitle": "PDF 簽名",
|
||||
"errorWithMessage": "錯誤:{message}",
|
||||
|
||||
"settings": "設定",
|
||||
"theme": "主題",
|
||||
"themeLight": "淺色",
|
||||
"themeDark": "深色",
|
||||
"themeSystem": "系統",
|
||||
"language": "語言",
|
||||
"languageEnglish": "英文",
|
||||
"languageChineseTraditional": "繁體中文",
|
||||
"languageSpanish": "西班牙文",
|
||||
"resetToDefaults": "重設為預設值",
|
||||
|
||||
"openPdf": "開啟 PDF…",
|
||||
"prev": "上一頁",
|
||||
"next": "下一頁",
|
||||
"pageInfo": "第 {current}/{total} 頁",
|
||||
"goTo": "前往:",
|
||||
"dpi": "DPI:",
|
||||
"markForSigning": "標記簽署",
|
||||
"unmarkSigning": "取消標記",
|
||||
"saveSignedPdf": "儲存已簽名 PDF",
|
||||
"loadSignatureFromFile": "從檔案載入簽名",
|
||||
"drawSignature": "手寫簽名",
|
||||
"noPdfLoaded": "尚未載入 PDF",
|
||||
"signature": "簽名",
|
||||
"lockAspectRatio": "鎖定長寬比",
|
||||
"backgroundRemoval": "去除背景",
|
||||
"contrast": "對比",
|
||||
"brightness": "亮度",
|
||||
"exportingPleaseWait": "匯出中…請稍候",
|
||||
|
||||
"nothingToSaveYet": "尚無可儲存的內容",
|
||||
"savedWithPath": "已儲存:{path}",
|
||||
"failedToSavePdf": "儲存 PDF 失敗",
|
||||
"downloadStarted": "已開始下載",
|
||||
"failedToGeneratePdf": "產生 PDF 失敗",
|
||||
"invalidOrUnsupportedFile": "無效或不支援的檔案",
|
||||
|
||||
"confirm": "確認",
|
||||
"undo": "復原",
|
||||
"clear": "清除"
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"@@locale": "zh_TW",
|
||||
"appTitle": "PDF 簽名",
|
||||
"errorWithMessage": "錯誤:{message}",
|
||||
|
||||
"settings": "設定",
|
||||
"theme": "主題",
|
||||
"themeLight": "淺色",
|
||||
"themeDark": "深色",
|
||||
"themeSystem": "系統",
|
||||
"language": "語言",
|
||||
"languageEnglish": "英文",
|
||||
"languageChineseTraditional": "繁體中文",
|
||||
"languageSpanish": "西班牙文",
|
||||
"resetToDefaults": "重設為預設值",
|
||||
|
||||
"openPdf": "開啟 PDF…",
|
||||
"prev": "上一頁",
|
||||
"next": "下一頁",
|
||||
"pageInfo": "第 {current}/{total} 頁",
|
||||
"goTo": "前往:",
|
||||
"dpi": "DPI:",
|
||||
"markForSigning": "標記簽署",
|
||||
"unmarkSigning": "取消標記",
|
||||
"saveSignedPdf": "儲存已簽名 PDF",
|
||||
"loadSignatureFromFile": "從檔案載入簽名",
|
||||
"drawSignature": "手寫簽名",
|
||||
"noPdfLoaded": "尚未載入 PDF",
|
||||
"signature": "簽名",
|
||||
"lockAspectRatio": "鎖定長寬比",
|
||||
"backgroundRemoval": "去除背景",
|
||||
"contrast": "對比",
|
||||
"brightness": "亮度",
|
||||
"exportingPleaseWait": "匯出中…請稍候",
|
||||
|
||||
"nothingToSaveYet": "尚無可儲存的內容",
|
||||
"savedWithPath": "已儲存:{path}",
|
||||
"failedToSavePdf": "儲存 PDF 失敗",
|
||||
"downloadStarted": "已開始下載",
|
||||
"failedToGeneratePdf": "產生 PDF 失敗",
|
||||
"invalidOrUnsupportedFile": "無效或不支援的檔案",
|
||||
|
||||
"confirm": "確認",
|
||||
"undo": "復原",
|
||||
"clear": "清除"
|
||||
}
|
|
@ -1,52 +1,16 @@
|
|||
part of 'viewer.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
class PdfState {
|
||||
final bool loaded;
|
||||
final int pageCount;
|
||||
final int currentPage;
|
||||
final bool markedForSigning;
|
||||
final String? pickedPdfPath;
|
||||
final Uint8List? pickedPdfBytes;
|
||||
final int? signedPage;
|
||||
const PdfState({
|
||||
required this.loaded,
|
||||
required this.pageCount,
|
||||
required this.currentPage,
|
||||
required this.markedForSigning,
|
||||
this.pickedPdfPath,
|
||||
this.pickedPdfBytes,
|
||||
this.signedPage,
|
||||
});
|
||||
factory PdfState.initial() => const PdfState(
|
||||
loaded: false,
|
||||
pageCount: 0,
|
||||
currentPage: 1,
|
||||
markedForSigning: false,
|
||||
pickedPdfBytes: null,
|
||||
signedPage: null,
|
||||
);
|
||||
PdfState copyWith({
|
||||
bool? loaded,
|
||||
int? pageCount,
|
||||
int? currentPage,
|
||||
bool? markedForSigning,
|
||||
String? pickedPdfPath,
|
||||
Uint8List? pickedPdfBytes,
|
||||
int? signedPage,
|
||||
}) => PdfState(
|
||||
loaded: loaded ?? this.loaded,
|
||||
pageCount: pageCount ?? this.pageCount,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
markedForSigning: markedForSigning ?? this.markedForSigning,
|
||||
pickedPdfPath: pickedPdfPath ?? this.pickedPdfPath,
|
||||
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
||||
signedPage: signedPage ?? this.signedPage,
|
||||
);
|
||||
}
|
||||
import '../../../../data/model/model.dart';
|
||||
|
||||
class PdfController extends StateNotifier<PdfState> {
|
||||
PdfController() : super(PdfState.initial());
|
||||
static const int samplePageCount = 5;
|
||||
|
||||
void openSample() {
|
||||
state = state.copyWith(
|
||||
loaded: true,
|
||||
|
@ -102,51 +66,6 @@ final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
|
|||
(ref) => PdfController(),
|
||||
);
|
||||
|
||||
class SignatureState {
|
||||
final Rect? rect;
|
||||
final bool aspectLocked;
|
||||
final bool bgRemoval;
|
||||
final double contrast;
|
||||
final double brightness;
|
||||
final List<List<Offset>> strokes;
|
||||
final Uint8List? imageBytes;
|
||||
const SignatureState({
|
||||
required this.rect,
|
||||
required this.aspectLocked,
|
||||
required this.bgRemoval,
|
||||
required this.contrast,
|
||||
required this.brightness,
|
||||
required this.strokes,
|
||||
this.imageBytes,
|
||||
});
|
||||
factory SignatureState.initial() => const SignatureState(
|
||||
rect: null,
|
||||
aspectLocked: false,
|
||||
bgRemoval: false,
|
||||
contrast: 1.0,
|
||||
brightness: 0.0,
|
||||
strokes: [],
|
||||
imageBytes: null,
|
||||
);
|
||||
SignatureState copyWith({
|
||||
Rect? rect,
|
||||
bool? aspectLocked,
|
||||
bool? bgRemoval,
|
||||
double? contrast,
|
||||
double? brightness,
|
||||
List<List<Offset>>? strokes,
|
||||
Uint8List? imageBytes,
|
||||
}) => SignatureState(
|
||||
rect: rect ?? this.rect,
|
||||
aspectLocked: aspectLocked ?? this.aspectLocked,
|
||||
bgRemoval: bgRemoval ?? this.bgRemoval,
|
||||
contrast: contrast ?? this.contrast,
|
||||
brightness: brightness ?? this.brightness,
|
||||
strokes: strokes ?? this.strokes,
|
||||
imageBytes: imageBytes ?? this.imageBytes,
|
||||
);
|
||||
}
|
||||
|
||||
class SignatureController extends StateNotifier<SignatureState> {
|
||||
SignatureController() : super(SignatureState.initial());
|
||||
static const Size pageSize = Size(400, 560);
|
||||
|
@ -178,9 +97,10 @@ class SignatureController extends StateNotifier<SignatureState> {
|
|||
}
|
||||
|
||||
void setInvalidSelected(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invalid or unsupported file')),
|
||||
);
|
||||
final l = AppLocalizations.of(context);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l.invalidOrUnsupportedFile)));
|
||||
}
|
||||
|
||||
void drag(Offset delta) {
|
||||
|
@ -275,3 +195,74 @@ final signatureProvider =
|
|||
StateNotifierProvider<SignatureController, SignatureState>(
|
||||
(ref) => SignatureController(),
|
||||
);
|
||||
|
||||
/// Derived provider that returns processed signature image bytes according to
|
||||
/// current adjustment settings (contrast/brightness) and background removal.
|
||||
/// Returns null if no image is loaded. The output is a PNG to preserve alpha.
|
||||
final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
|
||||
final s = ref.watch(signatureProvider);
|
||||
final bytes = s.imageBytes;
|
||||
if (bytes == null || bytes.isEmpty) return null;
|
||||
|
||||
// Decode (supports PNG/JPEG, etc.)
|
||||
final decoded = img.decodeImage(bytes);
|
||||
if (decoded == null) return bytes;
|
||||
|
||||
// Work on a copy and ensure an alpha channel is present (RGBA)
|
||||
var out = decoded.clone();
|
||||
if (out.hasPalette || !out.hasAlpha) {
|
||||
// Force truecolor RGBA image so per-pixel alpha writes take effect
|
||||
out = out.convert(numChannels: 4);
|
||||
}
|
||||
|
||||
// Parameters
|
||||
final double contrast = s.contrast; // [0..2], 1 = neutral
|
||||
final double brightness = s.brightness; // [-1..1], 0 = neutral
|
||||
const int thrLow = 220; // begin soft transparency from this avg luminance
|
||||
const int thrHigh = 245; // fully transparent from this avg luminance
|
||||
|
||||
// Helper to clamp int
|
||||
int clamp255(num v) => v.clamp(0, 255).toInt();
|
||||
|
||||
// Iterate pixels
|
||||
for (int y = 0; y < out.height; y++) {
|
||||
for (int x = 0; x < out.width; x++) {
|
||||
final p = out.getPixel(x, y);
|
||||
int a = clamp255(p.aNormalized * 255.0);
|
||||
int r = clamp255(p.rNormalized * 255.0);
|
||||
int g = clamp255(p.gNormalized * 255.0);
|
||||
int b = clamp255(p.bNormalized * 255.0);
|
||||
|
||||
// Apply contrast/brightness in sRGB space
|
||||
// new = (old-128)*contrast + 128 + brightness*255
|
||||
final double brOffset = brightness * 255.0;
|
||||
r = clamp255((r - 128) * contrast + 128 + brOffset);
|
||||
g = clamp255((g - 128) * contrast + 128 + brOffset);
|
||||
b = clamp255((b - 128) * contrast + 128 + brOffset);
|
||||
|
||||
// Near-white background removal (compute average luminance)
|
||||
final int avg = ((r + g + b) / 3).round();
|
||||
int remAlpha = 255; // 255 = fully opaque, 0 = transparent
|
||||
if (s.bgRemoval) {
|
||||
if (avg >= thrHigh) {
|
||||
remAlpha = 0;
|
||||
} else if (avg >= thrLow) {
|
||||
// Soft fade between thrLow..thrHigh
|
||||
final double t = (avg - thrLow) / (thrHigh - thrLow);
|
||||
remAlpha = clamp255(255 * (1.0 - t));
|
||||
} else {
|
||||
remAlpha = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine with existing alpha (preserve existing transparency)
|
||||
final newA = math.min(a, remAlpha);
|
||||
|
||||
out.setPixelRgba(x, y, r, g, b, newA);
|
||||
}
|
||||
}
|
||||
|
||||
// Encode as PNG to preserve transparency
|
||||
final png = img.encodePng(out, level: 6);
|
||||
return Uint8List.fromList(png);
|
||||
});
|
|
@ -1,4 +1,8 @@
|
|||
part of 'viewer.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
import 'package:hand_signature/signature.dart' as hand;
|
||||
|
||||
class DrawCanvas extends StatefulWidget {
|
||||
const DrawCanvas({
|
||||
|
@ -32,6 +36,7 @@ class _DrawCanvasState extends State<DrawCanvas> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
|
@ -52,11 +57,8 @@ class _DrawCanvasState extends State<DrawCanvas> {
|
|||
height: 512,
|
||||
);
|
||||
final bytes = data?.buffer.asUint8List();
|
||||
// print("onPressed, Exported signature bytes: ${bytes?.length}");
|
||||
// Notify tests if provided
|
||||
widget.debugBytesSink?.value = bytes;
|
||||
if (widget.onConfirm != null) {
|
||||
// print("onConfirm callback called");
|
||||
widget.onConfirm!(bytes);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
|
@ -64,19 +66,19 @@ class _DrawCanvasState extends State<DrawCanvas> {
|
|||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Confirm'),
|
||||
child: Text(l.confirm),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
key: const Key('btn_canvas_undo'),
|
||||
onPressed: () => _control.stepBack(),
|
||||
child: const Text('Undo'),
|
||||
child: Text(l.undo),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
key: const Key('btn_canvas_clear'),
|
||||
onPressed: () => _control.clear(),
|
||||
child: const Text('Clear'),
|
||||
child: Text(l.clear),
|
||||
),
|
||||
],
|
||||
),
|
|
@ -1,51 +1,18 @@
|
|||
import 'dart:math' as math;
|
||||
import 'package:file_selector/file_selector.dart' as fs;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:path_provider/path_provider.dart' as pp;
|
||||
import 'dart:typed_data';
|
||||
import '../share/export_service.dart';
|
||||
import 'package:hand_signature/signature.dart' as hand;
|
||||
import 'package:file_selector/file_selector.dart' as fs;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:printing/printing.dart' as printing;
|
||||
|
||||
part 'viewer_state.dart';
|
||||
part 'viewer_widgets.dart';
|
||||
|
||||
final useMockViewerProvider = Provider<bool>((_) => false);
|
||||
// Export service injection for testability
|
||||
final exportServiceProvider = Provider<ExportService>((_) => ExportService());
|
||||
// Export DPI setting (points per inch mapping), default 144 DPI
|
||||
final exportDpiProvider = StateProvider<double>((_) => 144.0);
|
||||
// Controls whether signature overlay is visible (used to hide on non-stamped pages during export)
|
||||
final signatureVisibilityProvider = StateProvider<bool>((_) => true);
|
||||
// Save path picker (injected for tests)
|
||||
final savePathPickerProvider = Provider<Future<String?> Function()>((ref) {
|
||||
return () async {
|
||||
String? initialDir;
|
||||
try {
|
||||
final d = await pp.getDownloadsDirectory();
|
||||
initialDir = d?.path;
|
||||
} catch (_) {}
|
||||
if (initialDir == null) {
|
||||
try {
|
||||
final d = await pp.getApplicationDocumentsDirectory();
|
||||
initialDir = d.path;
|
||||
} catch (_) {}
|
||||
}
|
||||
final location = await fs.getSaveLocation(
|
||||
suggestedName: 'signed.pdf',
|
||||
acceptedTypeGroups: [
|
||||
const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']),
|
||||
],
|
||||
initialDirectory: initialDir,
|
||||
);
|
||||
if (location == null) return null;
|
||||
final path = location.path;
|
||||
return path.toLowerCase().endsWith('.pdf') ? path : '$path.pdf';
|
||||
};
|
||||
});
|
||||
import '../../../../data/model/model.dart';
|
||||
import '../../../../data/services/providers.dart';
|
||||
import '../view_model/view_model.dart';
|
||||
import 'draw_canvas.dart';
|
||||
import '../../preferences/widgets/settings_screen.dart';
|
||||
|
||||
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||
const PdfSignatureHomePage({super.key});
|
||||
|
@ -102,8 +69,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
sig.setImageBytes(bytes);
|
||||
}
|
||||
|
||||
// removed invalid loader; not part of normal app
|
||||
|
||||
void _onDragSignature(Offset delta) {
|
||||
ref.read(signatureProvider.notifier).drag(delta);
|
||||
}
|
||||
|
@ -128,119 +93,142 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
}
|
||||
|
||||
Future<void> _saveSignedPdf() async {
|
||||
final pdf = ref.read(pdfProvider);
|
||||
final sig = ref.read(signatureProvider);
|
||||
if (!pdf.loaded || sig.rect == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Nothing to save yet'),
|
||||
), // guard per use-case
|
||||
);
|
||||
return;
|
||||
}
|
||||
final exporter = ref.read(exportServiceProvider);
|
||||
final targetDpi = ref.read(exportDpiProvider);
|
||||
final useMock = ref.read(useMockViewerProvider);
|
||||
bool ok = false;
|
||||
String? savedPath;
|
||||
if (kIsWeb) {
|
||||
// Web: prefer using picked bytes; share via Printing
|
||||
Uint8List? src = pdf.pickedPdfBytes;
|
||||
if (src == null) {
|
||||
ok = false;
|
||||
} else {
|
||||
final bytes = await exporter.exportSignedPdfFromBytes(
|
||||
srcBytes: src,
|
||||
signedPage: pdf.signedPage,
|
||||
signatureRectUi: sig.rect,
|
||||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: sig.imageBytes,
|
||||
targetDpi: targetDpi,
|
||||
// Set exporting state to show loading overlay and block interactions
|
||||
ref.read(exportingProvider.notifier).state = true;
|
||||
try {
|
||||
final pdf = ref.read(pdfProvider);
|
||||
final sig = ref.read(signatureProvider);
|
||||
// Cache messenger before any awaits to avoid using BuildContext across async gaps.
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (!pdf.loaded || sig.rect == null) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).nothingToSaveYet),
|
||||
), // guard per use-case
|
||||
);
|
||||
if (bytes != null) {
|
||||
try {
|
||||
await printing.Printing.sharePdf(
|
||||
bytes: bytes,
|
||||
filename: 'signed.pdf',
|
||||
);
|
||||
ok = true;
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
final exporter = ref.read(exportServiceProvider);
|
||||
final targetDpi = ref.read(exportDpiProvider);
|
||||
final useMock = ref.read(useMockViewerProvider);
|
||||
bool ok = false;
|
||||
String? savedPath;
|
||||
if (kIsWeb) {
|
||||
// Web: prefer using picked bytes; share via Printing
|
||||
Uint8List? src = pdf.pickedPdfBytes;
|
||||
if (src == null) {
|
||||
ok = false;
|
||||
} else {
|
||||
final processed = ref.read(processedSignatureImageProvider);
|
||||
final bytes = await exporter.exportSignedPdfFromBytes(
|
||||
srcBytes: src,
|
||||
signedPage: pdf.signedPage,
|
||||
signatureRectUi: sig.rect,
|
||||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: processed ?? sig.imageBytes,
|
||||
targetDpi: targetDpi,
|
||||
);
|
||||
if (bytes != null) {
|
||||
try {
|
||||
await printing.Printing.sharePdf(
|
||||
bytes: bytes,
|
||||
filename: 'signed.pdf',
|
||||
);
|
||||
ok = true;
|
||||
} catch (_) {
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Desktop/mobile: choose between bytes or file-based export
|
||||
final pick = ref.read(savePathPickerProvider);
|
||||
final path = await pick();
|
||||
if (path == null || path.trim().isEmpty) return;
|
||||
final fullPath = _ensurePdfExtension(path.trim());
|
||||
savedPath = fullPath;
|
||||
if (pdf.pickedPdfBytes != null) {
|
||||
final processed = ref.read(processedSignatureImageProvider);
|
||||
final out = await exporter.exportSignedPdfFromBytes(
|
||||
srcBytes: pdf.pickedPdfBytes!,
|
||||
signedPage: pdf.signedPage,
|
||||
signatureRectUi: sig.rect,
|
||||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: processed ?? sig.imageBytes,
|
||||
targetDpi: targetDpi,
|
||||
);
|
||||
if (useMock) {
|
||||
// In mock mode for tests, simulate success without file IO
|
||||
ok = out != null;
|
||||
} else if (out != null) {
|
||||
ok = await exporter.saveBytesToFile(
|
||||
bytes: out,
|
||||
outputPath: fullPath,
|
||||
);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
} else if (pdf.pickedPdfPath != null) {
|
||||
if (useMock) {
|
||||
// Simulate success in mock
|
||||
ok = true;
|
||||
} else {
|
||||
final processed = ref.read(processedSignatureImageProvider);
|
||||
ok = await exporter.exportSignedPdfFromFile(
|
||||
inputPath: pdf.pickedPdfPath!,
|
||||
outputPath: fullPath,
|
||||
signedPage: pdf.signedPage,
|
||||
signatureRectUi: sig.rect,
|
||||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: processed ?? sig.imageBytes,
|
||||
targetDpi: targetDpi,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Desktop/mobile: choose between bytes or file-based export
|
||||
final pick = ref.read(savePathPickerProvider);
|
||||
final path = await pick();
|
||||
if (path == null || path.trim().isEmpty) return;
|
||||
final fullPath = _ensurePdfExtension(path.trim());
|
||||
savedPath = fullPath;
|
||||
if (pdf.pickedPdfBytes != null) {
|
||||
final out = await exporter.exportSignedPdfFromBytes(
|
||||
srcBytes: pdf.pickedPdfBytes!,
|
||||
signedPage: pdf.signedPage,
|
||||
signatureRectUi: sig.rect,
|
||||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: sig.imageBytes,
|
||||
targetDpi: targetDpi,
|
||||
);
|
||||
if (useMock) {
|
||||
// In mock mode for tests, simulate success without file IO
|
||||
ok = out != null;
|
||||
} else if (out != null) {
|
||||
ok = await exporter.saveBytesToFile(bytes: out, outputPath: fullPath);
|
||||
if (!kIsWeb) {
|
||||
// Desktop/mobile: we had a concrete path
|
||||
if (ok) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).savedWithPath(savedPath ?? ''),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
} else if (pdf.pickedPdfPath != null) {
|
||||
if (useMock) {
|
||||
// Simulate success in mock
|
||||
ok = true;
|
||||
} else {
|
||||
ok = await exporter.exportSignedPdfFromFile(
|
||||
inputPath: pdf.pickedPdfPath!,
|
||||
outputPath: fullPath,
|
||||
signedPage: pdf.signedPage,
|
||||
signatureRectUi: sig.rect,
|
||||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: sig.imageBytes,
|
||||
targetDpi: targetDpi,
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).failedToSavePdf),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
// Desktop/mobile: we had a concrete path
|
||||
if (ok) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Saved: ${savedPath ?? ''}')));
|
||||
} else {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Failed to save PDF')));
|
||||
}
|
||||
} else {
|
||||
// Web: indicate whether we triggered a download dialog
|
||||
if (ok) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Download started')));
|
||||
} else {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Failed to generate PDF')));
|
||||
// Web: indicate whether we triggered a download dialog
|
||||
if (ok) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).downloadStarted),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).failedToGeneratePdf),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Clear exporting state when finished or on error
|
||||
ref.read(exportingProvider.notifier).state = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Removed manual full-path dialog; using file_selector.getSaveLocation via provider
|
||||
|
||||
String _ensurePdfExtension(String name) {
|
||||
if (!name.toLowerCase().endsWith('.pdf')) return '$name.pdf';
|
||||
return name;
|
||||
|
@ -249,41 +237,87 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pdf = ref.watch(pdfProvider);
|
||||
final isExporting = ref.watch(exportingProvider);
|
||||
final l = AppLocalizations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('PDF Signature')),
|
||||
appBar: AppBar(title: Text(l.appTitle)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildToolbar(pdf),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _buildPageArea(pdf)),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final sig = ref.watch(signatureProvider);
|
||||
return sig.rect != null
|
||||
? _buildAdjustmentsPanel(sig)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
Column(
|
||||
children: [
|
||||
_buildToolbar(pdf, disabled: isExporting),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: AbsorbPointer(
|
||||
absorbing: isExporting,
|
||||
child: _buildPageArea(pdf),
|
||||
),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final sig = ref.watch(signatureProvider);
|
||||
return sig.rect != null
|
||||
? AbsorbPointer(
|
||||
absorbing: isExporting,
|
||||
child: _buildAdjustmentsPanel(sig),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isExporting)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black45,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
l.exportingPleaseWait,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolbar(PdfState pdf) {
|
||||
Widget _buildToolbar(PdfState pdf, {bool disabled = false}) {
|
||||
final dpi = ref.watch(exportDpiProvider);
|
||||
final pageInfo = 'Page ${pdf.currentPage}/${pdf.pageCount}';
|
||||
final l = AppLocalizations.of(context);
|
||||
final pageInfo = l.pageInfo(pdf.currentPage, pdf.pageCount);
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
key: const Key('btn_open_settings'),
|
||||
onPressed:
|
||||
disabled
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
},
|
||||
child: Text(l.settings),
|
||||
),
|
||||
OutlinedButton(
|
||||
key: const Key('btn_open_pdf_picker'),
|
||||
onPressed: _pickPdf,
|
||||
child: const Text('Open PDF...'),
|
||||
onPressed: disabled ? null : _pickPdf,
|
||||
child: Text(l.openPdf),
|
||||
),
|
||||
if (pdf.loaded) ...[
|
||||
Row(
|
||||
|
@ -291,28 +325,31 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
children: [
|
||||
IconButton(
|
||||
key: const Key('btn_prev'),
|
||||
onPressed: () => _jumpToPage(pdf.currentPage - 1),
|
||||
onPressed:
|
||||
disabled ? null : () => _jumpToPage(pdf.currentPage - 1),
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
tooltip: 'Prev',
|
||||
tooltip: l.prev,
|
||||
),
|
||||
Text(pageInfo, key: const Key('lbl_page_info')),
|
||||
IconButton(
|
||||
key: const Key('btn_next'),
|
||||
onPressed: () => _jumpToPage(pdf.currentPage + 1),
|
||||
onPressed:
|
||||
disabled ? null : () => _jumpToPage(pdf.currentPage + 1),
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
tooltip: 'Next',
|
||||
tooltip: l.next,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Go to:'),
|
||||
Text(l.goTo),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: TextField(
|
||||
key: const Key('txt_goto'),
|
||||
keyboardType: TextInputType.number,
|
||||
enabled: !disabled,
|
||||
onSubmitted: (v) {
|
||||
final n = int.tryParse(v);
|
||||
if (n != null) _jumpToPage(n);
|
||||
|
@ -324,7 +361,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('DPI:'),
|
||||
Text(l.dpi),
|
||||
const SizedBox(width: 8),
|
||||
DropdownButton<double>(
|
||||
key: const Key('ddl_export_dpi'),
|
||||
|
@ -338,37 +375,40 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
ref.read(exportDpiProvider.notifier).state = v;
|
||||
}
|
||||
},
|
||||
onChanged:
|
||||
disabled
|
||||
? null
|
||||
: (v) {
|
||||
if (v != null) {
|
||||
ref.read(exportDpiProvider.notifier).state = v;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
key: const Key('btn_mark_signing'),
|
||||
onPressed: _toggleMarkForSigning,
|
||||
onPressed: disabled ? null : _toggleMarkForSigning,
|
||||
child: Text(
|
||||
pdf.markedForSigning ? 'Unmark Signing' : 'Mark for Signing',
|
||||
pdf.markedForSigning ? l.unmarkSigning : l.markForSigning,
|
||||
),
|
||||
),
|
||||
if (pdf.loaded)
|
||||
ElevatedButton(
|
||||
key: const Key('btn_save_pdf'),
|
||||
onPressed: _saveSignedPdf,
|
||||
child: const Text('Save Signed PDF'),
|
||||
onPressed: disabled ? null : _saveSignedPdf,
|
||||
child: Text(l.saveSignedPdf),
|
||||
),
|
||||
if (pdf.markedForSigning) ...[
|
||||
OutlinedButton(
|
||||
key: const Key('btn_load_signature_picker'),
|
||||
onPressed: _loadSignatureFromFile,
|
||||
child: const Text('Load Signature from file'),
|
||||
onPressed: disabled ? null : _loadSignatureFromFile,
|
||||
child: Text(l.loadSignatureFromFile),
|
||||
),
|
||||
ElevatedButton(
|
||||
key: const Key('btn_draw_signature'),
|
||||
onPressed: _openDrawCanvas,
|
||||
child: const Text('Draw Signature'),
|
||||
onPressed: disabled ? null : _openDrawCanvas,
|
||||
child: Text(l.drawSignature),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
@ -378,7 +418,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
|
||||
Widget _buildPageArea(PdfState pdf) {
|
||||
if (!pdf.loaded) {
|
||||
return const Center(child: Text('No PDF loaded'));
|
||||
return Center(child: Text(AppLocalizations.of(context).noPdfLoaded));
|
||||
}
|
||||
final useMock = ref.watch(useMockViewerProvider);
|
||||
if (useMock) {
|
||||
|
@ -395,7 +435,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
color: Colors.grey.shade200,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Page ${pdf.currentPage}/${pdf.pageCount}',
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).pageInfo(pdf.currentPage, pdf.pageCount),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.black54,
|
||||
|
@ -500,17 +542,32 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(
|
||||
color: Color.fromRGBO(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
||||
),
|
||||
border: Border.all(color: Colors.indigo, width: 2),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (sig.imageBytes != null)
|
||||
Image.memory(sig.imageBytes!, fit: BoxFit.contain)
|
||||
else
|
||||
const Center(child: Text('Signature')),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final processed = ref.watch(
|
||||
processedSignatureImageProvider,
|
||||
);
|
||||
final bytes = processed ?? sig.imageBytes;
|
||||
if (bytes == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).signature,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Image.memory(bytes, fit: BoxFit.contain);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
|
@ -552,7 +609,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
.read(signatureProvider.notifier)
|
||||
.toggleAspect(v ?? false),
|
||||
),
|
||||
const Text('Lock aspect ratio'),
|
||||
Text(AppLocalizations.of(context).lockAspectRatio),
|
||||
const SizedBox(width: 16),
|
||||
Switch(
|
||||
key: const Key('swt_bg_removal'),
|
||||
|
@ -560,12 +617,12 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
onChanged:
|
||||
(v) => ref.read(signatureProvider.notifier).setBgRemoval(v),
|
||||
),
|
||||
const Text('Background removal'),
|
||||
Text(AppLocalizations.of(context).backgroundRemoval),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Contrast'),
|
||||
Text(AppLocalizations.of(context).contrast),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
key: const Key('sld_contrast'),
|
||||
|
@ -581,7 +638,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Brightness'),
|
||||
Text(AppLocalizations.of(context).brightness),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
key: const Key('sld_brightness'),
|
|
@ -0,0 +1,143 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// Simple supported locales
|
||||
const supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('zh', 'TW'),
|
||||
Locale('es'),
|
||||
];
|
||||
|
||||
// Keys
|
||||
const _kTheme = 'theme'; // 'light'|'dark'|'system'
|
||||
const _kLanguage = 'language'; // 'en'|'zh-TW'|'es'
|
||||
|
||||
String _normalizeLanguageTag(String tag) {
|
||||
final parts = tag.split('-');
|
||||
if (parts.isEmpty) return 'en';
|
||||
final primary = parts[0].toLowerCase();
|
||||
if (primary == 'en') return 'en';
|
||||
if (primary == 'es') return 'es';
|
||||
if (primary == 'zh') {
|
||||
final region = parts.length > 1 ? parts[1].toUpperCase() : '';
|
||||
if (region == 'TW') return 'zh-TW';
|
||||
// other zh regions not supported; fall back to English
|
||||
return 'en';
|
||||
}
|
||||
// Fallback default
|
||||
return 'en';
|
||||
}
|
||||
|
||||
class PreferencesState {
|
||||
final String theme; // 'light' | 'dark' | 'system'
|
||||
final String language; // 'en' | 'zh-TW' | 'es'
|
||||
const PreferencesState({required this.theme, required this.language});
|
||||
|
||||
PreferencesState copyWith({String? theme, String? language}) =>
|
||||
PreferencesState(
|
||||
theme: theme ?? this.theme,
|
||||
language: language ?? this.language,
|
||||
);
|
||||
}
|
||||
|
||||
class PreferencesNotifier extends StateNotifier<PreferencesState> {
|
||||
final SharedPreferences prefs;
|
||||
PreferencesNotifier(this.prefs)
|
||||
: super(
|
||||
PreferencesState(
|
||||
theme: prefs.getString(_kTheme) ?? 'system',
|
||||
language: _normalizeLanguageTag(
|
||||
prefs.getString(_kLanguage) ??
|
||||
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),
|
||||
),
|
||||
),
|
||||
) {
|
||||
// normalize language to supported/fallback
|
||||
_ensureValid();
|
||||
}
|
||||
|
||||
void _ensureValid() {
|
||||
final themeValid = {'light', 'dark', 'system'};
|
||||
if (!themeValid.contains(state.theme)) {
|
||||
state = state.copyWith(theme: 'system');
|
||||
prefs.setString(_kTheme, 'system');
|
||||
}
|
||||
final normalized = _normalizeLanguageTag(state.language);
|
||||
if (normalized != state.language) {
|
||||
state = state.copyWith(language: normalized);
|
||||
prefs.setString(_kLanguage, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setTheme(String theme) async {
|
||||
final valid = {'light', 'dark', 'system'};
|
||||
if (!valid.contains(theme)) return;
|
||||
state = state.copyWith(theme: theme);
|
||||
await prefs.setString(_kTheme, theme);
|
||||
}
|
||||
|
||||
Future<void> setLanguage(String language) async {
|
||||
final normalized = _normalizeLanguageTag(language);
|
||||
state = state.copyWith(language: normalized);
|
||||
await prefs.setString(_kLanguage, normalized);
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
final device = WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag();
|
||||
final normalized = _normalizeLanguageTag(device);
|
||||
state = PreferencesState(theme: 'system', language: normalized);
|
||||
await prefs.setString(_kTheme, 'system');
|
||||
await prefs.setString(_kLanguage, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
final sharedPreferencesProvider = FutureProvider<SharedPreferences>((
|
||||
ref,
|
||||
) async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
return p;
|
||||
});
|
||||
|
||||
final preferencesProvider =
|
||||
StateNotifierProvider<PreferencesNotifier, PreferencesState>((ref) {
|
||||
// In tests, you can override sharedPreferencesProvider
|
||||
final prefs = ref
|
||||
.watch(sharedPreferencesProvider)
|
||||
.maybeWhen(
|
||||
data: (p) => p,
|
||||
orElse: () => throw StateError('SharedPreferences not ready'),
|
||||
);
|
||||
return PreferencesNotifier(prefs);
|
||||
});
|
||||
|
||||
/// Derive the active ThemeMode based on preference and platform brightness
|
||||
final themeModeProvider = Provider<ThemeMode>((ref) {
|
||||
final prefs = ref.watch(preferencesProvider);
|
||||
switch (prefs.theme) {
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
case 'system':
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
});
|
||||
|
||||
Locale _parseLanguageTag(String tag) {
|
||||
// 'zh-TW' -> ('zh','TW')
|
||||
final parts = tag.split('-');
|
||||
if (parts.length == 2) return Locale(parts[0], parts[1]);
|
||||
return Locale(parts[0]);
|
||||
}
|
||||
|
||||
final localeProvider = Provider<Locale?>((ref) {
|
||||
final prefs = ref.watch(preferencesProvider);
|
||||
// Return explicit Locale for supported ones; if not supported, null to follow device
|
||||
final supported = {'en', 'zh-TW', 'es'};
|
||||
if (supported.contains(prefs.language)) {
|
||||
return _parseLanguageTag(prefs.language);
|
||||
}
|
||||
return null;
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
import '../providers.dart';
|
||||
|
||||
class SettingsScreen extends ConsumerWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final prefs = ref.watch(preferencesProvider);
|
||||
final l = AppLocalizations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l.settings)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l.theme, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButton<String>(
|
||||
key: const Key('ddl_theme'),
|
||||
value: prefs.theme,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'light', child: Text(l.themeLight)),
|
||||
DropdownMenuItem(value: 'dark', child: Text(l.themeDark)),
|
||||
DropdownMenuItem(value: 'system', child: Text(l.themeSystem)),
|
||||
],
|
||||
onChanged:
|
||||
(v) =>
|
||||
v == null
|
||||
? null
|
||||
: ref.read(preferencesProvider.notifier).setTheme(v),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l.language,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButton<String>(
|
||||
key: const Key('ddl_language'),
|
||||
value: prefs.language,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'en', child: Text(l.languageEnglish)),
|
||||
DropdownMenuItem(
|
||||
value: 'zh-TW',
|
||||
child: Text(l.languageChineseTraditional),
|
||||
),
|
||||
DropdownMenuItem(value: 'es', child: Text(l.languageSpanish)),
|
||||
],
|
||||
onChanged:
|
||||
(v) =>
|
||||
v == null
|
||||
? null
|
||||
: ref
|
||||
.read(preferencesProvider.notifier)
|
||||
.setLanguage(v),
|
||||
),
|
||||
const Spacer(),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: OutlinedButton(
|
||||
key: const Key('btn_reset_defaults'),
|
||||
onPressed:
|
||||
() =>
|
||||
ref
|
||||
.read(preferencesProvider.notifier)
|
||||
.resetToDefaults(),
|
||||
child: Text(l.resetToDefaults),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
14
pubspec.yaml
14
pubspec.yaml
|
@ -39,31 +39,41 @@ dependencies:
|
|||
flutter_dotenv: ^6.0.0
|
||||
file_selector: ^1.0.3
|
||||
path_provider: ^2.1.5
|
||||
pdfrx: ^1.3.5
|
||||
pdfrx: ^2.1.9
|
||||
pdf: ^3.10.8
|
||||
hand_signature: ^3.1.0+2
|
||||
image: ^4.2.0
|
||||
printing: ^5.14.2
|
||||
result_dart: ^2.1.1
|
||||
go_router: ^16.2.0
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.12
|
||||
build: ^3.0.2
|
||||
bdd_widget_test: ^2.0.1
|
||||
custom_lint: ^0.7.6
|
||||
riverpod_lint: ^2.6.5
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
msix: ^3.16.12
|
||||
json_serializable: ^6.11.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:image/image.dart' as img;
|
|||
import 'package:pdf/pdf.dart' as pdf;
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
import 'package:pdf_signature/features/share/export_service.dart';
|
||||
import 'package:pdf_signature/data/services/export_service.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
Feature: App preferences
|
||||
|
||||
As a user
|
||||
I want to configure app preferences such as theme and language
|
||||
So that the app matches my personal and regional needs, and remembers them next time
|
||||
|
||||
Scenario Outline: Choose a theme and apply it immediately
|
||||
Given the settings screen is open
|
||||
When the user selects the "<theme>" theme
|
||||
Then the app UI updates to use the "<theme>" theme
|
||||
And the preference {theme} is saved as {"<theme>"}
|
||||
|
||||
Examples:
|
||||
| theme |
|
||||
| light |
|
||||
| dark |
|
||||
| system |
|
||||
|
||||
Scenario Outline: Choose a language and apply it immediately
|
||||
Given the settings screen is open
|
||||
When the user selects a supported language "<language>"
|
||||
Then all visible texts are displayed in "<language>"
|
||||
And the preference {language} is saved as {"<language>"}
|
||||
|
||||
Examples:
|
||||
| language |
|
||||
| en |
|
||||
| zh-TW |
|
||||
| es |
|
||||
|
||||
Scenario Outline: Remember preferences across app restarts
|
||||
Given the user previously set theme {"<theme>"} and language {"<language>"}
|
||||
When the app launches
|
||||
Then the app UI theme is {"<theme>"}
|
||||
And the app language is {"<language>"}
|
||||
|
||||
Examples:
|
||||
| theme | language |
|
||||
| dark | en |
|
||||
| light | zh-TW |
|
||||
| system | es |
|
||||
|
||||
Scenario: Follow system appearance when theme is set to system
|
||||
Given the user selects the "system" theme
|
||||
And the OS appearance switches to dark mode
|
||||
When the app is resumed or returns to foreground
|
||||
Then the app UI updates to use the "dark" theme
|
||||
|
||||
Scenario: Reset preferences to defaults
|
||||
Given the user has theme {"dark"} and language {"es"} saved
|
||||
When the user taps "Reset to defaults"
|
||||
Then the theme is set to {"system"}
|
||||
And the language is set to the device locale
|
||||
And both preferences are saved
|
||||
|
||||
Scenario: Ignore invalid stored values and fall back safely
|
||||
Given stored preferences contain theme {"sepia"} and language {"xx"}
|
||||
When the app launches
|
||||
Then the theme falls back to {"system"}
|
||||
And the language falls back to the device locale
|
||||
And invalid values are replaced with valid defaults in storage
|
|
@ -1,38 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/an_empty_signature_canvas.dart';
|
||||
import './step/the_user_draws_strokes_and_confirms.dart';
|
||||
import './step/a_signature_image_is_created.dart';
|
||||
import './step/it_is_placed_on_the_selected_page.dart';
|
||||
import './step/a_drawn_signature_exists_in_the_canvas.dart';
|
||||
import './step/the_user_clears_the_canvas.dart';
|
||||
import './step/the_canvas_becomes_blank.dart';
|
||||
import './step/multiple_strokes_were_drawn.dart';
|
||||
import './step/the_user_chooses_undo.dart';
|
||||
import './step/the_last_stroke_is_removed.dart';
|
||||
|
||||
void main() {
|
||||
group('''draw signature''', () {
|
||||
testWidgets('''Draw with mouse or touch and place on page''',
|
||||
(tester) async {
|
||||
await anEmptySignatureCanvas(tester);
|
||||
await theUserDrawsStrokesAndConfirms(tester);
|
||||
await aSignatureImageIsCreated(tester);
|
||||
await itIsPlacedOnTheSelectedPage(tester);
|
||||
});
|
||||
testWidgets('''Clear and redraw''', (tester) async {
|
||||
await aDrawnSignatureExistsInTheCanvas(tester);
|
||||
await theUserClearsTheCanvas(tester);
|
||||
await theCanvasBecomesBlank(tester);
|
||||
});
|
||||
testWidgets('''Undo the last stroke''', (tester) async {
|
||||
await multipleStrokesWereDrawn(tester);
|
||||
await theUserChoosesUndo(tester);
|
||||
await theLastStrokeIsRemoved(tester);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_signature_image_is_placed_on_the_page.dart';
|
||||
import './step/the_user_drags_handles_to_resize_and_drags_to_reposition.dart';
|
||||
import './step/the_size_and_position_update_in_real_time.dart';
|
||||
import './step/the_signature_remains_within_the_page_area.dart';
|
||||
import './step/a_signature_image_is_selected.dart';
|
||||
import './step/the_user_enables_aspect_ratio_lock_and_resizes.dart';
|
||||
import './step/the_image_scales_proportionally.dart';
|
||||
|
||||
void main() {
|
||||
group('''geometrically adjust signature picture''', () {
|
||||
testWidgets('''Resize and move the signature within page bounds''',
|
||||
(tester) async {
|
||||
await aSignatureImageIsPlacedOnThePage(tester);
|
||||
await theUserDragsHandlesToResizeAndDragsToReposition(tester);
|
||||
await theSizeAndPositionUpdateInRealTime(tester);
|
||||
await theSignatureRemainsWithinThePageArea(tester);
|
||||
});
|
||||
testWidgets('''Lock aspect ratio while resizing''', (tester) async {
|
||||
await aSignatureImageIsSelected(tester);
|
||||
await theUserEnablesAspectRatioLockAndResizes(tester);
|
||||
await theImageScalesProportionally(tester);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_signature_image_is_selected.dart';
|
||||
import './step/the_user_enables_background_removal.dart';
|
||||
import './step/nearwhite_background_becomes_transparent_in_the_preview.dart';
|
||||
import './step/the_user_can_apply_the_change.dart';
|
||||
import './step/the_user_changes_contrast_and_brightness_controls.dart';
|
||||
import './step/the_preview_updates_immediately.dart';
|
||||
import './step/the_user_can_apply_or_reset_adjustments.dart';
|
||||
|
||||
void main() {
|
||||
group('''graphically adjust signature picture''', () {
|
||||
testWidgets('''Remove background''', (tester) async {
|
||||
await aSignatureImageIsSelected(tester);
|
||||
await theUserEnablesBackgroundRemoval(tester);
|
||||
await nearwhiteBackgroundBecomesTransparentInThePreview(tester);
|
||||
await theUserCanApplyTheChange(tester);
|
||||
});
|
||||
testWidgets('''Adjust contrast and brightness''', (tester) async {
|
||||
await aSignatureImageIsSelected(tester);
|
||||
await theUserChangesContrastAndBrightnessControls(tester);
|
||||
await thePreviewUpdatesImmediately(tester);
|
||||
await theUserCanApplyOrResetAdjustments(tester);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -12,7 +12,7 @@ Feature: load signature picture
|
|||
And the image is not added to the document
|
||||
|
||||
Examples:
|
||||
| file |
|
||||
| corrupted.png |
|
||||
| signature.bmp |
|
||||
| empty.jpg |
|
||||
| file |
|
||||
| 'corrupted.png' |
|
||||
| 'signature.bmp' |
|
||||
| 'empty.jpg' |
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_pdf_page_is_selected_for_signing.dart';
|
||||
import './step/the_user_chooses_a_signature_image_file.dart';
|
||||
import './step/the_image_is_loaded_and_shown_as_a_signature_asset.dart';
|
||||
import './step/the_user_selects.dart';
|
||||
import './step/the_app_attempts_to_load_the_image.dart';
|
||||
import './step/the_user_is_notified_of_the_issue.dart';
|
||||
import './step/the_image_is_not_added_to_the_document.dart';
|
||||
import './step/_tokens.dart';
|
||||
|
||||
void main() {
|
||||
group('''load signature picture''', () {
|
||||
testWidgets('''Import a signature image''', (tester) async {
|
||||
await aPdfPageIsSelectedForSigning(tester);
|
||||
await theUserChoosesASignatureImageFile(tester);
|
||||
await theImageIsLoadedAndShownAsASignatureAsset(tester);
|
||||
});
|
||||
testWidgets(
|
||||
'''Outline: Handle invalid or unsupported files (corrupted.png)''',
|
||||
(tester) async {
|
||||
await theUserSelects(tester, corrupted.png);
|
||||
await theAppAttemptsToLoadTheImage(tester);
|
||||
await theUserIsNotifiedOfTheIssue(tester);
|
||||
await theImageIsNotAddedToTheDocument(tester);
|
||||
},
|
||||
);
|
||||
testWidgets(
|
||||
'''Outline: Handle invalid or unsupported files (signature.bmp)''',
|
||||
(tester) async {
|
||||
await theUserSelects(tester, signature.bmp);
|
||||
await theAppAttemptsToLoadTheImage(tester);
|
||||
await theUserIsNotifiedOfTheIssue(tester);
|
||||
await theImageIsNotAddedToTheDocument(tester);
|
||||
},
|
||||
);
|
||||
testWidgets(
|
||||
'''Outline: Handle invalid or unsupported files (empty.jpg)''',
|
||||
(tester) async {
|
||||
await theUserSelects(tester, empty.jpg);
|
||||
await theAppAttemptsToLoadTheImage(tester);
|
||||
await theUserIsNotifiedOfTheIssue(tester);
|
||||
await theImageIsNotAddedToTheDocument(tester);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_pdf_document_is_available.dart';
|
||||
import './step/the_user_opens_the_document.dart';
|
||||
import './step/the_first_page_is_displayed.dart';
|
||||
import './step/the_user_can_move_to_the_next_or_previous_page.dart';
|
||||
import './step/a_multipage_pdf_is_open.dart';
|
||||
import './step/the_user_selects_a_specific_page_number.dart';
|
||||
import './step/that_page_is_displayed.dart';
|
||||
|
||||
void main() {
|
||||
group('''PDF browser''', () {
|
||||
testWidgets('''Open a PDF and navigate pages''', (tester) async {
|
||||
await aPdfDocumentIsAvailable(tester);
|
||||
await theUserOpensTheDocument(tester);
|
||||
await theFirstPageIsDisplayed(tester);
|
||||
await theUserCanMoveToTheNextOrPreviousPage(tester);
|
||||
});
|
||||
testWidgets('''Jump to a specific page''', (tester) async {
|
||||
await aMultipagePdfIsOpen(tester);
|
||||
await theUserSelectsASpecificPageNumber(tester);
|
||||
await thatPageIsDisplayed(tester);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_new_provider_container.dart';
|
||||
import './step/i_openpicked_with_path_and_pagecount.dart';
|
||||
import './step/pdf_state_is_loaded.dart';
|
||||
import './step/pdf_picked_path_is.dart';
|
||||
import './step/pdf_page_count_is.dart';
|
||||
import './step/pdf_current_page_is.dart';
|
||||
import './step/pdf_marked_for_signing_is.dart';
|
||||
import './step/a_pdf_is_open_with_path_and_pagecount.dart';
|
||||
import './step/i_jumpto.dart';
|
||||
import './step/i_toggle_mark.dart';
|
||||
import './step/i_set_page_count.dart';
|
||||
|
||||
void main() {
|
||||
group('''PDF state logic''', () {
|
||||
testWidgets('''openPicked loads document and initializes state''',
|
||||
(tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await iOpenpickedWithPathAndPagecount(tester, 'test.pdf', 7);
|
||||
await pdfStateIsLoaded(tester, true);
|
||||
await pdfPickedPathIs(tester, 'test.pdf');
|
||||
await pdfPageCountIs(tester, 7);
|
||||
await pdfCurrentPageIs(tester, 1);
|
||||
await pdfMarkedForSigningIs(tester, false);
|
||||
});
|
||||
testWidgets('''jumpTo clamps within page boundaries''', (tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await aPdfIsOpenWithPathAndPagecount(tester, 'test.pdf', 5);
|
||||
await iJumpto(tester, 10);
|
||||
await pdfCurrentPageIs(tester, 5);
|
||||
await iJumpto(tester, 0);
|
||||
await pdfCurrentPageIs(tester, 1);
|
||||
await iJumpto(tester, 3);
|
||||
await pdfCurrentPageIs(tester, 3);
|
||||
});
|
||||
testWidgets('''setPageCount updates count without toggling other flags''',
|
||||
(tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await aPdfIsOpenWithPathAndPagecount(tester, 'test.pdf', 2);
|
||||
await iToggleMark(tester);
|
||||
await iSetPageCount(tester, 9);
|
||||
await pdfPageCountIs(tester, 9);
|
||||
await pdfStateIsLoaded(tester, true);
|
||||
await pdfMarkedForSigningIs(tester, true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_pdf_is_open_and_contains_at_least_one_placed_signature.dart';
|
||||
import './step/the_user_savesexports_the_document.dart';
|
||||
import './step/a_new_pdf_file_is_saved_at_specified_full_path_location_and_file_name.dart';
|
||||
import './step/the_signatures_appear_on_the_corresponding_page_in_the_output.dart';
|
||||
import './step/keep_other_unchanged_contentpages_intact_in_the_output.dart';
|
||||
import './step/a_signature_is_placed_with_a_position_and_size_relative_to_the_page.dart';
|
||||
import './step/the_signature_is_stamped_at_the_exact_pdf_page_coordinates_and_size.dart';
|
||||
import './step/the_stamp_remains_crisp_at_any_zoom_level_not_rasterized_by_the_screen.dart';
|
||||
import './step/other_page_content_remains_vector_and_unaltered.dart';
|
||||
import './step/a_pdf_is_open_with_no_signatures_placed.dart';
|
||||
import './step/the_user_attempts_to_save.dart';
|
||||
import './step/the_user_is_notified_there_is_nothing_to_save.dart';
|
||||
import './step/the_user_starts_exporting_the_document.dart';
|
||||
import './step/the_export_process_is_not_yet_finished.dart';
|
||||
import './step/the_user_is_notified_that_the_export_is_still_in_progress.dart';
|
||||
import './step/the_user_cannot_edit_the_document.dart';
|
||||
|
||||
void main() {
|
||||
group('''save signed PDF''', () {
|
||||
testWidgets(
|
||||
'''Export the signed document to a new file''',
|
||||
(tester) async {
|
||||
await aPdfIsOpenAndContainsAtLeastOnePlacedSignature(tester);
|
||||
await theUserSavesexportsTheDocument(tester);
|
||||
await aNewPdfFileIsSavedAtSpecifiedFullPathLocationAndFileName(tester);
|
||||
await theSignaturesAppearOnTheCorrespondingPageInTheOutput(tester);
|
||||
await keepOtherUnchangedContentpagesIntactInTheOutput(tester);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 30)),
|
||||
);
|
||||
testWidgets(
|
||||
'''Vector-accurate stamping into PDF page coordinates''',
|
||||
(tester) async {
|
||||
await aSignatureIsPlacedWithAPositionAndSizeRelativeToThePage(tester);
|
||||
await theUserSavesexportsTheDocument(tester);
|
||||
await theSignatureIsStampedAtTheExactPdfPageCoordinatesAndSize(tester);
|
||||
await theStampRemainsCrispAtAnyZoomLevelNotRasterizedByTheScreen(
|
||||
tester,
|
||||
);
|
||||
await otherPageContentRemainsVectorAndUnaltered(tester);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 30)),
|
||||
);
|
||||
testWidgets(
|
||||
'''Prevent saving when nothing is placed''',
|
||||
(tester) async {
|
||||
await aPdfIsOpenWithNoSignaturesPlaced(tester);
|
||||
await theUserAttemptsToSave(tester);
|
||||
await theUserIsNotifiedThereIsNothingToSave(tester);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 30)),
|
||||
);
|
||||
testWidgets(
|
||||
'''Loading sign when exporting/saving files''',
|
||||
(tester) async {
|
||||
await aSignatureIsPlacedWithAPositionAndSizeRelativeToThePage(tester);
|
||||
await theUserStartsExportingTheDocument(tester);
|
||||
await theExportProcessIsNotYetFinished(tester);
|
||||
await theUserIsNotifiedThatTheExportIsStillInProgress(tester);
|
||||
await theUserCannotEditTheDocument(tester);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 30)),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -32,9 +32,4 @@ Feature: Signature state logic
|
|||
And signature rect right <= {400}
|
||||
And signature rect bottom <= {560}
|
||||
|
||||
Scenario: setImageBytes ensures a rect exists for display
|
||||
Given a new provider container
|
||||
Then signature rect is null
|
||||
When I set tiny signature image bytes
|
||||
Then signature image bytes is not null
|
||||
And signature rect is not null
|
||||
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import './step/a_new_provider_container.dart';
|
||||
import './step/signature_rect_is_null.dart';
|
||||
import './step/i_place_default_signature_rect.dart';
|
||||
import './step/signature_rect_left.dart';
|
||||
import './step/signature_rect_top.dart';
|
||||
import './step/signature_rect_right.dart';
|
||||
import './step/signature_rect_bottom.dart';
|
||||
import './step/signature_rect_width.dart';
|
||||
import './step/signature_rect_height.dart';
|
||||
import './step/a_default_signature_rect_is_placed.dart';
|
||||
import './step/i_drag_signature_by.dart';
|
||||
import './step/signature_rect_moved_from_center.dart';
|
||||
import './step/aspect_lock_is.dart';
|
||||
import './step/i_resize_signature_by.dart';
|
||||
import './step/signature_aspect_ratio_is_preserved_within.dart';
|
||||
import './step/i_set_tiny_signature_image_bytes.dart';
|
||||
import './step/signature_image_bytes_is_not_null.dart';
|
||||
import './step/signature_rect_is_not_null.dart';
|
||||
|
||||
void main() {
|
||||
group('''Signature state logic''', () {
|
||||
testWidgets('''placeDefaultRect centers a reasonable default rect''',
|
||||
(tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await signatureRectIsNull(tester);
|
||||
await iPlaceDefaultSignatureRect(tester);
|
||||
await signatureRectLeft(tester, 0);
|
||||
await signatureRectTop(tester, 0);
|
||||
await signatureRectRight(tester, 400);
|
||||
await signatureRectBottom(tester, 560);
|
||||
await signatureRectWidth(tester, 50);
|
||||
await signatureRectHeight(tester, 20);
|
||||
});
|
||||
testWidgets('''drag clamps to canvas bounds''', (tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await aDefaultSignatureRectIsPlaced(tester);
|
||||
await iDragSignatureBy(tester, Offset(10000, -10000));
|
||||
await signatureRectLeft(tester, 0);
|
||||
await signatureRectTop(tester, 0);
|
||||
await signatureRectRight(tester, 400);
|
||||
await signatureRectBottom(tester, 560);
|
||||
await signatureRectMovedFromCenter(tester);
|
||||
});
|
||||
testWidgets('''resize respects aspect lock and clamps''', (tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await aDefaultSignatureRectIsPlaced(tester);
|
||||
await aspectLockIs(tester, true);
|
||||
await iResizeSignatureBy(tester, Offset(1000, 1000));
|
||||
await signatureAspectRatioIsPreservedWithin(tester, 0.05);
|
||||
await signatureRectLeft(tester, 0);
|
||||
await signatureRectTop(tester, 0);
|
||||
await signatureRectRight(tester, 400);
|
||||
await signatureRectBottom(tester, 560);
|
||||
});
|
||||
testWidgets('''setImageBytes ensures a rect exists for display''',
|
||||
(tester) async {
|
||||
await aNewProviderContainer(tester);
|
||||
await signatureRectIsNull(tester);
|
||||
await iSetTinySignatureImageBytes(tester);
|
||||
await signatureImageBytesIsNotNull(tester);
|
||||
await signatureRectIsNotNull(tester);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -2,7 +2,6 @@ import 'dart:typed_data';
|
|||
import 'dart:ui' show Rect, Size;
|
||||
import 'dart:io';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import '_world.dart';
|
||||
|
||||
// A lightweight fake exporter to avoid platform rasterization in tests.
|
||||
|
|
|
@ -22,6 +22,15 @@ class TestWorld {
|
|||
// Generic flags/values
|
||||
static int? selectedPage;
|
||||
|
||||
// Preferences & settings
|
||||
static Map<String, String> prefs = {};
|
||||
static String systemTheme = 'light'; // simulated OS theme: 'light' | 'dark'
|
||||
static String deviceLocale = 'en'; // simulated device locale
|
||||
static String? selectedTheme; // 'light' | 'dark' | 'system'
|
||||
static String? currentTheme; // actual UI theme applied: 'light' | 'dark'
|
||||
static String? currentLanguage; // 'en' | 'zh-TW' | 'es'
|
||||
static bool settingsOpen = false;
|
||||
|
||||
static void reset() {
|
||||
prevCenter = null;
|
||||
prevAspect = null;
|
||||
|
@ -32,5 +41,14 @@ class TestWorld {
|
|||
exportInProgress = false;
|
||||
nothingToSaveAttempt = false;
|
||||
selectedPage = null;
|
||||
|
||||
// Preferences
|
||||
prefs = {};
|
||||
systemTheme = 'light';
|
||||
deviceLocale = 'en';
|
||||
selectedTheme = null;
|
||||
currentTheme = null;
|
||||
currentLanguage = null;
|
||||
settingsOpen = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a default signature rect is placed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a drawn signature exists in the canvas
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a multi-page PDF is open
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a PDF document is available
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a PDF is open and contains at least one placed signature
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a PDF is open with no signatures placed
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a pdf is open with path {'test.pdf'} and pageCount {5}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a PDF page is selected for signing
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a signature image is created
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a signature image is placed on the page
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a signature image is selected
|
||||
|
@ -15,4 +15,6 @@ Future<void> aSignatureImageIsSelected(WidgetTester tester) async {
|
|||
container
|
||||
.read(signatureProvider.notifier)
|
||||
.setImageBytes(Uint8List.fromList([1, 2, 3]));
|
||||
// Allow provider scheduler to process queued updates fully
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:typed_data';
|
|||
import 'dart:ui';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: a signature is placed with a position and size relative to the page
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: all visible texts are displayed in "<language>"
|
||||
Future<void> allVisibleTextsAreDisplayedIn(
|
||||
WidgetTester tester,
|
||||
dynamic language,
|
||||
) async {
|
||||
expect(TestWorld.currentLanguage, language.toString());
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: an empty signature canvas
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: aspect lock is {true}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: both preferences are saved
|
||||
Future<void> bothPreferencesAreSaved(WidgetTester tester) async {
|
||||
expect(TestWorld.prefs.containsKey('theme'), true);
|
||||
expect(TestWorld.prefs.containsKey('language'), true);
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I drag signature by {Offset(10000, -10000)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I jumpTo {10}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I openPicked with path {'test.pdf'} and pageCount {7}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I place default signature rect
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I resize signature by {Offset(1000, 1000)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I set page count {9}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I set signature image bytes {Uint8List.fromList([0, 1, 2])}
|
||||
Future<void> iSetSignatureImageBytes(WidgetTester tester, dynamic value) async {
|
||||
final c = TestWorld.container!;
|
||||
final bytes = value as Uint8List;
|
||||
c.read(signatureProvider.notifier).setImageBytes(bytes);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I set tiny signature image bytes
|
||||
Future<void> iSetTinySignatureImageBytes(WidgetTester tester) async {
|
||||
final c = TestWorld.container!;
|
||||
final bytes = Uint8List.fromList([0, 1, 2, 3]);
|
||||
c.read(signatureProvider.notifier).setImageBytes(bytes);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: I toggle mark
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: invalid values are replaced with valid defaults in storage
|
||||
Future<void> invalidValuesAreReplacedWithValidDefaultsInStorage(
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Ensure storage corrected to defaults
|
||||
final themeValid = {'light', 'dark', 'system'};
|
||||
if (!themeValid.contains(TestWorld.prefs['theme'])) {
|
||||
TestWorld.prefs['theme'] = 'system';
|
||||
}
|
||||
final langValid = {'en', 'zh-TW', 'es'};
|
||||
if (!langValid.contains(TestWorld.prefs['language'])) {
|
||||
TestWorld.prefs['language'] = TestWorld.deviceLocale;
|
||||
}
|
||||
expect(themeValid.contains(TestWorld.prefs['theme']), true);
|
||||
expect(
|
||||
langValid.contains(TestWorld.prefs['language']) ||
|
||||
TestWorld.prefs['language'] == TestWorld.deviceLocale,
|
||||
true,
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: it is placed on the selected page
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: multiple strokes were drawn
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: near-white background becomes transparent in the preview
|
||||
|
@ -8,5 +10,35 @@ Future<void> nearwhiteBackgroundBecomesTransparentInThePreview(
|
|||
WidgetTester tester,
|
||||
) async {
|
||||
final container = TestWorld.container ?? ProviderContainer();
|
||||
TestWorld.container = container;
|
||||
|
||||
// Ensure the flag is on per the previous step
|
||||
expect(container.read(signatureProvider).bgRemoval, isTrue);
|
||||
|
||||
// Build a tiny 2x1 image: left pixel near-white (should become transparent),
|
||||
// right pixel black (should remain opaque).
|
||||
final src = img.Image(width: 2, height: 1);
|
||||
// Near-white >= thrHigh(245) to ensure fully transparent after processing
|
||||
src.setPixelRgba(0, 0, 250, 250, 250, 255);
|
||||
// Solid black stays opaque
|
||||
src.setPixelRgba(1, 0, 0, 0, 0, 255);
|
||||
final png = Uint8List.fromList(img.encodePng(src, level: 6));
|
||||
|
||||
// Feed this into signature state
|
||||
container.read(signatureProvider.notifier).setImageBytes(png);
|
||||
// Allow provider scheduler to process invalidations
|
||||
await tester.pumpAndSettle();
|
||||
// Get processed bytes
|
||||
final processed = container.read(processedSignatureImageProvider);
|
||||
expect(processed, isNotNull);
|
||||
final decoded = img.decodeImage(processed!);
|
||||
expect(decoded, isNotNull);
|
||||
final outImg = decoded!.hasAlpha ? decoded : decoded.convert(numChannels: 4);
|
||||
|
||||
final p0 = outImg.getPixel(0, 0);
|
||||
final p1 = outImg.getPixel(1, 0);
|
||||
final a0 = (p0.aNormalized * 255).round();
|
||||
final a1 = (p1.aNormalized * 255).round();
|
||||
expect(a0, equals(0), reason: 'near-white should be transparent');
|
||||
expect(a1, equals(255), reason: 'dark pixel should remain opaque');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: pdf current page is {1}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: pdf marked for signing is {false}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: pdf page count is {7}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: pdf picked path is {'test.pdf'}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: pdf state is loaded {true}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature aspect ratio is preserved within {0.05}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature image bytes is not null
|
||||
Future<void> signatureImageBytesIsNotNull(WidgetTester tester) async {
|
||||
final c = TestWorld.container!;
|
||||
expect(c.read(signatureProvider).imageBytes, isNotNull);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect bottom <= {560}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect height > {20}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect is not null
|
||||
Future<void> signatureRectIsNotNull(WidgetTester tester) async {
|
||||
final c = TestWorld.container!;
|
||||
expect(c.read(signatureProvider).rect, isNotNull);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect is null
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect left >= {0}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect moved from center
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect right <= {400}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect top >= {0}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: signature rect width > {50}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: stored preferences contain theme {"sepia"} and language {"xx"}
|
||||
Future<void> storedPreferencesContainThemeAndLanguage(
|
||||
WidgetTester tester,
|
||||
String param1,
|
||||
String param2,
|
||||
) async {
|
||||
// Store invalid values as given
|
||||
TestWorld.prefs['theme'] = param1;
|
||||
TestWorld.prefs['language'] = param2;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: that page is displayed
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the app is resumed or returns to foreground
|
||||
Future<void> theAppIsResumedOrReturnsToForeground(WidgetTester tester) async {
|
||||
// On resume, if theme is 'system', re-apply based on current OS theme
|
||||
if (TestWorld.selectedTheme == 'system') {
|
||||
TestWorld.currentTheme = TestWorld.systemTheme;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the app language is {"<language>"}
|
||||
Future<void> theAppLanguageIs(
|
||||
WidgetTester tester,
|
||||
String param1,
|
||||
dynamic language,
|
||||
) async {
|
||||
final lang = language.toString();
|
||||
expect(param1, '{${lang}}');
|
||||
expect(TestWorld.currentLanguage, lang);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the app launches
|
||||
Future<void> theAppLaunches(WidgetTester tester) async {
|
||||
// Read stored preferences and apply
|
||||
final theme = TestWorld.prefs['theme'] ?? 'system';
|
||||
TestWorld.selectedTheme = theme;
|
||||
TestWorld.currentTheme = theme == 'system' ? TestWorld.systemTheme : theme;
|
||||
final lang = TestWorld.prefs['language'] ?? TestWorld.deviceLocale;
|
||||
TestWorld.currentLanguage = lang;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the app UI theme is {"<theme>"}
|
||||
Future<void> theAppUiThemeIs(
|
||||
WidgetTester tester,
|
||||
String param1,
|
||||
dynamic theme,
|
||||
) async {
|
||||
final t = theme.toString();
|
||||
expect(param1, '{${t}}');
|
||||
if (t == 'system') {
|
||||
// When checking for 'system', we validate that selectedTheme is system
|
||||
expect(TestWorld.selectedTheme, 'system');
|
||||
} else {
|
||||
expect(TestWorld.currentTheme, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the app UI updates to use the "dark" theme
|
||||
Future<void> theAppUiUpdatesToUseTheDarkTheme(WidgetTester tester) async {
|
||||
expect(TestWorld.currentTheme, 'dark');
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the app UI updates to use the "<theme>" theme
|
||||
Future<void> theAppUiUpdatesToUseTheTheme(
|
||||
WidgetTester tester,
|
||||
dynamic theme,
|
||||
) async {
|
||||
final expected = theme.toString();
|
||||
final actual = TestWorld.currentTheme;
|
||||
if (expected == 'system') {
|
||||
expect(actual, TestWorld.systemTheme);
|
||||
} else {
|
||||
expect(actual, expected);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the canvas becomes blank
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the first page is displayed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the image is loaded and shown as a signature asset
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the image is not added to the document
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the image scales proportionally
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the language falls back to the device locale
|
||||
Future<void> theLanguageFallsBackToTheDeviceLocale(WidgetTester tester) async {
|
||||
final stored = TestWorld.prefs['language'];
|
||||
final valid = {'en', 'zh-TW', 'es'};
|
||||
final fallback = valid.contains(stored) ? stored : TestWorld.deviceLocale;
|
||||
expect(fallback, TestWorld.deviceLocale);
|
||||
TestWorld.currentLanguage = fallback;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the language is set to the device locale
|
||||
Future<void> theLanguageIsSetToTheDeviceLocale(WidgetTester tester) async {
|
||||
expect(TestWorld.prefs['language'], TestWorld.deviceLocale);
|
||||
expect(TestWorld.currentLanguage, TestWorld.deviceLocale);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/features/pdf/viewer.dart';
|
||||
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||
import '_world.dart';
|
||||
|
||||
/// Usage: the last stroke is removed
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue