mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-27 03:07:26 +01:00
Move reusable code from mobile to common
This will be useful for a future desktop application.
This commit is contained in:
parent
6e1255f26e
commit
711b19c998
40 changed files with 813 additions and 581 deletions
|
|
@ -1,371 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../selectors/instruments.dart';
|
||||
import '../selectors/person.dart';
|
||||
|
||||
class PartData {
|
||||
final titleController = TextEditingController();
|
||||
|
||||
Person composer;
|
||||
List<Instrument> instruments;
|
||||
|
||||
PartData({
|
||||
String title,
|
||||
this.composer,
|
||||
this.instruments = const [],
|
||||
}) {
|
||||
titleController.text = title ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
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'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
onComposerChanged(null);
|
||||
},
|
||||
),
|
||||
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'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
onInstrumentsChanged([]);
|
||||
}),
|
||||
onTap: () async {
|
||||
final List<Instrument> selection = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => InstrumentsSelector(
|
||||
multiple: true,
|
||||
selection: instruments,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
|
||||
if (selection != null) {
|
||||
onInstrumentsChanged(selection);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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',
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
onPressed: widget.onMore,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: widget.onDelete,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen for editing a work.
|
||||
///
|
||||
/// If the user is finished editing, the result will be returned as a [WorkInfo]
|
||||
/// object.
|
||||
class WorkEditor extends StatefulWidget {
|
||||
/// The work to edit.
|
||||
///
|
||||
/// If this is null, a new work will be created.
|
||||
final WorkInfo workInfo;
|
||||
|
||||
WorkEditor({
|
||||
this.workInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
_WorkEditorState createState() => _WorkEditorState();
|
||||
}
|
||||
|
||||
class _WorkEditorState extends State<WorkEditor> {
|
||||
final titleController = TextEditingController();
|
||||
|
||||
bool uploading = false;
|
||||
Person composer;
|
||||
List<Instrument> instruments = [];
|
||||
List<PartData> parts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
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,
|
||||
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;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onDelete: () {
|
||||
setState(() {
|
||||
parts.removeAt(i);
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Work'),
|
||||
actions: <Widget>[
|
||||
uploading
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: FlatButton(
|
||||
child: Text('DONE'),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
uploading = true;
|
||||
});
|
||||
|
||||
final workId = widget?.workInfo?.work?.id ?? generateId();
|
||||
|
||||
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,
|
||||
));
|
||||
}
|
||||
|
||||
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'),
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
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',
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(() {
|
||||
parts.add(PartData());
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue