mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 18:57:25 +01:00
Use the storage access framework
Everything related to file system access has been rewritten to make use of the storage access framework. This means that the WRITE_EXTERNAL_STORAGE is no longer needed. Because of that, the dependency on permission_handler could be dropped and all code related to permission handling has been removed. To be able to open a whole document tree, the minSdkVersion was bumped to 21. Finally the file selector was rewritten using custom platform dependent code.
This commit is contained in:
parent
febcf29cf1
commit
e9f0bd03e7
9 changed files with 204 additions and 281 deletions
|
|
@ -38,7 +38,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "de.johrpan.musicus"
|
applicationId "de.johrpan.musicus"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
<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"
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,117 @@
|
||||||
package de.johrpan.musicus
|
package de.johrpan.musicus
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import android.app.Activity
|
||||||
import androidx.core.content.ContextCompat
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.annotation.NonNull
|
||||||
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.plugin.common.MethodChannel
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||||
|
|
||||||
|
class Document(private val id: String, private val name: String, private val parentId: String?, private val isDirectory: Boolean) {
|
||||||
|
fun toMap(): Map<String, Any?> {
|
||||||
|
return mapOf(
|
||||||
|
"id" to id,
|
||||||
|
"name" to name,
|
||||||
|
"parentId" to parentId,
|
||||||
|
"isDirectory" to isDirectory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
private val CHANNEL = "de.johrpan.musicus/platform"
|
private val CHANNEL = "de.johrpan.musicus/platform"
|
||||||
|
private val AODT_REQUEST = 0
|
||||||
|
|
||||||
|
private var aodtResult: MethodChannel.Result? = null
|
||||||
|
|
||||||
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 ->
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||||
if (call.method == "getStorageRoots") {
|
if (call.method == "openTree") {
|
||||||
result.success(getStorageRoots())
|
aodtResult = result
|
||||||
|
// We will get the result within onActivityResult
|
||||||
|
openTree()
|
||||||
|
} else if (call.method == "getChildren") {
|
||||||
|
val treeUri = Uri.parse(call.argument<String>("treeUri"))
|
||||||
|
val parentId = call.argument<String>("parentId")
|
||||||
|
val children = getChildren(treeUri, parentId)
|
||||||
|
result.success(children.map { it.toMap() })
|
||||||
} else {
|
} else {
|
||||||
result.notImplemented()
|
result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStorageRoots(): ArrayList<String> {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
val result = ArrayList<String>()
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
ContextCompat.getExternalFilesDirs(this, null).forEach {
|
if (requestCode == AODT_REQUEST) {
|
||||||
val path = it.absolutePath;
|
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
||||||
val index = path.lastIndexOf("/Android/data/")
|
// Drop all old URIs
|
||||||
|
contentResolver.persistedUriPermissions.forEach {
|
||||||
|
contentResolver.releasePersistableUriPermission(it.uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
if (index > 0) {
|
// We already checked for null
|
||||||
result.add(path.substring(0, index))
|
val uri = data.data!!
|
||||||
|
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
|
||||||
|
aodtResult?.success(uri.toString())
|
||||||
|
} else {
|
||||||
|
aodtResult?.success(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
/**
|
||||||
|
* Open a document tree using the storage access framework
|
||||||
|
*
|
||||||
|
* The result is handled within [onActivityResult]
|
||||||
|
*/
|
||||||
|
private fun openTree() {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
|
||||||
|
startActivityForResult(intent, AODT_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List children of a directory
|
||||||
|
*
|
||||||
|
* @param treeUri The treeUri from the ACTION_OPEN_DOCUMENT_TREE request
|
||||||
|
* @param parentId Document ID of the parent directory or null for the top level directory
|
||||||
|
* @return List of directories and files within the directory
|
||||||
|
*/
|
||||||
|
private fun getChildren(treeUri: Uri, parentId: String?): List<Document> {
|
||||||
|
val realParentId = parentId ?: DocumentsContract.getTreeDocumentId(treeUri)
|
||||||
|
val children = ArrayList<Document>()
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, realParentId)
|
||||||
|
|
||||||
|
val cursor = contentResolver.query(
|
||||||
|
childrenUri,
|
||||||
|
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE),
|
||||||
|
null, null, null)
|
||||||
|
|
||||||
|
if (cursor != null) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getString(0)
|
||||||
|
val name = cursor.getString(1)
|
||||||
|
val isDirectory = cursor.getString(2) == DocumentsContract.Document.MIME_TYPE_DIR
|
||||||
|
|
||||||
|
// Use parentId here to let the consumer know that we are at the top level.
|
||||||
|
children.add(Document(id, name, parentId, isDirectory))
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
lib/app.dart
48
lib/app.dart
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'backend.dart';
|
import 'backend.dart';
|
||||||
import 'screens/home.dart';
|
import 'screens/home.dart';
|
||||||
import 'selectors/files.dart';
|
|
||||||
import 'widgets/player_bar.dart';
|
import 'widgets/player_bar.dart';
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends StatelessWidget {
|
||||||
|
|
@ -34,37 +33,6 @@ class App extends StatelessWidget {
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
);
|
);
|
||||||
} else if (backend.status == BackendStatus.needsPermissions) {
|
|
||||||
return Material(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
'Musicus needs permissions\nto access your files.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.headline6,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 16.0,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.done),
|
|
||||||
title: Text('Grant permissions'),
|
|
||||||
onTap: () {
|
|
||||||
backend.requestPermissions();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.settings),
|
|
||||||
title: Text('Open system\'s app settings'),
|
|
||||||
onTap: () {
|
|
||||||
backend.openAppSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (backend.status == BackendStatus.setup) {
|
} else if (backend.status == BackendStatus.setup) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
|
@ -82,20 +50,8 @@ class App extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.folder_open),
|
leading: const Icon(Icons.folder_open),
|
||||||
title: Text('Choose path'),
|
title: Text('Choose path'),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final path = await Navigator.push<String>(
|
backend.chooseMusicLibraryUri();
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => FilesSelector(
|
|
||||||
mode: FilesSelectorMode.directory,
|
|
||||||
),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (path != null) {
|
|
||||||
backend.setMusicLibraryPath(path);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
|
@ -7,7 +7,6 @@ import 'database.dart';
|
||||||
|
|
||||||
enum BackendStatus {
|
enum BackendStatus {
|
||||||
loading,
|
loading,
|
||||||
needsPermissions,
|
|
||||||
setup,
|
setup,
|
||||||
ready,
|
ready,
|
||||||
}
|
}
|
||||||
|
|
@ -27,15 +26,15 @@ class Backend extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackendState extends State<Backend> {
|
class BackendState extends State<Backend> {
|
||||||
final _permissionHandler = PermissionHandler();
|
static const _platform = MethodChannel('de.johrpan.musicus/platform');
|
||||||
|
|
||||||
final playerActive = BehaviorSubject.seeded(false);
|
final playerActive = BehaviorSubject.seeded(false);
|
||||||
final playing = BehaviorSubject.seeded(false);
|
final playing = BehaviorSubject.seeded(false);
|
||||||
final position = BehaviorSubject.seeded(0.0);
|
final position = BehaviorSubject.seeded(0.0);
|
||||||
|
|
||||||
Database db;
|
|
||||||
BackendStatus status = BackendStatus.loading;
|
BackendStatus status = BackendStatus.loading;
|
||||||
String musicLibraryPath;
|
Database db;
|
||||||
|
String musicLibraryUri;
|
||||||
|
|
||||||
SharedPreferences _shPref;
|
SharedPreferences _shPref;
|
||||||
|
|
||||||
|
|
@ -54,25 +53,15 @@ class BackendState extends State<Backend> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
_shPref = await SharedPreferences.getInstance();
|
|
||||||
musicLibraryPath = _shPref.getString('musicLibraryPath');
|
|
||||||
|
|
||||||
db = Database('musicus.sqlite');
|
db = Database('musicus.sqlite');
|
||||||
|
_shPref = await SharedPreferences.getInstance();
|
||||||
|
musicLibraryUri = _shPref.getString('musicLibraryUri');
|
||||||
|
|
||||||
final permissionStatus =
|
_loadMusicLibrary();
|
||||||
await _permissionHandler.checkPermissionStatus(PermissionGroup.storage);
|
|
||||||
|
|
||||||
if (permissionStatus != PermissionStatus.granted) {
|
|
||||||
setState(() {
|
|
||||||
status = BackendStatus.needsPermissions;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await _loadMusicLibrary();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadMusicLibrary() async {
|
Future<void> _loadMusicLibrary() async {
|
||||||
if (musicLibraryPath == null) {
|
if (musicLibraryUri == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
status = BackendStatus.setup;
|
status = BackendStatus.setup;
|
||||||
});
|
});
|
||||||
|
|
@ -83,26 +72,19 @@ class BackendState extends State<Backend> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> requestPermissions() async {
|
Future<void> chooseMusicLibraryUri() async {
|
||||||
final result =
|
final uri = await _platform.invokeMethod<String>('openTree');
|
||||||
await _permissionHandler.requestPermissions([PermissionGroup.storage]);
|
|
||||||
|
|
||||||
if (result[PermissionGroup.storage] == PermissionStatus.granted) {
|
if (uri != null) {
|
||||||
_loadMusicLibrary();
|
musicLibraryUri = uri;
|
||||||
|
await _shPref.setString('musicLibraryUri', uri);
|
||||||
|
setState(() {
|
||||||
|
status = BackendStatus.loading;
|
||||||
|
});
|
||||||
|
await _loadMusicLibrary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openAppSettings() => _permissionHandler.openAppSettings();
|
|
||||||
|
|
||||||
Future<void> setMusicLibraryPath(String path) async {
|
|
||||||
musicLibraryPath = path;
|
|
||||||
await _shPref.setString('musicLibraryPath', path);
|
|
||||||
setState(() {
|
|
||||||
status = BackendStatus.loading;
|
|
||||||
});
|
|
||||||
await _loadMusicLibrary();
|
|
||||||
}
|
|
||||||
|
|
||||||
void startPlayer() {
|
void startPlayer() {
|
||||||
playerActive.add(true);
|
playerActive.add(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../backend.dart';
|
import '../backend.dart';
|
||||||
import '../database.dart';
|
import '../database.dart';
|
||||||
|
|
@ -7,6 +6,7 @@ import '../selectors/files.dart';
|
||||||
import '../selectors/recording.dart';
|
import '../selectors/recording.dart';
|
||||||
import '../widgets/recording_tile.dart';
|
import '../widgets/recording_tile.dart';
|
||||||
|
|
||||||
|
// TODO: Update for storage access framework.
|
||||||
class TrackModel {
|
class TrackModel {
|
||||||
String path;
|
String path;
|
||||||
|
|
||||||
|
|
@ -58,22 +58,9 @@ class _TracksEditorState extends State<TracksEditor> {
|
||||||
final Set<String> paths = await Navigator.push(
|
final Set<String> paths = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => FilesSelector(
|
builder: (context) => FilesSelector(),
|
||||||
baseDirectory: backend.musicLibraryPath,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (paths != null) {
|
|
||||||
setState(() {
|
|
||||||
for (final path in paths) {
|
|
||||||
tracks.add(TrackModel(p.relative(
|
|
||||||
path,
|
|
||||||
from: backend.musicLibraryPath,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../backend.dart';
|
import '../backend.dart';
|
||||||
import '../selectors/files.dart';
|
|
||||||
|
|
||||||
class SettingsScreen extends StatelessWidget {
|
class SettingsScreen extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
|
|
@ -17,21 +16,9 @@ class SettingsScreen extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.library_music),
|
leading: Icon(Icons.library_music),
|
||||||
title: Text('Music library path'),
|
title: Text('Music library path'),
|
||||||
subtitle: Text(backend.musicLibraryPath),
|
subtitle: Text(backend.musicLibraryUri),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final path = await Navigator.push<String>(
|
backend.chooseMusicLibraryUri();
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => FilesSelector(
|
|
||||||
mode: FilesSelectorMode.directory,
|
|
||||||
),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (path != null) {
|
|
||||||
backend.setMusicLibraryPath(path);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
|
|
||||||
enum FilesSelectorMode {
|
import '../backend.dart';
|
||||||
files,
|
|
||||||
directory,
|
class Document {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String parent;
|
||||||
|
final bool isDirectory;
|
||||||
|
|
||||||
|
Document.fromMap(Map<dynamic, dynamic> map)
|
||||||
|
: id = map['id'],
|
||||||
|
name = map['name'],
|
||||||
|
parent = map['parent'],
|
||||||
|
isDirectory = map['isDirectory'];
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilesSelector extends StatefulWidget {
|
class FilesSelector extends StatefulWidget {
|
||||||
final FilesSelectorMode mode;
|
|
||||||
final String baseDirectory;
|
|
||||||
|
|
||||||
FilesSelector({
|
|
||||||
this.mode = FilesSelectorMode.files,
|
|
||||||
this.baseDirectory,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FilesSelectorState createState() => _FilesSelectorState();
|
_FilesSelectorState createState() => _FilesSelectorState();
|
||||||
}
|
}
|
||||||
|
|
@ -25,104 +24,21 @@ class FilesSelector extends StatefulWidget {
|
||||||
class _FilesSelectorState extends State<FilesSelector> {
|
class _FilesSelectorState extends State<FilesSelector> {
|
||||||
static const platform = MethodChannel('de.johrpan.musicus/platform');
|
static const platform = MethodChannel('de.johrpan.musicus/platform');
|
||||||
|
|
||||||
Directory baseDirectory;
|
BackendState backend;
|
||||||
List<Directory> storageRoots;
|
List<Document> history = [];
|
||||||
List<Directory> directories = [];
|
List<Document> children = [];
|
||||||
List<FileSystemEntity> contents = [];
|
Set<String> selectedIds = {};
|
||||||
Set<String> selectedPaths = {};
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void didChangeDependencies() {
|
||||||
super.initState();
|
super.didChangeDependencies();
|
||||||
|
|
||||||
if (widget.baseDirectory == null) {
|
backend = Backend.of(context);
|
||||||
platform.invokeListMethod<String>('getStorageRoots').then((sr) {
|
loadChildren();
|
||||||
setState(() {
|
|
||||||
storageRoots = sr.map((path) => Directory(path)).toList();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
baseDirectory = Directory(widget.baseDirectory);
|
|
||||||
openDirectory(baseDirectory);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String titleText;
|
|
||||||
Widget body;
|
|
||||||
|
|
||||||
if (directories.isEmpty && storageRoots != null) {
|
|
||||||
titleText = 'Storage devices';
|
|
||||||
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 if (contents != null) {
|
|
||||||
if (directories.isEmpty) {
|
|
||||||
titleText = 'Base directory';
|
|
||||||
} else {
|
|
||||||
titleText = path.basename(directories.last.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (widget.mode == FilesSelectorMode.files) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
result = ListTile(
|
|
||||||
leading: const Icon(Icons.insert_drive_file),
|
|
||||||
title: Text(path.basename(fse.path)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
body = Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -135,14 +51,9 @@ class _FilesSelectorState extends State<FilesSelector> {
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text(
|
child: Text('DONE'),
|
||||||
widget.mode == FilesSelectorMode.files ? 'DONE' : 'SELECT'),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(
|
Navigator.pop(context, selectedIds);
|
||||||
context,
|
|
||||||
widget.mode == FilesSelectorMode.files
|
|
||||||
? selectedPaths
|
|
||||||
: directories.last?.path);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -152,71 +63,96 @@ class _FilesSelectorState extends State<FilesSelector> {
|
||||||
Material(
|
Material(
|
||||||
elevation: 2.0,
|
elevation: 2.0,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: directories.isNotEmpty
|
leading: IconButton(
|
||||||
? IconButton(
|
icon: const Icon(Icons.arrow_upward),
|
||||||
icon: const Icon(Icons.arrow_upward),
|
onPressed: history.isNotEmpty ? up : null,
|
||||||
onPressed: up,
|
),
|
||||||
)
|
title: Text(
|
||||||
: null,
|
history.isNotEmpty ? history.last.name : 'Music library'),
|
||||||
title: Text(titleText),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: body,
|
child: ListView.builder(
|
||||||
|
itemCount: children.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = children[index];
|
||||||
|
|
||||||
|
if (document.isDirectory) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.folder),
|
||||||
|
title: Text(document.name),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
history.add(document);
|
||||||
|
});
|
||||||
|
loadChildren();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CheckboxListTile(
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
secondary: const Icon(Icons.insert_drive_file),
|
||||||
|
title: Text(document.name),
|
||||||
|
value: selectedIds.contains(document.id),
|
||||||
|
onChanged: (selected) {
|
||||||
|
setState(() {
|
||||||
|
if (selected) {
|
||||||
|
selectedIds.add(document.id);
|
||||||
|
} else {
|
||||||
|
selectedIds.remove(document.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onWillPop: () {
|
onWillPop: () => Future.value(up()),
|
||||||
if (directories.isNotEmpty) {
|
|
||||||
up();
|
|
||||||
return Future.value(false);
|
|
||||||
} else {
|
|
||||||
return Future.value(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openDirectory(Directory directory) async {
|
Future<void> loadChildren() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
contents.clear();
|
children = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
final fses = await directory.list().toList();
|
final childrenMaps = await platform.invokeListMethod<Map<dynamic, dynamic>>(
|
||||||
fses.sort((fse1, fse2) {
|
'getChildren',
|
||||||
int compareBasenames() =>
|
{
|
||||||
path.basename(fse1.path).compareTo(path.basename(fse2.path));
|
'treeUri': backend.musicLibraryUri,
|
||||||
|
'parentId': history.isNotEmpty ? history.last.id : null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (fse1 is Directory) {
|
final newChildren = childrenMaps.map((m) => Document.fromMap(m)).toList();
|
||||||
if (fse2 is Directory) {
|
newChildren.sort((d1, d2) {
|
||||||
return compareBasenames();
|
if (d1.isDirectory != d2.isDirectory) {
|
||||||
} else {
|
return d1.isDirectory ? -1 : 1;
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
} else if (fse2 is Directory) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
} else {
|
||||||
return compareBasenames();
|
return d1.name.compareTo(d2.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
contents = fses;
|
children = newChildren;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void up() {
|
bool up() {
|
||||||
if (directories.isNotEmpty) {
|
if (history.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
directories.removeLast();
|
history.removeLast();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (directories.isNotEmpty) {
|
loadChildren();
|
||||||
openDirectory(directories.last);
|
|
||||||
} else if (baseDirectory != null) {
|
return false;
|
||||||
openDirectory(baseDirectory);
|
} else {
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
moor_flutter:
|
moor_flutter:
|
||||||
path:
|
path:
|
||||||
permission_handler:
|
|
||||||
rxdart:
|
rxdart:
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue