fix(ios): use security-scoped bookmarks for download directory persistence

- Switch iOS bookmark creation from .minimalBookmark to .withSecurityScope
- Add .withSecurityScope option when resolving bookmarks
- Add downloadDirectoryBookmark field to AppSettings for persisting iOS bookmarks
- Resolve bookmark and startAccessingIosBookmark before queue processing
- Guarantee stopAccessingIosBookmark cleanup via try/finally
- Create bookmark on folder pick in both setup screen and download settings
- Clear bookmark when switching to SAF mode or iOS path normalization
- Fix stale bottom sheet context usage (ctx -> context) in download settings
This commit is contained in:
zarzet
2026-05-02 01:19:39 +07:00
parent 64dbf4441c
commit 45fa33e1ec
7 changed files with 161 additions and 41 deletions
@@ -1420,13 +1420,13 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
try {
result = await FilePicker.platform.getDirectoryPath();
} catch (e) {
if (ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
ctx.l10n.snackbarFolderPickerFailed(e.toString()),
context.l10n.snackbarFolderPickerFailed(e.toString()),
),
backgroundColor: Theme.of(ctx).colorScheme.error,
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 4),
),
);
@@ -1439,24 +1439,55 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
if (Platform.isIOS) {
final validation = validateIosPath(result);
if (!validation.isValid) {
if (ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
validation.errorReason ??
context.l10n.setupIcloudNotSupported,
),
backgroundColor: Theme.of(ctx).colorScheme.error,
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 4),
),
);
}
return;
}
final bookmark =
await PlatformBridge.createIosBookmarkFromPath(result);
if (bookmark == null || bookmark.isEmpty) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarFolderPickerFailed(
'Could not keep access to the selected folder',
),
),
backgroundColor: Theme.of(
context,
).colorScheme.error,
duration: const Duration(seconds: 4),
),
);
}
return;
}
ref
.read(settingsProvider.notifier)
.setDownloadDirectory(result, iosBookmark: bookmark);
return;
}
ref
.read(settingsProvider.notifier)
.setDownloadDirectory(result);
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.setupNoFolderSelected)),
);
}
},
),
+69 -27
View File
@@ -30,6 +30,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
bool _storagePermissionGranted = false;
bool _notificationPermissionGranted = false;
String? _selectedDirectory;
String? _selectedDirectoryBookmark;
String? _selectedTreeUri;
bool _isLoading = false;
int _androidSdkVersion = 0;
@@ -338,7 +339,10 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
title: Text(context.l10n.setupAppDocumentsFolder),
onTap: () async {
final dir = await _getDefaultDirectory();
setState(() => _selectedDirectory = dir);
setState(() {
_selectedDirectory = dir;
_selectedDirectoryBookmark = null;
});
if (ctx.mounted) Navigator.pop(ctx);
},
),
@@ -369,30 +373,62 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
return;
}
if (result != null) {
// iOS: Validate the selected path is writable
if (Platform.isIOS) {
final validation = validateIosPath(result);
if (!validation.isValid) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
validation.errorReason ??
'Invalid folder selected',
),
backgroundColor: Theme.of(
context,
).colorScheme.error,
duration: const Duration(seconds: 4),
),
);
}
return;
}
if (result == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.setupNoFolderSelected),
),
);
}
setState(() => _selectedDirectory = result);
return;
}
// iOS: Validate the selected path is writable
if (Platform.isIOS) {
final validation = validateIosPath(result);
if (!validation.isValid) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
validation.errorReason ?? 'Invalid folder selected',
),
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 4),
),
);
}
return;
}
final bookmark =
await PlatformBridge.createIosBookmarkFromPath(result);
if (bookmark == null || bookmark.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarFolderPickerFailed(
'Could not keep access to the selected folder',
),
),
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 4),
),
);
}
return;
}
setState(() {
_selectedDirectory = result;
_selectedDirectoryBookmark = bookmark;
});
return;
}
setState(() => _selectedDirectory = result);
},
),
const SizedBox(height: 16),
@@ -426,14 +462,20 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
if (!Platform.isAndroid ||
_selectedTreeUri == null ||
_selectedTreeUri!.isEmpty) {
final dir = Directory(_selectedDirectory!);
if (!await dir.exists()) {
await dir.create(recursive: true);
final iosBookmark = Platform.isIOS ? _selectedDirectoryBookmark : null;
if (iosBookmark == null || iosBookmark.isEmpty) {
final dir = Directory(_selectedDirectory!);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
}
ref.read(settingsProvider.notifier).setStorageMode('app');
ref
.read(settingsProvider.notifier)
.setDownloadDirectory(_selectedDirectory!);
.setDownloadDirectory(
_selectedDirectory!,
iosBookmark: iosBookmark,
);
ref.read(settingsProvider.notifier).setDownloadTreeUri('');
} else {
ref.read(settingsProvider.notifier).setStorageMode('saf');