Add basic files selector

This required to add a platform channel with a method to get the
toplevel storage devices. The app will need to request the
WRITE_EXTERNAL_STORAGE permission later. Also the dependency on "path"
was introduced.
This commit is contained in:
Elias Projahn 2020-03-27 17:33:47 +01:00
parent 02e283b8cc
commit 24a4911665
4 changed files with 213 additions and 2 deletions

View file

@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.johrpan.musicus"> package="de.johrpan.musicus">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:name="io.flutter.app.FlutterApplication" android:name="io.flutter.app.FlutterApplication"
android:label="Musicus" android:label="Musicus"

View file

@ -1,12 +1,39 @@
package de.johrpan.musicus package de.johrpan.musicus
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() { class MainActivity : FlutterActivity() {
private val CHANNEL = "de.johrpan.musicus/platform"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine); GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getStorageRoots") {
result.success(getStorageRoots())
} else {
result.notImplemented()
}
}
}
private fun getStorageRoots(): ArrayList<String> {
val result = ArrayList<String>()
ContextCompat.getExternalFilesDirs(this, null).forEach {
val path = it.absolutePath;
val index = path.lastIndexOf("/Android/data/")
if (index > 0) {
result.add(path.substring(0, index))
}
}
return result
} }
} }

181
lib/selectors/files.dart Normal file
View file

@ -0,0 +1,181 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;
class FilesSelector extends StatefulWidget {
@override
_FilesSelectorState createState() => _FilesSelectorState();
}
class _FilesSelectorState extends State<FilesSelector> {
static const platform = MethodChannel('de.johrpan.musicus/platform');
List<Directory> storageRoots = [];
List<Directory> directories = [];
List<FileSystemEntity> contents = [];
Set<String> selectedPaths = {};
@override
void initState() {
super.initState();
platform.invokeListMethod<String>('getStorageRoots').then((sr) {
setState(() {
storageRoots = sr.map((path) => Directory(path)).toList();
});
});
}
@override
Widget build(BuildContext context) {
Widget body;
if (directories.isEmpty) {
if (storageRoots != null) {
body = ListView(
children: storageRoots
.map((dir) => ListTile(
leading: const Icon(Icons.storage),
title: Text(dir.path),
onTap: () {
setState(() {
directories.add(dir);
});
openDirectory(dir);
},
))
.toList(),
);
} else {
body = Container();
}
} else {
if (contents != null) {
body = ListView(
children: contents.map((fse) {
Widget result;
if (fse is Directory) {
result = ListTile(
leading: const Icon(Icons.folder),
title: Text(path.basename(fse.path)),
onTap: () {
setState(() {
directories.add(fse);
});
openDirectory(fse);
},
);
} else if (fse is File) {
result = CheckboxListTile(
value: selectedPaths.contains(fse.path),
secondary: Icon(Icons.insert_drive_file),
title: Text(path.basename(fse.path)),
onChanged: (selected) {
setState(() {
if (selected) {
selectedPaths.add(fse.path);
} else {
selectedPaths.remove(fse.path);
}
});
},
);
}
return result;
}).toList(),
);
} else {
body = Container();
}
}
return WillPopScope(
child: Scaffold(
appBar: AppBar(
title: Text('Choose files'),
actions: <Widget>[
FlatButton(
child: Text('DONE'),
onPressed: () {
Navigator.pop(context, selectedPaths);
},
),
],
),
body: Column(
children: <Widget>[
Material(
elevation: 2.0,
child: ListTile(
leading: directories.isNotEmpty ? IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: up,
) : null,
title: Text(directories.isEmpty
? 'Storage devices'
: directories.last.path),
),
),
Expanded(
child: body,
),
],
),
),
onWillPop: () {
if (directories.isNotEmpty) {
up();
return Future.value(false);
} else {
return Future.value(true);
}
},
);
}
Future<void> openDirectory(Directory directory) async {
setState(() {
contents.clear();
});
final fses = await directory.list().toList();
fses.sort((fse1, fse2) {
int compareBasenames() =>
path.basename(fse1.path).compareTo(path.basename(fse2.path));
if (fse1 is Directory) {
if (fse2 is Directory) {
return compareBasenames();
} else {
return -1;
}
} else if (fse2 is Directory) {
return 1;
} else {
return compareBasenames();
}
});
setState(() {
contents = fses;
});
}
void up() {
if (directories.isNotEmpty) {
setState(() {
directories.removeLast();
});
if (directories.isNotEmpty) {
openDirectory(directories.last);
}
}
}
}

View file

@ -12,6 +12,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
moor_flutter: moor_flutter:
path:
rxdart: rxdart:
dev_dependencies: dev_dependencies: