2019-12-11 13:07:46 +01:00
|
|
|
import 'package:flutter/material.dart';
|
2020-04-24 22:41:52 +02:00
|
|
|
import 'package:musicus_database/musicus_database.dart';
|
2019-12-11 13:07:46 +01:00
|
|
|
|
|
|
|
|
import '../backend.dart';
|
2019-12-11 13:17:23 +01:00
|
|
|
import '../selectors/instruments.dart';
|
2019-12-11 13:07:46 +01:00
|
|
|
import '../selectors/person.dart';
|
|
|
|
|
|
2019-12-13 22:51:55 +01:00
|
|
|
class PartData {
|
|
|
|
|
final titleController = TextEditingController();
|
|
|
|
|
|
2019-12-12 14:57:51 +01:00
|
|
|
Person composer;
|
2020-03-21 15:42:45 +01:00
|
|
|
List<Instrument> instruments;
|
|
|
|
|
|
|
|
|
|
PartData({
|
|
|
|
|
String title,
|
|
|
|
|
this.composer,
|
|
|
|
|
this.instruments = const [],
|
|
|
|
|
}) {
|
|
|
|
|
titleController.text = title ?? '';
|
|
|
|
|
}
|
2019-12-12 14:57:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class WorkProperties extends StatelessWidget {
|
|
|
|
|
final TextEditingController titleController;
|
|
|
|
|
final Person composer;
|
|
|
|
|
final List<Instrument> instruments;
|
|
|
|
|
final void Function(Person) onComposerChanged;
|
|
|
|
|
final void Function(List<Instrument>) onInstrumentsChanged;
|
|
|
|
|
|
|
|
|
|
WorkProperties({
|
|
|
|
|
@required this.titleController,
|
|
|
|
|
@required this.composer,
|
|
|
|
|
@required this.instruments,
|
|
|
|
|
@required this.onComposerChanged,
|
|
|
|
|
@required this.onInstrumentsChanged,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Column(
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: titleController,
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
labelText: 'Title',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Composer'),
|
|
|
|
|
subtitle: Text(composer != null
|
|
|
|
|
? '${composer.firstName} ${composer.lastName}'
|
|
|
|
|
: 'Select composer'),
|
2020-03-22 14:13:42 +01:00
|
|
|
trailing: IconButton(
|
|
|
|
|
icon: const Icon(Icons.delete),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
onComposerChanged(null);
|
|
|
|
|
},
|
|
|
|
|
),
|
2019-12-12 14:57:51 +01:00
|
|
|
onTap: () async {
|
|
|
|
|
final Person person = await Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
builder: (context) => PersonsSelector(),
|
|
|
|
|
fullscreenDialog: true,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
if (person != null) {
|
|
|
|
|
onComposerChanged(person);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Instruments'),
|
|
|
|
|
subtitle: Text(instruments.isNotEmpty
|
|
|
|
|
? instruments.map((i) => i.name).join(', ')
|
|
|
|
|
: 'Select instruments'),
|
2020-03-22 14:13:42 +01:00
|
|
|
trailing: IconButton(
|
|
|
|
|
icon: const Icon(Icons.delete),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
onInstrumentsChanged([]);
|
|
|
|
|
}),
|
2019-12-12 14:57:51 +01:00
|
|
|
onTap: () async {
|
|
|
|
|
final List<Instrument> selection = await Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
builder: (context) => InstrumentsSelector(
|
2020-03-31 15:49:15 +02:00
|
|
|
multiple: true,
|
2019-12-12 14:57:51 +01:00
|
|
|
selection: instruments,
|
|
|
|
|
),
|
|
|
|
|
fullscreenDialog: true,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
if (selection != null) {
|
|
|
|
|
onInstrumentsChanged(selection);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-13 22:51:55 +01:00
|
|
|
class PartTile extends StatefulWidget {
|
|
|
|
|
final PartData part;
|
|
|
|
|
final void Function() onMore;
|
|
|
|
|
final void Function() onDelete;
|
|
|
|
|
|
|
|
|
|
PartTile({
|
|
|
|
|
Key key,
|
|
|
|
|
@required this.part,
|
|
|
|
|
@required this.onMore,
|
|
|
|
|
@required this.onDelete,
|
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
_PartTileState createState() => _PartTileState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _PartTileState extends State<PartTile> {
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2020-04-24 21:18:24 +02:00
|
|
|
return Row(
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.only(left: 16.0, right: 8.0),
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.drag_handle,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: widget.part.titleController,
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
border: InputBorder.none,
|
|
|
|
|
hintText: 'Part title',
|
2019-12-13 22:51:55 +01:00
|
|
|
),
|
2020-04-24 21:18:24 +02:00
|
|
|
),
|
2019-12-13 22:51:55 +01:00
|
|
|
),
|
2020-04-24 21:18:24 +02:00
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.more_horiz),
|
|
|
|
|
onPressed: widget.onMore,
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.delete),
|
|
|
|
|
onPressed: widget.onDelete,
|
|
|
|
|
),
|
|
|
|
|
],
|
2019-12-13 22:51:55 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-26 15:35:45 +02:00
|
|
|
/// Screen for editing a work.
|
2020-04-26 16:48:05 +02:00
|
|
|
///
|
2020-04-26 15:35:45 +02:00
|
|
|
/// If the user is finished editing, the result will be returned as a [WorkInfo]
|
|
|
|
|
/// object.
|
2019-12-11 13:07:46 +01:00
|
|
|
class WorkEditor extends StatefulWidget {
|
2020-04-26 17:26:19 +02:00
|
|
|
/// The work to edit.
|
|
|
|
|
///
|
|
|
|
|
/// If this is null, a new work will be created.
|
|
|
|
|
final WorkInfo workInfo;
|
2019-12-11 13:07:46 +01:00
|
|
|
|
|
|
|
|
WorkEditor({
|
2020-04-26 17:26:19 +02:00
|
|
|
this.workInfo,
|
2019-12-11 13:07:46 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
_WorkEditorState createState() => _WorkEditorState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _WorkEditorState extends State<WorkEditor> {
|
|
|
|
|
final titleController = TextEditingController();
|
|
|
|
|
|
2020-04-26 18:29:21 +02:00
|
|
|
bool uploading = false;
|
2019-12-13 22:51:55 +01:00
|
|
|
Person composer;
|
|
|
|
|
List<Instrument> instruments = [];
|
|
|
|
|
List<PartData> parts = [];
|
2019-12-11 13:07:46 +01:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
|
2020-04-26 17:26:19 +02:00
|
|
|
if (widget.workInfo != null) {
|
|
|
|
|
titleController.text = widget.workInfo.work.title;
|
|
|
|
|
// TODO: Theoretically this includes the composers of all parts.
|
|
|
|
|
composer = widget.workInfo.composers.first;
|
|
|
|
|
instruments = List.from(widget.workInfo.instruments);
|
|
|
|
|
|
|
|
|
|
for (final partInfo in widget.workInfo.parts) {
|
|
|
|
|
parts.add(PartData(
|
|
|
|
|
title: partInfo.work.title,
|
|
|
|
|
composer: partInfo.composer,
|
|
|
|
|
instruments: List.from(partInfo.instruments),
|
|
|
|
|
));
|
2019-12-11 13:17:23 +01:00
|
|
|
}
|
2019-12-11 13:07:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2020-04-26 17:26:19 +02:00
|
|
|
final backend = Backend.of(context);
|
|
|
|
|
|
2019-12-13 22:51:55 +01:00
|
|
|
final List<Widget> partTiles = [];
|
|
|
|
|
for (var i = 0; i < parts.length; i++) {
|
|
|
|
|
final part = parts[i];
|
|
|
|
|
|
|
|
|
|
partTiles.add(PartTile(
|
|
|
|
|
key: Key(part.hashCode.toString()),
|
|
|
|
|
part: part,
|
2020-03-22 13:56:46 +01:00
|
|
|
onMore: () {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) => StatefulBuilder(
|
|
|
|
|
builder: (context, setState) => Dialog(
|
|
|
|
|
child: ListView(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
WorkProperties(
|
|
|
|
|
titleController: part.titleController,
|
|
|
|
|
composer: part.composer,
|
|
|
|
|
instruments: part.instruments,
|
|
|
|
|
onComposerChanged: (composer) {
|
|
|
|
|
setState(() {
|
|
|
|
|
part.composer = composer;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onInstrumentsChanged: (instruments) {
|
|
|
|
|
setState(() {
|
|
|
|
|
part.instruments = instruments;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
2019-12-13 22:51:55 +01:00
|
|
|
onDelete: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
parts.removeAt(i);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-11 13:07:46 +01:00
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
title: Text('Work'),
|
|
|
|
|
actions: <Widget>[
|
2020-04-26 18:29:21 +02:00
|
|
|
uploading
|
|
|
|
|
? Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: Center(
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: 24.0,
|
|
|
|
|
height: 24.0,
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
strokeWidth: 2.0,
|
|
|
|
|
),
|
|
|
|
|
),
|
2020-03-21 14:56:59 +01:00
|
|
|
),
|
2020-04-26 18:29:21 +02:00
|
|
|
)
|
|
|
|
|
: FlatButton(
|
|
|
|
|
child: Text('DONE'),
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
setState(() {
|
|
|
|
|
uploading = true;
|
|
|
|
|
});
|
2020-03-21 14:56:59 +01:00
|
|
|
|
2020-04-26 18:29:21 +02:00
|
|
|
final workId = widget?.workInfo?.work?.id ?? generateId();
|
2020-04-26 16:48:05 +02:00
|
|
|
|
2020-04-26 18:29:21 +02:00
|
|
|
List<PartInfo> partInfos = [];
|
|
|
|
|
for (var i = 0; i < parts.length; i++) {
|
|
|
|
|
final part = parts[i];
|
|
|
|
|
partInfos.add(PartInfo(
|
|
|
|
|
work: Work(
|
|
|
|
|
id: generateId(),
|
|
|
|
|
title: part.titleController.text,
|
|
|
|
|
composer: part.composer?.id,
|
|
|
|
|
partOf: workId,
|
|
|
|
|
partIndex: i,
|
|
|
|
|
),
|
|
|
|
|
instruments: part.instruments,
|
|
|
|
|
composer: part.composer,
|
|
|
|
|
));
|
|
|
|
|
}
|
2020-04-25 10:36:19 +02:00
|
|
|
|
2020-04-26 18:29:21 +02:00
|
|
|
final workInfo = WorkInfo(
|
|
|
|
|
work: Work(
|
|
|
|
|
id: workId,
|
|
|
|
|
title: titleController.text,
|
|
|
|
|
composer: composer?.id,
|
|
|
|
|
),
|
|
|
|
|
instruments: instruments,
|
|
|
|
|
// TODO: Theoretically, this should include all composers
|
|
|
|
|
// from the parts.
|
|
|
|
|
composers: [composer],
|
|
|
|
|
parts: partInfos,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final success = await backend.client.putWork(workInfo);
|
|
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
uploading = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
Navigator.pop(context, workInfo);
|
|
|
|
|
} else {
|
|
|
|
|
Scaffold.of(context).showSnackBar(SnackBar(
|
|
|
|
|
content: Text('Failed to upload'),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
2019-12-11 13:07:46 +01:00
|
|
|
],
|
|
|
|
|
),
|
2019-12-13 22:51:55 +01:00
|
|
|
body: ReorderableListView(
|
|
|
|
|
header: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
WorkProperties(
|
|
|
|
|
titleController: titleController,
|
|
|
|
|
composer: composer,
|
|
|
|
|
instruments: instruments,
|
|
|
|
|
onComposerChanged: (newComposer) {
|
|
|
|
|
setState(() {
|
|
|
|
|
composer = newComposer;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onInstrumentsChanged: (newInstruments) {
|
|
|
|
|
setState(() {
|
|
|
|
|
instruments = newInstruments;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
if (parts.length > 0)
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
|
|
|
|
child: Text(
|
|
|
|
|
'Parts',
|
2020-04-24 21:18:24 +02:00
|
|
|
style: Theme.of(context).textTheme.subtitle1,
|
2019-12-13 22:51:55 +01:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
children: partTiles,
|
|
|
|
|
onReorder: (i1, i2) {
|
|
|
|
|
setState(() {
|
|
|
|
|
final part = parts.removeAt(i1);
|
|
|
|
|
final newIndex = i2 > i1 ? i2 - 1 : i2;
|
|
|
|
|
|
|
|
|
|
parts.insert(newIndex, part);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
|
|
|
icon: const Icon(Icons.add),
|
|
|
|
|
label: Text('Add part'),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
2020-04-24 21:18:24 +02:00
|
|
|
parts.add(PartData());
|
2019-12-13 22:51:55 +01:00
|
|
|
});
|
|
|
|
|
},
|
2019-12-11 13:07:46 +01:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|