feat: remember uploaded file name on web and use it when downloading
This commit is contained in:
parent
0c38178502
commit
5a03793b54
|
|
@ -31,7 +31,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||
onPickPdf: () => sessionVm.pickAndOpenPdf(),
|
||||
onOpenPdf:
|
||||
({String? path, Uint8List? bytes, String? fileName}) =>
|
||||
sessionVm.openPdf(path: path, bytes: bytes),
|
||||
sessionVm.openPdf(
|
||||
path: path,
|
||||
bytes: bytes,
|
||||
fileName: fileName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -43,6 +47,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||
onPickPdf: () => sessionVm.pickAndOpenPdf(),
|
||||
onClosePdf: () => sessionVm.closePdf(),
|
||||
currentFile: sessionVm.currentFile,
|
||||
currentFileName: sessionVm.displayFileName,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,14 +10,27 @@ class PdfExportViewModel extends ChangeNotifier {
|
|||
|
||||
// Dependencies (injectable via constructor for tests)
|
||||
final ExportService _exporter;
|
||||
// Zero-arg picker retained for backward compatibility with tests.
|
||||
final Future<String?> Function() _savePathPicker;
|
||||
// Preferred picker that accepts a suggested filename.
|
||||
final Future<String?> Function(String suggestedName)
|
||||
_savePathPickerWithSuggestedName;
|
||||
|
||||
PdfExportViewModel(
|
||||
this.ref, {
|
||||
ExportService? exporter,
|
||||
Future<String?> Function()? savePathPicker,
|
||||
Future<String?> Function(String suggestedName)?
|
||||
savePathPickerWithSuggestedName,
|
||||
}) : _exporter = exporter ?? ExportService(),
|
||||
_savePathPicker = savePathPicker ?? _defaultSavePathPicker;
|
||||
_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);
|
||||
|
||||
bool get exporting => _exporting;
|
||||
|
||||
|
|
@ -35,11 +48,22 @@ class PdfExportViewModel extends ChangeNotifier {
|
|||
return _savePathPicker();
|
||||
}
|
||||
|
||||
/// Show save dialog with a suggested name and return the chosen path.
|
||||
Future<String?> pickSavePathWithSuggestedName(String suggestedName) async {
|
||||
return _savePathPickerWithSuggestedName(suggestedName);
|
||||
}
|
||||
|
||||
static Future<String?> _defaultSavePathPicker() async {
|
||||
return _defaultSavePathPickerWithSuggestedName('signed.pdf');
|
||||
}
|
||||
|
||||
static Future<String?> _defaultSavePathPickerWithSuggestedName(
|
||||
String suggestedName,
|
||||
) async {
|
||||
final group = fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
||||
final location = await fs.getSaveLocation(
|
||||
acceptedTypeGroups: [group],
|
||||
suggestedName: 'signed.pdf',
|
||||
suggestedName: suggestedName,
|
||||
confirmButtonText: 'Save',
|
||||
);
|
||||
return location?.path; // null if user cancels
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||
|
|
@ -243,14 +244,21 @@ class PdfSessionViewModel extends ChangeNotifier {
|
|||
final Ref ref;
|
||||
final GoRouter router;
|
||||
fs.XFile _currentFile = fs.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 = '';
|
||||
|
||||
PdfSessionViewModel({required this.ref, required this.router});
|
||||
|
||||
fs.XFile get currentFile => _currentFile;
|
||||
String get displayFileName => _displayFileName;
|
||||
|
||||
Future<void> pickAndOpenPdf() async {
|
||||
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
||||
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
||||
final XFile? file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
||||
if (file != null) {
|
||||
Uint8List? bytes;
|
||||
try {
|
||||
|
|
@ -258,11 +266,15 @@ class PdfSessionViewModel extends ChangeNotifier {
|
|||
} catch (_) {
|
||||
bytes = null;
|
||||
}
|
||||
await openPdf(path: file.path, bytes: bytes);
|
||||
await openPdf(path: file.path, bytes: bytes, fileName: file.name);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openPdf({String? path, Uint8List? bytes}) async {
|
||||
Future<void> openPdf({
|
||||
String? path,
|
||||
Uint8List? bytes,
|
||||
String? fileName,
|
||||
}) async {
|
||||
int pageCount = 1; // default
|
||||
if (bytes != null) {
|
||||
try {
|
||||
|
|
@ -272,8 +284,31 @@ class PdfSessionViewModel extends ChangeNotifier {
|
|||
// ignore invalid bytes
|
||||
}
|
||||
}
|
||||
if (path != null) {
|
||||
if (path != null && path.isNotEmpty) {
|
||||
_currentFile = fs.XFile(path);
|
||||
} else if (bytes != null && (fileName != null && fileName.isNotEmpty)) {
|
||||
// Keep in-memory XFile so .name is available for suggestion
|
||||
try {
|
||||
_currentFile = fs.XFile.fromData(
|
||||
bytes,
|
||||
name: fileName,
|
||||
mimeType: 'application/pdf',
|
||||
);
|
||||
} catch (_) {
|
||||
_currentFile = fs.XFile(fileName);
|
||||
}
|
||||
} else {
|
||||
_currentFile = fs.XFile('');
|
||||
}
|
||||
|
||||
// Update display name: prefer explicit fileName (from picker/drop),
|
||||
// fall back to basename of path, otherwise empty.
|
||||
if (fileName != null && fileName.isNotEmpty) {
|
||||
_displayFileName = fileName;
|
||||
} else if (path != null && path.isNotEmpty) {
|
||||
_displayFileName = path.split('/').last.split('\\').last;
|
||||
} else {
|
||||
_displayFileName = '';
|
||||
}
|
||||
ref
|
||||
.read(documentRepositoryProvider.notifier)
|
||||
|
|
@ -287,6 +322,7 @@ class PdfSessionViewModel extends ChangeNotifier {
|
|||
ref.read(documentRepositoryProvider.notifier).close();
|
||||
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
||||
_currentFile = fs.XFile('');
|
||||
_displayFileName = '';
|
||||
router.go('/');
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,18 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
|||
final Future<void> Function() onPickPdf;
|
||||
final VoidCallback onClosePdf;
|
||||
final fs.XFile currentFile;
|
||||
// Optional display name for the currently opened file. On Linux
|
||||
// xdg-desktop-portal, XFile.name/path can be a UUID-like value. When
|
||||
// available, this name preserves the user-selected filename so we can
|
||||
// suggest a proper "signed_*.pdf" on save.
|
||||
final String? currentFileName;
|
||||
|
||||
const PdfSignatureHomePage({
|
||||
super.key,
|
||||
required this.onPickPdf,
|
||||
required this.onClosePdf,
|
||||
required this.currentFile,
|
||||
this.currentFileName,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -152,8 +158,23 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
bool ok = false;
|
||||
String? savedPath;
|
||||
|
||||
// Derive a suggested filename based on the opened file. Prefer the
|
||||
// provided display name if available (see Linux portal note above).
|
||||
final display = widget.currentFileName;
|
||||
final originalName =
|
||||
(display != null && display.trim().isNotEmpty)
|
||||
? display.trim()
|
||||
: widget.currentFile.name.isNotEmpty
|
||||
? widget.currentFile.name
|
||||
: widget.currentFile.path.isNotEmpty
|
||||
? widget.currentFile.path.split('/').last.split('\\').last
|
||||
: 'document.pdf';
|
||||
final suggested = _suggestSignedName(originalName);
|
||||
|
||||
if (!kIsWeb) {
|
||||
final path = await ref.read(pdfExportViewModelProvider).pickSavePath();
|
||||
final path = await ref
|
||||
.read(pdfExportViewModelProvider)
|
||||
.pickSavePathWithSuggestedName(suggested);
|
||||
if (path == null || path.trim().isEmpty) return;
|
||||
final fullPath = _ensurePdfExtension(path.trim());
|
||||
savedPath = fullPath;
|
||||
|
|
@ -179,9 +200,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
targetDpi: targetDpi,
|
||||
);
|
||||
if (out != null) {
|
||||
// Use a sensible default filename (cannot prompt path on web)
|
||||
ok = await downloadBytes(out, filename: 'signed.pdf');
|
||||
savedPath = 'signed.pdf';
|
||||
// Use suggested filename for browser download
|
||||
ok = await downloadBytes(out, filename: suggested);
|
||||
savedPath = suggested;
|
||||
}
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
|
|
@ -224,6 +245,15 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
return name;
|
||||
}
|
||||
|
||||
String _suggestSignedName(String original) {
|
||||
// Normalize to a base filename
|
||||
final base = original.split('/').last.split('\\').last;
|
||||
if (base.toLowerCase().endsWith('.pdf')) {
|
||||
return 'signed_' + base;
|
||||
}
|
||||
return 'signed_' + base + '.pdf';
|
||||
}
|
||||
|
||||
void _onControllerChanged() {
|
||||
if (mounted) {
|
||||
if (_viewModel.controller.isReady) {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Future<void> handleDroppedFiles(
|
|||
bytes = null;
|
||||
}
|
||||
final String path = pdf.path ?? pdf.name;
|
||||
await onOpenPdf(path: path, bytes: bytes);
|
||||
await onOpenPdf(path: path, bytes: bytes, fileName: pdf.name);
|
||||
}
|
||||
|
||||
class WelcomeScreen extends ConsumerStatefulWidget {
|
||||
|
|
|
|||
Loading…
Reference in New Issue