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';
class PartData {
final bool isSection;
final titleController = TextEditingController();
Person composer;
List<Instrument> instruments;
PartData({
this.isSection = false,
String title,
this.composer,
this.instruments = const [],
@ -111,7 +113,7 @@ class PartTile extends StatefulWidget {
PartTile({
Key key,
@required this.part,
@required this.onMore,
this.onMore,
@required this.onDelete,
}) : super(key: key);
@ -122,10 +124,12 @@ class PartTile extends StatefulWidget {
class _PartTileState extends State<PartTile> {
@override
Widget build(BuildContext context) {
final isSection = widget.part.isSection;
return Row(
children: <Widget>[
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(
Icons.drag_handle,
),
@ -135,14 +139,15 @@ class _PartTileState extends State<PartTile> {
controller: widget.part.titleController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Part title',
hintText: isSection ? 'Section title' : 'Part title',
),
),
),
IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: widget.onMore,
),
if (!isSection)
IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: widget.onMore,
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: widget.onDelete,
@ -273,19 +278,31 @@ class _WorkEditorState extends State<WorkEditor> {
final workId = widget?.workInfo?.work?.id ?? generateId();
List<PartInfo> partInfos = [];
List<WorkSection> sections = [];
int sectionCount = 0;
for (var i = 0; i < parts.length; i++) {
final part = parts[i];
partInfos.add(PartInfo(
part: WorkPart(
if (part.isSection) {
sections.add(WorkSection(
id: generateId(),
work: workId,
title: part.titleController.text,
composer: part.composer?.id,
partOf: workId,
partIndex: i,
),
instruments: part.instruments,
composer: part.composer,
));
beforePartIndex: i - sectionCount,
));
sectionCount++;
} else {
partInfos.add(PartInfo(
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(
@ -299,6 +316,7 @@ class _WorkEditorState extends State<WorkEditor> {
// from the parts.
composers: [composer],
parts: partInfos,
sections: sections,
);
final success = await backend.client.putWork(workInfo);
@ -337,14 +355,37 @@ class _WorkEditorState extends State<WorkEditor> {
});
},
),
if (parts.length > 0)
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
child: Text(
'Parts',
style: Theme.of(context).textTheme.subtitle1,
),
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
child: Row(
children: <Widget>[
Expanded(
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,
@ -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(
work: work,
instruments: instruments,
composers: composers,
parts: parts,
sections: sections,
);
}
@ -160,8 +166,8 @@ class Database extends _$Database {
await transaction(() async {
final workId = workInfo.work.id;
// Delete old work data first. The parts and instrumentations will be
// deleted automatically due to their foreign key constraints.
// Delete old work data first. The parts, sections and instrumentations
// will be deleted automatically due to their foreign key constraints.
await deleteWork(workId);
// 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
);
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 (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL
@ -80,6 +87,9 @@ SELECT * FROM works WHERE id = :id LIMIT 1;
partsByWork:
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:
SELECT DISTINCT works.* FROM works
JOIN work_parts ON work_parts.part_of = works.id

View file

@ -11,7 +11,7 @@ class PartInfo {
final List<Instrument> instruments;
/// The composer of this part.
///
///
/// This is null, if this part doesn't have a specific composer.
final Person composer;
@ -45,7 +45,7 @@ class WorkInfo {
final Work work;
/// A list of instruments.
///
///
/// This will not the include the instruments, that are specific to the work
/// parts.
final List<Instrument> instruments;
@ -56,11 +56,15 @@ class WorkInfo {
/// All available information on the work parts.
final List<PartInfo> parts;
/// The sections of this work.
final List<WorkSection> sections;
WorkInfo({
this.work,
this.instruments,
this.composers,
this.parts,
this.sections,
});
factory WorkInfo.fromJson(Map<String, dynamic> json) => WorkInfo(
@ -72,6 +76,9 @@ class WorkInfo {
json['composers'].map<Person>((j) => Person.fromJson(j)).toList(),
parts:
json['parts'].map<PartInfo>((j) => PartInfo.fromJson(j)).toList(),
sections: json['sections']
.map<WorkSection>((j) => WorkSection.fromJson(j))
.toList(),
);
Map<String, dynamic> toJson() => {
@ -79,6 +86,7 @@ class WorkInfo {
'instruments': instruments.map((i) => i.toJson()).toList(),
'composers': composers.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.
WorkInfo workInfo;
// The index of the last displayed section.
int lastSectionIndex;
for (var i = 0; i < playlist.length; i++) {
// The widgets displayed for this track.
List<Widget> children = [];
@ -113,6 +116,18 @@ class _ProgramScreenState extends State<ProgramScreen> {
for (final partId in partIds) {
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(
padding: const EdgeInsets.only(
left: 8.0,
@ -183,21 +198,20 @@ class _ProgramScreenState extends State<ProgramScreen> {
},
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Remove from playlist'),
onTap: () {
backend.playback.removeTrack(index);
Navigator.pop(context);
},
),
],
);
}
);
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Remove from playlist'),
onTap: () {
backend.playback.removeTrack(index);
Navigator.pop(context);
},
),
],
);
});
},
);
},

View file

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