mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 10:47:25 +01:00
Support work sections
This commit is contained in:
parent
813fa2e47a
commit
93a5a06b55
6 changed files with 134 additions and 51 deletions
|
|
@ -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());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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(', '));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue