Support work sections

This commit is contained in:
Elias Projahn 2020-05-13 20:52:25 +02:00
parent 813fa2e47a
commit 93a5a06b55
6 changed files with 134 additions and 51 deletions

View file

@ -6,12 +6,14 @@ import '../selectors/instruments.dart';
import '../selectors/person.dart'; import '../selectors/person.dart';
class PartData { class PartData {
final bool isSection;
final titleController = TextEditingController(); final titleController = TextEditingController();
Person composer; Person composer;
List<Instrument> instruments; List<Instrument> instruments;
PartData({ PartData({
this.isSection = false,
String title, String title,
this.composer, this.composer,
this.instruments = const [], this.instruments = const [],
@ -111,7 +113,7 @@ class PartTile extends StatefulWidget {
PartTile({ PartTile({
Key key, Key key,
@required this.part, @required this.part,
@required this.onMore, this.onMore,
@required this.onDelete, @required this.onDelete,
}) : super(key: key); }) : super(key: key);
@ -122,10 +124,12 @@ class PartTile extends StatefulWidget {
class _PartTileState extends State<PartTile> { class _PartTileState extends State<PartTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isSection = widget.part.isSection;
return Row( return Row(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.only(left: 16.0, right: 8.0), padding: EdgeInsets.only(left: isSection ? 8.0 : 24.0, right: 8.0),
child: Icon( child: Icon(
Icons.drag_handle, Icons.drag_handle,
), ),
@ -135,14 +139,15 @@ class _PartTileState extends State<PartTile> {
controller: widget.part.titleController, controller: widget.part.titleController,
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
hintText: 'Part title', hintText: isSection ? 'Section title' : 'Part title',
), ),
), ),
), ),
IconButton( if (!isSection)
icon: const Icon(Icons.more_horiz), IconButton(
onPressed: widget.onMore, icon: const Icon(Icons.more_horiz),
), onPressed: widget.onMore,
),
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: widget.onDelete, onPressed: widget.onDelete,
@ -273,19 +278,31 @@ class _WorkEditorState extends State<WorkEditor> {
final workId = widget?.workInfo?.work?.id ?? generateId(); final workId = widget?.workInfo?.work?.id ?? generateId();
List<PartInfo> partInfos = []; List<PartInfo> partInfos = [];
List<WorkSection> sections = [];
int sectionCount = 0;
for (var i = 0; i < parts.length; i++) { for (var i = 0; i < parts.length; i++) {
final part = parts[i]; final part = parts[i];
partInfos.add(PartInfo( if (part.isSection) {
part: WorkPart( sections.add(WorkSection(
id: generateId(), id: generateId(),
work: workId,
title: part.titleController.text, title: part.titleController.text,
composer: part.composer?.id, beforePartIndex: i - sectionCount,
partOf: workId, ));
partIndex: i, sectionCount++;
), } else {
instruments: part.instruments, partInfos.add(PartInfo(
composer: part.composer, part: WorkPart(
)); id: generateId(),
title: part.titleController.text,
composer: part.composer?.id,
partOf: workId,
partIndex: i - sectionCount,
),
instruments: part.instruments,
composer: part.composer,
));
}
} }
final workInfo = WorkInfo( final workInfo = WorkInfo(
@ -299,6 +316,7 @@ class _WorkEditorState extends State<WorkEditor> {
// from the parts. // from the parts.
composers: [composer], composers: [composer],
parts: partInfos, parts: partInfos,
sections: sections,
); );
final success = await backend.client.putWork(workInfo); final success = await backend.client.putWork(workInfo);
@ -337,14 +355,37 @@ class _WorkEditorState extends State<WorkEditor> {
}); });
}, },
), ),
if (parts.length > 0) Padding(
Padding( padding: const EdgeInsets.only(left: 16.0, top: 16.0),
padding: const EdgeInsets.only(left: 16.0, top: 16.0), child: Row(
child: Text( children: <Widget>[
'Parts', Expanded(
style: Theme.of(context).textTheme.subtitle1, child: Text(
), 'Parts',
style: Theme.of(context).textTheme.subtitle1,
),
),
FlatButton(
child: Text('ADD SECTION'),
onPressed: () {
setState(() {
parts.add(PartData(
isSection: true,
));
});
},
),
FlatButton(
child: Text('ADD PART'),
onPressed: () {
setState(() {
parts.add(PartData());
});
},
),
],
), ),
),
], ],
), ),
children: partTiles, children: partTiles,
@ -357,15 +398,6 @@ class _WorkEditorState extends State<WorkEditor> {
}); });
}, },
), ),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text('Add part'),
onPressed: () {
setState(() {
parts.add(PartData());
});
},
),
); );
} }
} }

View file

@ -112,11 +112,17 @@ class Database extends _$Database {
)); ));
} }
final List<WorkSection> sections = [];
for (final section in await sectionsByWork(id).get()) {
sections.add(section);
}
return WorkInfo( return WorkInfo(
work: work, work: work,
instruments: instruments, instruments: instruments,
composers: composers, composers: composers,
parts: parts, parts: parts,
sections: sections,
); );
} }
@ -160,8 +166,8 @@ class Database extends _$Database {
await transaction(() async { await transaction(() async {
final workId = workInfo.work.id; final workId = workInfo.work.id;
// Delete old work data first. The parts and instrumentations will be // Delete old work data first. The parts, sections and instrumentations
// deleted automatically due to their foreign key constraints. // will be deleted automatically due to their foreign key constraints.
await deleteWork(workId); await deleteWork(workId);
// This will also include the composers of the work's parts. // This will also include the composers of the work's parts.
@ -194,6 +200,10 @@ class Database extends _$Database {
)); ));
} }
} }
for (final section in workInfo.sections) {
await into(workSections).insert(section);
}
}); });
} }

View file

@ -35,6 +35,13 @@ CREATE TABLE part_instrumentations (
instrument INTEGER NOT NULL REFERENCES instruments(id) ON DELETE CASCADE instrument INTEGER NOT NULL REFERENCES instruments(id) ON DELETE CASCADE
); );
CREATE TABLE work_sections (
id INTEGER NOT NULL PRIMARY KEY,
work INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE,
title TEXT NOT NULL,
before_part_index INTEGER NOT NULL
);
CREATE TABLE ensembles ( CREATE TABLE ensembles (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL name TEXT NOT NULL
@ -80,6 +87,9 @@ SELECT * FROM works WHERE id = :id LIMIT 1;
partsByWork: partsByWork:
SELECT * FROM work_parts WHERE part_of = :id ORDER BY part_index; SELECT * FROM work_parts WHERE part_of = :id ORDER BY part_index;
sectionsByWork:
SELECT * FROM work_sections WHERE work = :id ORDER BY before_part_index;
worksByComposer: worksByComposer:
SELECT DISTINCT works.* FROM works SELECT DISTINCT works.* FROM works
JOIN work_parts ON work_parts.part_of = works.id JOIN work_parts ON work_parts.part_of = works.id

View file

@ -11,7 +11,7 @@ class PartInfo {
final List<Instrument> instruments; final List<Instrument> instruments;
/// The composer of this part. /// The composer of this part.
/// ///
/// This is null, if this part doesn't have a specific composer. /// This is null, if this part doesn't have a specific composer.
final Person composer; final Person composer;
@ -45,7 +45,7 @@ class WorkInfo {
final Work work; final Work work;
/// A list of instruments. /// A list of instruments.
/// ///
/// This will not the include the instruments, that are specific to the work /// This will not the include the instruments, that are specific to the work
/// parts. /// parts.
final List<Instrument> instruments; final List<Instrument> instruments;
@ -56,11 +56,15 @@ class WorkInfo {
/// All available information on the work parts. /// All available information on the work parts.
final List<PartInfo> parts; final List<PartInfo> parts;
/// The sections of this work.
final List<WorkSection> sections;
WorkInfo({ WorkInfo({
this.work, this.work,
this.instruments, this.instruments,
this.composers, this.composers,
this.parts, this.parts,
this.sections,
}); });
factory WorkInfo.fromJson(Map<String, dynamic> json) => WorkInfo( factory WorkInfo.fromJson(Map<String, dynamic> json) => WorkInfo(
@ -72,6 +76,9 @@ class WorkInfo {
json['composers'].map<Person>((j) => Person.fromJson(j)).toList(), json['composers'].map<Person>((j) => Person.fromJson(j)).toList(),
parts: parts:
json['parts'].map<PartInfo>((j) => PartInfo.fromJson(j)).toList(), json['parts'].map<PartInfo>((j) => PartInfo.fromJson(j)).toList(),
sections: json['sections']
.map<WorkSection>((j) => WorkSection.fromJson(j))
.toList(),
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -79,6 +86,7 @@ class WorkInfo {
'instruments': instruments.map((i) => i.toJson()).toList(), 'instruments': instruments.map((i) => i.toJson()).toList(),
'composers': composers.map((c) => c.toJson()).toList(), 'composers': composers.map((c) => c.toJson()).toList(),
'parts': parts.map((c) => c.toJson()).toList(), 'parts': parts.map((c) => c.toJson()).toList(),
'sections': sections.map((s) => s.toJson()).toList(),
}; };
} }

View file

@ -79,6 +79,9 @@ class _ProgramScreenState extends State<ProgramScreen> {
// This will contain information on the last new work. // This will contain information on the last new work.
WorkInfo workInfo; WorkInfo workInfo;
// The index of the last displayed section.
int lastSectionIndex;
for (var i = 0; i < playlist.length; i++) { for (var i = 0; i < playlist.length; i++) {
// The widgets displayed for this track. // The widgets displayed for this track.
List<Widget> children = []; List<Widget> children = [];
@ -113,6 +116,18 @@ class _ProgramScreenState extends State<ProgramScreen> {
for (final partId in partIds) { for (final partId in partIds) {
final partInfo = workInfo.parts[partId]; final partInfo = workInfo.parts[partId];
final sectionIndex = workInfo.sections
.lastIndexWhere((s) => s.beforePartIndex <= partId);
if (sectionIndex != lastSectionIndex) {
lastSectionIndex = sectionIndex;
children.add(Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
),
child: Text(workInfo.sections[sectionIndex].title),
));
}
children.add(Padding( children.add(Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, left: 8.0,
@ -183,21 +198,20 @@ class _ProgramScreenState extends State<ProgramScreen> {
}, },
onLongPress: () { onLongPress: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return SimpleDialog( return SimpleDialog(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Remove from playlist'), title: Text('Remove from playlist'),
onTap: () { onTap: () {
backend.playback.removeTrack(index); backend.playback.removeTrack(index);
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
], ],
); );
} });
);
}, },
); );
}, },

View file

@ -61,6 +61,15 @@ class _PlayerBarState extends State<PlayerBar> {
if (_partIds.isNotEmpty) { if (_partIds.isNotEmpty) {
subtitleBuffer.write(': '); subtitleBuffer.write(': ');
final section = _workInfo.sections
.lastWhere((s) => s.beforePartIndex <= _partIds[0]);
if (section != null) {
subtitleBuffer.write(section.title);
subtitleBuffer.write(': ');
}
subtitleBuffer.write( subtitleBuffer.write(
_partIds.map((i) => _workInfo.parts[i].part.title).join(', ')); _partIds.map((i) => _workInfo.parts[i].part.title).join(', '));
} }