mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-27 03:07:26 +01:00
Add music library
This commit is contained in:
parent
9da8f8891b
commit
b1994d1067
6 changed files with 368 additions and 21 deletions
|
|
@ -11,6 +11,7 @@ import 'package:path_provider/path_provider.dart' as pp;
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'database.dart';
|
||||
import 'music_library.dart';
|
||||
import 'player.dart';
|
||||
|
||||
// The following code was taken from
|
||||
|
|
@ -86,6 +87,7 @@ class BackendState extends State<Backend> {
|
|||
BackendStatus status = BackendStatus.loading;
|
||||
Database db;
|
||||
String musicLibraryUri;
|
||||
MusicLibrary ml;
|
||||
|
||||
MoorIsolate _moorIsolate;
|
||||
SharedPreferences _shPref;
|
||||
|
|
@ -122,6 +124,8 @@ class BackendState extends State<Backend> {
|
|||
status = BackendStatus.setup;
|
||||
});
|
||||
} else {
|
||||
ml = MusicLibrary(musicLibraryUri);
|
||||
await ml.load();
|
||||
setState(() {
|
||||
status = BackendStatus.ready;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import '../backend.dart';
|
||||
import '../database.dart';
|
||||
import '../music_library.dart';
|
||||
import '../selectors/files.dart';
|
||||
import '../selectors/recording.dart';
|
||||
import '../widgets/recording_tile.dart';
|
||||
|
||||
// TODO: Update for storage access framework.
|
||||
class TrackModel {
|
||||
String path;
|
||||
String fileName;
|
||||
|
||||
TrackModel(this.path);
|
||||
TrackModel(this.fileName);
|
||||
}
|
||||
|
||||
class TracksEditor extends StatefulWidget {
|
||||
|
|
@ -20,7 +20,8 @@ class TracksEditor extends StatefulWidget {
|
|||
|
||||
class _TracksEditorState extends State<TracksEditor> {
|
||||
int recordingId;
|
||||
List<TrackModel> tracks = [];
|
||||
String parentId;
|
||||
List<TrackModel> trackModels = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -33,7 +34,22 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
FlatButton(
|
||||
child: Text('DONE'),
|
||||
onPressed: () async {
|
||||
// TODO: Save tracks.
|
||||
final List<Track> tracks = [];
|
||||
|
||||
for (var i = 0; i < trackModels.length; i++) {
|
||||
final trackModel = trackModels[i];
|
||||
|
||||
tracks.add(Track(
|
||||
fileName: trackModel.fileName,
|
||||
recordingId: recordingId,
|
||||
index: i,
|
||||
partIds: [],
|
||||
));
|
||||
}
|
||||
|
||||
backend.ml.addTracks(parentId, tracks);
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -63,22 +79,27 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
);
|
||||
|
||||
if (result != null) {
|
||||
// TODO: Add tracks.
|
||||
setState(() {
|
||||
parentId = result.parentId;
|
||||
for (final document in result.selection) {
|
||||
trackModels.add(TrackModel(document.name));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: tracks
|
||||
children: trackModels
|
||||
.map((t) => ListTile(
|
||||
key: Key(t.hashCode.toString()),
|
||||
title: Text(t.path),
|
||||
title: Text(t.fileName),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
tracks.remove(t);
|
||||
trackModels.remove(t);
|
||||
});
|
||||
},
|
||||
),
|
||||
|
|
@ -86,9 +107,9 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
.toList(),
|
||||
onReorder: (i1, i2) {
|
||||
setState(() {
|
||||
final track = tracks.removeAt(i1);
|
||||
final track = trackModels.removeAt(i1);
|
||||
final newIndex = i2 > i1 ? i2 - 1 : i2;
|
||||
tracks.insert(newIndex, track);
|
||||
trackModels.insert(newIndex, track);
|
||||
});
|
||||
},
|
||||
),
|
||||
|
|
|
|||
155
lib/music_library.dart
Normal file
155
lib/music_library.dart
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'platform.dart';
|
||||
|
||||
/// Description of a concrete audio file.
|
||||
///
|
||||
/// This gets stored in the folder of the audio file and links the audio file
|
||||
/// to a recording in the database.
|
||||
class Track {
|
||||
/// The name of the file that contains the track's audio.
|
||||
///
|
||||
/// This corresponds to a document ID in terms of the Android Storage Access
|
||||
/// Framework.
|
||||
final String fileName;
|
||||
|
||||
/// Index within the list of tracks for the corresponding recording.
|
||||
final int index;
|
||||
|
||||
/// Of which recording this track is a part of.
|
||||
final int recordingId;
|
||||
|
||||
/// Which work parts of the recorded work are contained in this track.
|
||||
final List<int> partIds;
|
||||
|
||||
Track({
|
||||
this.fileName,
|
||||
this.index,
|
||||
this.recordingId,
|
||||
this.partIds,
|
||||
});
|
||||
|
||||
factory Track.fromJson(Map<String, dynamic> json) => Track(
|
||||
fileName: json['fileName'],
|
||||
index: json['index'],
|
||||
recordingId: json['recording'],
|
||||
partIds: List.from(json['parts']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'fileName': fileName,
|
||||
'index': index,
|
||||
'recording': recordingId,
|
||||
'parts': partIds,
|
||||
};
|
||||
}
|
||||
|
||||
/// Representation of all tracked audio files in one folder.
|
||||
class MusicusFile {
|
||||
/// Current version of the Musicus file format.
|
||||
///
|
||||
/// If incompatible changes are made, this will be increased by one.
|
||||
static const currentVersion = 0;
|
||||
|
||||
/// Musicus file format version in use.
|
||||
///
|
||||
/// This will be used in the future, if incompatible changes are made.
|
||||
final int version;
|
||||
|
||||
/// List of [Track] objects.
|
||||
final List<Track> tracks;
|
||||
|
||||
MusicusFile({
|
||||
this.version = currentVersion,
|
||||
List<Track> tracks,
|
||||
}) : tracks = tracks ?? [];
|
||||
|
||||
factory MusicusFile.fromJson(Map<String, dynamic> json) => MusicusFile(
|
||||
version: json['version'],
|
||||
tracks: json['tracks']
|
||||
.map<Track>((trackJson) => Track.fromJson(trackJson))
|
||||
.toList(growable: true),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'version': version,
|
||||
'tracks': tracks.map((t) => t.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Manager for all available tracks and their representation on disk.
|
||||
class MusicLibrary {
|
||||
static const platform = MethodChannel('de.johrpan.musicus/platform');
|
||||
|
||||
/// URI of the music library folder.
|
||||
///
|
||||
/// This is a tree URI in the terms of the Android Storage Access Framework.
|
||||
final String treeUri;
|
||||
|
||||
/// Map of all available tracks by recording ID.
|
||||
final Map<int, List<Track>> tracks = {};
|
||||
|
||||
MusicLibrary(this.treeUri);
|
||||
|
||||
/// Load all available tracks.
|
||||
///
|
||||
/// This recursively searches through the whole music library, reads the
|
||||
/// content of all files called musicus.json and stores all track information
|
||||
/// that it found.
|
||||
Future<void> load() async {
|
||||
// TODO: Consider capping the recursion somewhere.
|
||||
Future<void> recurse([String parentId]) async {
|
||||
final children = await Platform.getChildren(treeUri, parentId);
|
||||
|
||||
for (final child in children) {
|
||||
if (child.isDirectory) {
|
||||
recurse(child.id);
|
||||
} else if (child.name == 'musicus.json') {
|
||||
final content = await Platform.readFile(treeUri, child.id);
|
||||
final musicusFile = MusicusFile.fromJson(jsonDecode(content));
|
||||
for (final track in musicusFile.tracks) {
|
||||
if (tracks.containsKey(track.recordingId)) {
|
||||
tracks[track.recordingId].add(track);
|
||||
} else {
|
||||
tracks[track.recordingId] = [track];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await recurse();
|
||||
}
|
||||
|
||||
/// Add a list of new tracks to the music library.
|
||||
///
|
||||
/// They are stored in this instance and on disk in the directory denoted by
|
||||
/// [parentId].
|
||||
Future<void> addTracks(String parentId, List<Track> newTracks) async {
|
||||
MusicusFile musicusFile;
|
||||
|
||||
final oldContent =
|
||||
await Platform.readFileByName(treeUri, parentId, 'musicus.json');
|
||||
|
||||
if (oldContent != null) {
|
||||
musicusFile = MusicusFile.fromJson(jsonDecode(oldContent));
|
||||
} else {
|
||||
musicusFile = MusicusFile();
|
||||
}
|
||||
|
||||
for (final track in newTracks) {
|
||||
musicusFile.tracks.add(track);
|
||||
|
||||
if (tracks.containsKey(track.recordingId)) {
|
||||
tracks[track.recordingId].add(track);
|
||||
} else {
|
||||
tracks[track.recordingId] = [track];
|
||||
}
|
||||
}
|
||||
|
||||
await Platform.writeFileByName(
|
||||
treeUri, parentId, 'musicus.json', jsonEncode(musicusFile.toJson()));
|
||||
}
|
||||
}
|
||||
|
|
@ -47,4 +47,51 @@ class Platform {
|
|||
.map((childJson) => Document.fromJson(childJson))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Read contents of file.
|
||||
///
|
||||
/// [treeId] is the base URI from the SAF, [id] is the document ID of the
|
||||
/// file.
|
||||
static Future<String> readFile(String treeUri, String id) async {
|
||||
return await _platform.invokeMethod(
|
||||
'readFile',
|
||||
{
|
||||
'treeUri': treeUri,
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Read contents of file by name
|
||||
///
|
||||
/// [treeId] is the base URI from the SAF, [parentId] is the document ID of
|
||||
/// the parent directory.
|
||||
static Future<String> readFileByName(
|
||||
String treeUri, String parentId, String fileName) async {
|
||||
return await _platform.invokeMethod(
|
||||
'readFileByName',
|
||||
{
|
||||
'treeUri': treeUri,
|
||||
'parentId': parentId,
|
||||
'fileName': fileName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Write to file by name
|
||||
///
|
||||
/// [treeId] is the base URI from the SAF, [parentId] is the document ID of
|
||||
/// the parent directory.
|
||||
static Future<void> writeFileByName(
|
||||
String treeUri, String parentId, String fileName, String content) async {
|
||||
await _platform.invokeMethod(
|
||||
'writeFileByName',
|
||||
{
|
||||
'treeUri': treeUri,
|
||||
'parentId': parentId,
|
||||
'fileName': fileName,
|
||||
'content': content,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ import '../backend.dart';
|
|||
import '../platform.dart';
|
||||
|
||||
/// Result of the user's interaction with the files selector.
|
||||
///
|
||||
///
|
||||
/// This will be given back when popping the navigator.
|
||||
class FilesSelectorResult {
|
||||
/// Document ID of the parent directory of the selected files.
|
||||
///
|
||||
///
|
||||
/// This will be null, if they are in the toplevel directory.
|
||||
final String parentId;
|
||||
|
||||
/// Document IDs of the selected files.
|
||||
final List<String> trackIds;
|
||||
/// Selected files.
|
||||
final Set<Document> selection;
|
||||
|
||||
FilesSelectorResult(this.parentId, this.trackIds);
|
||||
FilesSelectorResult(this.parentId, this.selection);
|
||||
}
|
||||
|
||||
class FilesSelector extends StatefulWidget {
|
||||
|
|
@ -27,7 +27,7 @@ class _FilesSelectorState extends State<FilesSelector> {
|
|||
BackendState backend;
|
||||
List<Document> history = [];
|
||||
List<Document> children = [];
|
||||
Set<String> selectedIds = {};
|
||||
Set<Document> selection = {};
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
|
|
@ -57,7 +57,7 @@ class _FilesSelectorState extends State<FilesSelector> {
|
|||
context,
|
||||
FilesSelectorResult(
|
||||
history.isNotEmpty ? history.last.id : null,
|
||||
selectedIds.toList(),
|
||||
selection,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -99,13 +99,13 @@ class _FilesSelectorState extends State<FilesSelector> {
|
|||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
secondary: const Icon(Icons.insert_drive_file),
|
||||
title: Text(document.name),
|
||||
value: selectedIds.contains(document.id),
|
||||
value: selection.contains(document),
|
||||
onChanged: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
selectedIds.add(document.id);
|
||||
selection.add(document);
|
||||
} else {
|
||||
selectedIds.remove(document.id);
|
||||
selection.remove(document);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -124,6 +124,10 @@ class _FilesSelectorState extends State<FilesSelector> {
|
|||
Future<void> loadChildren() async {
|
||||
setState(() {
|
||||
children = [];
|
||||
|
||||
// We reset the selection here, because the user should not be able to
|
||||
// select files from multiple directories for now.
|
||||
selection = {};
|
||||
});
|
||||
|
||||
final newChildren = await Platform.getChildren(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue