diff --git a/common/lib/src/editors/work.dart b/common/lib/src/editors/work.dart index 9094816..f139d70 100644 --- a/common/lib/src/editors/work.dart +++ b/common/lib/src/editors/work.dart @@ -6,12 +6,14 @@ import '../selectors/instruments.dart'; import '../selectors/person.dart'; class PartData { + final bool isSection; final titleController = TextEditingController(); Person composer; List 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 { @override Widget build(BuildContext context) { + final isSection = widget.part.isSection; + return Row( children: [ 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 { 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 { final workId = widget?.workInfo?.work?.id ?? generateId(); List partInfos = []; + List 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 { // from the parts. composers: [composer], parts: partInfos, + sections: sections, ); final success = await backend.client.putWork(workInfo); @@ -337,14 +355,37 @@ class _WorkEditorState extends State { }); }, ), - 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: [ + 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 { }); }, ), - floatingActionButton: FloatingActionButton.extended( - icon: const Icon(Icons.add), - label: Text('Add part'), - onPressed: () { - setState(() { - parts.add(PartData()); - }); - }, - ), ); } } diff --git a/database/lib/src/database.dart b/database/lib/src/database.dart index 9067e85..9fbb21d 100644 --- a/database/lib/src/database.dart +++ b/database/lib/src/database.dart @@ -112,11 +112,17 @@ class Database extends _$Database { )); } + final List 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); + } }); } diff --git a/database/lib/src/database.moor b/database/lib/src/database.moor index 5d531ef..56affab 100644 --- a/database/lib/src/database.moor +++ b/database/lib/src/database.moor @@ -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 diff --git a/database/lib/src/info.dart b/database/lib/src/info.dart index ea7bf76..d337b3d 100644 --- a/database/lib/src/info.dart +++ b/database/lib/src/info.dart @@ -11,7 +11,7 @@ class PartInfo { final List 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 instruments; @@ -56,11 +56,15 @@ class WorkInfo { /// All available information on the work parts. final List parts; + /// The sections of this work. + final List sections; + WorkInfo({ this.work, this.instruments, this.composers, this.parts, + this.sections, }); factory WorkInfo.fromJson(Map json) => WorkInfo( @@ -72,6 +76,9 @@ class WorkInfo { json['composers'].map((j) => Person.fromJson(j)).toList(), parts: json['parts'].map((j) => PartInfo.fromJson(j)).toList(), + sections: json['sections'] + .map((j) => WorkSection.fromJson(j)) + .toList(), ); Map 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(), }; } diff --git a/mobile/lib/screens/program.dart b/mobile/lib/screens/program.dart index c65ea75..fe6bad4 100644 --- a/mobile/lib/screens/program.dart +++ b/mobile/lib/screens/program.dart @@ -79,6 +79,9 @@ class _ProgramScreenState extends State { // 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 children = []; @@ -113,6 +116,18 @@ class _ProgramScreenState extends State { 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 { }, onLongPress: () { showDialog( - context: context, - builder: (context) { - return SimpleDialog( - children: [ - ListTile( - title: Text('Remove from playlist'), - onTap: () { - backend.playback.removeTrack(index); - Navigator.pop(context); - }, - ), - ], - ); - } - ); + context: context, + builder: (context) { + return SimpleDialog( + children: [ + ListTile( + title: Text('Remove from playlist'), + onTap: () { + backend.playback.removeTrack(index); + Navigator.pop(context); + }, + ), + ], + ); + }); }, ); }, diff --git a/mobile/lib/widgets/player_bar.dart b/mobile/lib/widgets/player_bar.dart index 82e00ec..76694d7 100644 --- a/mobile/lib/widgets/player_bar.dart +++ b/mobile/lib/widgets/player_bar.dart @@ -61,6 +61,15 @@ class _PlayerBarState extends State { 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(', ')); }