#136 hidden paths: select directory with custom picker instead of SAF one
This commit is contained in:
parent
b837c0a5b6
commit
db78210a37
13 changed files with 373 additions and 55 deletions
|
@ -151,8 +151,7 @@ class MainActivity : FlutterActivity() {
|
||||||
DELETE_SINGLE_PERMISSION_REQUEST,
|
DELETE_SINGLE_PERMISSION_REQUEST,
|
||||||
MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode)
|
MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode)
|
||||||
CREATE_FILE_REQUEST,
|
CREATE_FILE_REQUEST,
|
||||||
OPEN_FILE_REQUEST,
|
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
|
||||||
SELECT_DIRECTORY_REQUEST -> onStorageAccessResult(requestCode, data?.data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,9 +289,8 @@ class MainActivity : FlutterActivity() {
|
||||||
const val OPEN_FROM_ANALYSIS_SERVICE = 2
|
const val OPEN_FROM_ANALYSIS_SERVICE = 2
|
||||||
const val CREATE_FILE_REQUEST = 3
|
const val CREATE_FILE_REQUEST = 3
|
||||||
const val OPEN_FILE_REQUEST = 4
|
const val OPEN_FILE_REQUEST = 4
|
||||||
const val SELECT_DIRECTORY_REQUEST = 5
|
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
|
||||||
const val DELETE_SINGLE_PERMISSION_REQUEST = 6
|
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
|
||||||
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 7
|
|
||||||
|
|
||||||
const val INTENT_DATA_KEY_ACTION = "action"
|
const val INTENT_DATA_KEY_ACTION = "action"
|
||||||
const val INTENT_DATA_KEY_FILTERS = "filters"
|
const val INTENT_DATA_KEY_FILTERS = "filters"
|
||||||
|
|
|
@ -11,7 +11,6 @@ import deckers.thibault.aves.MainActivity
|
||||||
import deckers.thibault.aves.PendingStorageAccessResultHandler
|
import deckers.thibault.aves.PendingStorageAccessResultHandler
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.PermissionManager
|
import deckers.thibault.aves.utils.PermissionManager
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.EventChannel.EventSink
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -44,7 +43,6 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
||||||
"requestMediaFileAccess" -> GlobalScope.launch(Dispatchers.IO) { requestMediaFileAccess() }
|
"requestMediaFileAccess" -> GlobalScope.launch(Dispatchers.IO) { requestMediaFileAccess() }
|
||||||
"createFile" -> GlobalScope.launch(Dispatchers.IO) { createFile() }
|
"createFile" -> GlobalScope.launch(Dispatchers.IO) { createFile() }
|
||||||
"openFile" -> GlobalScope.launch(Dispatchers.IO) { openFile() }
|
"openFile" -> GlobalScope.launch(Dispatchers.IO) { openFile() }
|
||||||
"selectDirectory" -> GlobalScope.launch(Dispatchers.IO) { selectDirectory() }
|
|
||||||
else -> endOfStream()
|
else -> endOfStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,25 +156,6 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
||||||
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
|
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectDirectory() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
|
||||||
|
|
||||||
MainActivity.pendingStorageAccessResultHandlers[MainActivity.SELECT_DIRECTORY_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
|
|
||||||
success(StorageUtils.convertTreeUriToDirPath(activity, uri))
|
|
||||||
endOfStream()
|
|
||||||
}, {
|
|
||||||
success(null)
|
|
||||||
endOfStream()
|
|
||||||
})
|
|
||||||
activity.startActivityForResult(intent, MainActivity.SELECT_DIRECTORY_REQUEST)
|
|
||||||
} else {
|
|
||||||
// TODO TLAD support KitKat
|
|
||||||
success(null)
|
|
||||||
endOfStream()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {}
|
override fun onCancel(arguments: Any?) {}
|
||||||
|
|
||||||
private fun success(result: Any?) {
|
private fun success(result: Any?) {
|
||||||
|
|
|
@ -1064,5 +1064,16 @@
|
||||||
"@panoramaDisableSensorControl": {},
|
"@panoramaDisableSensorControl": {},
|
||||||
|
|
||||||
"sourceViewerPageTitle": "Source",
|
"sourceViewerPageTitle": "Source",
|
||||||
"@sourceViewerPageTitle": {}
|
"@sourceViewerPageTitle": {},
|
||||||
|
|
||||||
|
"filePickerShowHiddenFiles": "Show hidden files",
|
||||||
|
"@filePickerShowHiddenFiles": {},
|
||||||
|
"filePickerDoNotShowHiddenFiles": "Don’t show hidden files",
|
||||||
|
"@filePickerDoNotShowHiddenFiles": {},
|
||||||
|
"filePickerOpenFrom": "Open from",
|
||||||
|
"@filePickerOpenFrom": {},
|
||||||
|
"filePickerNoItems": "No items",
|
||||||
|
"@filePickerNoItems": {},
|
||||||
|
"filePickerUseThisFolder": "Use this folder",
|
||||||
|
"@filePickerUseThisFolder": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,11 @@
|
||||||
"panoramaDisableSensorControl": "Désactiver le contrôle par capteurs",
|
"panoramaDisableSensorControl": "Désactiver le contrôle par capteurs",
|
||||||
|
|
||||||
"sourceViewerPageTitle": "Code source",
|
"sourceViewerPageTitle": "Code source",
|
||||||
"@sourceViewerPageTitle": {}
|
"@sourceViewerPageTitle": {},
|
||||||
|
|
||||||
|
"filePickerShowHiddenFiles": "Afficher les fichiers masqués",
|
||||||
|
"filePickerDoNotShowHiddenFiles": "Ne pas afficher les fichiers masqués",
|
||||||
|
"filePickerOpenFrom": "Ouvrir à partir de",
|
||||||
|
"filePickerNoItems": "Aucun élément",
|
||||||
|
"filePickerUseThisFolder": "Utiliser ce dossier"
|
||||||
}
|
}
|
||||||
|
|
|
@ -508,5 +508,11 @@
|
||||||
"panoramaEnableSensorControl": "센서 제어 활성화",
|
"panoramaEnableSensorControl": "센서 제어 활성화",
|
||||||
"panoramaDisableSensorControl": "센서 제어 비활성화",
|
"panoramaDisableSensorControl": "센서 제어 비활성화",
|
||||||
|
|
||||||
"sourceViewerPageTitle": "소스 코드"
|
"sourceViewerPageTitle": "소스 코드",
|
||||||
|
|
||||||
|
"filePickerShowHiddenFiles": "숨겨진 파일 표시",
|
||||||
|
"filePickerDoNotShowHiddenFiles": "숨겨진 파일 표시 안함",
|
||||||
|
"filePickerOpenFrom": "다음에서 열기:",
|
||||||
|
"filePickerNoItems": "항목 없음",
|
||||||
|
"filePickerUseThisFolder": "이 폴더 사용"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,11 @@
|
||||||
"panoramaEnableSensorControl": "Включить сенсорное управление",
|
"panoramaEnableSensorControl": "Включить сенсорное управление",
|
||||||
"panoramaDisableSensorControl": "Отключить сенсорное управление",
|
"panoramaDisableSensorControl": "Отключить сенсорное управление",
|
||||||
|
|
||||||
"sourceViewerPageTitle": "Источник"
|
"sourceViewerPageTitle": "Источник",
|
||||||
|
|
||||||
|
"filePickerShowHiddenFiles": "Показывать скрытые файлы",
|
||||||
|
"filePickerDoNotShowHiddenFiles": "Не показывать скрытые файлы",
|
||||||
|
"filePickerOpenFrom": "Открыть",
|
||||||
|
"filePickerNoItems": "Ничего нет.",
|
||||||
|
"filePickerUseThisFolder": "Использовать эту папку"
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,4 +99,7 @@ class SettingsDefaults {
|
||||||
// accessibility
|
// accessibility
|
||||||
static const accessibilityAnimations = AccessibilityAnimations.system;
|
static const accessibilityAnimations = AccessibilityAnimations.system;
|
||||||
static const timeToTakeAction = AccessibilityTimeout.appDefault; // `timeToTakeAction` has a contextual default value
|
static const timeToTakeAction = AccessibilityTimeout.appDefault; // `timeToTakeAction` has a contextual default value
|
||||||
|
|
||||||
|
// file picker
|
||||||
|
static const filePickerShowHiddenFiles = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,9 @@ class Settings extends ChangeNotifier {
|
||||||
// version
|
// version
|
||||||
static const lastVersionCheckDateKey = 'last_version_check_date';
|
static const lastVersionCheckDateKey = 'last_version_check_date';
|
||||||
|
|
||||||
|
// file picker
|
||||||
|
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
|
||||||
|
|
||||||
// platform settings
|
// platform settings
|
||||||
// cf Android `Settings.System.ACCELEROMETER_ROTATION`
|
// cf Android `Settings.System.ACCELEROMETER_ROTATION`
|
||||||
static const platformAccelerometerRotationKey = 'accelerometer_rotation';
|
static const platformAccelerometerRotationKey = 'accelerometer_rotation';
|
||||||
|
@ -451,6 +454,12 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch);
|
set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch);
|
||||||
|
|
||||||
|
// file picker
|
||||||
|
|
||||||
|
bool get filePickerShowHiddenFiles => getBoolOrDefault(filePickerShowHiddenFilesKey, SettingsDefaults.filePickerShowHiddenFiles);
|
||||||
|
|
||||||
|
set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue);
|
||||||
|
|
||||||
// convenience methods
|
// convenience methods
|
||||||
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
@ -597,6 +606,7 @@ class Settings extends ChangeNotifier {
|
||||||
case enableVideoAutoPlayKey:
|
case enableVideoAutoPlayKey:
|
||||||
case subtitleShowOutlineKey:
|
case subtitleShowOutlineKey:
|
||||||
case saveSearchHistoryKey:
|
case saveSearchHistoryKey:
|
||||||
|
case filePickerShowHiddenFilesKey:
|
||||||
if (value is bool) {
|
if (value is bool) {
|
||||||
_prefs!.setBool(key, value);
|
_prefs!.setBool(key, value);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -37,8 +37,6 @@ abstract class StorageService {
|
||||||
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
|
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
|
||||||
|
|
||||||
Future<Uint8List> openFile(String mimeType);
|
Future<Uint8List> openFile(String mimeType);
|
||||||
|
|
||||||
Future<String?> selectDirectory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformStorageService implements StorageService {
|
class PlatformStorageService implements StorageService {
|
||||||
|
@ -255,25 +253,4 @@ class PlatformStorageService implements StorageService {
|
||||||
}
|
}
|
||||||
return Uint8List(0);
|
return Uint8List(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> selectDirectory() async {
|
|
||||||
try {
|
|
||||||
final completer = Completer<String?>();
|
|
||||||
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
|
|
||||||
'op': 'selectDirectory',
|
|
||||||
}).listen(
|
|
||||||
(data) => completer.complete(data as String?),
|
|
||||||
onError: completer.completeError,
|
|
||||||
onDone: () {
|
|
||||||
if (!completer.isCompleted) completer.complete(null);
|
|
||||||
},
|
|
||||||
cancelOnError: true,
|
|
||||||
);
|
|
||||||
return completer.future;
|
|
||||||
} on PlatformException catch (e, stack) {
|
|
||||||
await reportService.recordError(e, stack);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,13 @@ class AIcons {
|
||||||
static const IconData date = Icons.calendar_today_outlined;
|
static const IconData date = Icons.calendar_today_outlined;
|
||||||
static const IconData disc = Icons.fiber_manual_record;
|
static const IconData disc = Icons.fiber_manual_record;
|
||||||
static const IconData error = Icons.error_outline;
|
static const IconData error = Icons.error_outline;
|
||||||
|
static const IconData folder = Icons.folder_outlined;
|
||||||
static const IconData grid = Icons.grid_on_outlined;
|
static const IconData grid = Icons.grid_on_outlined;
|
||||||
static const IconData home = Icons.home_outlined;
|
static const IconData home = Icons.home_outlined;
|
||||||
static const IconData language = Icons.translate_outlined;
|
static const IconData language = Icons.translate_outlined;
|
||||||
static const IconData location = Icons.place_outlined;
|
static const IconData location = Icons.place_outlined;
|
||||||
static const IconData locationOff = Icons.location_off_outlined;
|
static const IconData locationOff = Icons.location_off_outlined;
|
||||||
|
static const IconData mainStorage = Icons.smartphone_outlined;
|
||||||
static const IconData privacy = MdiIcons.shieldAccountOutline;
|
static const IconData privacy = MdiIcons.shieldAccountOutline;
|
||||||
static const IconData raw = Icons.raw_on_outlined;
|
static const IconData raw = Icons.raw_on_outlined;
|
||||||
static const IconData shooting = Icons.camera_outlined;
|
static const IconData shooting = Icons.camera_outlined;
|
||||||
|
|
107
lib/widgets/settings/privacy/file_picker/crumb_line.dart
Normal file
107
lib/widgets/settings/privacy/file_picker/crumb_line.dart
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
class CrumbLine extends StatefulWidget {
|
||||||
|
final VolumeRelativeDirectory directory;
|
||||||
|
final void Function(String path) onTap;
|
||||||
|
|
||||||
|
const CrumbLine({
|
||||||
|
Key? key,
|
||||||
|
required this.directory,
|
||||||
|
required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CrumbLineState createState() => _CrumbLineState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CrumbLineState extends State<CrumbLine> {
|
||||||
|
final ScrollController _controller = ScrollController();
|
||||||
|
|
||||||
|
VolumeRelativeDirectory get directory => widget.directory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant CrumbLine oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.directory.relativeDir.length > oldWidget.directory.relativeDir.length) {
|
||||||
|
// scroll to show last crumb
|
||||||
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
|
final extent = _controller.position.maxScrollExtent;
|
||||||
|
_controller.animateTo(
|
||||||
|
extent,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeOutQuad,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<String> parts = [
|
||||||
|
directory.getVolumeDescription(context),
|
||||||
|
...p.split(directory.relativeDir),
|
||||||
|
];
|
||||||
|
final crumbStyle = Theme.of(context).textTheme.bodyText2;
|
||||||
|
final crumbColor = crumbStyle!.color!.withOpacity(.4);
|
||||||
|
return DefaultTextStyle(
|
||||||
|
style: crumbStyle.copyWith(
|
||||||
|
color: crumbColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
controller: _controller,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
Widget _buildText(String text) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index >= parts.length) return const SizedBox();
|
||||||
|
final text = parts[index];
|
||||||
|
if (index == parts.length - 1) {
|
||||||
|
return Center(
|
||||||
|
child: DefaultTextStyle.merge(
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
child: _buildText(text),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
final path = p.joinAll([
|
||||||
|
directory.volumePath,
|
||||||
|
...parts.skip(1).take(index),
|
||||||
|
]);
|
||||||
|
widget.onTap(path);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
// use a `Container` with a dummy color to make it expand
|
||||||
|
// so that we can also detect taps around the title `Text`
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildText(text),
|
||||||
|
Icon(
|
||||||
|
AIcons.next,
|
||||||
|
color: crumbColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: parts.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
203
lib/widgets/settings/privacy/file_picker/file_picker.dart
Normal file
203
lib/widgets/settings/privacy/file_picker/file_picker.dart
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
|
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
class FilePicker extends StatefulWidget {
|
||||||
|
static const routeName = '/file_picker';
|
||||||
|
|
||||||
|
const FilePicker({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FilePickerState createState() => _FilePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FilePickerState extends State<FilePicker> {
|
||||||
|
late VolumeRelativeDirectory _directory;
|
||||||
|
List<Directory>? _contents;
|
||||||
|
|
||||||
|
Set<StorageVolume> get volumes => androidFileUtils.storageVolumes;
|
||||||
|
|
||||||
|
String get currentDirectoryPath => p.join(_directory.volumePath, _directory.relativeDir);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final primaryVolume = volumes.firstWhere((v) => v.isPrimary);
|
||||||
|
_goTo(primaryVolume.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final showHidden = settings.filePickerShowHiddenFiles;
|
||||||
|
final visibleContents = _contents?.where((v) {
|
||||||
|
if (showHidden) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
final isHidden = p.split(v.path).last.startsWith('.');
|
||||||
|
return !isHidden;
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () {
|
||||||
|
if (_directory.relativeDir.isEmpty) {
|
||||||
|
return SynchronousFuture(true);
|
||||||
|
}
|
||||||
|
final parent = p.dirname(currentDirectoryPath);
|
||||||
|
_goTo(parent);
|
||||||
|
setState(() {});
|
||||||
|
return SynchronousFuture(false);
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_getTitle(context)),
|
||||||
|
actions: [
|
||||||
|
MenuIconTheme(
|
||||||
|
child: PopupMenuButton<_PickerAction>(
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _PickerAction.toggleHiddenView,
|
||||||
|
child: MenuRow(text: showHidden ? l10n.filePickerDoNotShowHiddenFiles : l10n.filePickerShowHiddenFiles),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
onSelected: (action) async {
|
||||||
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
|
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||||
|
switch (action) {
|
||||||
|
case _PickerAction.toggleHiddenView:
|
||||||
|
settings.filePickerShowHiddenFiles = !showHidden;
|
||||||
|
setState(() {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawer: _buildDrawer(context),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: kMinInteractiveDimension,
|
||||||
|
child: CrumbLine(
|
||||||
|
directory: _directory,
|
||||||
|
onTap: (path) {
|
||||||
|
_goTo(path);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
Expanded(
|
||||||
|
child: visibleContents == null
|
||||||
|
? const SizedBox()
|
||||||
|
: visibleContents.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: EmptyContent(
|
||||||
|
icon: AIcons.folder,
|
||||||
|
text: l10n.filePickerNoItems,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: visibleContents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return index < visibleContents.length ? _buildContentLine(context, visibleContents[index]) : const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: AvesOutlinedButton(
|
||||||
|
label: l10n.filePickerUseThisFolder,
|
||||||
|
onPressed: () => Navigator.pop(context, currentDirectoryPath),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getTitle(BuildContext context) {
|
||||||
|
if (_directory.relativeDir.isEmpty) {
|
||||||
|
return _directory.getVolumeDescription(context);
|
||||||
|
}
|
||||||
|
return p.split(_directory.relativeDir).last;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDrawer(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.filePickerOpenFrom,
|
||||||
|
style: Theme.of(context).textTheme.headline5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...volumes.map((v) {
|
||||||
|
final icon = v.isRemovable ? AIcons.removableStorage : AIcons.mainStorage;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: Text(v.getDescription(context)),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await Future.delayed(Durations.drawerTransitionAnimation);
|
||||||
|
_goTo(v.path);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
selected: _directory.volumePath == v.path,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContentLine(BuildContext context, FileSystemEntity content) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(AIcons.folder),
|
||||||
|
title: Text(p.split(content.path).last),
|
||||||
|
onTap: () {
|
||||||
|
_goTo(content.path);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goTo(String path) {
|
||||||
|
_directory = VolumeRelativeDirectory.fromPath(path)!;
|
||||||
|
_contents = null;
|
||||||
|
final contents = <Directory>[];
|
||||||
|
Directory(currentDirectoryPath).list().listen((event) {
|
||||||
|
final entity = event.absolute;
|
||||||
|
if (entity is Directory) {
|
||||||
|
contents.add(entity);
|
||||||
|
}
|
||||||
|
}, onDone: () {
|
||||||
|
_contents = contents..sort((a, b) => compareAsciiUpperCase(p.split(a.path).last, p.split(b.path).last));
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _PickerAction { toggleHiddenView }
|
|
@ -2,14 +2,16 @@ import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/path.dart';
|
import 'package:aves/model/filters/path.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/settings/privacy/file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -164,7 +166,15 @@ class _HiddenPaths extends StatelessWidget {
|
||||||
icon: const Icon(AIcons.add),
|
icon: const Icon(AIcons.add),
|
||||||
label: context.l10n.addPathTooltip,
|
label: context.l10n.addPathTooltip,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final path = await storageService.selectDirectory();
|
final path = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<String>(
|
||||||
|
settings: const RouteSettings(name: FilePicker.routeName),
|
||||||
|
builder: (context) => const FilePicker(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// wait for the dialog to hide as applying the change may block the UI
|
||||||
|
await Future.delayed(Durations.pageTransitionAnimation * timeDilation);
|
||||||
if (path != null && path.isNotEmpty) {
|
if (path != null && path.isNotEmpty) {
|
||||||
context.read<CollectionSource>().changeFilterVisibility({PathFilter(path)}, false);
|
context.read<CollectionSource>().changeFilterVisibility({PathFilter(path)}, false);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue