mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-26 05:51:18 +02:00
feat(downloads): detect lost SAF grant and offer folder re-selection
A persisted SAF tree grant can die silently (folder deleted or renamed, SD card removed, app data reset, system revocation). Downloads then fail with no recovery path beyond digging through Settings. - Add isSafTreeAccessible bridge method: the grant must still be in the system's persisted permission list and the tree writable - Show a warning banner in Files & Folders when the saved SAF folder is no longer accessible, with an inline re-select action - In the download error dialog, replace Retry with a Re-select folder action when the failure is a lost folder grant (Android SAF or iOS bookmark); a successful re-pick retries the item automatically - Share the folder-access error messages as constants so the UI can match on them reliably
This commit is contained in:
@@ -2414,6 +2414,27 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(exists)
|
||||
}
|
||||
"isSafTreeAccessible" -> {
|
||||
val uriStr = call.argument<String>("tree_uri") ?: ""
|
||||
val accessible = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val uri = Uri.parse(uriStr)
|
||||
val persisted = contentResolver.persistedUriPermissions.any {
|
||||
it.uri == uri && it.isReadPermission && it.isWritePermission
|
||||
}
|
||||
if (!persisted) {
|
||||
false
|
||||
} else {
|
||||
val doc = DocumentFile.fromTreeUri(this@MainActivity, uri)
|
||||
doc != null && doc.exists() && doc.canWrite()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("SpotiFLAC", "SAF tree access check failed: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
result.success(accessible)
|
||||
}
|
||||
"safDelete" -> {
|
||||
val uriStr = call.argument<String>("uri") ?: ""
|
||||
val deleted = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -5335,6 +5335,24 @@ abstract class AppLocalizations {
|
||||
/// **'Pick any folder, including SD card'**
|
||||
String get storageModeSafSubtitle;
|
||||
|
||||
/// Title of the warning banner shown in Files settings when the saved SAF folder grant is no longer valid
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download folder access lost'**
|
||||
String get downloadFolderAccessLostTitle;
|
||||
|
||||
/// Subtitle of the warning banner shown when the saved SAF folder grant is no longer valid
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloads will fail until you re-select the folder'**
|
||||
String get downloadFolderAccessLostSubtitle;
|
||||
|
||||
/// Button that reopens the folder picker to restore download folder access
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Re-select folder'**
|
||||
String get downloadFolderReselect;
|
||||
|
||||
/// Description shown in filename format editor
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3169,6 +3169,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get storageModeSafSubtitle =>
|
||||
'Wähle einen beliebigen Ordner, inklusive SD-Karte';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3213,6 +3213,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get storageModeSafSubtitle =>
|
||||
'Choisissez n\'importe quel dossier, y compris la carte SD';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3112,6 +3112,16 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3116,6 +3116,16 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3114,6 +3114,16 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3184,6 +3184,16 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3160,6 +3160,16 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get storageModeSafSubtitle =>
|
||||
'SD kart dahil herhangi bir klasörü seçin';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3176,6 +3176,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -3129,6 +3129,16 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get storageModeSafSubtitle => 'Pick any folder, including SD card';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostTitle => 'Download folder access lost';
|
||||
|
||||
@override
|
||||
String get downloadFolderAccessLostSubtitle =>
|
||||
'Downloads will fail until you re-select the folder';
|
||||
|
||||
@override
|
||||
String get downloadFolderReselect => 'Re-select folder';
|
||||
|
||||
@override
|
||||
String downloadFilenameDescription(
|
||||
Object album,
|
||||
|
||||
@@ -4110,6 +4110,18 @@
|
||||
"@storageModeSafSubtitle": {
|
||||
"description": "Subtitle for SAF storage mode"
|
||||
},
|
||||
"downloadFolderAccessLostTitle": "Download folder access lost",
|
||||
"@downloadFolderAccessLostTitle": {
|
||||
"description": "Title of the warning banner shown in Files settings when the saved SAF folder grant is no longer valid"
|
||||
},
|
||||
"downloadFolderAccessLostSubtitle": "Downloads will fail until you re-select the folder",
|
||||
"@downloadFolderAccessLostSubtitle": {
|
||||
"description": "Subtitle of the warning banner shown when the saved SAF folder grant is no longer valid"
|
||||
},
|
||||
"downloadFolderReselect": "Re-select folder",
|
||||
"@downloadFolderReselect": {
|
||||
"description": "Button that reopens the folder picker to restore download folder access"
|
||||
},
|
||||
"downloadFilenameDescription": "Use {artist}, {title}, {album}, {track}, {year}, {date}, {disc} as placeholders.",
|
||||
"@downloadFilenameDescription": {
|
||||
"description": "Description shown in filename format editor"
|
||||
|
||||
@@ -30,6 +30,16 @@ export 'package:spotiflac_android/services/history_database.dart'
|
||||
final _log = AppLogger('DownloadQueue');
|
||||
final _historyLog = AppLogger('DownloadHistory');
|
||||
|
||||
/// Set on queued items when the persisted Android SAF grant fails validation.
|
||||
/// The queue UI matches on this to offer re-selecting the download folder.
|
||||
const String safPermissionLostErrorMessage =
|
||||
'SAF permission invalid or revoked. Please reconfigure download location in Settings.';
|
||||
|
||||
/// Set on queued items when the iOS download folder bookmark can no longer be
|
||||
/// opened. The queue UI matches on this to offer re-selecting the folder.
|
||||
const String downloadFolderAccessLostErrorMessage =
|
||||
'Download folder access lost. Please re-select your download folder in Settings.';
|
||||
|
||||
final _invalidFolderChars = RegExp(r'[<>:"/\\|?*]');
|
||||
final _trimDotsAndSpacesRegex = RegExp(r'^[. ]+|[. ]+$');
|
||||
final _trimUnderscoresAndSpacesRegex = RegExp(r'^[_ ]+|[_ ]+$');
|
||||
@@ -7114,8 +7124,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.failed,
|
||||
error:
|
||||
'SAF permission invalid or revoked. Please reconfigure download location in Settings.',
|
||||
error: safPermissionLostErrorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7146,8 +7155,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.failed,
|
||||
error:
|
||||
'Download folder access lost. Please re-select your download folder in Settings.',
|
||||
error: downloadFolderAccessLostErrorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3412,6 +3412,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
) async {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isRateLimit = item.errorType == DownloadErrorType.rateLimit;
|
||||
final isFolderAccessLost =
|
||||
item.errorMessage == safPermissionLostErrorMessage ||
|
||||
item.errorMessage == downloadFolderAccessLostErrorMessage;
|
||||
final title = isRateLimit
|
||||
? context.l10n.queueRateLimitTitle
|
||||
: context.l10n.updateDownloadFailed;
|
||||
@@ -3453,10 +3456,16 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop('retry'),
|
||||
child: Text(context.l10n.dialogRetry),
|
||||
),
|
||||
if (isFolderAccessLost)
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop('reselect'),
|
||||
child: Text(context.l10n.downloadFolderReselect),
|
||||
)
|
||||
else
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop('retry'),
|
||||
child: Text(context.l10n.dialogRetry),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -3465,9 +3474,69 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
ref.read(downloadQueueProvider.notifier).retryItem(item.id);
|
||||
} else if (action == 'remove') {
|
||||
ref.read(downloadQueueProvider.notifier).removeItem(item.id);
|
||||
} else if (action == 'reselect') {
|
||||
final reselected = await _reselectDownloadFolder();
|
||||
if (reselected && mounted) {
|
||||
ref.read(downloadQueueProvider.notifier).retryItem(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reopen the platform folder picker to restore download folder access,
|
||||
/// then persist the new location. Returns true when a folder was saved.
|
||||
Future<bool> _reselectDownloadFolder() async {
|
||||
if (Platform.isAndroid) {
|
||||
final result = await PlatformBridge.pickSafTree();
|
||||
if (result == null) return false;
|
||||
final treeUri = result['tree_uri'] as String? ?? '';
|
||||
final displayName = result['display_name'] as String? ?? '';
|
||||
if (treeUri.isEmpty) return false;
|
||||
final notifier = ref.read(settingsProvider.notifier);
|
||||
notifier.setStorageMode('saf');
|
||||
notifier.setDownloadTreeUri(
|
||||
treeUri,
|
||||
displayName: displayName.isNotEmpty ? displayName : treeUri,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
IosPickedDirectory? picked;
|
||||
try {
|
||||
picked = await PlatformBridge.pickIosDirectory();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarFolderPickerFailed(e.toString()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (picked == null) return false;
|
||||
final validation = validateIosPath(picked.path);
|
||||
if (!validation.isValid) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
validation.errorReason ?? context.l10n.setupIcloudNotSupported,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDownloadDirectory(picked.path, iosBookmark: picked.bookmark);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget _buildDownloadGridItem(
|
||||
BuildContext context,
|
||||
DownloadItem item,
|
||||
|
||||
@@ -23,11 +23,40 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
int _androidSdkVersion = 0;
|
||||
bool _hasAllFilesAccess = false;
|
||||
bool _artistFolderFiltersExpanded = false;
|
||||
bool _safAccessLost = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initDeviceInfo();
|
||||
_checkSafAccess();
|
||||
}
|
||||
|
||||
Future<void> _checkSafAccess() async {
|
||||
if (!Platform.isAndroid) return;
|
||||
final settings = ref.read(settingsProvider);
|
||||
if (settings.storageMode != 'saf' || settings.downloadTreeUri.isEmpty) {
|
||||
if (mounted && _safAccessLost) setState(() => _safAccessLost = false);
|
||||
return;
|
||||
}
|
||||
final accessible = await PlatformBridge.isSafTreeAccessible(
|
||||
settings.downloadTreeUri,
|
||||
);
|
||||
if (mounted) setState(() => _safAccessLost = !accessible);
|
||||
}
|
||||
|
||||
Future<void> _pickSafTreeAndSave() async {
|
||||
final result = await PlatformBridge.pickSafTree();
|
||||
if (result == null) return;
|
||||
final treeUri = result['tree_uri'] as String? ?? '';
|
||||
final displayName = result['display_name'] as String? ?? '';
|
||||
if (treeUri.isEmpty) return;
|
||||
final notifier = ref.read(settingsProvider.notifier);
|
||||
notifier.setStorageMode('saf');
|
||||
notifier.setDownloadTreeUri(
|
||||
treeUri,
|
||||
displayName: displayName.isNotEmpty ? displayName : treeUri,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initDeviceInfo() async {
|
||||
@@ -156,6 +185,62 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (Platform.isAndroid &&
|
||||
_safAccessLost &&
|
||||
settings.storageMode == 'saf' &&
|
||||
settings.downloadTreeUri.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.downloadFolderAccessLostTitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
context.l10n.downloadFolderAccessLostSubtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onErrorContainer
|
||||
.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _pickSafTreeAndSave();
|
||||
await _checkSafAccess();
|
||||
},
|
||||
child: Text(context.l10n.downloadFolderReselect),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(
|
||||
@@ -522,22 +607,8 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
trailing: isSafMode ? const Icon(Icons.check) : null,
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
final result = await PlatformBridge.pickSafTree();
|
||||
if (result != null) {
|
||||
final treeUri = result['tree_uri'] as String? ?? '';
|
||||
final displayName = result['display_name'] as String? ?? '';
|
||||
if (treeUri.isNotEmpty) {
|
||||
ref.read(settingsProvider.notifier).setStorageMode('saf');
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDownloadTreeUri(
|
||||
treeUri,
|
||||
displayName: displayName.isNotEmpty
|
||||
? displayName
|
||||
: treeUri,
|
||||
);
|
||||
}
|
||||
}
|
||||
await _pickSafTreeAndSave();
|
||||
await _checkSafAccess();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -598,6 +598,22 @@ class PlatformBridge {
|
||||
return result as bool;
|
||||
}
|
||||
|
||||
/// Whether the persisted SAF grant for [treeUri] is still usable: the
|
||||
/// permission is present in the system's persisted list and the tree
|
||||
/// document still exists and is writable. Returns true on channel errors
|
||||
/// so a bridge problem never raises a false "access lost" alarm.
|
||||
static Future<bool> isSafTreeAccessible(String treeUri) async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod('isSafTreeAccessible', {
|
||||
'tree_uri': treeUri,
|
||||
});
|
||||
return result as bool? ?? true;
|
||||
} catch (e) {
|
||||
_log.w('Failed to check SAF tree accessibility: $e');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> safDelete(String uri) async {
|
||||
final result = await _channel.invokeMethod('safDelete', {'uri': uri});
|
||||
return result as bool;
|
||||
|
||||
Reference in New Issue
Block a user