mobile: Integrate with server

This commit is contained in:
Elias Projahn 2020-04-26 15:35:45 +02:00
parent 60a474ea56
commit c93ebf17a0
20 changed files with 751 additions and 740 deletions

View file

@ -42,7 +42,7 @@ class _EnsembleEditorState extends State<EnsembleEditor> {
name: nameController.text, name: nameController.text,
); );
await backend.db.updateEnsemble(ensemble); await backend.client.putEnsemble(ensemble);
Navigator.pop(context, ensemble); Navigator.pop(context, ensemble);
}, },
) )

View file

@ -42,7 +42,7 @@ class _InstrumentEditorState extends State<InstrumentEditor> {
name: nameController.text, name: nameController.text,
); );
await backend.db.updateInstrument(instrument); await backend.client.putInstrument(instrument);
Navigator.pop(context, instrument); Navigator.pop(context, instrument);
}, },
) )

View file

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart';
import '../selectors/ensemble.dart';
import '../selectors/instruments.dart';
import '../selectors/person.dart';
class PerformanceEditor extends StatefulWidget {
final PerformanceInfo performanceInfo;
PerformanceEditor({
this.performanceInfo,
});
@override
_PerformanceEditorState createState() => _PerformanceEditorState();
}
class _PerformanceEditorState extends State<PerformanceEditor> {
Person person;
Ensemble ensemble;
Instrument role;
@override
void initState() {
super.initState();
if (widget.performanceInfo != null) {
person = widget.performanceInfo.person;
ensemble = widget.performanceInfo.ensemble;
role = widget.performanceInfo.role;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit performer'),
actions: <Widget>[
FlatButton(
child: Text('DONE'),
onPressed: () => Navigator.pop(
context,
PerformanceInfo(
person: person,
ensemble: ensemble,
role: role,
),
),
),
],
),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Person'),
subtitle: Text(person != null
? '${person.firstName} ${person.lastName}'
: 'Select person'),
onTap: () async {
final Person newPerson = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PersonsSelector(),
fullscreenDialog: true,
),
);
if (newPerson != null) {
setState(() {
person = newPerson;
});
}
},
),
ListTile(
title: Text('Ensemble'),
subtitle: Text(ensemble?.name ?? 'Select ensemble'),
onTap: () async {
final Ensemble newEnsemble = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EnsembleSelector(),
fullscreenDialog: true,
),
);
if (newEnsemble != null) {
setState(() {
ensemble = newEnsemble;
});
}
},
),
ListTile(
title: Text('Role'),
subtitle: Text(role?.name ?? 'Select instrument/role'),
onTap: () async {
final Instrument newRole = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InstrumentsSelector(),
fullscreenDialog: true,
),
);
if (newRole != null) {
setState(() {
role = newRole;
});
}
},
),
],
),
);
}
}

View file

@ -45,7 +45,7 @@ class _PersonEditorState extends State<PersonEditor> {
lastName: lastNameController.text, lastName: lastNameController.text,
); );
await backend.db.updatePerson(person); await backend.client.putPerson(person);
Navigator.pop(context, person); Navigator.pop(context, person);
}, },
), ),

View file

@ -2,10 +2,14 @@ import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart'; import '../backend.dart';
import '../selectors/performer.dart'; import '../editors/performance.dart';
import '../selectors/recording.dart';
import '../selectors/work.dart'; import '../selectors/work.dart';
import '../widgets/texts.dart';
/// Screen for editing a recording.
///
/// If the user has finished editing, the result will be returned using the
/// navigator as a [RecordingSelectorResult] object.
class RecordingEditor extends StatefulWidget { class RecordingEditor extends StatefulWidget {
final Recording recording; final Recording recording;
@ -20,8 +24,8 @@ class RecordingEditor extends StatefulWidget {
class _RecordingEditorState extends State<RecordingEditor> { class _RecordingEditorState extends State<RecordingEditor> {
final commentController = TextEditingController(); final commentController = TextEditingController();
Work work; WorkInfo workInfo;
List<PerformanceModel> performanceModels = []; List<PerformanceInfo> performanceInfos = [];
@override @override
void initState() { void initState() {
@ -37,20 +41,56 @@ class _RecordingEditorState extends State<RecordingEditor> {
final backend = Backend.of(context); final backend = Backend.of(context);
Future<void> selectWork() async { Future<void> selectWork() async {
final Work newWork = await Navigator.push( final WorkInfo newWorkInfo = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => WorkSelector(), builder: (context) => WorkSelector(),
fullscreenDialog: true, fullscreenDialog: true,
)); ));
if (newWork != null) { if (newWorkInfo != null) {
setState(() { setState(() {
work = newWork; workInfo = newWorkInfo;
}); });
} }
} }
final List<Widget> performanceTiles = [];
for (var i = 0; i < performanceInfos.length; i++) {
final p = performanceInfos[i];
performanceTiles.add(ListTile(
title: Text(p.person != null
? '${p.person.firstName} ${p.person.lastName}'
: p.ensemble.name),
subtitle: p.role != null ? Text(p.role.name) : null,
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
performanceInfos.remove(p);
});
},
),
onTap: () async {
final PerformanceInfo performanceInfo = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PerformanceEditor(
performanceInfo: p,
),
fullscreenDialog: true,
));
if (performanceInfo != null) {
setState(() {
performanceInfos[i] = performanceInfo;
});
}
},
));
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Recording'), title: Text('Recording'),
@ -60,11 +100,11 @@ class _RecordingEditorState extends State<RecordingEditor> {
onPressed: () async { onPressed: () async {
final recording = Recording( final recording = Recording(
id: widget.recording?.id ?? generateId(), id: widget.recording?.id ?? generateId(),
work: work.id, work: workInfo.work.id,
comment: commentController.text, comment: commentController.text,
); );
final performances = performanceModels final performances = performanceInfos
.map((m) => Performance( .map((m) => Performance(
recording: recording.id, recording: recording.id,
person: m.person?.id, person: m.person?.id,
@ -73,22 +113,28 @@ class _RecordingEditorState extends State<RecordingEditor> {
)) ))
.toList(); .toList();
await backend.db.updateRecording(RecordingData( final recordingInfo =
await backend.client.putRecording(RecordingData(
recording: recording, recording: recording,
performances: performances, performances: performances,
)); ));
Navigator.pop(context, recording); Navigator.pop(context, RecordingSelectorResult(
workInfo: workInfo,
recordingInfo: recordingInfo,
));
}, },
) )
], ],
), ),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
work != null workInfo != null
? ListTile( ? ListTile(
title: WorkText(work.id), title: Text(workInfo.work.title),
subtitle: ComposersText(work.id), subtitle: Text(workInfo.composers
.map((p) => '${p.firstName} ${p.lastName}')
.join(', ')),
onTap: selectWork, onTap: selectWork,
) )
: ListTile( : ListTile(
@ -115,37 +161,22 @@ class _RecordingEditorState extends State<RecordingEditor> {
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () async { onPressed: () async {
final PerformanceModel model = await Navigator.push( final PerformanceInfo model = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PerformerSelector(), builder: (context) => PerformanceEditor(),
fullscreenDialog: true, fullscreenDialog: true,
)); ));
if (model != null) { if (model != null) {
setState(() { setState(() {
performanceModels.add(model); performanceInfos.add(model);
}); });
} }
}, },
), ),
), ),
for (final performance in performanceModels) ...performanceTiles,
ListTile(
title: Text(performance.person != null
? '${performance.person.firstName} ${performance.person.lastName}'
: performance.ensemble.name),
subtitle:
performance.role != null ? Text(performance.role.name) : null,
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
performanceModels.remove(performance);
});
},
),
),
], ],
), ),
); );

View file

@ -22,7 +22,8 @@ class TracksEditor extends StatefulWidget {
class _TracksEditorState extends State<TracksEditor> { class _TracksEditorState extends State<TracksEditor> {
BackendState backend; BackendState backend;
int recordingId; WorkInfo workInfo;
RecordingInfo recordingInfo;
String parentId; String parentId;
List<TrackModel> trackModels = []; List<TrackModel> trackModels = [];
@ -44,12 +45,72 @@ class _TracksEditorState extends State<TracksEditor> {
tracks.add(Track( tracks.add(Track(
fileName: trackModel.fileName, fileName: trackModel.fileName,
recordingId: recordingId, recordingId: recordingInfo.recording.id,
index: i, index: i,
partIds: [trackModel.workPartIndex], partIds: [trackModel.workPartIndex],
)); ));
} }
// We need to copy all information associated with this track we
// got by asking the server to our local database. For now, we
// will just override everything that we already had previously.
// TODO: Think about efficiency.
backend.db.transaction(() async {
for (final composer in workInfo.composers) {
await backend.db.updatePerson(composer);
}
for (final instrument in workInfo.instruments) {
await backend.db.updateInstrument(instrument);
}
for (final partInfo in workInfo.parts) {
for (final instrument in partInfo.instruments) {
await backend.db.updateInstrument(instrument);
}
}
await backend.db.updateWork(WorkData(
data: WorkPartData(
work: workInfo.work,
instrumentIds:
workInfo.instruments.map((i) => i.id).toList(),
),
partData: workInfo.parts
.map((p) => WorkPartData(
work: p.work,
instrumentIds:
p.instruments.map((i) => i.id).toList(),
))
.toList(),
));
for (final performance in recordingInfo.performances) {
if (performance.person != null) {
await backend.db.updatePerson(performance.person);
}
if (performance.ensemble != null) {
await backend.db.updateEnsemble(performance.ensemble);
}
if (performance.role != null) {
await backend.db.updateInstrument(performance.role);
}
}
await backend.db.updateRecording(RecordingData(
recording: recordingInfo.recording,
performances: recordingInfo.performances
.map((p) => Performance(
recording: recordingInfo.recording.id,
person: p.person?.id,
ensemble: p.ensemble?.id,
role: p.role?.id,
))
.toList(),
));
});
backend.ml.addTracks(parentId, tracks); backend.ml.addTracks(parentId, tracks);
Navigator.pop(context); Navigator.pop(context);
@ -61,9 +122,10 @@ class _TracksEditorState extends State<TracksEditor> {
header: Column( header: Column(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: recordingId != null title: recordingInfo != null
? RecordingTile( ? RecordingTile(
recordingId: recordingId, workInfo: workInfo,
recordingInfo: recordingInfo,
) )
: Text('Select recording'), : Text('Select recording'),
onTap: selectRecording, onTap: selectRecording,
@ -92,7 +154,7 @@ class _TracksEditorState extends State<TracksEditor> {
trackModels = newTrackModels; trackModels = newTrackModels;
}); });
if (recordingId != null) { if (recordingInfo != null) {
updateAutoParts(); updateAutoParts();
} }
} }
@ -121,16 +183,17 @@ class _TracksEditorState extends State<TracksEditor> {
} }
Future<void> selectRecording() async { Future<void> selectRecording() async {
final Recording recording = await Navigator.push( final RecordingSelectorResult result = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => RecordingsSelector(), builder: (context) => RecordingSelector(),
), ),
); );
if (recording != null) { if (result != null) {
setState(() { setState(() {
recordingId = recording.id; workInfo = result.workInfo;
recordingInfo = result.recordingInfo;
}); });
updateAutoParts(); updateAutoParts();
@ -139,18 +202,14 @@ class _TracksEditorState extends State<TracksEditor> {
/// Automatically associate the tracks with work parts. /// Automatically associate the tracks with work parts.
Future<void> updateAutoParts() async { Future<void> updateAutoParts() async {
final recording = await backend.db.recordingById(recordingId).getSingle();
final workId = recording.work;
final workParts = await backend.db.workParts(workId).get();
setState(() { setState(() {
for (var i = 0; i < trackModels.length; i++) { for (var i = 0; i < trackModels.length; i++) {
if (i >= workParts.length) { if (i >= workInfo.parts.length) {
trackModels[i].workPartIndex = null; trackModels[i].workPartIndex = null;
trackModels[i].workPartTitle = null; trackModels[i].workPartTitle = null;
} else { } else {
trackModels[i].workPartIndex = workParts[i].partIndex; trackModels[i].workPartIndex = workInfo.parts[i].work.partIndex;
trackModels[i].workPartTitle = workParts[i].title; trackModels[i].workPartTitle = workInfo.parts[i].work.title;
} }
} }
}); });

View file

@ -152,6 +152,10 @@ class _PartTileState extends State<PartTile> {
} }
} }
/// Screen for editing a work.
///
/// If the user is finished editing, the result will be returned as a [WorkInfo]
/// object.
class WorkEditor extends StatefulWidget { class WorkEditor extends StatefulWidget {
final Work work; final Work work;
@ -319,12 +323,12 @@ class _WorkEditorState extends State<WorkEditor> {
)); ));
} }
await backend.db.updateWork(WorkData( final workInfo = await backend.client.putWork(WorkData(
data: data, data: data,
partData: partData, partData: partData,
)); ));
Navigator.pop(context, data.work); Navigator.pop(context, workInfo);
}, },
), ),
], ],

View file

@ -55,7 +55,6 @@ class HomeScreen extends StatelessWidget {
), ),
], ],
), ),
// For debugging purposes
body: StreamBuilder<List<Person>>( body: StreamBuilder<List<Person>>(
stream: backend.db.allPersons().watch(), stream: backend.db.allPersons().watch(),
builder: (context, snapshot) { builder: (context, snapshot) {

View file

@ -16,7 +16,7 @@ class PersonScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backend = Backend.of(context); final backend = Backend.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('${person.firstName} ${person.lastName}'), title: Text('${person.firstName} ${person.lastName}'),

View file

@ -8,90 +8,6 @@ import '../music_library.dart';
import '../widgets/play_pause_button.dart'; import '../widgets/play_pause_button.dart';
import '../widgets/recording_tile.dart'; import '../widgets/recording_tile.dart';
/// Data class to bundle information from the database on one track.
class ProgramItem {
/// ID of the recording.
///
/// We don't need the real recording, as the [RecordingTile] widget handles
/// that for us. If the recording is the same one, as the one from the
/// previous track, this will be null.
final int recordingId;
/// List of work parts contained in this track.
///
/// This will include the parts linked in the track as well as all parents of
/// them, if there are gaps between them (i.e. some parts are missing).
final List<Work> workParts;
ProgramItem({
this.recordingId,
this.workParts,
});
}
/// Widget displaying a [ProgramItem].
class ProgramTile extends StatelessWidget {
final ProgramItem item;
final bool isPlaying;
ProgramTile({
this.item,
this.isPlaying,
});
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(4.0),
child: isPlaying
? const Icon(Icons.play_arrow)
: SizedBox(
width: 24.0,
height: 24.0,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (item.recordingId != null) ...[
RecordingTile(
recordingId: item.recordingId,
),
SizedBox(
height: 8.0,
),
],
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (final part in item.workParts)
Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
child: Text(
part.title,
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
),
],
),
],
),
),
),
],
);
}
}
class ProgramScreen extends StatefulWidget { class ProgramScreen extends StatefulWidget {
@override @override
_ProgramScreenState createState() => _ProgramScreenState(); _ProgramScreenState createState() => _ProgramScreenState();
@ -103,7 +19,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
StreamSubscription<bool> playerActiveSubscription; StreamSubscription<bool> playerActiveSubscription;
StreamSubscription<List<InternalTrack>> playlistSubscription; StreamSubscription<List<InternalTrack>> playlistSubscription;
List<ProgramItem> items = []; List<Widget> widgets = [];
StreamSubscription<double> positionSubscription; StreamSubscription<double> positionSubscription;
double position = 0.0; double position = 0.0;
@ -149,7 +65,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
/// Go through the tracks of [playlist] and preprocess them for displaying. /// Go through the tracks of [playlist] and preprocess them for displaying.
Future<void> updateProgram(List<InternalTrack> playlist) async { Future<void> updateProgram(List<InternalTrack> playlist) async {
List<ProgramItem> newItems = []; List<Widget> newWidgets = [];
// The following variables exist to adapt the resulting ProgramItem to its // The following variables exist to adapt the resulting ProgramItem to its
// predecessor. // predecessor.
@ -162,42 +78,59 @@ class _ProgramScreenState extends State<ProgramScreen> {
// from the database again. // from the database again.
int lastWorkId; int lastWorkId;
// This will always contain the parts of the current work. // This will contain information on the last new work.
List<Work> workParts = []; WorkInfo workInfo;
for (var i = 0; i < playlist.length; i++) { for (var i = 0; i < playlist.length; i++) {
// The data that will be stored in the resulting ProgramItem. // The widgets displayed for this track.
int newRecordingId; List<Widget> children = [];
List<Work> newWorkParts = [];
final track = playlist[i]; final track = playlist[i];
final recordingId = track.track.recordingId; final recordingId = track.track.recordingId;
final partIds = track.track.partIds; final partIds = track.track.partIds;
// newRecordingId will be null, if the recording ID is the same. This // If the recording is the same, the work will also be the same, so
// also means, that the work is the same, so workParts doesn't have to // workInfo doesn't have to be updated either.
// be updated either.
if (recordingId != lastRecordingId) { if (recordingId != lastRecordingId) {
lastRecordingId = recordingId; lastRecordingId = recordingId;
newRecordingId = recordingId;
final recording = final recordingInfo = await backend.db.getRecording(recordingId);
await backend.db.recordingById(recordingId).getSingle();
if (recording.work != lastWorkId) { if (recordingInfo.recording.work != lastWorkId) {
workParts = await backend.db.workParts(recording.work).get(); lastWorkId = recordingInfo.recording.work;
workInfo = await backend.db.getWork(lastWorkId);
} }
lastWorkId = recording.work; children.addAll([
RecordingTile(
workInfo: workInfo,
recordingInfo: recordingInfo,
),
SizedBox(
height: 8.0,
),
]);
} }
for (final partId in partIds) { for (final partId in partIds) {
newWorkParts.add(workParts[partId]); final partInfo = workInfo.parts[partId];
children.add(Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
child: Text(
partInfo.work.title,
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
));
} }
newItems.add(ProgramItem( newWidgets.add(Column(
recordingId: newRecordingId, crossAxisAlignment: CrossAxisAlignment.stretch,
workParts: newWorkParts, children: children,
)); ));
} }
@ -205,7 +138,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
// function might take some time. // function might take some time.
if (mounted) { if (mounted) {
setState(() { setState(() {
items = newItems; widgets = newWidgets;
}); });
} }
} }
@ -225,12 +158,27 @@ class _ProgramScreenState extends State<ProgramScreen> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return ListView.builder( return ListView.builder(
itemCount: items.length, itemCount: widgets.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return InkWell( return InkWell(
child: ProgramTile( child: Row(
item: items[index], children: <Widget>[
isPlaying: index == snapshot?.data, Padding(
padding: const EdgeInsets.all(4.0),
child: index == snapshot.data
? const Icon(Icons.play_arrow)
: SizedBox(
width: 24.0,
height: 24.0,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: widgets[index],
),
),
],
), ),
onTap: () { onTap: () {
backend.player.skipTo(index); backend.player.skipTo(index);

View file

@ -44,8 +44,20 @@ class WorkScreen extends StatelessWidget {
itemCount: snapshot.data.length, itemCount: snapshot.data.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final recording = snapshot.data[index]; final recording = snapshot.data[index];
return ListTile( return ListTile(
title: PerformancesText(recording.id), title: FutureBuilder<RecordingInfo>(
future: backend.db.getRecordingInfo(recording),
builder: (context, snapshot) {
if (snapshot.hasData) {
return PerformancesText(
performanceInfos: snapshot.data.performances,
);
} else {
return Text('...');
}
}
),
onTap: () async { onTap: () async {
final tracks = backend.ml.tracks[recording.id]; final tracks = backend.ml.tracks[recording.id];
tracks.sort( tracks.sort(

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart';
import '../editors/ensemble.dart';
import '../widgets/lists.dart';
/// A screen to select an ensemble.
///
/// If the user has selected one, it will be returned as an [Ensemble] object
/// using the navigator.
class EnsembleSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Select ensemble'),
),
body: EnsemblesList(
onSelected: (ensemble) {
Navigator.pop(context, ensemble);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final Ensemble ensemble = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EnsembleEditor(),
fullscreenDialog: true,
),
);
if (ensemble != null) {
Navigator.pop(context, ensemble);
}
},
),
);
}
}

View file

@ -35,7 +35,9 @@ class _InstrumentsSelectorState extends State<InstrumentsSelector> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.multiple ? 'Select instruments/roles' : 'Select instrument/role'), title: Text(widget.multiple
? 'Select instruments/roles'
: 'Select instrument/role'),
actions: widget.multiple actions: widget.multiple
? <Widget>[ ? <Widget>[
FlatButton( FlatButton(
@ -45,8 +47,8 @@ class _InstrumentsSelectorState extends State<InstrumentsSelector> {
] ]
: null, : null,
), ),
body: StreamBuilder( body: FutureBuilder<List<Instrument>>(
stream: backend.db.allInstruments().watch(), future: backend.client.getInstruments(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return ListView.builder( return ListView.builder(

View file

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/ensemble.dart';
import '../editors/person.dart';
import 'instruments.dart';
class PerformanceModel {
final Person person;
final Ensemble ensemble;
final Instrument role;
PerformanceModel({
this.person,
this.ensemble,
this.role,
});
}
class PerformerSelector extends StatefulWidget {
@override
_PerformerSelectorState createState() => _PerformerSelectorState();
}
class _Selection {
final bool isPerson;
final Person person;
final Ensemble ensemble;
_Selection.person(this.person)
: isPerson = true,
ensemble = null;
_Selection.ensemble(this.ensemble)
: isPerson = false,
person = null;
}
class _PerformerSelectorState extends State<PerformerSelector> {
Instrument role;
_Selection selection;
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Select performer'),
actions: <Widget>[
FlatButton(
child: Text('DONE'),
onPressed: () => Navigator.pop(
context,
PerformanceModel(
person: selection?.person,
ensemble: selection?.ensemble,
role: role,
),
),
),
],
),
body: Column(
children: <Widget>[
Material(
elevation: 2.0,
child: ListTile(
title: Text('Instrument/Role'),
subtitle:
Text(role != null ? role.name : 'Select instrument/role'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
role = null;
});
},
),
onTap: () async {
final Instrument newRole = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InstrumentsSelector(),
fullscreenDialog: true,
));
if (newRole != null) {
setState(() {
role = newRole;
});
}
},
),
),
Expanded(
child: ListView(
children: <Widget>[
StreamBuilder<List<Person>>(
stream: backend.db.allPersons().watch(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.isNotEmpty) {
return ExpansionTile(
initiallyExpanded: true,
title: Text('Persons'),
children: snapshot.data
.map((person) => RadioListTile<Person>(
title: Text(
'${person.lastName}, ${person.firstName}'),
value: person,
groupValue: selection?.person,
onChanged: (person) {
setState(() {
selection = _Selection.person(person);
});
},
))
.toList(),
);
} else {
return Container();
}
},
),
StreamBuilder<List<Ensemble>>(
stream: backend.db.allEnsembles().watch(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.isNotEmpty) {
return ExpansionTile(
initiallyExpanded: true,
title: Text('Ensembles'),
children: snapshot.data
.map((ensemble) => RadioListTile<Ensemble>(
title: Text(ensemble.name),
value: ensemble,
groupValue: selection?.ensemble,
onChanged: (ensemble) {
setState(() {
selection = _Selection.ensemble(ensemble);
});
},
))
.toList(),
);
} else {
return Container();
}
},
),
],
),
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
showModalBottomSheet(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const Icon(Icons.add),
title: Text('Add person'),
onTap: () async {
final Person person = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PersonEditor(),
fullscreenDialog: true,
));
if (person != null) {
setState(() {
selection = _Selection.person(person);
});
}
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.add),
title: Text('Add ensemble'),
onTap: () async {
final Ensemble ensemble = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EnsembleEditor(),
fullscreenDialog: true,
));
if (ensemble != null) {
setState(() {
selection = _Selection.ensemble(ensemble);
});
}
Navigator.pop(context);
},
),
],
),
);
},
),
);
}
}

View file

@ -1,47 +1,35 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/person.dart'; import '../editors/person.dart';
import '../widgets/lists.dart';
/// A screen to select a person.
///
/// If the user has selected a person, it will be returned as a [Person] object
/// using the navigator.
class PersonsSelector extends StatelessWidget { class PersonsSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backend = Backend.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Select person'), title: Text('Select person'),
), ),
body: StreamBuilder( body: PersonsList(
stream: backend.db.allPersons().watch(), onSelected: (person) {
builder: (context, snapshot) { Navigator.pop(context, person);
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final person = snapshot.data[index];
return ListTile(
title: Text('${person.lastName}, ${person.firstName}'),
onTap: () => Navigator.pop(context, person),
);
},
);
} else {
return Container();
}
}, },
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), child: const Icon(Icons.add),
onPressed: () async { onPressed: () async {
final Person person = await Navigator.push( final Person person = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PersonEditor(), builder: (context) => PersonEditor(),
fullscreenDialog: true, fullscreenDialog: true,
)); ),
);
if (person != null) { if (person != null) {
Navigator.pop(context, person); Navigator.pop(context, person);

View file

@ -1,211 +1,90 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/recording.dart'; import '../editors/recording.dart';
import '../widgets/texts.dart'; import '../widgets/lists.dart';
import '../widgets/works_by_composer.dart';
class PersonList extends StatelessWidget { class RecordingSelectorResult {
final void Function(int personId) onSelect; final WorkInfo workInfo;
final RecordingInfo recordingInfo;
PersonList({ RecordingSelectorResult({
this.onSelect, this.workInfo,
this.recordingInfo,
}); });
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return Column(
children: <Widget>[
Material(
elevation: 2.0,
child: ListTile(
title: Text('Composers'),
),
),
Expanded(
child: StreamBuilder<List<Person>>(
stream: backend.db.allPersons().watch(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final person = snapshot.data[index];
return ListTile(
title: Text('${person.lastName}, ${person.firstName}'),
onTap: () => onSelect(person.id),
);
},
);
} else {
return Container();
}
},
),
),
],
);
}
} }
class WorkList extends StatelessWidget { /// A screen to select a recording.
final int composerId; ///
final void Function(int workId) onSelect; /// If the user has selected a recording, a [RecordingSelectorResult] containing
/// the selected recording and the recorded work will be returned using the
/// navigator.
class RecordingSelector extends StatefulWidget {
@override
_RecordingSelectorState createState() => _RecordingSelectorState();
}
WorkList({ class _RecordingSelectorState extends State<RecordingSelector> {
this.composerId, Person person;
this.onSelect, WorkInfo workInfo;
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( Widget body;
children: <Widget>[
Material( if (person == null) {
elevation: 2.0, body = PersonsList(
child: ListTile( onSelected: (newPerson) {
leading: IconButton( setState(() {
icon: const Icon(Icons.arrow_back), person = newPerson;
onPressed: () => Navigator.pop(context), });
},
);
} else if (workInfo == null) {
body = WorksList(
personId: person.id,
onSelected: (newWorkInfo) {
setState(() {
workInfo = newWorkInfo;
});
},
);
} else {
body = RecordingsList(
workId: workInfo.work.id,
onSelected: (recordingInfo) {
Navigator.pop(
context,
RecordingSelectorResult(
workInfo: workInfo,
recordingInfo: recordingInfo,
), ),
title: PersonText(composerId), );
), },
), );
Expanded(
child: WorksByComposer(
personId: composerId,
onTap: (selectedWork) => onSelect(selectedWork.id),
),
),
],
);
}
}
class RecordingList extends StatelessWidget {
final int workId;
final void Function(Recording recording) onSelect;
RecordingList({
this.workId,
this.onSelect,
});
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return Column(
children: <Widget>[
Material(
elevation: 2.0,
child: ListTile(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
title: WorkText(workId),
subtitle: ComposersText(workId),
),
),
Expanded(
child: StreamBuilder<List<Recording>>(
stream: backend.db.recordingsByWork(workId).watch(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final recording = snapshot.data[index];
return ListTile(
title: PerformancesText(recording.id),
onTap: () => onSelect(recording),
);
},
);
} else {
return Container();
}
},
),
),
],
);
}
}
class RecordingsSelector extends StatefulWidget {
@override
_RecordingsSelectorState createState() => _RecordingsSelectorState();
}
class _RecordingsSelectorState extends State<RecordingsSelector> {
final nestedNavigator = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
// This exists to circumvent the nested navigator when selecting a
// recording.
void popUpperNavigator(Recording recording) {
Navigator.pop(context, recording);
} }
return WillPopScope( return Scaffold(
child: Scaffold( appBar: AppBar(
appBar: AppBar( title: Text('Select recording'),
leading: IconButton( ),
icon: const Icon(Icons.close), body: body,
onPressed: () => Navigator.pop(context), floatingActionButton: FloatingActionButton(
), child: const Icon(Icons.add),
title: Text('Select recording'), onPressed: () async {
), final RecordingSelectorResult result = await Navigator.push(
body: Navigator( context,
key: nestedNavigator, MaterialPageRoute(
onGenerateRoute: (settings) => settings.name == '/' builder: (context) => RecordingEditor(),
? MaterialPageRoute( fullscreenDialog: true,
builder: (context) => PersonList( ),
onSelect: (personId) => nestedNavigator.currentState.push( );
MaterialPageRoute(
builder: (context) => WorkList( if (result != null) {
composerId: personId, Navigator.pop(context, result);
onSelect: (workId) => }
nestedNavigator.currentState.push( },
MaterialPageRoute(
builder: (context) => RecordingList(
workId: workId,
onSelect: (recording) =>
popUpperNavigator(recording),
),
),
),
),
),
),
),
)
: null,
initialRoute: '/',
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final recording = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RecordingEditor(),
fullscreenDialog: true,
),
);
if (recording != null) {
Navigator.pop(context, recording);
}
},
),
), ),
onWillPop: () async => !(await nestedNavigator.currentState.maybePop()),
); );
} }
} }

View file

@ -1,68 +1,62 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/work.dart'; import '../editors/work.dart';
import '../widgets/lists.dart';
/// A screen to select a work.
///
/// If the user has selected a work, a [WorkInfo] will be returned
/// using the navigator.
class WorkSelector extends StatefulWidget {
@override
_WorkSelectorState createState() => _WorkSelectorState();
}
class _WorkSelectorState extends State<WorkSelector> {
Person person;
// TODO: Lazy load works and/or optimize queries.
class WorkSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backend = Backend.of(context); Widget body;
if (person == null) {
body = PersonsList(
onSelected: (newPerson) {
setState(() {
person = newPerson;
});
},
);
} else {
body = WorksList(
personId: person.id,
onSelected: (workInfo) {
setState(() {
Navigator.pop(context, workInfo);
});
},
);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Select work'), title: Text('Select work'),
), ),
body: StreamBuilder<List<Person>>( body: body,
stream: backend.db.allPersons().watch(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final person = snapshot.data[index];
final title = Text('${person.lastName}, ${person.firstName}');
return StreamBuilder<List<Work>>(
stream: backend.db.worksByComposer(person.id).watch(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.isNotEmpty) {
return ExpansionTile(
title: title,
children: <Widget>[
for (final work in snapshot.data)
ListTile(
title: Text(work.title),
onTap: () => Navigator.pop(context, work),
),
],
);
} else {
return ListTile(
title: title,
);
}
},
);
},
);
} else {
return Container();
}
},
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), child: const Icon(Icons.add),
onPressed: () async { onPressed: () async {
final Work work = await Navigator.push( final WorkInfo workInfo = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => WorkEditor(), builder: (context) => WorkEditor(),
fullscreenDialog: true, fullscreenDialog: true,
)); ),
);
if (work != null) { if (workInfo != null) {
Navigator.pop(context, work); Navigator.pop(context, workInfo);
} }
}, },
), ),

View file

@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../widgets/texts.dart';
/// A list of persons.
class PersonsList extends StatelessWidget {
/// Called, when the user has selected a person.
final void Function(Person person) onSelected;
PersonsList({
this.onSelected,
});
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return FutureBuilder<List<Person>>(
future: backend.client.getPersons(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final person = snapshot.data[index];
return ListTile(
title: Text('${person.lastName}, ${person.firstName}'),
onTap: () {
if (onSelected != null) {
onSelected(person);
}
},
);
},
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}
/// A list of ensembles.
class EnsemblesList extends StatelessWidget {
/// Called, when the user has selected an ensemble.
final void Function(Ensemble ensemble) onSelected;
EnsemblesList({
this.onSelected,
});
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return FutureBuilder<List<Ensemble>>(
future: backend.client.getEnsembles(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final ensemble = snapshot.data[index];
return ListTile(
title: Text(ensemble.name),
onTap: () {
if (onSelected != null) {
onSelected(ensemble);
}
},
);
},
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}
/// A list of works by one composer.
class WorksList extends StatelessWidget {
/// The ID of the composer.
final int personId;
/// Called, when the user has selected a work.
final void Function(WorkInfo workInfo) onSelected;
WorksList({
this.personId,
this.onSelected,
});
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return FutureBuilder<List<WorkInfo>>(
future: backend.client.getWorks(personId),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final workInfo = snapshot.data[index];
return ListTile(
title: Text(workInfo.work.title),
onTap: () {
if (onSelected != null) {
onSelected(workInfo);
}
},
);
},
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}
/// A list of recordings of a work.
class RecordingsList extends StatelessWidget {
/// The ID of the work.
final int workId;
/// Called, when the user has selected a recording.
final void Function(RecordingInfo recordingInfo) onSelected;
RecordingsList({
this.workId,
this.onSelected,
});
@override
Widget build(BuildContext context) {
final backend = Backend.of(context);
return FutureBuilder<List<RecordingInfo>>(
future: backend.client.getRecordings(workId),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final recordingInfo = snapshot.data[index];
return ListTile(
title: PerformancesText(
performanceInfos: recordingInfo.performances,
),
onTap: () {
if (onSelected != null) {
onSelected(recordingInfo);
}
},
);
},
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}

View file

@ -1,50 +1,48 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import 'texts.dart'; import 'texts.dart';
class RecordingTile extends StatelessWidget { class RecordingTile extends StatelessWidget {
final int recordingId; final WorkInfo workInfo;
final RecordingInfo recordingInfo;
RecordingTile({ RecordingTile({
this.recordingId, this.workInfo,
this.recordingInfo,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backend = Backend.of(context);
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return StreamBuilder<Recording>( return Padding(
stream: backend.db.recordingById(recordingId).watchSingle(), padding: const EdgeInsets.symmetric(
builder: (context, snapshot) => Padding( vertical: 8.0,
padding: const EdgeInsets.symmetric( ),
vertical: 8.0, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: <Widget>[
crossAxisAlignment: CrossAxisAlignment.start, DefaultTextStyle(
children: <Widget>[ style: textTheme.subtitle1,
if (snapshot.hasData) ...[ child: Text(workInfo.composers
DefaultTextStyle( .map((p) => '${p.firstName} ${p.lastName}')
style: textTheme.subtitle1, .join(', ')),
child: ComposersText(snapshot.data.work), ),
), DefaultTextStyle(
DefaultTextStyle( style: textTheme.headline6,
style: textTheme.headline6, child: Text(workInfo.work.title),
child: WorkText(snapshot.data.work), ),
), const SizedBox(
], height: 4.0,
const SizedBox( ),
height: 4.0, DefaultTextStyle(
style: textTheme.bodyText1,
child: PerformancesText(
performanceInfos: recordingInfo.performances,
), ),
DefaultTextStyle( ),
style: textTheme.bodyText1, ],
child: PerformancesText(recordingId),
),
],
),
), ),
); );
} }

View file

@ -49,72 +49,38 @@ class PersonText extends StatelessWidget {
} }
} }
class PerformancesText extends StatefulWidget { /// A widget showing information on a list of performances.
final int recordingId; class PerformancesText extends StatelessWidget {
/// The information to show.
final List<PerformanceInfo> performanceInfos;
PerformancesText(this.recordingId); PerformancesText({
this.performanceInfos,
@override });
_PerformancesTextState createState() => _PerformancesTextState();
}
class _PerformancesTextState extends State<PerformancesText> {
BackendState backend;
StreamSubscription<List<Performance>> performancesSubscription;
String text = '...';
@override
void didChangeDependencies() {
super.didChangeDependencies();
performancesSubscription?.cancel();
backend = Backend.of(context);
performancesSubscription = backend.db
.performancesByRecording(widget.recordingId)
.watch()
.listen((performances) async {
final List<String> texts = [];
for (final performance in performances) {
final buffer = StringBuffer();
if (performance.person != null) {
final person =
await backend.db.personById(performance.person).getSingle();
buffer.write('${person.firstName} ${person.lastName}');
} else if (performance.ensemble != null) {
final ensemble =
await backend.db.ensembleById(performance.ensemble).getSingle();
buffer.write(ensemble.name);
} else {
buffer.write('Unknown');
}
if (performance.role != null) {
final role =
await backend.db.instrumentById(performance.role).getSingle();
buffer.write(' (${role.name})');
}
texts.add(buffer.toString());
}
setState(() {
text = texts.join(', ');
});
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Text(text); final List<String> performanceTexts = [];
}
@override for (final p in performanceInfos) {
void dispose() { final buffer = StringBuffer();
super.dispose();
performancesSubscription?.cancel(); if (p.person != null) {
buffer.write('${p.person.firstName} ${p.person.lastName}');
} else if (p.ensemble != null) {
buffer.write(p.ensemble.name);
} else {
buffer.write('Unknown');
}
if (p.role != null) {
buffer.write(' (${p.role.name})');
}
performanceTexts.add(buffer.toString());
}
return Text(performanceTexts.join(', '));
} }
} }