mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 10:47: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 {
|
||||
applicationId "de.johrpan.musicus"
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="de.johrpan.musicus">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="Musicus"
|
||||
|
|
|
|||
|
|
@ -1,39 +1,117 @@
|
|||
package de.johrpan.musicus
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.app.Activity
|
||||
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.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
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() {
|
||||
private val CHANNEL = "de.johrpan.musicus/platform"
|
||||
private val AODT_REQUEST = 0
|
||||
|
||||
private var aodtResult: MethodChannel.Result? = null
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
if (call.method == "getStorageRoots") {
|
||||
result.success(getStorageRoots())
|
||||
if (call.method == "openTree") {
|
||||
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 {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStorageRoots(): ArrayList<String> {
|
||||
val result = ArrayList<String>()
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
ContextCompat.getExternalFilesDirs(this, null).forEach {
|
||||
val path = it.absolutePath;
|
||||
val index = path.lastIndexOf("/Android/data/")
|
||||
if (requestCode == AODT_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
||||
// Drop all old URIs
|
||||
contentResolver.persistedUriPermissions.forEach {
|
||||
contentResolver.releasePersistableUriPermission(it.uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
result.add(path.substring(0, index))
|
||||
// We already checked for null
|
||||
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 'screens/home.dart';
|
||||
import 'selectors/files.dart';
|
||||
import 'widgets/player_bar.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
|
|
@ -34,37 +33,6 @@ class App extends StatelessWidget {
|
|||
return Material(
|
||||
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) {
|
||||
return Material(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
|
|
@ -82,20 +50,8 @@ class App extends StatelessWidget {
|
|||
ListTile(
|
||||
leading: const Icon(Icons.folder_open),
|
||||
title: Text('Choose path'),
|
||||
onTap: () async {
|
||||
final path = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FilesSelector(
|
||||
mode: FilesSelectorMode.directory,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (path != null) {
|
||||
backend.setMusicLibraryPath(path);
|
||||
}
|
||||
onTap: () {
|
||||
backend.chooseMusicLibraryUri();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
|
@ -7,7 +7,6 @@ import 'database.dart';
|
|||
|
||||
enum BackendStatus {
|
||||
loading,
|
||||
needsPermissions,
|
||||
setup,
|
||||
ready,
|
||||
}
|
||||
|
|
@ -27,15 +26,15 @@ class Backend extends StatefulWidget {
|
|||
}
|
||||
|
||||
class BackendState extends State<Backend> {
|
||||
final _permissionHandler = PermissionHandler();
|
||||
static const _platform = MethodChannel('de.johrpan.musicus/platform');
|
||||
|
||||
final playerActive = BehaviorSubject.seeded(false);
|
||||
final playing = BehaviorSubject.seeded(false);
|
||||
final position = BehaviorSubject.seeded(0.0);
|
||||
|
||||
Database db;
|
||||
BackendStatus status = BackendStatus.loading;
|
||||
String musicLibraryPath;
|
||||
Database db;
|
||||
String musicLibraryUri;
|
||||
|
||||
SharedPreferences _shPref;
|
||||
|
||||
|
|
@ -54,25 +53,15 @@ class BackendState extends State<Backend> {
|
|||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
_shPref = await SharedPreferences.getInstance();
|
||||
musicLibraryPath = _shPref.getString('musicLibraryPath');
|
||||
|
||||
db = Database('musicus.sqlite');
|
||||
_shPref = await SharedPreferences.getInstance();
|
||||
musicLibraryUri = _shPref.getString('musicLibraryUri');
|
||||
|
||||
final permissionStatus =
|
||||
await _permissionHandler.checkPermissionStatus(PermissionGroup.storage);
|
||||
|
||||
if (permissionStatus != PermissionStatus.granted) {
|
||||
setState(() {
|
||||
status = BackendStatus.needsPermissions;
|
||||
});
|
||||
} else {
|
||||
await _loadMusicLibrary();
|
||||
}
|
||||
_loadMusicLibrary();
|
||||
}
|
||||
|
||||
Future<void> _loadMusicLibrary() async {
|
||||
if (musicLibraryPath == null) {
|
||||
if (musicLibraryUri == null) {
|
||||
setState(() {
|
||||
status = BackendStatus.setup;
|
||||
});
|
||||
|
|
@ -83,26 +72,19 @@ class BackendState extends State<Backend> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> requestPermissions() async {
|
||||
final result =
|
||||
await _permissionHandler.requestPermissions([PermissionGroup.storage]);
|
||||
Future<void> chooseMusicLibraryUri() async {
|
||||
final uri = await _platform.invokeMethod<String>('openTree');
|
||||
|
||||
if (result[PermissionGroup.storage] == PermissionStatus.granted) {
|
||||
_loadMusicLibrary();
|
||||
if (uri != null) {
|
||||
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() {
|
||||
playerActive.add(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../backend.dart';
|
||||
import '../database.dart';
|
||||
|
|
@ -7,6 +6,7 @@ import '../selectors/files.dart';
|
|||
import '../selectors/recording.dart';
|
||||
import '../widgets/recording_tile.dart';
|
||||
|
||||
// TODO: Update for storage access framework.
|
||||
class TrackModel {
|
||||
String path;
|
||||
|
||||
|
|
@ -58,22 +58,9 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
final Set<String> paths = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FilesSelector(
|
||||
baseDirectory: backend.musicLibraryPath,
|
||||
),
|
||||
builder: (context) => FilesSelector(),
|
||||
),
|
||||
);
|
||||
|
||||
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 '../backend.dart';
|
||||
import '../selectors/files.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
@override
|
||||
|
|
@ -17,21 +16,9 @@ class SettingsScreen extends StatelessWidget {
|
|||
ListTile(
|
||||
leading: Icon(Icons.library_music),
|
||||
title: Text('Music library path'),
|
||||
subtitle: Text(backend.musicLibraryPath),
|
||||
onTap: () async {
|
||||
final path = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => FilesSelector(
|
||||
mode: FilesSelectorMode.directory,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (path != null) {
|
||||
backend.setMusicLibraryPath(path);
|
||||
}
|
||||
subtitle: Text(backend.musicLibraryUri),
|
||||
onTap: () {
|
||||
backend.chooseMusicLibraryUri();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
enum FilesSelectorMode {
|
||||
files,
|
||||
directory,
|
||||
import '../backend.dart';
|
||||
|
||||
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 {
|
||||
final FilesSelectorMode mode;
|
||||
final String baseDirectory;
|
||||
|
||||
FilesSelector({
|
||||
this.mode = FilesSelectorMode.files,
|
||||
this.baseDirectory,
|
||||
});
|
||||
|
||||
@override
|
||||
_FilesSelectorState createState() => _FilesSelectorState();
|
||||
}
|
||||
|
|
@ -25,104 +24,21 @@ class FilesSelector extends StatefulWidget {
|
|||
class _FilesSelectorState extends State<FilesSelector> {
|
||||
static const platform = MethodChannel('de.johrpan.musicus/platform');
|
||||
|
||||
Directory baseDirectory;
|
||||
List<Directory> storageRoots;
|
||||
List<Directory> directories = [];
|
||||
List<FileSystemEntity> contents = [];
|
||||
Set<String> selectedPaths = {};
|
||||
BackendState backend;
|
||||
List<Document> history = [];
|
||||
List<Document> children = [];
|
||||
Set<String> selectedIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
if (widget.baseDirectory == null) {
|
||||
platform.invokeListMethod<String>('getStorageRoots').then((sr) {
|
||||
setState(() {
|
||||
storageRoots = sr.map((path) => Directory(path)).toList();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
baseDirectory = Directory(widget.baseDirectory);
|
||||
openDirectory(baseDirectory);
|
||||
}
|
||||
backend = Backend.of(context);
|
||||
loadChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
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(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -135,14 +51,9 @@ class _FilesSelectorState extends State<FilesSelector> {
|
|||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text(
|
||||
widget.mode == FilesSelectorMode.files ? 'DONE' : 'SELECT'),
|
||||
child: Text('DONE'),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
widget.mode == FilesSelectorMode.files
|
||||
? selectedPaths
|
||||
: directories.last?.path);
|
||||
Navigator.pop(context, selectedIds);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -152,71 +63,96 @@ class _FilesSelectorState extends State<FilesSelector> {
|
|||
Material(
|
||||
elevation: 2.0,
|
||||
child: ListTile(
|
||||
leading: directories.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
onPressed: up,
|
||||
)
|
||||
: null,
|
||||
title: Text(titleText),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
onPressed: history.isNotEmpty ? up : null,
|
||||
),
|
||||
title: Text(
|
||||
history.isNotEmpty ? history.last.name : 'Music library'),
|
||||
),
|
||||
),
|
||||
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: () {
|
||||
if (directories.isNotEmpty) {
|
||||
up();
|
||||
return Future.value(false);
|
||||
} else {
|
||||
return Future.value(true);
|
||||
}
|
||||
},
|
||||
onWillPop: () => Future.value(up()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openDirectory(Directory directory) async {
|
||||
Future<void> loadChildren() async {
|
||||
setState(() {
|
||||
contents.clear();
|
||||
children = [];
|
||||
});
|
||||
|
||||
final fses = await directory.list().toList();
|
||||
fses.sort((fse1, fse2) {
|
||||
int compareBasenames() =>
|
||||
path.basename(fse1.path).compareTo(path.basename(fse2.path));
|
||||
final childrenMaps = await platform.invokeListMethod<Map<dynamic, dynamic>>(
|
||||
'getChildren',
|
||||
{
|
||||
'treeUri': backend.musicLibraryUri,
|
||||
'parentId': history.isNotEmpty ? history.last.id : null,
|
||||
},
|
||||
);
|
||||
|
||||
if (fse1 is Directory) {
|
||||
if (fse2 is Directory) {
|
||||
return compareBasenames();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (fse2 is Directory) {
|
||||
return 1;
|
||||
final newChildren = childrenMaps.map((m) => Document.fromMap(m)).toList();
|
||||
newChildren.sort((d1, d2) {
|
||||
if (d1.isDirectory != d2.isDirectory) {
|
||||
return d1.isDirectory ? -1 : 1;
|
||||
} else {
|
||||
return compareBasenames();
|
||||
return d1.name.compareTo(d2.name);
|
||||
}
|
||||
});
|
||||
|
||||
setState(() {
|
||||
contents = fses;
|
||||
children = newChildren;
|
||||
});
|
||||
}
|
||||
|
||||
void up() {
|
||||
if (directories.isNotEmpty) {
|
||||
bool up() {
|
||||
if (history.isNotEmpty) {
|
||||
setState(() {
|
||||
directories.removeLast();
|
||||
history.removeLast();
|
||||
});
|
||||
|
||||
if (directories.isNotEmpty) {
|
||||
openDirectory(directories.last);
|
||||
} else if (baseDirectory != null) {
|
||||
openDirectory(baseDirectory);
|
||||
}
|
||||
loadChildren();
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ dependencies:
|
|||
sdk: flutter
|
||||
moor_flutter:
|
||||
path:
|
||||
permission_handler:
|
||||
rxdart:
|
||||
shared_preferences:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue