chore: migrate to riverpod3

feat: update code to riverpod3
This commit is contained in:
insleker 2025-12-15 09:59:35 +08:00
parent f1b1141da8
commit 0587e50360
23 changed files with 396 additions and 232 deletions

25
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,25 @@
version: 2
updates:
# Dart/Flutter dependencies (pubspec.yaml)
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "cron"
cronjob: "0 0 * * 2,5"
timezone: "Etc/UTC"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "dart"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "cron"
cronjob: "0 0 * * 2,5"
timezone: "Etc/UTC"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"

65
.github/workflows/pr-unit-tests.yml vendored Normal file
View File

@ -0,0 +1,65 @@
name: Run unit tests on Pull Request
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened]
permissions:
contents: read
checks: write
pull-requests: write
concurrency:
group: "pr-unit-tests"
cancel-in-progress: false
jobs:
unit-tests:
runs-on: ubuntu-latest
defaults:
run:
working-directory: .
steps:
- name: Checkout repository
uses: actions/checkout@v4
continue-on-error: true
id: checkout_https
- name: Checkout repository via SSH (fallback)
if: steps.checkout_https.outcome == 'failure'
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-strict: false
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Install dependencies
run: flutter pub get
- name: Run build_runner (if needed)
run: flutter pub run build_runner build --delete-conflicting-outputs
- name: Run static analysis (for logs)
run: flutter analyze || true
- name: Setup reviewdog
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Annotate analyzer results with reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# run analyzer and pipe into reviewdog so PRs get inline annotations
flutter analyze 2>&1 | reviewdog -efm="%f:%l:%c: %m" -name="flutter analyze" -reporter=github-pr-check -filter-mode=added -level=warning || true
# - name: Run tests
# run: flutter test

View File

@ -8,12 +8,14 @@ import 'package:pdfrx/pdfrx.dart';
import '../../domain/models/model.dart';
class DocumentStateNotifier extends StateNotifier<Document> {
DocumentStateNotifier({ExportService? service})
: _service = service ?? ExportService(),
super(Document.initial());
class DocumentStateNotifier extends Notifier<Document> {
late final ExportService _service;
final ExportService _service;
@override
Document build() {
_service = ExportService();
return Document.initial();
}
@visibleForTesting
void openSample() {
@ -278,8 +280,8 @@ class DocumentStateNotifier extends StateNotifier<Document> {
}
final documentRepositoryProvider =
StateNotifierProvider<DocumentStateNotifier, Document>(
(ref) => DocumentStateNotifier(),
NotifierProvider<DocumentStateNotifier, Document>(
DocumentStateNotifier.new,
);
/// --- Isolate helpers of DocumentRepository ---

View File

@ -89,9 +89,39 @@ String _normalizeLanguageTag(String tag) {
return tags.contains('en') ? 'en' : tags.first;
}
class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
class PreferencesStateNotifier extends Notifier<PreferencesState> {
late final SharedPreferences _prefs;
final Completer<void> _ready = Completer<void>();
@override
PreferencesState build() {
// Initialize with defaults
final defaultState = PreferencesState(
theme: 'system',
language: _normalizeLanguageTag(
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),
),
exportDpi: 144.0,
theme_color: '#FF2196F3', // blue
);
// Start async initialization
_initAsync();
return defaultState;
}
void _initAsync() async {
_prefs = await SharedPreferences.getInstance();
await _load();
_ready.complete();
}
// For testing - can be called with mock SharedPreferences
void initWithPrefs(SharedPreferences prefs) {
_prefs = prefs;
_load();
_ready.complete();
}
static Color? _tryParseColor(String? s) {
if (s == null || s.isEmpty) return null;
final v = s.trim();
@ -162,22 +192,7 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
return '#$a$r$g$b';
}
PreferencesStateNotifier([SharedPreferences? prefs])
: super(
PreferencesState(
theme: 'system',
language: _normalizeLanguageTag(
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),
),
exportDpi: 144.0,
theme_color: '#FF2196F3', // blue
),
) {
_init(prefs);
}
Future<void> _init(SharedPreferences? injected) async {
_prefs = injected ?? await SharedPreferences.getInstance();
Future<void> _load() async {
// Load persisted values (with sane defaults)
final loaded = PreferencesState(
theme: _prefs.getString(_kTheme) ?? 'system',
@ -291,9 +306,8 @@ class PreferencesStateNotifier extends StateNotifier<PreferencesState> {
}
final preferencesRepositoryProvider =
StateNotifierProvider<PreferencesStateNotifier, PreferencesState>((ref) {
// Construct with lazy SharedPreferences initialization.
return PreferencesStateNotifier();
});
NotifierProvider<PreferencesStateNotifier, PreferencesState>(
PreferencesStateNotifier.new,
);
// pageViewModeProvider removed; the app always runs in continuous mode.

View File

@ -3,8 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/domain/models/model.dart';
///
class SignatureAssetRepository extends StateNotifier<List<SignatureAsset>> {
SignatureAssetRepository() : super(const []);
class SignatureAssetRepository extends Notifier<List<SignatureAsset>> {
@override
List<SignatureAsset> build() => const [];
/// Preferred API: add from an already decoded image to avoid re-decodes.
void addImage(img.Image image, {String? name}) {
@ -17,6 +18,6 @@ class SignatureAssetRepository extends StateNotifier<List<SignatureAsset>> {
}
final signatureAssetRepositoryProvider =
StateNotifierProvider<SignatureAssetRepository, List<SignatureAsset>>(
(ref) => SignatureAssetRepository(),
NotifierProvider<SignatureAssetRepository, List<SignatureAsset>>(
SignatureAssetRepository.new,
);

View File

@ -54,8 +54,9 @@ class CachedSignatureCard {
);
}
class SignatureCardStateNotifier extends StateNotifier<List<SignatureCard>> {
SignatureCardStateNotifier() : super(const []);
class SignatureCardStateNotifier extends Notifier<List<SignatureCard>> {
@override
List<SignatureCard> build() => const [];
// Internal storage with cache
final List<CachedSignatureCard> _cards = <CachedSignatureCard>[];
@ -207,6 +208,6 @@ class SignatureCardStateNotifier extends StateNotifier<List<SignatureCard>> {
}
final signatureCardRepositoryProvider =
StateNotifierProvider<SignatureCardStateNotifier, List<SignatureCard>>(
(ref) => SignatureCardStateNotifier(),
NotifierProvider<SignatureCardStateNotifier, List<SignatureCard>>(
SignatureCardStateNotifier.new,
);

View File

@ -26,15 +26,16 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
path: '/',
builder: (context, state) {
final sessionVm = ref.read(pdfSessionViewModelProvider(router));
final sessionVm = ref.read(pdfSessionViewModelProvider.notifier);
return WelcomeScreen(
onPickPdf: () => sessionVm.pickAndOpenPdf(),
onPickPdf: () => sessionVm.pickAndOpenPdf(router),
onOpenPdf:
({String? path, Uint8List? bytes, String? fileName}) =>
sessionVm.openPdf(
path: path,
bytes: bytes ?? Uint8List(0),
fileName: fileName,
router: router,
),
);
},
@ -42,12 +43,13 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
path: '/pdf',
builder: (context, state) {
final sessionVm = ref.read(pdfSessionViewModelProvider(router));
final sessionVm = ref.read(pdfSessionViewModelProvider.notifier);
final sessionState = ref.read(pdfSessionViewModelProvider);
return PdfSignatureHomePage(
onPickPdf: () => sessionVm.pickAndOpenPdf(),
onClosePdf: () => sessionVm.closePdf(),
currentFile: sessionVm.currentFile,
currentFileName: sessionVm.displayFileName,
onPickPdf: () => sessionVm.pickAndOpenPdf(router),
onClosePdf: () => sessionVm.closePdf(router),
currentFile: sessionState.currentFile,
currentFileName: sessionState.displayFileName,
);
},
),

View File

@ -0,0 +1,14 @@
/// Immutable state for PdfExportViewModel
class PdfExportState {
final bool exporting;
const PdfExportState({required this.exporting});
factory PdfExportState.initial() {
return const PdfExportState(exporting: false);
}
PdfExportState copyWith({bool? exporting}) {
return PdfExportState(exporting: exporting ?? this.exporting);
}
}

View File

@ -5,39 +5,30 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_export_state.dart';
/// ViewModel for export-related UI state and helpers.
class PdfExportViewModel extends ChangeNotifier {
final Ref ref;
bool _exporting = false;
class PdfExportViewModel extends Notifier<PdfExportState> {
// Dependencies (injectable via constructor for tests)
// Zero-arg picker retained for backward compatibility with tests.
final Future<String?> Function() _savePathPicker;
late final Future<String?> Function() _savePathPicker;
// Preferred picker that accepts a suggested filename.
final Future<String?> Function(String suggestedName)
late final Future<String?> Function(String suggestedName)
_savePathPickerWithSuggestedName;
PdfExportViewModel(
this.ref, {
Future<String?> Function()? savePathPicker,
Future<String?> Function(String suggestedName)?
savePathPickerWithSuggestedName,
}) : _savePathPicker = savePathPicker ?? _defaultSavePathPicker,
// Prefer provided suggested-name picker; otherwise, if only zero-arg
// picker is given (tests), wrap it; else use default that honors name.
_savePathPickerWithSuggestedName =
savePathPickerWithSuggestedName ??
(savePathPicker != null
? ((_) => savePathPicker())
: _defaultSavePathPickerWithSuggestedName);
@override
PdfExportState build() {
// Initialize with default pickers
_savePathPicker = _defaultSavePathPicker;
_savePathPickerWithSuggestedName = _defaultSavePathPickerWithSuggestedName;
return PdfExportState.initial();
}
bool get exporting => _exporting;
bool get exporting => state.exporting;
void setExporting(bool value) {
if (_exporting == value) return;
_exporting = value;
notifyListeners();
if (state.exporting == value) return;
state = state.copyWith(exporting: value);
}
/// Perform export via document repository. Returns true on success.
@ -122,8 +113,7 @@ class PdfExportViewModel extends ChangeNotifier {
}
}
final pdfExportViewModelProvider = ChangeNotifierProvider<PdfExportViewModel>((
ref,
) {
return PdfExportViewModel(ref);
});
final pdfExportViewModelProvider =
NotifierProvider<PdfExportViewModel, PdfExportState>(
PdfExportViewModel.new,
);

View File

@ -0,0 +1,23 @@
import 'package:cross_file/cross_file.dart';
/// Immutable state for PdfSessionViewModel
class PdfSessionState {
final XFile currentFile;
final String displayFileName;
const PdfSessionState({
required this.currentFile,
required this.displayFileName,
});
factory PdfSessionState.initial() {
return PdfSessionState(currentFile: XFile(''), displayFileName: '');
}
PdfSessionState copyWith({XFile? currentFile, String? displayFileName}) {
return PdfSessionState(
currentFile: currentFile ?? this.currentFile,
displayFileName: displayFileName ?? this.displayFileName,
);
}
}

View File

@ -7,103 +7,70 @@ import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdf_signature/domain/models/model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/document_version.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_state.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_session_state.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:go_router/go_router.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/foundation.dart';
class PdfViewModel extends ChangeNotifier {
final Ref ref;
PdfViewerController _controller = PdfViewerController();
PdfViewerController get controller => _controller;
int _currentPage = 1;
late final bool _useMockViewer;
bool _isDisposed = false;
// Active rect for signature placement overlay
Rect? _activeRect;
Rect? get activeRect => _activeRect;
set activeRect(Rect? value) {
_activeRect = value;
if (!_isDisposed) {
notifyListeners();
}
class PdfViewModel extends Notifier<PdfViewState> {
@override
PdfViewState build() {
return PdfViewState.initial();
}
// Locked placements: Set of (page, index) tuples
final Set<String> _lockedPlacements = {};
Set<String> get lockedPlacements => Set.unmodifiable(_lockedPlacements);
PdfViewerController get controller => state.controller;
int get currentPage => state.currentPage;
bool get useMockViewer => state.useMockViewer;
Rect? get activeRect => state.activeRect;
Set<String> get lockedPlacements => state.lockedPlacements;
// Document version tracking for UI consistency
DocumentVersion _documentVersion = DocumentVersion.initial();
set activeRect(Rect? value) {
state = state.copyWith(activeRect: value, clearActiveRect: value == null);
}
set currentPage(int value) {
final doc = ref.read(documentRepositoryProvider);
final clamped = value.clamp(1, doc.pageCount);
debugPrint('PdfViewModel.currentPage set to $clamped');
state = state.copyWith(currentPage: clamped);
}
// Get current document source name for PdfDocumentRefData
String get documentSourceName {
// Ensure document version is up to date, but only update if really needed
_updateDocumentVersionIfNeeded();
return _documentVersion.sourceName;
return state.documentVersion.sourceName;
}
void _updateDocumentVersionIfNeeded() {
final document = ref.read(documentRepositoryProvider);
if (!identical(_documentVersion.lastBytes, document.pickedPdfBytes)) {
_documentVersion = DocumentVersion(
version: _documentVersion.version + 1,
lastBytes: document.pickedPdfBytes,
if (!identical(state.documentVersion.lastBytes, document.pickedPdfBytes)) {
state = state.copyWith(
documentVersion: DocumentVersion(
version: state.documentVersion.version + 1,
lastBytes: document.pickedPdfBytes,
),
);
}
}
// const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
PdfViewModel(this.ref, {bool? useMockViewer})
: _useMockViewer =
useMockViewer ??
const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
bool get useMockViewer => _useMockViewer;
int get currentPage => _currentPage;
set currentPage(int value) {
_currentPage = value.clamp(1, document.pageCount);
// ignore: avoid_print
debugPrint('PdfViewModel.currentPage set to $_currentPage');
if (!_isDisposed) {
notifyListeners();
}
}
// Do not watch the document repository here; watching would cause this
// ChangeNotifier to be disposed/recreated on every document change, which
// notifier to be disposed/recreated on every document change, which
// resets transient UI state like locked placements. Read instead.
Document get document => ref.read(documentRepositoryProvider);
void jumpToPage(int page) {
// ignore: avoid_print
debugPrint('PdfViewModel.jumpToPage ' + page.toString());
debugPrint('PdfViewModel.jumpToPage $page');
currentPage = page;
}
// Make this view model "int-like" for tests that compare it directly to an
// integer or use it as a Map key for page lookups.
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is int) {
return other == currentPage;
}
return false;
}
@override
int get hashCode => currentPage.hashCode;
// Allow repositories to request a UI refresh without mutating provider state
void notifyPlacementsChanged() {
if (!_isDisposed) {
notifyListeners();
}
// Force a rebuild by updating state
state = state.copyWith();
}
// Document repository methods
@ -146,10 +113,9 @@ class PdfViewModel extends ChangeNotifier {
.read(documentRepositoryProvider.notifier)
.removePlacement(page: page, index: index);
// Also remove from locked placements if it was locked
_lockedPlacements.remove(_placementKey(page, index));
if (!_isDisposed) {
notifyListeners();
}
final newLocked = Set<String>.from(state.lockedPlacements)
..remove(_placementKey(page, index));
state = state.copyWith(lockedPlacements: newLocked);
}
void updatePlacementRect({
@ -177,23 +143,21 @@ class PdfViewModel extends ChangeNotifier {
// Check if a placement is locked
bool isPlacementLocked({required int page, required int index}) {
return _lockedPlacements.contains(_placementKey(page, index));
return state.lockedPlacements.contains(_placementKey(page, index));
}
// Lock a placement
void lockPlacement({required int page, required int index}) {
_lockedPlacements.add(_placementKey(page, index));
if (!_isDisposed) {
notifyListeners();
}
final newLocked = Set<String>.from(state.lockedPlacements)
..add(_placementKey(page, index));
state = state.copyWith(lockedPlacements: newLocked);
}
// Unlock a placement
void unlockPlacement({required int page, required int index}) {
_lockedPlacements.remove(_placementKey(page, index));
if (!_isDisposed) {
notifyListeners();
}
final newLocked = Set<String>.from(state.lockedPlacements)
..remove(_placementKey(page, index));
state = state.copyWith(lockedPlacements: newLocked);
}
// Toggle lock state of a placement
@ -250,37 +214,24 @@ class PdfViewModel extends ChangeNotifier {
void clearAllSignatureCards() {
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
}
@override
void dispose() {
_isDisposed = true;
super.dispose();
}
}
final pdfViewModelProvider = ChangeNotifierProvider<PdfViewModel>((ref) {
return PdfViewModel(ref);
});
final pdfViewModelProvider = NotifierProvider<PdfViewModel, PdfViewState>(
PdfViewModel.new,
);
/// ViewModel managing PDF session lifecycle (file picking/open/close) and
/// navigation. Replaces the previous PdfManager helper.
class PdfSessionViewModel extends ChangeNotifier {
final Ref ref;
final GoRouter router;
XFile _currentFile = XFile('');
// Keep a human display name in addition to XFile, because on Linux via
// xdg-desktop-portal the path can look like /run/user/.../doc/<UUID>, and
// XFile.name derives from that basename, yielding a random UUID instead of
// the actual filename the user selected. We preserve the picker/drop name
// here to offer a sensible default like "signed_<original>.pdf".
String _displayFileName = '';
class PdfSessionViewModel extends Notifier<PdfSessionState> {
@override
PdfSessionState build() {
return PdfSessionState.initial();
}
PdfSessionViewModel({required this.ref, required this.router});
XFile get currentFile => state.currentFile;
String get displayFileName => state.displayFileName;
XFile get currentFile => _currentFile;
String get displayFileName => _displayFileName;
Future<void> pickAndOpenPdf() async {
Future<void> pickAndOpenPdf(GoRouter router) async {
final result = await fp.FilePicker.platform.pickFiles(
type: fp.FileType.custom,
allowedExtensions: const ['pdf'],
@ -304,7 +255,12 @@ class PdfSessionViewModel extends ChangeNotifier {
}
}
if (effectiveBytes != null) {
await openPdf(path: path, bytes: effectiveBytes, fileName: name);
await openPdf(
path: path,
bytes: effectiveBytes,
fileName: name,
router: router,
);
} else {
debugPrint(
'[PdfSessionViewModel] No PDF data available to open (path=$path, name=$name)',
@ -316,6 +272,7 @@ class PdfSessionViewModel extends ChangeNotifier {
String? path,
required Uint8List bytes,
String? fileName,
required GoRouter router,
}) async {
int pageCount = 1; // default
try {
@ -355,36 +312,44 @@ class PdfSessionViewModel extends ChangeNotifier {
);
debugPrint(st.toString());
}
XFile newFile;
if (path != null && path.isNotEmpty) {
_currentFile = XFile(path);
newFile = XFile(path);
} else if ((fileName != null && fileName.isNotEmpty)) {
// Keep in-memory XFile so .name is available for suggestion
try {
_currentFile = XFile.fromData(
newFile = XFile.fromData(
bytes,
name: fileName,
mimeType: 'application/pdf',
);
} catch (e, st) {
_currentFile = XFile(fileName);
newFile = XFile(fileName);
debugPrint(
'[PdfSessionViewModel] Failed to create XFile.fromData name=$fileName error=$e',
);
debugPrint(st.toString());
}
} else {
_currentFile = XFile('');
newFile = XFile('');
}
// Update display name: prefer explicit fileName (from picker/drop),
// fall back to basename of path, otherwise empty.
String newDisplayFileName;
if (fileName != null && fileName.isNotEmpty) {
_displayFileName = fileName;
newDisplayFileName = fileName;
} else if (path != null && path.isNotEmpty) {
_displayFileName = path.split('/').last.split('\\').last;
newDisplayFileName = path.split('/').last.split('\\').last;
} else {
_displayFileName = '';
newDisplayFileName = '';
}
state = state.copyWith(
currentFile: newFile,
displayFileName: newDisplayFileName,
);
// If fast path failed to set repository (e.g., exception earlier), fallback to async derive.
if (ref.read(documentRepositoryProvider).pickedPdfBytes != bytes) {
debugPrint(
@ -397,21 +362,18 @@ class PdfSessionViewModel extends ChangeNotifier {
// relies on this behavior. Placements are reset by openPicked() above.
debugPrint('[PdfSessionViewModel] Navigating to /pdf');
router.go('/pdf');
debugPrint('[PdfSessionViewModel] Notifying listeners after open');
notifyListeners();
debugPrint('[PdfSessionViewModel] State updated after open');
}
void closePdf() {
void closePdf(GoRouter router) {
ref.read(documentRepositoryProvider.notifier).close();
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
_currentFile = XFile('');
_displayFileName = '';
state = state.copyWith(currentFile: XFile(''), displayFileName: '');
router.go('/');
notifyListeners();
}
}
final pdfSessionViewModelProvider =
ChangeNotifierProvider.family<PdfSessionViewModel, GoRouter>((ref, router) {
return PdfSessionViewModel(ref: ref, router: router);
});
NotifierProvider<PdfSessionViewModel, PdfSessionState>(
PdfSessionViewModel.new,
);

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/document_version.dart';
import 'package:pdfrx/pdfrx.dart';
/// Immutable state for PdfViewModel
class PdfViewState {
final PdfViewerController controller;
final int currentPage;
final bool useMockViewer;
final Rect? activeRect;
final Set<String> lockedPlacements;
final DocumentVersion documentVersion;
const PdfViewState({
required this.controller,
required this.currentPage,
required this.useMockViewer,
this.activeRect,
required this.lockedPlacements,
required this.documentVersion,
});
factory PdfViewState.initial({bool? useMockViewer}) {
return PdfViewState(
controller: PdfViewerController(),
currentPage: 1,
useMockViewer:
useMockViewer ??
const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false),
activeRect: null,
lockedPlacements: const {},
documentVersion: DocumentVersion.initial(),
);
}
PdfViewState copyWith({
PdfViewerController? controller,
int? currentPage,
bool? useMockViewer,
Rect? activeRect,
bool clearActiveRect = false,
Set<String>? lockedPlacements,
DocumentVersion? documentVersion,
}) {
return PdfViewState(
controller: controller ?? this.controller,
currentPage: currentPage ?? this.currentPage,
useMockViewer: useMockViewer ?? this.useMockViewer,
activeRect: clearActiveRect ? null : (activeRect ?? this.activeRect),
lockedPlacements: lockedPlacements ?? this.lockedPlacements,
documentVersion: documentVersion ?? this.documentVersion,
);
}
}

View File

@ -112,11 +112,12 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
@override
Widget build(BuildContext context) {
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdfViewState = ref.watch(pdfViewModelProvider);
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
final pdf = pdfViewModel.document;
const pageViewMode = 'continuous';
// React to PdfViewModel currentPage changes. With ChangeNotifierProvider,
// prev/next are the same instance, so compare to a local cache.
// React to PdfViewModel currentPage changes. With NotifierProvider,
// prev/next are now different state objects, so we can compare them directly.
ref.listen(pdfViewModelProvider, (prev, next) {
// Only perform manual scrolling in mock viewer mode. In real viewer mode,
// PdfViewerController + onPageChanged keep things in sync, and attempting

View File

@ -36,12 +36,12 @@ class PdfPageOverlays extends ConsumerWidget {
if (!overlaysEnabled) {
return const SizedBox.shrink();
}
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdfViewState = ref.watch(pdfViewModelProvider);
// Subscribe to document changes to rebuild overlays
final pdf = ref.watch(documentRepositoryProvider);
final placed =
pdf.placementsByPage[pageNumber] ?? const <SignaturePlacement>[];
final activeRect = pdfViewModel.activeRect;
final activeRect = pdfViewState.activeRect;
final widgets = <Widget>[];
// Base DragTarget filling the whole page to accept drops from signature cards.
@ -116,10 +116,10 @@ class PdfPageOverlays extends ConsumerWidget {
// TODO:Add active overlay if present and not using mock (mock has its own)
final useMock = pdfViewModel.useMockViewer;
final useMock = pdfViewState.useMockViewer;
if (!useMock &&
activeRect != null &&
pageNumber == pdfViewModel.currentPage) {
pageNumber == pdfViewState.currentPage) {
widgets.add(
LayoutBuilder(
builder: (context, constraints) {

View File

@ -195,7 +195,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
if (!kIsWeb) {
final path = await ref
.read(pdfExportViewModelProvider)
.read(pdfExportViewModelProvider.notifier)
.pickSavePathWithSuggestedName(suggested);
if (path == null || path.trim().isEmpty) return;
final fullPath = _ensurePdfExtension(path.trim());
@ -203,7 +203,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
// ignore: avoid_print
debugPrint('_saveSignedPdf: picked save path ' + fullPath);
ok = await ref
.read(pdfExportViewModelProvider)
.read(pdfExportViewModelProvider.notifier)
.exportToPath(
outputPath: fullPath,
uiPageSize: _pageSize,
@ -329,7 +329,8 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
}(),
child: Consumer(
builder: (context, ref, child) {
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdfViewState = ref.watch(pdfViewModelProvider);
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
final pdf = pdfViewModel.document;
final documentRef =

View File

@ -59,11 +59,11 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
@override
Widget build(BuildContext context) {
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdfViewState = ref.watch(pdfViewModelProvider);
final pdf = ref.watch(
documentRepositoryProvider,
); // Watch document directly for updates
final currentPage = pdfViewModel.currentPage;
final currentPage = pdfViewState.currentPage;
final l = AppLocalizations.of(context);
final pageInfo = l.pageInfo(currentPage, pdf.pageCount);

View File

@ -51,7 +51,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
final bytes = doc.pickedPdfBytes!;
if (!identical(bytes, _lastBytes)) {
_lastBytes = bytes;
final viewModel = ref.read(pdfViewModelProvider);
final viewModel = ref.read(pdfViewModelProvider.notifier);
debugPrint(
'[PdfViewerWidget] New PDF bytes detected -> ${viewModel.documentSourceName}',
);
@ -84,9 +84,9 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
@override
Widget build(BuildContext context) {
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdfViewState = ref.watch(pdfViewModelProvider);
final document = ref.watch(documentRepositoryProvider);
final useMock = pdfViewModel.useMockViewer;
final useMock = pdfViewState.useMockViewer;
_updateDocRef(document);
if (useMock) {
@ -113,7 +113,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
}
return Center(child: Text(text));
}
final pdfViewModel = ref.read(pdfViewModelProvider);
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
final viewerKey =
widget.innerViewerKey ??
Key('pdf_viewer_${pdfViewModel.documentSourceName}');

View File

@ -42,9 +42,10 @@ class SignatureOverlay extends ConsumerWidget {
Future<void> _showContextMenu(Offset position) async {
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
final isLocked = ref
.watch(pdfViewModelProvider)
.isPlacementLocked(page: pageNumber, index: placedIndex);
final isLocked = pdfViewModel.isPlacementLocked(
page: pageNumber,
index: placedIndex,
);
final selected = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(
@ -92,7 +93,7 @@ class SignatureOverlay extends ConsumerWidget {
allowContentFlipping: false,
onChanged:
ref
.watch(pdfViewModelProvider)
.read(pdfViewModelProvider.notifier)
.isPlacementLocked(
page: pageNumber,
index: placedIndex,
@ -118,7 +119,7 @@ class SignatureOverlay extends ConsumerWidget {
// Keep default handles; you can customize later if needed
contentBuilder: (context, boxRect, flip) {
final isLocked = ref
.watch(pdfViewModelProvider)
.read(pdfViewModelProvider.notifier)
.isPlacementLocked(page: pageNumber, index: placedIndex);
return DecoratedBox(
decoration: BoxDecoration(

View File

@ -1,6 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Global flag indicating whether a signature card is currently being dragged.
final isDraggingSignatureViewModelProvider = StateProvider<bool>(
(ref) => false,
);
class IsDraggingSignatureNotifier extends Notifier<bool> {
@override
bool build() => false;
void setDragging(bool value) => state = value;
}
final isDraggingSignatureViewModelProvider =
NotifierProvider<IsDraggingSignatureNotifier, bool>(
IsDraggingSignatureNotifier.new,
);

View File

@ -158,10 +158,14 @@ class _SignatureCardViewState extends ConsumerState<SignatureCardView> {
),
),
onDragStarted: () {
ref.read(isDraggingSignatureViewModelProvider.notifier).state = true;
ref
.read(isDraggingSignatureViewModelProvider.notifier)
.setDragging(true);
},
onDragEnd: (_) {
ref.read(isDraggingSignatureViewModelProvider.notifier).state = false;
ref
.read(isDraggingSignatureViewModelProvider.notifier)
.setDragging(false);
},
feedback: Opacity(
opacity: 0.9,

View File

@ -20,8 +20,8 @@ class WelcomeViewModel {
}
// Use PdfSessionViewModel to open and navigate.
final session = ref.read(pdfSessionViewModelProvider(router));
await session.openPdf(path: path, bytes: bytes);
final session = ref.read(pdfSessionViewModelProvider.notifier);
await session.openPdf(path: path, bytes: bytes, router: router);
}
}

View File

@ -25,9 +25,6 @@ class _DropReadableFromDesktop implements DropReadable {
Future<Uint8List> readAsBytes() => inner.readAsBytes();
}
// Allow injecting Riverpod's read function from either WidgetRef or ProviderContainer
typedef Reader = T Function<T>(ProviderListenable<T> provider);
// Select first .pdf file (case-insensitive) or fall back to first entry.
Future<void> handleDroppedFiles(
Future<void> Function({String? path, Uint8List? bytes, String? fileName})

View File

@ -2,7 +2,7 @@ name: pdf_signature
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
@ -42,28 +42,28 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_riverpod: ^2.6.1
flutter_riverpod: ^3.0.3
shared_preferences: ^2.5.3
flutter_dotenv: ^6.0.0
path_provider: ^2.1.5
pdfrx: ^2.1.9
pdfrx: ^2.2.16
pdf: ^3.10.8
# printing: ^5.14.2 # extension of pdf pkg
hand_signature: ^3.1.0+2
image: ^4.2.0
result_dart: ^2.1.1
go_router: ^16.2.0
go_router: ^17.0.0
flutter_localizations:
sdk: flutter
intl: any
flutter_localized_locales: ^2.0.5
desktop_drop: ^0.6.1
desktop_drop: ^0.7.0
multi_split_view: ^3.6.1
freezed_annotation: ^3.1.0
json_annotation: ^4.9.0
share_plus: ^12.0.0
logging: ^1.3.0
riverpod_annotation: ^2.6.1
riverpod_annotation: ^3.0.3
colorfilter_generator: ^0.0.8
flutter_box_transform: ^0.4.7
file_picker: ^10.3.3
@ -78,12 +78,12 @@ dev_dependencies:
integration_test:
sdk: flutter
build_runner: ^2.4.12
build: ^3.0.2
bdd_widget_test: ^2.0.1
build: ^4.0.3
bdd_widget_test: ^2.1.3
mocktail: ^1.0.4
freezed: ^3.0.0
custom_lint: ^0.7.6
riverpod_lint: ^2.6.5
# custom_lint: ^0.7.6
# riverpod_lint: ^2.6.5
go_router_builder: ^4.0.1
# The "flutter_lints" package below contains a set of recommended lints to
@ -141,7 +141,6 @@ flutter:
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
flutter_launcher_icons:
image_path: "assets/icon/pdf_signature-icon.png"
android: true