mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 02:37:25 +01:00
mobile: Integrate with server
This commit is contained in:
parent
60a474ea56
commit
c93ebf17a0
20 changed files with 751 additions and 740 deletions
|
|
@ -42,7 +42,7 @@ class _EnsembleEditorState extends State<EnsembleEditor> {
|
|||
name: nameController.text,
|
||||
);
|
||||
|
||||
await backend.db.updateEnsemble(ensemble);
|
||||
await backend.client.putEnsemble(ensemble);
|
||||
Navigator.pop(context, ensemble);
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class _InstrumentEditorState extends State<InstrumentEditor> {
|
|||
name: nameController.text,
|
||||
);
|
||||
|
||||
await backend.db.updateInstrument(instrument);
|
||||
await backend.client.putInstrument(instrument);
|
||||
Navigator.pop(context, instrument);
|
||||
},
|
||||
)
|
||||
|
|
|
|||
119
mobile/lib/editors/performance.dart
Normal file
119
mobile/lib/editors/performance.dart
Normal 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;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ class _PersonEditorState extends State<PersonEditor> {
|
|||
lastName: lastNameController.text,
|
||||
);
|
||||
|
||||
await backend.db.updatePerson(person);
|
||||
await backend.client.putPerson(person);
|
||||
Navigator.pop(context, person);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../selectors/performer.dart';
|
||||
import '../editors/performance.dart';
|
||||
import '../selectors/recording.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 {
|
||||
final Recording recording;
|
||||
|
||||
|
|
@ -20,8 +24,8 @@ class RecordingEditor extends StatefulWidget {
|
|||
class _RecordingEditorState extends State<RecordingEditor> {
|
||||
final commentController = TextEditingController();
|
||||
|
||||
Work work;
|
||||
List<PerformanceModel> performanceModels = [];
|
||||
WorkInfo workInfo;
|
||||
List<PerformanceInfo> performanceInfos = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -37,20 +41,56 @@ class _RecordingEditorState extends State<RecordingEditor> {
|
|||
final backend = Backend.of(context);
|
||||
|
||||
Future<void> selectWork() async {
|
||||
final Work newWork = await Navigator.push(
|
||||
final WorkInfo newWorkInfo = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkSelector(),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
|
||||
if (newWork != null) {
|
||||
if (newWorkInfo != null) {
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title: Text('Recording'),
|
||||
|
|
@ -60,11 +100,11 @@ class _RecordingEditorState extends State<RecordingEditor> {
|
|||
onPressed: () async {
|
||||
final recording = Recording(
|
||||
id: widget.recording?.id ?? generateId(),
|
||||
work: work.id,
|
||||
work: workInfo.work.id,
|
||||
comment: commentController.text,
|
||||
);
|
||||
|
||||
final performances = performanceModels
|
||||
final performances = performanceInfos
|
||||
.map((m) => Performance(
|
||||
recording: recording.id,
|
||||
person: m.person?.id,
|
||||
|
|
@ -73,22 +113,28 @@ class _RecordingEditorState extends State<RecordingEditor> {
|
|||
))
|
||||
.toList();
|
||||
|
||||
await backend.db.updateRecording(RecordingData(
|
||||
final recordingInfo =
|
||||
await backend.client.putRecording(RecordingData(
|
||||
recording: recording,
|
||||
performances: performances,
|
||||
));
|
||||
|
||||
Navigator.pop(context, recording);
|
||||
Navigator.pop(context, RecordingSelectorResult(
|
||||
workInfo: workInfo,
|
||||
recordingInfo: recordingInfo,
|
||||
));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
work != null
|
||||
workInfo != null
|
||||
? ListTile(
|
||||
title: WorkText(work.id),
|
||||
subtitle: ComposersText(work.id),
|
||||
title: Text(workInfo.work.title),
|
||||
subtitle: Text(workInfo.composers
|
||||
.map((p) => '${p.firstName} ${p.lastName}')
|
||||
.join(', ')),
|
||||
onTap: selectWork,
|
||||
)
|
||||
: ListTile(
|
||||
|
|
@ -115,37 +161,22 @@ class _RecordingEditorState extends State<RecordingEditor> {
|
|||
trailing: IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final PerformanceModel model = await Navigator.push(
|
||||
final PerformanceInfo model = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PerformerSelector(),
|
||||
builder: (context) => PerformanceEditor(),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
|
||||
if (model != null) {
|
||||
setState(() {
|
||||
performanceModels.add(model);
|
||||
performanceInfos.add(model);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
for (final performance in performanceModels)
|
||||
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);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
...performanceTiles,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class TracksEditor extends StatefulWidget {
|
|||
|
||||
class _TracksEditorState extends State<TracksEditor> {
|
||||
BackendState backend;
|
||||
int recordingId;
|
||||
WorkInfo workInfo;
|
||||
RecordingInfo recordingInfo;
|
||||
String parentId;
|
||||
List<TrackModel> trackModels = [];
|
||||
|
||||
|
|
@ -44,12 +45,72 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
|
||||
tracks.add(Track(
|
||||
fileName: trackModel.fileName,
|
||||
recordingId: recordingId,
|
||||
recordingId: recordingInfo.recording.id,
|
||||
index: i,
|
||||
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);
|
||||
|
||||
Navigator.pop(context);
|
||||
|
|
@ -61,9 +122,10 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
header: Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: recordingId != null
|
||||
title: recordingInfo != null
|
||||
? RecordingTile(
|
||||
recordingId: recordingId,
|
||||
workInfo: workInfo,
|
||||
recordingInfo: recordingInfo,
|
||||
)
|
||||
: Text('Select recording'),
|
||||
onTap: selectRecording,
|
||||
|
|
@ -92,7 +154,7 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
trackModels = newTrackModels;
|
||||
});
|
||||
|
||||
if (recordingId != null) {
|
||||
if (recordingInfo != null) {
|
||||
updateAutoParts();
|
||||
}
|
||||
}
|
||||
|
|
@ -121,16 +183,17 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
}
|
||||
|
||||
Future<void> selectRecording() async {
|
||||
final Recording recording = await Navigator.push(
|
||||
final RecordingSelectorResult result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RecordingsSelector(),
|
||||
builder: (context) => RecordingSelector(),
|
||||
),
|
||||
);
|
||||
|
||||
if (recording != null) {
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
recordingId = recording.id;
|
||||
workInfo = result.workInfo;
|
||||
recordingInfo = result.recordingInfo;
|
||||
});
|
||||
|
||||
updateAutoParts();
|
||||
|
|
@ -139,18 +202,14 @@ class _TracksEditorState extends State<TracksEditor> {
|
|||
|
||||
/// Automatically associate the tracks with work parts.
|
||||
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(() {
|
||||
for (var i = 0; i < trackModels.length; i++) {
|
||||
if (i >= workParts.length) {
|
||||
if (i >= workInfo.parts.length) {
|
||||
trackModels[i].workPartIndex = null;
|
||||
trackModels[i].workPartTitle = null;
|
||||
} else {
|
||||
trackModels[i].workPartIndex = workParts[i].partIndex;
|
||||
trackModels[i].workPartTitle = workParts[i].title;
|
||||
trackModels[i].workPartIndex = workInfo.parts[i].work.partIndex;
|
||||
trackModels[i].workPartTitle = workInfo.parts[i].work.title;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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,
|
||||
partData: partData,
|
||||
));
|
||||
|
||||
Navigator.pop(context, data.work);
|
||||
Navigator.pop(context, workInfo);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ class HomeScreen extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
// For debugging purposes
|
||||
body: StreamBuilder<List<Person>>(
|
||||
stream: backend.db.allPersons().watch(),
|
||||
builder: (context, snapshot) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class PersonScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${person.firstName} ${person.lastName}'),
|
||||
|
|
|
|||
|
|
@ -8,90 +8,6 @@ import '../music_library.dart';
|
|||
import '../widgets/play_pause_button.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 {
|
||||
@override
|
||||
_ProgramScreenState createState() => _ProgramScreenState();
|
||||
|
|
@ -103,7 +19,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
StreamSubscription<bool> playerActiveSubscription;
|
||||
|
||||
StreamSubscription<List<InternalTrack>> playlistSubscription;
|
||||
List<ProgramItem> items = [];
|
||||
List<Widget> widgets = [];
|
||||
|
||||
StreamSubscription<double> positionSubscription;
|
||||
double position = 0.0;
|
||||
|
|
@ -149,7 +65,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
|
||||
/// Go through the tracks of [playlist] and preprocess them for displaying.
|
||||
Future<void> updateProgram(List<InternalTrack> playlist) async {
|
||||
List<ProgramItem> newItems = [];
|
||||
List<Widget> newWidgets = [];
|
||||
|
||||
// The following variables exist to adapt the resulting ProgramItem to its
|
||||
// predecessor.
|
||||
|
|
@ -162,42 +78,59 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
// from the database again.
|
||||
int lastWorkId;
|
||||
|
||||
// This will always contain the parts of the current work.
|
||||
List<Work> workParts = [];
|
||||
// This will contain information on the last new work.
|
||||
WorkInfo workInfo;
|
||||
|
||||
for (var i = 0; i < playlist.length; i++) {
|
||||
// The data that will be stored in the resulting ProgramItem.
|
||||
int newRecordingId;
|
||||
List<Work> newWorkParts = [];
|
||||
// The widgets displayed for this track.
|
||||
List<Widget> children = [];
|
||||
|
||||
final track = playlist[i];
|
||||
final recordingId = track.track.recordingId;
|
||||
final partIds = track.track.partIds;
|
||||
|
||||
// newRecordingId will be null, if the recording ID is the same. This
|
||||
// also means, that the work is the same, so workParts doesn't have to
|
||||
// be updated either.
|
||||
// If the recording is the same, the work will also be the same, so
|
||||
// workInfo doesn't have to be updated either.
|
||||
if (recordingId != lastRecordingId) {
|
||||
lastRecordingId = recordingId;
|
||||
newRecordingId = recordingId;
|
||||
|
||||
final recording =
|
||||
await backend.db.recordingById(recordingId).getSingle();
|
||||
final recordingInfo = await backend.db.getRecording(recordingId);
|
||||
|
||||
if (recording.work != lastWorkId) {
|
||||
workParts = await backend.db.workParts(recording.work).get();
|
||||
if (recordingInfo.recording.work != lastWorkId) {
|
||||
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) {
|
||||
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(
|
||||
recordingId: newRecordingId,
|
||||
workParts: newWorkParts,
|
||||
newWidgets.add(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +138,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
// function might take some time.
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
items = newItems;
|
||||
widgets = newWidgets;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -225,12 +158,27 @@ class _ProgramScreenState extends State<ProgramScreen> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemCount: widgets.length,
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
child: ProgramTile(
|
||||
item: items[index],
|
||||
isPlaying: index == snapshot?.data,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
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: () {
|
||||
backend.player.skipTo(index);
|
||||
|
|
|
|||
|
|
@ -44,8 +44,20 @@ class WorkScreen extends StatelessWidget {
|
|||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final recording = snapshot.data[index];
|
||||
|
||||
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 {
|
||||
final tracks = backend.ml.tracks[recording.id];
|
||||
tracks.sort(
|
||||
|
|
|
|||
41
mobile/lib/selectors/ensemble.dart
Normal file
41
mobile/lib/selectors/ensemble.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,9 @@ class _InstrumentsSelectorState extends State<InstrumentsSelector> {
|
|||
|
||||
return Scaffold(
|
||||
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
|
||||
? <Widget>[
|
||||
FlatButton(
|
||||
|
|
@ -45,8 +47,8 @@ class _InstrumentsSelectorState extends State<InstrumentsSelector> {
|
|||
]
|
||||
: null,
|
||||
),
|
||||
body: StreamBuilder(
|
||||
stream: backend.db.allInstruments().watch(),
|
||||
body: FutureBuilder<List<Instrument>>(
|
||||
future: backend.client.getInstruments(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return ListView.builder(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.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 {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Select person'),
|
||||
),
|
||||
body: StreamBuilder(
|
||||
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: () => Navigator.pop(context, person),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
body: PersonsList(
|
||||
onSelected: (person) {
|
||||
Navigator.pop(context, person);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final Person person = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PersonEditor(),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PersonEditor(),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (person != null) {
|
||||
Navigator.pop(context, person);
|
||||
|
|
|
|||
|
|
@ -1,211 +1,90 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../editors/recording.dart';
|
||||
import '../widgets/texts.dart';
|
||||
import '../widgets/works_by_composer.dart';
|
||||
import '../widgets/lists.dart';
|
||||
|
||||
class PersonList extends StatelessWidget {
|
||||
final void Function(int personId) onSelect;
|
||||
class RecordingSelectorResult {
|
||||
final WorkInfo workInfo;
|
||||
final RecordingInfo recordingInfo;
|
||||
|
||||
PersonList({
|
||||
this.onSelect,
|
||||
RecordingSelectorResult({
|
||||
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 {
|
||||
final int composerId;
|
||||
final void Function(int workId) onSelect;
|
||||
/// A screen to select a recording.
|
||||
///
|
||||
/// 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({
|
||||
this.composerId,
|
||||
this.onSelect,
|
||||
});
|
||||
class _RecordingSelectorState extends State<RecordingSelector> {
|
||||
Person person;
|
||||
WorkInfo workInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Material(
|
||||
elevation: 2.0,
|
||||
child: ListTile(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
Widget body;
|
||||
|
||||
if (person == null) {
|
||||
body = PersonsList(
|
||||
onSelected: (newPerson) {
|
||||
setState(() {
|
||||
person = newPerson;
|
||||
});
|
||||
},
|
||||
);
|
||||
} 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(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text('Select recording'),
|
||||
),
|
||||
body: Navigator(
|
||||
key: nestedNavigator,
|
||||
onGenerateRoute: (settings) => settings.name == '/'
|
||||
? MaterialPageRoute(
|
||||
builder: (context) => PersonList(
|
||||
onSelect: (personId) => nestedNavigator.currentState.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkList(
|
||||
composerId: personId,
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Select recording'),
|
||||
),
|
||||
body: body,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final RecordingSelectorResult result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RecordingEditor(),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
Navigator.pop(context, result);
|
||||
}
|
||||
},
|
||||
),
|
||||
onWillPop: () async => !(await nestedNavigator.currentState.maybePop()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,62 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.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
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title: Text('Select work'),
|
||||
),
|
||||
body: 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];
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
body: body,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final Work work = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkEditor(),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
final WorkInfo workInfo = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkEditor(),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (work != null) {
|
||||
Navigator.pop(context, work);
|
||||
if (workInfo != null) {
|
||||
Navigator.pop(context, workInfo);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
183
mobile/lib/widgets/lists.dart
Normal file
183
mobile/lib/widgets/lists.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
|
||||
import 'texts.dart';
|
||||
|
||||
class RecordingTile extends StatelessWidget {
|
||||
final int recordingId;
|
||||
final WorkInfo workInfo;
|
||||
final RecordingInfo recordingInfo;
|
||||
|
||||
RecordingTile({
|
||||
this.recordingId,
|
||||
this.workInfo,
|
||||
this.recordingInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return StreamBuilder<Recording>(
|
||||
stream: backend.db.recordingById(recordingId).watchSingle(),
|
||||
builder: (context, snapshot) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (snapshot.hasData) ...[
|
||||
DefaultTextStyle(
|
||||
style: textTheme.subtitle1,
|
||||
child: ComposersText(snapshot.data.work),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.headline6,
|
||||
child: WorkText(snapshot.data.work),
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
DefaultTextStyle(
|
||||
style: textTheme.subtitle1,
|
||||
child: Text(workInfo.composers
|
||||
.map((p) => '${p.firstName} ${p.lastName}')
|
||||
.join(', ')),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.headline6,
|
||||
child: Text(workInfo.work.title),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.bodyText1,
|
||||
child: PerformancesText(
|
||||
performanceInfos: recordingInfo.performances,
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.bodyText1,
|
||||
child: PerformancesText(recordingId),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,72 +49,38 @@ class PersonText extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class PerformancesText extends StatefulWidget {
|
||||
final int recordingId;
|
||||
/// A widget showing information on a list of performances.
|
||||
class PerformancesText extends StatelessWidget {
|
||||
/// The information to show.
|
||||
final List<PerformanceInfo> performanceInfos;
|
||||
|
||||
PerformancesText(this.recordingId);
|
||||
|
||||
@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(', ');
|
||||
});
|
||||
});
|
||||
}
|
||||
PerformancesText({
|
||||
this.performanceInfos,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(text);
|
||||
}
|
||||
final List<String> performanceTexts = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
performancesSubscription?.cancel();
|
||||
for (final p in performanceInfos) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
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(', '));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue