🐛 Ensure mobile site management request permission

This commit is contained in:
2025-12-06 21:48:16 +08:00
parent 9d4d0f2e48
commit 648d5225f6
6 changed files with 139 additions and 89 deletions

View File

@@ -12,6 +12,8 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
@@ -159,4 +161,4 @@
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
</manifest>

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
@@ -10,6 +11,7 @@ import 'package:island/pods/site_files.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/sites/file_upload_dialog.dart';
import 'package:island/widgets/sites/file_item.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' as p;
@@ -53,6 +55,9 @@ class FileManagementSection extends HookConsumerWidget {
PopupMenuButton<String>(
icon: const Icon(Symbols.upload),
onSelected: (String choice) async {
if (!kIsWeb) {
await Permission.storage.request();
}
List<File> files = [];
List<Map<String, dynamic>>? results;
if (choice == 'files') {
@@ -65,17 +70,17 @@ class FileManagementSection extends HookConsumerWidget {
selectedFiles.files.isEmpty) {
return; // User canceled
}
files =
selectedFiles.files
.map((f) => File(f.path!))
.toList();
files = selectedFiles.files
.map((f) => File(f.path!))
.toList();
} else if (choice == 'folder') {
final dirPath =
await FilePicker.platform.getDirectoryPath();
final dirPath = await FilePicker.platform
.getDirectoryPath();
if (dirPath == null) return;
results = await _getFilesRecursive(dirPath);
files =
results.map((m) => m['file'] as File).toList();
files = results
.map((m) => m['file'] as File)
.toList();
if (files.isEmpty) {
showSnackBar('noFilesFoundInFolder'.tr());
return;
@@ -88,51 +93,46 @@ class FileManagementSection extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => FileUploadDialog(
selectedFiles: files,
site: site,
relativePaths:
results
?.map(
(m) => m['relativePath'] as String,
)
.toList(),
onUploadComplete: () {
// Refresh file list
ref.invalidate(
siteFilesProvider(
siteId: site.id,
path: currentPath.value,
),
);
},
),
builder: (context) => FileUploadDialog(
selectedFiles: files,
site: site,
relativePaths: results
?.map((m) => m['relativePath'] as String)
.toList(),
onUploadComplete: () {
// Refresh file list
ref.invalidate(
siteFilesProvider(
siteId: site.id,
path: currentPath.value,
),
);
},
),
);
},
itemBuilder:
(BuildContext context) => [
PopupMenuItem<String>(
value: 'files',
child: Row(
children: [
Icon(Symbols.file_copy),
Gap(12),
Text('siteFiles'.tr()),
],
),
),
PopupMenuItem<String>(
value: 'folder',
child: Row(
children: [
Icon(Symbols.folder),
Gap(12),
Text('siteFolder'.tr()),
],
),
),
],
itemBuilder: (BuildContext context) => [
PopupMenuItem<String>(
value: 'files',
child: Row(
children: [
Icon(Symbols.file_copy),
Gap(12),
Text('siteFiles'.tr()),
],
),
),
PopupMenuItem<String>(
value: 'folder',
child: Row(
children: [
Icon(Symbols.folder),
Gap(12),
Text('siteFolder'.tr()),
],
),
),
],
style: ButtonStyle(
visualDensity: const VisualDensity(
horizontal: -4,
@@ -156,19 +156,17 @@ class FileManagementSection extends HookConsumerWidget {
IconButton(
icon: Icon(Symbols.arrow_back),
onPressed: () {
final pathParts =
currentPath.value!
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final pathParts = currentPath.value!
.split('/')
.where((part) => part.isNotEmpty)
.toList();
if (pathParts.isEmpty) {
currentPath.value = null;
} else {
pathParts.removeLast();
currentPath.value =
pathParts.isEmpty
? null
: pathParts.join('/');
currentPath.value = pathParts.isEmpty
? null
: pathParts.join('/');
}
},
visualDensity: const VisualDensity(
@@ -185,11 +183,10 @@ class FileManagementSection extends HookConsumerWidget {
child: Text('siteRoot'.tr()),
),
...() {
final parts =
currentPath.value!
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final parts = currentPath.value!
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final widgets = <Widget>[];
String currentBuilder = '';
for (final part in parts) {
@@ -200,8 +197,8 @@ class FileManagementSection extends HookConsumerWidget {
widgets.addAll([
const Text(' / '),
InkWell(
onTap:
() => currentPath.value = pathToSet,
onTap: () =>
currentPath.value = pathToSet,
child: Text(part),
),
]);
@@ -253,33 +250,31 @@ class FileManagementSection extends HookConsumerWidget {
return FileItem(
file: file,
site: site,
onNavigateDirectory:
(path) => currentPath.value = path,
onNavigateDirectory: (path) =>
currentPath.value = path,
);
},
);
},
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => Center(
child: Column(
children: [
Text('failedToLoadFiles'.tr()),
const Gap(8),
ElevatedButton(
onPressed:
() => ref.invalidate(
siteFilesProvider(
siteId: site.id,
path: currentPath.value,
),
),
child: Text('retry'.tr()),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
children: [
Text('failedToLoadFiles'.tr()),
const Gap(8),
ElevatedButton(
onPressed: () => ref.invalidate(
siteFilesProvider(
siteId: site.id,
path: currentPath.value,
),
],
),
child: Text('retry'.tr()),
),
),
],
),
),
),
],
),

View File

@@ -1917,6 +1917,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0+3"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:

View File

@@ -171,6 +171,7 @@ dependencies:
http_parser: ^4.1.2
flutter_code_editor: ^0.3.5
skeletonizer: ^2.1.1
permission_handler: ^12.0.1
dev_dependencies:
flutter_test:

View File

@@ -24,6 +24,7 @@
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
@@ -73,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
PasteboardPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PasteboardPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
RecordWindowsPluginCApiRegisterWithRegistrar(

View File

@@ -21,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_windows_video
media_kit_video
pasteboard
permission_handler_windows
protocol_handler_windows
record_windows
screen_retriever_windows