Android: 외장 SD 카드 · USB 메모리 선택
- 앱 안 파일 고르기: 바로 가기에 저장소 (내장 · SD 카드 · USB) 를 보이고, 그 안에서 폴더 이동 · 위로 가기 - 설정 > 저장 위치 (MKV · 자막, 다운로드): Android 는 앱 안 폴더 고르기 (SD 카드 폴더를 실제 경로로) - [폴더 열기] · [결과 폴더]: SD 카드 경로도 파일 앱으로 (문서 ID XXXX-XXXX:경로) - MainActivity.storageVolumes: StorageManager 의 연결된 저장소 목록 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
18d003f392
commit
519877105c
|
|
@ -27,6 +27,7 @@ class MainActivity : FlutterActivity() {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"storageRoot" -> result.success(Environment.getExternalStorageDirectory().path)
|
"storageRoot" -> result.success(Environment.getExternalStorageDirectory().path)
|
||||||
|
"storageVolumes" -> result.success(storageVolumes())
|
||||||
"openUrl" -> result.success(openUrl(call.argument<String>("url") ?: ""))
|
"openUrl" -> result.success(openUrl(call.argument<String>("url") ?: ""))
|
||||||
"installApk" -> try {
|
"installApk" -> try {
|
||||||
installApk(call.argument<String>("path") ?: "")
|
installApk(call.argument<String>("path") ?: "")
|
||||||
|
|
@ -82,6 +83,33 @@ class MainActivity : FlutterActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 쓸 수 있는 저장소 (내장 · SD 카드 · USB 메모리): [{path, label, removable}]
|
||||||
|
private fun storageVolumes(): List<Map<String, Any>> {
|
||||||
|
val sm = getSystemService(STORAGE_SERVICE) as android.os.storage.StorageManager
|
||||||
|
val out = mutableListOf<Map<String, Any>>()
|
||||||
|
for (v in sm.storageVolumes) {
|
||||||
|
if (v.state != Environment.MEDIA_MOUNTED) continue
|
||||||
|
val dir: File? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
v.directory
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
v.javaClass.getMethod("getPathFile").invoke(v) as File?
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dir == null) continue
|
||||||
|
out.add(
|
||||||
|
mapOf(
|
||||||
|
"path" to dir.absolutePath,
|
||||||
|
"label" to (v.getDescription(this) ?: dir.name),
|
||||||
|
"removable" to v.isRemovable,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/// 웹 주소를 기기의 기본 브라우저로
|
/// 웹 주소를 기기의 기본 브라우저로
|
||||||
private fun openUrl(url: String): Boolean = try {
|
private fun openUrl(url: String): Boolean = try {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
|
@ -92,9 +120,16 @@ class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
/// 폴더를 파일 앱으로 연다 (내장 저장소 안의 폴더만). 열 앱이 없으면 false.
|
/// 폴더를 파일 앱으로 연다 (내장 저장소 안의 폴더만). 열 앱이 없으면 false.
|
||||||
private fun openFolder(path: String): Boolean {
|
private fun openFolder(path: String): Boolean {
|
||||||
|
// 내장 저장소 (primary) 또는 SD 카드 · USB (/storage/XXXX-XXXX → 문서 ID "XXXX-XXXX:...")
|
||||||
val root = Environment.getExternalStorageDirectory().path
|
val root = Environment.getExternalStorageDirectory().path
|
||||||
if (!path.startsWith(root)) return false
|
val (volumeId, rel) = when {
|
||||||
val rel = path.removePrefix(root).trim('/')
|
path.startsWith(root) -> "primary" to path.removePrefix(root).trim('/')
|
||||||
|
path.startsWith("/storage/") -> {
|
||||||
|
val rest = path.removePrefix("/storage/")
|
||||||
|
rest.substringBefore('/') to rest.substringAfter('/', "").trim('/')
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
// 1. 삼성 "내 파일": 그 폴더로 바로 연다
|
// 1. 삼성 "내 파일": 그 폴더로 바로 연다
|
||||||
try {
|
try {
|
||||||
startActivity(
|
startActivity(
|
||||||
|
|
@ -108,7 +143,7 @@ class MainActivity : FlutterActivity() {
|
||||||
}
|
}
|
||||||
// 2. 시스템 "파일" 앱 (Google / AOSP). 다른 파일 관리자 (X-plore 등) 는 이 주소의 폴더로 가지 않아 앱을 정해 연다
|
// 2. 시스템 "파일" 앱 (Google / AOSP). 다른 파일 관리자 (X-plore 등) 는 이 주소의 폴더로 가지 않아 앱을 정해 연다
|
||||||
val uri = android.provider.DocumentsContract.buildDocumentUri(
|
val uri = android.provider.DocumentsContract.buildDocumentUri(
|
||||||
"com.android.externalstorage.documents", "primary:$rel"
|
"com.android.externalstorage.documents", "$volumeId:$rel"
|
||||||
)
|
)
|
||||||
for (pkg in listOf("com.google.android.documentsui", "com.android.documentsui", null)) {
|
for (pkg in listOf("com.google.android.documentsui", "com.android.documentsui", null)) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,21 @@ class AndroidAccess {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 쓸 수 있는 저장소: (경로, 이름, 빼낼 수 있는지). 내장 저장소가 먼저.
|
||||||
|
/// 읽지 못하면 내장 저장소만.
|
||||||
|
static Future<List<(String, String, bool)>> volumes() async {
|
||||||
|
try {
|
||||||
|
final list = await _ch.invokeMethod<List<Object?>>('storageVolumes') ?? const [];
|
||||||
|
final out = [
|
||||||
|
for (final v in list.cast<Map<Object?, Object?>>())
|
||||||
|
(v['path'] as String, v['label'] as String? ?? '', v['removable'] == true),
|
||||||
|
];
|
||||||
|
out.sort((a, b) => (a.$3 ? 1 : 0) - (b.$3 ? 1 : 0));
|
||||||
|
if (out.isNotEmpty) return out;
|
||||||
|
} catch (_) {}
|
||||||
|
return [(await storageRoot(), '내장 저장소', false)];
|
||||||
|
}
|
||||||
|
|
||||||
/// 내장 저장소 맨 위 (예: /storage/emulated/0)
|
/// 내장 저장소 맨 위 (예: /storage/emulated/0)
|
||||||
static Future<String> storageRoot() async {
|
static Future<String> storageRoot() async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'theme.dart';
|
||||||
String? _lastDir;
|
String? _lastDir;
|
||||||
|
|
||||||
/// Android 앱 안 파일 고르기: 폴더를 오가며 [extensions] 파일을 여러 개 고른다. 고른 파일의 실제 경로를 돌려준다.
|
/// Android 앱 안 파일 고르기: 폴더를 오가며 [extensions] 파일을 여러 개 고른다. 고른 파일의 실제 경로를 돌려준다.
|
||||||
|
/// 내장 저장소와 SD 카드 · USB 메모리를 오갈 수 있다.
|
||||||
Future<List<String>?> showAndroidFileBrowser(
|
Future<List<String>?> showAndroidFileBrowser(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String title,
|
required String title,
|
||||||
|
|
@ -21,18 +22,31 @@ Future<List<String>?> showAndroidFileBrowser(
|
||||||
builder: (_) => _FileBrowser(title: title, extensions: extensions, initialDirectory: initialDirectory),
|
builder: (_) => _FileBrowser(title: title, extensions: extensions, initialDirectory: initialDirectory),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
/// Android 앱 안 폴더 고르기 (저장 위치 등). 고른 폴더의 실제 경로, 취소하면 null.
|
||||||
|
Future<String?> showAndroidFolderBrowser(BuildContext context, {required String title, String? initialDirectory}) async {
|
||||||
|
final r = await Navigator.of(context).push<List<String>>(MaterialPageRoute(
|
||||||
|
fullscreenDialog: true,
|
||||||
|
builder: (_) => _FileBrowser(title: title, extensions: const [], initialDirectory: initialDirectory, folder: true),
|
||||||
|
));
|
||||||
|
return r?.firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
class _FileBrowser extends StatefulWidget {
|
class _FileBrowser extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<String> extensions;
|
final List<String> extensions;
|
||||||
final String? initialDirectory;
|
final String? initialDirectory;
|
||||||
const _FileBrowser({required this.title, required this.extensions, this.initialDirectory});
|
|
||||||
|
/// 폴더 고르기 (파일은 보이지 않고 "이 폴더 선택")
|
||||||
|
final bool folder;
|
||||||
|
const _FileBrowser({required this.title, required this.extensions, this.initialDirectory, this.folder = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FileBrowser> createState() => _FileBrowserState();
|
State<_FileBrowser> createState() => _FileBrowserState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileBrowserState extends State<_FileBrowser> {
|
class _FileBrowserState extends State<_FileBrowser> {
|
||||||
String _root = '/storage/emulated/0';
|
/// (경로, 이름, 빼낼 수 있는지)
|
||||||
|
List<(String, String, bool)> _volumes = const [('/storage/emulated/0', '내장 저장소', false)];
|
||||||
String? _dir;
|
String? _dir;
|
||||||
bool? _allowed;
|
bool? _allowed;
|
||||||
List<Directory> _dirs = [];
|
List<Directory> _dirs = [];
|
||||||
|
|
@ -46,15 +60,25 @@ class _FileBrowserState extends State<_FileBrowser> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _start() async {
|
Future<void> _start() async {
|
||||||
_root = await AndroidAccess.storageRoot();
|
_volumes = await AndroidAccess.volumes();
|
||||||
final allowed = await AndroidAccess.hasAllFiles();
|
final allowed = await AndroidAccess.hasAllFiles();
|
||||||
final first = [widget.initialDirectory, _lastDir, _root].firstWhere(
|
final first = [widget.initialDirectory, _lastDir, _volumes.first.$1].firstWhere(
|
||||||
(d) => d != null && Directory(d).existsSync(),
|
(d) => d != null && Directory(d).existsSync() && _volumeOf(d) != null,
|
||||||
orElse: () => _root)!;
|
orElse: () => _volumes.first.$1)!;
|
||||||
setState(() => _allowed = allowed);
|
setState(() => _allowed = allowed);
|
||||||
if (allowed) _open(first);
|
if (allowed) _open(first);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [dir] 이 들어 있는 저장소 (맨 위 폴더 · 이름)
|
||||||
|
(String, String, bool)? _volumeOf(String dir) {
|
||||||
|
for (final v in _volumes) {
|
||||||
|
if (p.equals(v.$1, dir) || p.isWithin(v.$1, dir)) return v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _volumeName((String, String, bool) v) => v.$3 ? (v.$2.isEmpty ? 'SD 카드' : v.$2) : '내장 저장소';
|
||||||
|
|
||||||
bool _match(String path) => widget.extensions.contains(p.extension(path).replaceFirst('.', '').toLowerCase());
|
bool _match(String path) => widget.extensions.contains(p.extension(path).replaceFirst('.', '').toLowerCase());
|
||||||
|
|
||||||
void _open(String dir) {
|
void _open(String dir) {
|
||||||
|
|
@ -65,7 +89,7 @@ class _FileBrowserState extends State<_FileBrowser> {
|
||||||
final name = p.basename(e.path);
|
final name = p.basename(e.path);
|
||||||
if (name.startsWith('.')) continue;
|
if (name.startsWith('.')) continue;
|
||||||
if (e is Directory) dirs.add(e);
|
if (e is Directory) dirs.add(e);
|
||||||
if (e is File && _match(e.path)) files.add(e);
|
if (!widget.folder && e is File && _match(e.path)) files.add(e);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
int byName(FileSystemEntity a, FileSystemEntity b) =>
|
int byName(FileSystemEntity a, FileSystemEntity b) =>
|
||||||
|
|
@ -88,7 +112,10 @@ class _FileBrowserState extends State<_FileBrowser> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dir = _dir;
|
final dir = _dir;
|
||||||
final atRoot = dir == null || p.equals(dir, _root) || !p.isWithin(_root, dir);
|
final vol = dir == null ? null : _volumeOf(dir);
|
||||||
|
final root = vol?.$1;
|
||||||
|
final atRoot = dir == null || root == null || p.equals(dir, root);
|
||||||
|
final internal = _volumes.first.$1;
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: atRoot,
|
canPop: atRoot,
|
||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
|
|
@ -98,45 +125,63 @@ class _FileBrowserState extends State<_FileBrowser> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
actions: [
|
actions: [
|
||||||
if (_files.isNotEmpty)
|
if (widget.folder)
|
||||||
TextButton(
|
Padding(
|
||||||
onPressed: () => setState(() {
|
padding: const EdgeInsets.only(right: 8),
|
||||||
final all = _files.every((f) => _picked.contains(f.path));
|
child: FilledButton.icon(
|
||||||
for (final f in _files) {
|
onPressed: dir == null ? null : () => Navigator.pop(context, [dir]),
|
||||||
all ? _picked.remove(f.path) : _picked.add(f.path);
|
icon: const Icon(Icons.check, size: 18),
|
||||||
}
|
label: const Text('이 폴더 선택'),
|
||||||
}),
|
),
|
||||||
child: const Text('이 폴더 전체'),
|
)
|
||||||
|
else ...[
|
||||||
|
if (_files.isNotEmpty)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
final all = _files.every((f) => _picked.contains(f.path));
|
||||||
|
for (final f in _files) {
|
||||||
|
all ? _picked.remove(f.path) : _picked.add(f.path);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
child: const Text('이 폴더 전체'),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: _picked.isEmpty ? null : () => Navigator.pop(context, _picked.toList()),
|
||||||
|
child: Text('추가 (${_picked.length})'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
],
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: _picked.isEmpty ? null : () => Navigator.pop(context, _picked.toList()),
|
|
||||||
child: Text('추가 (${_picked.length})'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: switch (_allowed) {
|
body: switch (_allowed) {
|
||||||
null => const Center(child: CircularProgressIndicator()),
|
null => const Center(child: CircularProgressIndicator()),
|
||||||
false => _permission(),
|
false => _permission(),
|
||||||
true => Column(children: [
|
true => Column(children: [
|
||||||
// 바로 가기 · 지금 폴더
|
// 바로 가기: 저장소 (내장 · SD 카드 · USB) → 내장 저장소의 자주 쓰는 폴더
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 2),
|
padding: const EdgeInsets.fromLTRB(8, 6, 8, 2),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
|
for (final v in _volumes)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 6),
|
||||||
|
child: ActionChip(
|
||||||
|
avatar: Icon(v.$3 ? Icons.sd_card_outlined : Icons.phone_android, size: 18),
|
||||||
|
label: Text(_volumeName(v)),
|
||||||
|
onPressed: () => _open(v.$1),
|
||||||
|
),
|
||||||
|
),
|
||||||
for (final (label, sub) in [
|
for (final (label, sub) in [
|
||||||
('내장 저장소', ''),
|
|
||||||
('Download', 'Download'),
|
('Download', 'Download'),
|
||||||
('Movies', 'Movies'),
|
('Movies', 'Movies'),
|
||||||
('DCIM', 'DCIM'),
|
('DCIM', 'DCIM'),
|
||||||
('jj_mkv', 'Movies/jj_mkv'),
|
|
||||||
])
|
])
|
||||||
if (Directory(p.join(_root, sub)).existsSync())
|
if (Directory(p.join(internal, sub)).existsSync())
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 6),
|
padding: const EdgeInsets.only(right: 6),
|
||||||
child: ActionChip(label: Text(label), onPressed: () => _open(p.join(_root, sub))),
|
child: ActionChip(label: Text(label), onPressed: () => _open(p.join(internal, sub))),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
@ -148,14 +193,18 @@ class _FileBrowserState extends State<_FileBrowser> {
|
||||||
onPressed: atRoot ? null : () => _open(p.dirname(dir)),
|
onPressed: atRoot ? null : () => _open(p.dirname(dir)),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
dir == null ? '' : (p.equals(dir, _root) ? '내장 저장소' : p.relative(dir, from: _root)),
|
dir == null || vol == null
|
||||||
|
? ''
|
||||||
|
: (atRoot ? _volumeName(vol) : '${_volumeName(vol)} / ${p.relative(dir, from: root)}'),
|
||||||
style: const TextStyle(color: JjColors.textDim),
|
style: const TextStyle(color: JjColors.textDim),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _dirs.isEmpty && _files.isEmpty
|
child: _dirs.isEmpty && _files.isEmpty
|
||||||
? const Center(child: Text('이 폴더에는 고를 파일이 없습니다', style: TextStyle(color: JjColors.textDim)))
|
? Center(
|
||||||
|
child: Text(widget.folder ? '하위 폴더가 없습니다' : '이 폴더에는 고를 파일이 없습니다',
|
||||||
|
style: const TextStyle(color: JjColors.textDim)))
|
||||||
: ListView(children: [
|
: ListView(children: [
|
||||||
for (final d in _dirs)
|
for (final d in _dirs)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import 'setup_dialog.dart';
|
||||||
import 'update_dialog.dart';
|
import 'update_dialog.dart';
|
||||||
import '../app/settings.dart';
|
import '../app/settings.dart';
|
||||||
import '../platform/windows/cef_runtime.dart';
|
import '../platform/windows/cef_runtime.dart';
|
||||||
|
import 'android_file_browser.dart';
|
||||||
import 'app_actions.dart';
|
import 'app_actions.dart';
|
||||||
import 'cef_setup.dart';
|
import 'cef_setup.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
@ -221,7 +222,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _pickDir(String title) => FilePicker.getDirectoryPath(dialogTitle: title);
|
/// 폴더 고르기. Android 는 앱 안 화면 (내장 저장소 · SD 카드 · USB 를 실제 경로로 고를 수 있음)
|
||||||
|
Future<String?> _pickDir(String title, [String? initial]) => Platform.isAndroid
|
||||||
|
? showAndroidFolderBrowser(context, title: title, initialDirectory: initial)
|
||||||
|
: FilePicker.getDirectoryPath(dialogTitle: title, initialDirectory: initial);
|
||||||
|
|
||||||
Future<void> _applyHotkey() async {
|
Future<void> _applyHotkey() async {
|
||||||
final text = _hotkey.text.trim();
|
final text = _hotkey.text.trim();
|
||||||
|
|
@ -388,7 +392,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
value: s.mkvOutputRoot,
|
value: s.mkvOutputRoot,
|
||||||
defaultText: '동영상이 있는 폴더 아래 jj_mkv (기본)',
|
defaultText: '동영상이 있는 폴더 아래 jj_mkv (기본)',
|
||||||
onPick: () async {
|
onPick: () async {
|
||||||
final d = await _pickDir('MKV · 자막 저장 위치');
|
final d = await _pickDir('MKV · 자막 저장 위치', s.mkvOutputRoot);
|
||||||
if (d != null) await c.updateSettings((x) => x.mkvOutputRoot = d);
|
if (d != null) await c.updateSettings((x) => x.mkvOutputRoot = d);
|
||||||
},
|
},
|
||||||
onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null),
|
onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null),
|
||||||
|
|
@ -398,7 +402,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
value: s.downloadRoot,
|
value: s.downloadRoot,
|
||||||
defaultText: '${desk ? '프로그램 폴더' : '내장 저장소'} (기본): ${s.resolvedDownloadRoot()}',
|
defaultText: '${desk ? '프로그램 폴더' : '내장 저장소'} (기본): ${s.resolvedDownloadRoot()}',
|
||||||
onPick: () async {
|
onPick: () async {
|
||||||
final d = await _pickDir('다운로드 위치');
|
final d = await _pickDir('다운로드 위치', s.resolvedDownloadRoot());
|
||||||
if (d != null) await c.updateSettings((x) => x.downloadRoot = d);
|
if (d != null) await c.updateSettings((x) => x.downloadRoot = d);
|
||||||
},
|
},
|
||||||
onReset: () => c.updateSettings((x) => x.downloadRoot = null),
|
onReset: () => c.updateSettings((x) => x.downloadRoot = null),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue