mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-25 19:27:24 +02:00
common: Adapt to database changes
This commit is contained in:
parent
9e485eac11
commit
84b700236b
9 changed files with 85 additions and 270 deletions
|
|
@ -37,7 +37,6 @@ class MusicusApp extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MusicusBackend(
|
||||
dbPath: dbPath,
|
||||
settingsStorage: settingsStorage,
|
||||
playback: playback,
|
||||
platform: platform,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/isolate.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
|
|
@ -44,9 +37,6 @@ enum MusicusBackendStatus {
|
|||
/// The backend maintains a Musicus database within a Moor isolate. The connect
|
||||
/// port will be registered as 'moor' in the [IsolateNameServer].
|
||||
class MusicusBackend extends StatefulWidget {
|
||||
/// Path to the database file.
|
||||
final String dbPath;
|
||||
|
||||
/// An object to persist the settings.
|
||||
final MusicusSettingsStorage settingsStorage;
|
||||
|
||||
|
|
@ -64,7 +54,6 @@ class MusicusBackend extends StatefulWidget {
|
|||
final Widget child;
|
||||
|
||||
MusicusBackend({
|
||||
@required this.dbPath,
|
||||
@required this.settingsStorage,
|
||||
@required this.playback,
|
||||
@required this.platform,
|
||||
|
|
@ -79,31 +68,19 @@ class MusicusBackend extends StatefulWidget {
|
|||
}
|
||||
|
||||
class MusicusBackendState extends State<MusicusBackend> {
|
||||
/// Starts the database isolate.
|
||||
///
|
||||
/// It will create a database connection for [request.path] and will send the
|
||||
/// drift send port through [request.sendPort].
|
||||
static void _dbIsolateEntrypoint(_IsolateStartRequest request) {
|
||||
final executor = NativeDatabase(File(request.path));
|
||||
|
||||
final driftIsolate =
|
||||
DriftIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor));
|
||||
|
||||
request.sendPort.send(driftIsolate.connectPort);
|
||||
}
|
||||
|
||||
/// The current backend status.
|
||||
///
|
||||
/// If this is not [MusicusBackendStatus.ready], the [child] widget should
|
||||
/// prevent all access to the backend.
|
||||
MusicusBackendStatus status = MusicusBackendStatus.loading;
|
||||
|
||||
MusicusClientDatabase db;
|
||||
MusicusPlayback playback;
|
||||
MusicusSettings settings;
|
||||
MusicusPlatform platform;
|
||||
MusicusLibrary library;
|
||||
|
||||
MusicusClientDatabase get db => library.db;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -112,23 +89,6 @@ class MusicusBackendState extends State<MusicusBackend> {
|
|||
|
||||
/// Initialize resources.
|
||||
Future<void> _load() async {
|
||||
SendPort driftPort = IsolateNameServer.lookupPortByName('moor');
|
||||
|
||||
if (driftPort == null) {
|
||||
final receivePort = ReceivePort();
|
||||
|
||||
await Isolate.spawn(_dbIsolateEntrypoint,
|
||||
_IsolateStartRequest(receivePort.sendPort, widget.dbPath));
|
||||
|
||||
driftPort = await receivePort.first;
|
||||
IsolateNameServer.registerPortWithName(driftPort, 'drift');
|
||||
}
|
||||
|
||||
final driftIsolate = DriftIsolate.fromConnectPort(driftPort);
|
||||
db = MusicusClientDatabase.connect(
|
||||
connection: await driftIsolate.connect(),
|
||||
);
|
||||
|
||||
playback = widget.playback;
|
||||
await playback.setup();
|
||||
|
||||
|
|
@ -186,14 +146,6 @@ class MusicusBackendState extends State<MusicusBackend> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Bundles arguments for the database isolate.
|
||||
class _IsolateStartRequest {
|
||||
final SendPort sendPort;
|
||||
final String path;
|
||||
|
||||
_IsolateStartRequest(this.sendPort, this.path);
|
||||
}
|
||||
|
||||
/// Helper widget passing the current backend state down the widget tree.
|
||||
class _InheritedBackend extends InheritedWidget {
|
||||
final Widget child;
|
||||
|
|
|
|||
|
|
@ -1,123 +1,39 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/isolate.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import 'platform.dart';
|
||||
|
||||
/// Bundles a [Track] with information on how to find the corresponding file.
|
||||
class InternalTrack {
|
||||
/// The represented track.
|
||||
final Track track;
|
||||
|
||||
/// A string identifying the track for playback.
|
||||
///
|
||||
/// This will be the result of calling the platform objects getIdentifier()
|
||||
/// function with the file name of the track.
|
||||
final String identifier;
|
||||
|
||||
InternalTrack({
|
||||
this.track,
|
||||
this.identifier,
|
||||
});
|
||||
|
||||
factory InternalTrack.fromJson(Map<String, dynamic> json) => InternalTrack(
|
||||
track: Track.fromJson(json['track']),
|
||||
identifier: json['identifier'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'track': track.toJson(),
|
||||
'identifier': identifier,
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 MusicusLibrary {
|
||||
/// Starts the database isolate.
|
||||
///
|
||||
/// It will create a database connection for [request.path] and will send the
|
||||
/// drift send port through [request.sendPort].
|
||||
static void _dbIsolateEntrypoint(_IsolateStartRequest request) {
|
||||
final executor = NativeDatabase(File(request.path));
|
||||
|
||||
final driftIsolate =
|
||||
DriftIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor));
|
||||
|
||||
request.sendPort.send(driftIsolate.connectPort);
|
||||
}
|
||||
|
||||
/// String representing the music library base path.
|
||||
final String basePath;
|
||||
|
||||
/// The actual music library database.
|
||||
MusicusClientDatabase db;
|
||||
|
||||
/// Access to platform dependent functionality.
|
||||
final MusicusPlatform platform;
|
||||
|
||||
/// Map of all available tracks by recording ID.
|
||||
///
|
||||
/// These are [InternalTrack] objects to store the URI of the corresponding
|
||||
/// audio file alongside the real [Track] object.
|
||||
final Map<int, List<InternalTrack>> tracks = {};
|
||||
|
||||
MusicusLibrary(this.basePath, this.platform);
|
||||
|
||||
/// Load all available tracks.
|
||||
|
|
@ -126,62 +42,32 @@ class MusicusLibrary {
|
|||
/// 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(parentId);
|
||||
SendPort driftPort = IsolateNameServer.lookupPortByName('drift');
|
||||
|
||||
for (final child in children) {
|
||||
if (child.isDirectory) {
|
||||
recurse(child.id);
|
||||
} else if (child.name == 'musicus.json') {
|
||||
final content = await platform.readDocument(child.id);
|
||||
final musicusFile = MusicusFile.fromJson(jsonDecode(content));
|
||||
for (final track in musicusFile.tracks) {
|
||||
_indexTrack(parentId, track);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (driftPort == null) {
|
||||
final receivePort = ReceivePort();
|
||||
|
||||
await Isolate.spawn(
|
||||
_dbIsolateEntrypoint,
|
||||
_IsolateStartRequest(
|
||||
receivePort.sendPort, p.join(basePath, 'musicus.db')),
|
||||
);
|
||||
|
||||
driftPort = await receivePort.first;
|
||||
IsolateNameServer.registerPortWithName(driftPort, 'drift');
|
||||
}
|
||||
|
||||
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.readDocumentByName(parentId, 'musicus.json');
|
||||
|
||||
if (oldContent != null) {
|
||||
musicusFile = MusicusFile.fromJson(jsonDecode(oldContent));
|
||||
} else {
|
||||
musicusFile = MusicusFile();
|
||||
}
|
||||
|
||||
for (final track in newTracks) {
|
||||
_indexTrack(parentId, track);
|
||||
musicusFile.tracks.add(track);
|
||||
}
|
||||
|
||||
await platform.writeDocumentByName(
|
||||
parentId, 'musicus.json', jsonEncode(musicusFile.toJson()));
|
||||
}
|
||||
|
||||
/// Add a track to the map of available tracks.
|
||||
Future<void> _indexTrack(String parentId, Track track) async {
|
||||
final iTrack = InternalTrack(
|
||||
track: track,
|
||||
identifier: await platform.getIdentifier(parentId, track.fileName),
|
||||
final driftIsolate = DriftIsolate.fromConnectPort(driftPort);
|
||||
db = MusicusClientDatabase.connect(
|
||||
connection: await driftIsolate.connect(),
|
||||
);
|
||||
|
||||
if (tracks.containsKey(track.recordingId)) {
|
||||
tracks[track.recordingId].add(iTrack);
|
||||
} else {
|
||||
tracks[track.recordingId] = [iTrack];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundles arguments for the database isolate.
|
||||
class _IsolateStartRequest {
|
||||
final SendPort sendPort;
|
||||
final String path;
|
||||
|
||||
_IsolateStartRequest(this.sendPort, this.path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import 'library.dart';
|
||||
|
||||
/// Base class for Musicus playback.
|
||||
abstract class MusicusPlayback {
|
||||
/// Whether the player is active.
|
||||
|
|
@ -14,7 +13,7 @@ abstract class MusicusPlayback {
|
|||
/// The current playlist.
|
||||
///
|
||||
/// If the player is not active, this will be an empty list.
|
||||
final playlist = BehaviorSubject.seeded(<InternalTrack>[]);
|
||||
final playlist = BehaviorSubject.seeded(<Track>[]);
|
||||
|
||||
/// Index of the currently played (or paused) track within the playlist.
|
||||
///
|
||||
|
|
@ -24,7 +23,7 @@ abstract class MusicusPlayback {
|
|||
/// The currently played track.
|
||||
///
|
||||
/// This will be null, if there is no current track.
|
||||
final currentTrack = BehaviorSubject<InternalTrack>.seeded(null);
|
||||
final currentTrack = BehaviorSubject<Track>.seeded(null);
|
||||
|
||||
/// Whether we are currently playing or not.
|
||||
///
|
||||
|
|
@ -50,7 +49,7 @@ abstract class MusicusPlayback {
|
|||
Future<void> setup();
|
||||
|
||||
/// Add a list of tracks to the players playlist.
|
||||
Future<void> addTracks(List<InternalTrack> tracks);
|
||||
Future<void> addTracks(List<Track> tracks);
|
||||
|
||||
/// Remove the track at [index] from the playlist.
|
||||
Future<void> removeTrack(int index);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../library.dart';
|
||||
import '../widgets/play_pause_button.dart';
|
||||
import '../widgets/recording_tile.dart';
|
||||
|
||||
|
|
@ -18,7 +17,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
|
||||
StreamSubscription<bool> playerActiveSubscription;
|
||||
|
||||
StreamSubscription<List<InternalTrack>> playlistSubscription;
|
||||
StreamSubscription<List<Track>> playlistSubscription;
|
||||
List<Widget> widgets = [];
|
||||
|
||||
StreamSubscription<double> positionSubscription;
|
||||
|
|
@ -64,7 +63,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
}
|
||||
|
||||
/// Go through the tracks of [playlist] and preprocess them for displaying.
|
||||
Future<void> updateProgram(List<InternalTrack> playlist) async {
|
||||
Future<void> updateProgram(List<Track> playlist) async {
|
||||
List<Widget> newWidgets = [];
|
||||
|
||||
// The following variables exist to adapt the resulting ProgramItem to its
|
||||
|
|
@ -72,25 +71,22 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
|
||||
// If the previous recording was the same, we won't need to include the
|
||||
// recording data again.
|
||||
int lastRecordingId;
|
||||
String lastRecordingId;
|
||||
|
||||
// If the previous work was the same, we won't need to retrieve its parts
|
||||
// from the database again.
|
||||
int lastWorkId;
|
||||
String lastWorkId;
|
||||
|
||||
// This will contain information on the last new work.
|
||||
WorkInfo workInfo;
|
||||
|
||||
// The index of the last displayed section.
|
||||
int lastSectionIndex;
|
||||
|
||||
for (var i = 0; i < playlist.length; i++) {
|
||||
// The widgets displayed for this track.
|
||||
List<Widget> children = [];
|
||||
|
||||
final track = playlist[i];
|
||||
final recordingId = track.track.recordingId;
|
||||
final partIds = track.track.partIds;
|
||||
final recordingId = track.recording;
|
||||
final partIds = track.workParts;
|
||||
|
||||
// If the recording is the same, the work will also be the same, so
|
||||
// workInfo doesn't have to be updated either.
|
||||
|
|
@ -102,7 +98,6 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
if (recordingInfo.recording.work != lastWorkId) {
|
||||
lastWorkId = recordingInfo.recording.work;
|
||||
workInfo = await backend.db.getWork(lastWorkId);
|
||||
lastSectionIndex = null;
|
||||
}
|
||||
|
||||
children.addAll([
|
||||
|
|
@ -116,27 +111,20 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
]);
|
||||
}
|
||||
|
||||
for (final partId in partIds) {
|
||||
final partInfo = workInfo.parts[partId];
|
||||
|
||||
final sectionIndex = workInfo.sections
|
||||
.lastIndexWhere((s) => s.beforePartIndex <= partId);
|
||||
if (sectionIndex != lastSectionIndex && sectionIndex >= 0) {
|
||||
lastSectionIndex = sectionIndex;
|
||||
children.add(Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: Text(workInfo.sections[sectionIndex].title),
|
||||
));
|
||||
for (final part_id_unparsed in partIds.split(',')) {
|
||||
if (part_id_unparsed.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final partId = int.parse(part_id_unparsed);
|
||||
final partInfo = workInfo.parts[partId];
|
||||
|
||||
children.add(Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
partInfo.part.title,
|
||||
partInfo.title,
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ class WorkScreen extends StatelessWidget {
|
|||
title: PerformancesText(
|
||||
performanceInfos: recordingInfo.performances,
|
||||
),
|
||||
onTap: () {
|
||||
final tracks = backend.library.tracks[recordingId];
|
||||
tracks.sort((t1, t2) => t1.track.index.compareTo(t2.track.index));
|
||||
onTap: () async {
|
||||
final tracks = await backend.db.tracksByRecording(recordingId).get();
|
||||
backend.playback.addTracks(tracks);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../library.dart';
|
||||
import '../screens/program.dart';
|
||||
|
||||
import 'play_pause_button.dart';
|
||||
|
|
@ -16,7 +15,7 @@ class PlayerBar extends StatefulWidget {
|
|||
|
||||
class _PlayerBarState extends State<PlayerBar> {
|
||||
MusicusBackendState _backend;
|
||||
StreamSubscription<InternalTrack> _currentTrackSubscribtion;
|
||||
StreamSubscription<Track> _currentTrackSubscribtion;
|
||||
WorkInfo _workInfo;
|
||||
List<int> _partIds;
|
||||
|
||||
|
|
@ -29,16 +28,22 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
_currentTrackSubscribtion?.cancel();
|
||||
_currentTrackSubscribtion = _backend.playback.currentTrack.listen((track) {
|
||||
if (track != null) {
|
||||
_setTrack(track.track);
|
||||
_setTrack(track);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _setTrack(Track track) async {
|
||||
final recording =
|
||||
await _backend.db.recordingById(track.recordingId).getSingle();
|
||||
await _backend.db.recordingById(track.recording).getSingle();
|
||||
|
||||
final workInfo = await _backend.db.getWork(recording.work);
|
||||
final partIds = track.partIds;
|
||||
|
||||
final partIds = track.workParts
|
||||
.split(',')
|
||||
.where((p) => p.isNotEmpty)
|
||||
.map((p) => int.parse(p))
|
||||
.toList();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
@ -54,27 +59,14 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
String subtitle;
|
||||
|
||||
if (_workInfo != null) {
|
||||
title = _workInfo.composers
|
||||
.map((p) => '${p.firstName} ${p.lastName}')
|
||||
.join(', ');
|
||||
title = '${_workInfo.composer.firstName} ${_workInfo.composer.lastName}';
|
||||
|
||||
final subtitleBuffer = StringBuffer(_workInfo.work.title);
|
||||
|
||||
if (_partIds.isNotEmpty) {
|
||||
subtitleBuffer.write(': ');
|
||||
|
||||
final section = _workInfo.sections.lastWhere(
|
||||
(s) => s.beforePartIndex <= _partIds[0],
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (section != null) {
|
||||
subtitleBuffer.write(section.title);
|
||||
subtitleBuffer.write(': ');
|
||||
}
|
||||
|
||||
subtitleBuffer.write(
|
||||
_partIds.map((i) => _workInfo.parts[i].part.title).join(', '));
|
||||
subtitleBuffer
|
||||
.write(_partIds.map((i) => _workInfo.parts[i].title).join(', '));
|
||||
}
|
||||
|
||||
subtitle = subtitleBuffer.toString();
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@ class RecordingTile extends StatelessWidget {
|
|||
children: <Widget>[
|
||||
DefaultTextStyle(
|
||||
style: textTheme.subtitle1,
|
||||
child: Text(workInfo.composers
|
||||
.map((p) => '${p.firstName} ${p.lastName}')
|
||||
.join(', ')),
|
||||
child: Text(
|
||||
'${workInfo.composer.firstName} ${workInfo.composer.lastName}'),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.headline6,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ dependencies:
|
|||
meta:
|
||||
musicus_database:
|
||||
path: ../database
|
||||
path: ^1.8.1
|
||||
rxdart:
|
||||
url_launcher: ^6.1.0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue