common: Adapt to dependencies and remove editing

This commit is contained in:
Elias Projahn 2022-05-06 13:56:10 +02:00
parent 0ddf0ff84a
commit 777c89fed4
32 changed files with 45 additions and 3217 deletions

View file

@ -2,11 +2,11 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:drift/native.dart';
import 'package:flutter/widgets.dart';
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import 'library.dart';
import 'platform.dart';
@ -79,15 +79,17 @@ class MusicusBackend extends StatefulWidget {
}
class MusicusBackendState extends State<MusicusBackend> {
/// Starts the Moor isolate.
/// Starts the database isolate.
///
/// It will create a database connection for [request.path] and will send the
/// Moor send port through [request.sendPort].
static void _moorIsolateEntrypoint(_IsolateStartRequest request) {
final executor = VmDatabase(File(request.path));
final moorIsolate =
MoorIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor));
request.sendPort.send(moorIsolate.connectPort);
/// drift send port through [request.sendPort].
static void _dbIsolateEntrypoint(_IsolateStartRequest request) {
final executor = NativeDatabase(File(request.path));
final driftIsolate =
DriftIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor));
request.sendPort.send(driftIsolate.connectPort);
}
/// The current backend status.
@ -99,7 +101,6 @@ class MusicusBackendState extends State<MusicusBackend> {
MusicusClientDatabase db;
MusicusPlayback playback;
MusicusSettings settings;
MusicusClient client;
MusicusPlatform platform;
MusicusLibrary library;
@ -111,17 +112,22 @@ class MusicusBackendState extends State<MusicusBackend> {
/// Initialize resources.
Future<void> _load() async {
SendPort moorPort = IsolateNameServer.lookupPortByName('moor');
if (moorPort == null) {
SendPort driftPort = IsolateNameServer.lookupPortByName('moor');
if (driftPort == null) {
final receivePort = ReceivePort();
await Isolate.spawn(_moorIsolateEntrypoint,
await Isolate.spawn(_dbIsolateEntrypoint,
_IsolateStartRequest(receivePort.sendPort, widget.dbPath));
moorPort = await receivePort.first;
IsolateNameServer.registerPortWithName(moorPort, 'moor');
driftPort = await receivePort.first;
IsolateNameServer.registerPortWithName(driftPort, 'drift');
}
final moorIsolate = MoorIsolate.fromConnectPort(moorPort);
db = MusicusClientDatabase.connect(connection: await moorIsolate.connect());
final driftIsolate = DriftIsolate.fromConnectPort(driftPort);
db = MusicusClientDatabase.connect(
connection: await driftIsolate.connect(),
);
playback = widget.playback;
await playback.setup();
@ -136,18 +142,7 @@ class MusicusBackendState extends State<MusicusBackend> {
_updateMusicLibrary(path);
});
settings.server.listen((serverSettings) {
_updateClient(serverSettings);
});
settings.account.listen((credentials) {
client.credentials = credentials;
});
// This will also check for existing account settings.
_updateClient(settings.server.value);
final path = settings.musicLibraryPath.value;
final path = settings.musicLibraryPath.valueOrNull;
platform = widget.platform;
platform.setBasePath(path);
@ -172,20 +167,6 @@ class MusicusBackendState extends State<MusicusBackend> {
}
}
/// Create a new client based on [serverSettings].
void _updateClient(MusicusServerSettings serverSettings) {
client?.dispose();
client = MusicusClient(
host: serverSettings.host,
port: serverSettings.port,
basePath: serverSettings.apiPath,
credentials: settings.account.value,
);
// TODO: Maybe don't change the client in the middle of synchronization.
db.client = client;
}
@override
Widget build(BuildContext context) {
return _InheritedBackend(
@ -202,11 +183,10 @@ class MusicusBackendState extends State<MusicusBackend> {
/// We don't stop the Moor isolate, because it can be used elsewhere.
db.close();
client.dispose();
}
}
/// Bundles arguments for the moor isolate.
/// Bundles arguments for the database isolate.
class _IsolateStartRequest {
final SendPort sendPort;
final String path;

View file

@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
class EnsembleEditor extends StatefulWidget {
final Ensemble ensemble;
EnsembleEditor({
this.ensemble,
});
@override
_EnsembleEditorState createState() => _EnsembleEditorState();
}
class _EnsembleEditorState extends State<EnsembleEditor> {
final nameController = TextEditingController();
bool uploading = false;
bool _sync = true;
@override
void initState() {
super.initState();
if (widget.ensemble != null) {
nameController.text = widget.ensemble.name;
_sync = widget.ensemble.sync;
}
}
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Ensemble'),
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 ensemble = Ensemble(
id: widget.ensemble?.id ?? generateId(),
name: nameController.text,
sync: _sync,
synced: false,
);
await backend.db.updateEnsemble(ensemble);
setState(() {
uploading = false;
});
Navigator.pop(context, ensemble);
},
),
],
),
body: ListView(
children: <Widget>[
SwitchListTile(
title: Text('Synchronize changes'),
subtitle: Text(_sync
? 'Publish changes on the server'
: 'Keep changes private'),
value: _sync,
onChanged: (value) {
setState(() {
_sync = value;
});
},
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'Name',
),
),
),
],
),
);
}
}

View file

@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
class InstrumentEditor extends StatefulWidget {
final Instrument instrument;
InstrumentEditor({
this.instrument,
});
@override
_InstrumentEditorState createState() => _InstrumentEditorState();
}
class _InstrumentEditorState extends State<InstrumentEditor> {
final nameController = TextEditingController();
bool uploading = false;
bool _sync = true;
@override
void initState() {
super.initState();
if (widget.instrument != null) {
nameController.text = widget.instrument.name;
_sync = widget.instrument.sync;
}
}
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Instrument/Role'),
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 instrument = Instrument(
id: widget.instrument?.id ?? generateId(),
name: nameController.text,
sync: _sync,
synced: false,
);
await backend.db.updateInstrument(instrument);
setState(() {
uploading = false;
});
Navigator.pop(context, instrument);
},
),
],
),
body: ListView(
children: <Widget>[
SwitchListTile(
title: Text('Synchronize changes'),
subtitle: Text(_sync
? 'Publish changes on the server'
: 'Keep changes private'),
value: _sync,
onChanged: (value) {
setState(() {
_sync = value;
});
},
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'Name',
),
),
),
],
),
);
}
}

View file

@ -1,131 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../selectors/ensemble.dart';
import '../selectors/instruments.dart';
import '../selectors/person.dart';
class PerformanceEditor extends StatefulWidget {
final PerformanceInfo performanceInfo;
PerformanceEditor({
this.performanceInfo,
});
@override
_PerformanceEditorState createState() => _PerformanceEditorState();
}
class _PerformanceEditorState extends State<PerformanceEditor> {
Person person;
Ensemble ensemble;
Instrument role;
@override
void initState() {
super.initState();
if (widget.performanceInfo != null) {
person = widget.performanceInfo.person;
ensemble = widget.performanceInfo.ensemble;
role = widget.performanceInfo.role;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit performer'),
actions: <Widget>[
FlatButton(
child: Text('DONE'),
onPressed: () => Navigator.pop(
context,
PerformanceInfo(
person: person,
ensemble: ensemble,
role: role,
),
),
),
],
),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Person'),
subtitle: Text(person != null
? '${person.firstName} ${person.lastName}'
: 'Select person'),
onTap: () async {
final Person newPerson = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PersonsSelector(),
fullscreenDialog: true,
),
);
if (newPerson != null) {
setState(() {
person = newPerson;
ensemble = null;
});
}
},
),
ListTile(
title: Text('Ensemble'),
subtitle: Text(ensemble?.name ?? 'Select ensemble'),
onTap: () async {
final Ensemble newEnsemble = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EnsembleSelector(),
fullscreenDialog: true,
),
);
if (newEnsemble != null) {
setState(() {
ensemble = newEnsemble;
person = null;
});
}
},
),
ListTile(
title: Text('Role'),
subtitle: Text(role?.name ?? 'Select instrument/role'),
trailing: role != null
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
role = null;
});
},
)
: null,
onTap: () async {
final Instrument newRole = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InstrumentsSelector(),
fullscreenDialog: true,
),
);
if (newRole != null) {
setState(() {
role = newRole;
});
}
},
),
],
),
);
}
}

View file

@ -1,118 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
class PersonEditor extends StatefulWidget {
final Person person;
PersonEditor({
this.person,
});
@override
_PersonEditorState createState() => _PersonEditorState();
}
class _PersonEditorState extends State<PersonEditor> {
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
bool uploading = false;
bool _sync = true;
@override
void initState() {
super.initState();
if (widget.person != null) {
firstNameController.text = widget.person.firstName;
lastNameController.text = widget.person.lastName;
_sync = widget.person.sync;
}
}
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Person'),
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 person = Person(
id: widget.person?.id ?? generateId(),
firstName: firstNameController.text,
lastName: lastNameController.text,
sync: _sync,
synced: false,
);
await backend.db.updatePerson(person);
setState(() {
uploading = false;
});
Navigator.pop(context, person);
},
),
],
),
body: ListView(
children: <Widget>[
SwitchListTile(
title: Text('Synchronize changes'),
subtitle: Text(_sync
? 'Publish changes on the server'
: 'Keep changes private'),
value: _sync,
onChanged: (value) {
setState(() {
_sync = value;
});
},
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: firstNameController,
decoration: InputDecoration(
labelText: 'First name',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: lastNameController,
decoration: InputDecoration(
labelText: 'Last name',
),
),
),
],
),
);
}
}

View file

@ -1,231 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
import '../editors/performance.dart';
import '../selectors/recording.dart';
import '../selectors/work.dart';
/// Screen for editing a recording.
///
/// If the user has finished editing, the result will be returned using the
/// navigator as a [RecordingSelectorResult] object.
class RecordingEditor extends StatefulWidget {
/// The recording to edit.
///
/// If this is null, a new recording will be created.
final RecordingInfo recordingInfo;
RecordingEditor({
this.recordingInfo,
});
@override
_RecordingEditorState createState() => _RecordingEditorState();
}
class _RecordingEditorState extends State<RecordingEditor> {
final _commentController = TextEditingController();
MusicusBackendState _backend;
bool _uploading = false;
bool _sync = true;
WorkInfo _workInfo;
List<PerformanceInfo> _performanceInfos = [];
@override
void didChangeDependencies() {
super.didChangeDependencies();
_backend = MusicusBackend.of(context);
if (widget.recordingInfo != null &&
_workInfo == null &&
_performanceInfos.isEmpty) {
_init();
}
}
Future<void> _init() async {
_workInfo = await _backend.db.getWork(widget.recordingInfo.recording.work);
_performanceInfos = List.from(widget.recordingInfo.performances);
setState(() {
this._workInfo = _workInfo;
this._performanceInfos = _performanceInfos;
_sync = widget.recordingInfo.recording.sync;
});
}
@override
Widget build(BuildContext context) {
Future<void> selectWork() async {
final WorkInfo newWorkInfo = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WorkSelector(),
fullscreenDialog: true,
));
if (newWorkInfo != null) {
setState(() {
_workInfo = newWorkInfo;
});
}
}
final List<Widget> performanceTiles = [];
for (var i = 0; i < _performanceInfos.length; i++) {
final p = _performanceInfos[i];
performanceTiles.add(ListTile(
title: Text(p.person != null
? '${p.person.firstName} ${p.person.lastName}'
: p.ensemble.name),
subtitle: p.role != null ? Text(p.role.name) : null,
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_performanceInfos.remove(p);
});
},
),
onTap: () async {
final PerformanceInfo performanceInfo = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PerformanceEditor(
performanceInfo: p,
),
fullscreenDialog: true,
));
if (performanceInfo != null) {
setState(() {
_performanceInfos[i] = performanceInfo;
});
}
},
));
}
return Scaffold(
appBar: AppBar(
title: Text('Recording'),
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 recordingInfo = RecordingInfo(
recording: Recording(
id: widget?.recordingInfo?.recording?.id ??
generateId(),
work: _workInfo.work.id,
comment: _commentController.text,
sync: _sync,
synced: false,
),
performances: _performanceInfos,
);
await _backend.db.updateRecording(recordingInfo);
setState(() {
_uploading = false;
});
Navigator.pop(
context,
RecordingSelectorResult(
workInfo: _workInfo,
recordingInfo: recordingInfo,
),
);
},
),
],
),
body: ListView(
children: <Widget>[
SwitchListTile(
title: Text('Synchronize changes'),
subtitle: Text(_sync
? 'Publish changes on the server'
: 'Keep changes private'),
value: _sync,
onChanged: (value) {
setState(() {
_sync = value;
});
},
),
_workInfo != null
? ListTile(
title: Text(_workInfo.work.title),
subtitle: Text(_workInfo.composers
.map((p) => '${p.firstName} ${p.lastName}')
.join(', ')),
onTap: selectWork,
)
: ListTile(
title: Text('Work'),
subtitle: Text('Select work'),
onTap: selectWork,
),
Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 0.0,
bottom: 16.0,
),
child: TextField(
controller: _commentController,
decoration: InputDecoration(
labelText: 'Comment',
),
),
),
ListTile(
title: Text('Performers'),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
final PerformanceInfo model = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PerformanceEditor(),
fullscreenDialog: true,
));
if (model != null) {
setState(() {
_performanceInfos.add(model);
});
}
},
),
),
...performanceTiles,
],
),
);
}
}

View file

@ -1,165 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
import '../library.dart';
import '../selectors/files.dart';
import '../selectors/recording.dart';
import '../widgets/recording_tile.dart';
class TrackModel {
int workPartIndex;
String workPartTitle;
String fileName;
TrackModel(this.fileName);
}
class TracksEditor extends StatefulWidget {
@override
_TracksEditorState createState() => _TracksEditorState();
}
class _TracksEditorState extends State<TracksEditor> {
MusicusBackendState backend;
WorkInfo workInfo;
RecordingInfo recordingInfo;
String parentId;
List<TrackModel> trackModels = [];
@override
Widget build(BuildContext context) {
backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Tracks'),
actions: <Widget>[
FlatButton(
child: Text('DONE'),
onPressed: () async {
final List<Track> tracks = [];
for (var i = 0; i < trackModels.length; i++) {
final trackModel = trackModels[i];
tracks.add(Track(
fileName: trackModel.fileName,
recordingId: recordingInfo.recording.id,
index: i,
partIds: trackModel.workPartIndex != null
? [trackModel.workPartIndex]
: [],
));
}
// We need to copy all information associated with this track we
// got by asking the server to our local database. For now, we
// will just override everything that we already had previously.
backend.db.updateWork(workInfo);
backend.db.updateRecording(recordingInfo);
backend.library.addTracks(parentId, tracks);
Navigator.pop(context);
},
),
],
),
body: ReorderableListView(
header: Column(
children: <Widget>[
ListTile(
title: recordingInfo != null
? RecordingTile(
workInfo: workInfo,
recordingInfo: recordingInfo,
)
: Text('Select recording'),
onTap: selectRecording,
),
ListTile(
title: Text('Files'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
final FilesSelectorResult result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FilesSelector(),
),
);
if (result != null) {
final List<TrackModel> newTrackModels = [];
for (final document in result.selection) {
newTrackModels.add(TrackModel(document.name));
}
setState(() {
parentId = result.parentId;
trackModels = newTrackModels;
});
if (recordingInfo != null) {
updateAutoParts();
}
}
},
),
),
],
),
children: trackModels
.map((t) => ListTile(
key: Key(t.hashCode.toString()),
leading: const Icon(Icons.drag_handle),
title: Text(t.workPartTitle ?? 'Set work part'),
subtitle: Text(t.fileName),
))
.toList(),
onReorder: (i1, i2) {
setState(() {
final track = trackModels.removeAt(i1);
final newIndex = i2 > i1 ? i2 - 1 : i2;
trackModels.insert(newIndex, track);
});
},
),
);
}
Future<void> selectRecording() async {
final RecordingSelectorResult result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RecordingSelector(),
),
);
if (result != null) {
setState(() {
workInfo = result.workInfo;
recordingInfo = result.recordingInfo;
});
updateAutoParts();
}
}
/// Automatically associate the tracks with work parts.
Future<void> updateAutoParts() async {
setState(() {
for (var i = 0; i < trackModels.length; i++) {
if (i >= workInfo.parts.length) {
trackModels[i].workPartIndex = null;
trackModels[i].workPartTitle = null;
} else {
trackModels[i].workPartIndex = workInfo.parts[i].part.partIndex;
trackModels[i].workPartTitle = workInfo.parts[i].part.title;
}
}
});
}
}

View file

@ -1,414 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
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 [],
}) {
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,
this.onMore,
@required this.onDelete,
}) : super(key: key);
@override
_PartTileState createState() => _PartTileState();
}
class _PartTileState extends State<PartTile> {
@override
Widget build(BuildContext context) {
final isSection = widget.part.isSection;
return Row(
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: isSection ? 8.0 : 24.0, right: 8.0),
child: Icon(
Icons.drag_handle,
),
),
Expanded(
child: TextField(
controller: widget.part.titleController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: isSection ? 'Section title' : 'Part title',
),
),
),
if (!isSection)
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;
bool _sync = true;
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.part.title,
composer: partInfo.composer,
instruments: List.from(partInfo.instruments),
));
}
_sync = widget.workInfo.work.sync;
}
}
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.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 = [];
List<WorkSection> sections = [];
int sectionCount = 0;
for (var i = 0; i < parts.length; i++) {
final part = parts[i];
if (part.isSection) {
sections.add(WorkSection(
id: generateId(),
work: workId,
title: part.titleController.text,
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(
work: Work(
id: workId,
title: titleController.text,
composer: composer?.id,
sync: _sync,
synced: false,
),
instruments: instruments,
// TODO: Theoretically, this should include all composers
// from the parts.
composers: [composer],
parts: partInfos,
sections: sections,
);
await backend.db.updateWork(workInfo);
setState(() {
uploading = false;
});
Navigator.pop(context, workInfo);
},
),
],
),
body: ReorderableListView(
header: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SwitchListTile(
title: Text('Synchronize changes'),
subtitle: Text(_sync
? 'Publish changes on the server'
: 'Keep changes private'),
value: _sync,
onChanged: (value) {
setState(() {
_sync = value;
});
},
),
WorkProperties(
titleController: titleController,
composer: composer,
instruments: instruments,
onComposerChanged: (newComposer) {
setState(() {
composer = newComposer;
});
},
onInstrumentsChanged: (newInstruments) {
setState(() {
instruments = newInstruments;
});
},
),
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,
onReorder: (i1, i2) {
setState(() {
final part = parts.removeAt(i1);
final newIndex = i2 > i1 ? i2 - 1 : i2;
parts.insert(newIndex, part);
});
},
),
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:url_launcher/url_launcher.dart' as url;
import 'package:url_launcher/url_launcher.dart';
class AboutScreen extends StatelessWidget {
@override
@ -29,7 +29,7 @@ class AboutScreen extends StatelessWidget {
decoration: TextDecoration.underline,
),
),
onTapLink: (link) => url.launch(link),
onTapLink: (text, href, title) => launchUrl(Uri.parse(href)),
);
} else {
return Container();

View file

@ -1,265 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
import 'delete_account.dart';
import 'email.dart';
import 'password.dart';
import 'register.dart';
class AccountSettingsScreen extends StatefulWidget {
@override
_AccountSettingsScreenState createState() => _AccountSettingsScreenState();
}
class _AccountSettingsScreenState extends State<AccountSettingsScreen> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
MusicusBackendState _backend;
StreamSubscription<MusicusAccountCredentials> _accountSubscription;
bool _loading = false;
bool _loggedIn = false;
String _username;
String _email;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_backend = MusicusBackend.of(context);
final credentials = _backend.settings.account.value;
if (credentials != null) {
_setCredentials(credentials);
_getDetails();
}
_accountSubscription = _backend.settings.account.listen((credentials) {
_setCredentials(credentials);
});
}
Future<void> _setCredentials(MusicusAccountCredentials credentials) async {
if (mounted) {
if (credentials != null) {
setState(() {
_loggedIn = true;
_username = credentials.username;
});
} else {
setState(() {
_loggedIn = false;
});
}
}
}
Future<void> _getDetails() async {
setState(() {
_email = null;
});
final email = (await _backend.client.getAccountDetails()).email;
if (mounted) {
setState(() {
_email = email;
});
}
}
@override
Widget build(BuildContext context) {
List<Widget> children;
if (_loggedIn) {
children = [
Material(
elevation: 2.0,
child: ListTile(
title: Text('Logged in as: $_username'),
),
),
ListTile(
title: Text('E-mail address'),
subtitle: Text(
_email != null ? _email.isNotEmpty ? _email : 'Not set' : '...'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EmailScreen(
email: _email,
),
),
);
_getDetails();
},
),
ListTile(
title: Text('Change password'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PasswordScreen(),
),
);
},
),
ListTile(
title: Text('Delete this account'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DeleteAccountScreen(),
),
);
},
),
ListTile(
title: Text('Logout'),
onTap: () async {
await _backend.settings.clearAccount();
Navigator.pop(context);
},
),
];
} else {
children = [
Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
bottom: 8.0,
),
child: Text(
'Enter your Musicus account credentials:',
style: Theme.of(context).textTheme.subtitle1,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'User name',
),
),
),
SizedBox(
height: 16.0,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
),
),
),
SizedBox(
height: 32.0,
),
ListTile(
title: Text('Create a new account'),
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => RegisterScreen(
username: _usernameController.text,
password: _passwordController.text,
),
),
);
},
),
];
}
return Scaffold(
appBar: AppBar(
title: Text('Musicus account'),
actions: <Widget>[
Builder(
builder: (context) {
if (_loggedIn) {
return Container();
} else if (_loading) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
),
);
} else {
return FlatButton(
onPressed: () async {
setState(() {
_loading = true;
});
final credentials = MusicusAccountCredentials(
username: _usernameController.text,
password: _passwordController.text,
);
_backend.client.credentials = credentials;
try {
await _backend.client.login();
await _backend.settings.setAccount(credentials);
Navigator.pop(context);
} on MusicusLoginFailedException {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Login failed'),
),
);
}
setState(() {
_loading = false;
});
},
child: Text('LOGIN'),
);
}
},
),
],
),
body: ListView(
children: children,
),
);
}
@override
void dispose() {
super.dispose();
_accountSubscription.cancel();
}
}

View file

@ -1,97 +0,0 @@
import 'package:flutter/material.dart';
import '../backend.dart';
class DeleteAccountScreen extends StatefulWidget {
@override
_DeleteAccountScreenState createState() => _DeleteAccountScreenState();
}
class _DeleteAccountScreenState extends State<DeleteAccountScreen> {
final _passwordController = TextEditingController();
bool _loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Delete account'),
actions: <Widget>[
Builder(
builder: (context) {
if (_loading) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
),
);
} else {
return FlatButton(
onPressed: () async {
final backend = MusicusBackend.of(context);
if (_passwordController.text ==
backend.settings.account.value.password) {
setState(() {
_loading = true;
});
await backend.client.deleteAccount();
await backend.settings.clearAccount();
setState(() {
_loading = false;
});
Navigator.pop(context);
} else {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('Wrong password'),
));
}
},
child: Text('DELETE'),
);
}
},
),
],
),
body: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
bottom: 8.0,
),
child: Text(
'If you really want to delete your account, enter your password '
'below.',
style: Theme.of(context).textTheme.subtitle1,
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
),
),
),
],
),
);
}
}

View file

@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import '../backend.dart';
class EmailScreen extends StatefulWidget {
final String email;
EmailScreen({
this.email,
});
@override
_EmailScreenState createState() => _EmailScreenState();
}
class _EmailScreenState extends State<EmailScreen> {
final _emailController = TextEditingController();
bool _loading = false;
@override
void initState() {
super.initState();
if (widget.email != null) {
_emailController.text = widget.email;
}
}
Future<void> _setEmail(String email) async {
setState(() {
_loading = true;
});
final backend = MusicusBackend.of(context);
await backend.client.updateAccount(
newEmail: email,
);
setState(() {
_loading = false;
});
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('E-mail address'),
actions: <Widget>[
Builder(
builder: (context) {
if (_loading) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
),
);
} else {
return FlatButton(
onPressed: () {
_setEmail(_emailController.text);
},
child: Text('DONE'),
);
}
},
),
],
),
body: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'E-mail',
),
),
),
ListTile(
title: Text('Delete E-mail address'),
onTap: () {
_setEmail('');
},
),
],
),
);
}
}

View file

@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/person.dart';
import '../editors/tracks.dart';
import '../icons.dart';
import '../widgets/lists.dart';
@ -46,34 +44,22 @@ class _HomeScreenState extends State<HomeScreen> {
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text('Add tracks'),
),
PopupMenuItem(
value: 2,
child: Text('Settings'),
),
PopupMenuItem(
value: 3,
value: 2,
child: Text('About'),
),
],
onSelected: (selected) {
if (selected == 1) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TracksEditor(),
fullscreenDialog: true,
),
);
} else if (selected == 2) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SettingsScreen(),
),
);
} else if (selected == 3) {
} else if (selected == 2) {
Navigator.push(
context,
MaterialPageRoute(
@ -100,31 +86,6 @@ class _HomeScreenState extends State<HomeScreen> {
),
),
),
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Edit person'),
onTap: () async {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => PersonEditor(
person: person,
),
fullscreenDialog: true,
),
);
},
),
],
);
},
);
},
),
),
);

View file

@ -1,117 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
class PasswordScreen extends StatefulWidget {
@override
_PasswordScreenState createState() => _PasswordScreenState();
}
class _PasswordScreenState extends State<PasswordScreen> {
final _oldPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
final _repeatController = TextEditingController();
bool _loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Change password'),
actions: <Widget>[
Builder(
builder: (context) {
if (_loading) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
),
);
} else {
return FlatButton(
onPressed: () async {
final backend = MusicusBackend.of(context);
final password = _newPasswordController.text;
if (_oldPasswordController.text ==
backend.settings.account.value.password &&
password.isNotEmpty &&
password == _repeatController.text) {
setState(() {
_loading = true;
});
await backend.client.updateAccount(
newPassword: password,
);
await backend.settings
.setAccount(MusicusAccountCredentials(
username: backend.settings.account.value.username,
password: password,
));
setState(() {
_loading = false;
});
Navigator.pop(context);
} else {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('Invalid inputs'),
));
}
},
child: Text('DONE'),
);
}
},
),
],
),
body: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _oldPasswordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Old password',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _newPasswordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'New password',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _repeatController,
obscureText: true,
decoration: InputDecoration(
labelText: 'New password (repeat)',
),
),
),
],
),
);
}
}

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/work.dart';
import '../widgets/lists.dart';
import 'work.dart';
@ -55,31 +54,6 @@ class _PersonScreenState extends State<PersonScreen> {
),
),
),
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Edit work'),
onTap: () async {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => WorkEditor(
workInfo: workInfo,
),
fullscreenDialog: true,
),
);
},
),
],
);
},
);
},
),
),
);

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../library.dart';

View file

@ -1,163 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
/// A screen for creating a new Musicus account.
class RegisterScreen extends StatefulWidget {
final String username;
final String password;
RegisterScreen({
this.username,
this.password,
});
@override
_RegisterScreenState createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final nameController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final repeatController = TextEditingController();
bool _loading = false;
@override
void initState() {
super.initState();
if (widget.username != null) {
nameController.text = widget.username;
}
if (widget.password != null) {
passwordController.text = widget.password;
}
}
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Create account'),
actions: <Widget>[
Builder(
builder: (context) {
if (!_loading) {
return FlatButton(
onPressed: () async {
if (_verify()) {
setState(() {
_loading = true;
});
final success = await backend.client.registerAccount(
username: nameController.text,
email: emailController.text,
password: passwordController.text,
);
setState(() {
_loading = false;
});
if (success) {
await backend.settings
.setAccount(MusicusAccountCredentials(
username: nameController.text,
password: passwordController.text,
));
Navigator.pop(context);
} else {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Failed to create account'),
),
);
}
} else {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Invalid inputs'),
),
);
}
},
child: Text('REGISTER'),
);
} else {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
),
);
}
},
),
],
),
body: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'User name',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: emailController,
decoration: InputDecoration(
labelText: 'E-mail address (optional)',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: repeatController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password (repeat)',
),
),
),
],
),
);
}
/// Check whether all requirements are met.
bool _verify() {
return nameController.text.isNotEmpty &&
passwordController.text.isNotEmpty &&
passwordController.text == repeatController.text;
}
}

View file

@ -1,110 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_common/musicus_common.dart';
import '../backend.dart';
class ServerSettingsScreen extends StatefulWidget {
@override
_ServerSettingsScreenState createState() => _ServerSettingsScreenState();
}
class _ServerSettingsScreenState extends State<ServerSettingsScreen> {
final hostController = TextEditingController();
final portController = TextEditingController();
final apiPathController = TextEditingController();
MusicusBackendState backend;
StreamSubscription<MusicusServerSettings> serverSubscription;
@override
void didChangeDependencies() {
super.didChangeDependencies();
backend = MusicusBackend.of(context);
if (serverSubscription != null) {
serverSubscription.cancel();
}
_settingsChanged(backend.settings.server.value);
serverSubscription = backend.settings.server.listen((settings) {
_settingsChanged(settings);
});
}
void _settingsChanged(MusicusServerSettings settings) {
hostController.text = settings.host;
portController.text = settings.port.toString();
apiPathController.text = settings.apiPath;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Server settings'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.restore),
tooltip: 'Reset to default',
onPressed: () {
backend.settings.resetServer();
},
),
FlatButton(
onPressed: () async {
await backend.settings.setServer(MusicusServerSettings(
host: hostController.text,
port: int.parse(portController.text),
apiPath: apiPathController.text,
));
Navigator.pop(context);
},
child: Text('DONE'),
),
],
),
body: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: hostController,
decoration: InputDecoration(
labelText: 'Host',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: portController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Port',
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: apiPathController,
decoration: InputDecoration(
labelText: 'API path',
),
),
),
],
),
);
}
@override
void dispose() {
super.dispose();
serverSubscription.cancel();
}
}

View file

@ -1,13 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_common/musicus_common.dart';
import '../backend.dart';
import 'account_settings.dart';
import 'server_settings.dart';
class SettingsScreen extends StatelessWidget {
static const _platform = MethodChannel('de.johrpan.musicus/platform');
@ -39,52 +34,6 @@ class SettingsScreen extends StatelessWidget {
);
},
),
StreamBuilder<MusicusServerSettings>(
stream: settings.server,
builder: (context, snapshot) {
final s = snapshot.data;
return ListTile(
title: Text('Musicus server'),
subtitle:
Text(s != null ? '${s.host}:${s.port}${s.apiPath}' : '...'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final MusicusServerSettings result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ServerSettingsScreen(),
),
);
if (result != null) {
settings.setServer(result);
}
},
);
},
),
StreamBuilder<MusicusAccountCredentials>(
stream: settings.account,
builder: (context, snapshot) {
final credentials = snapshot.data;
return ListTile(
title: Text('Account settings'),
subtitle: Text(
credentials != null ? credentials.username : 'No account'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AccountSettingsScreen(),
),
);
},
);
},
),
],
),
);

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../editors/recording.dart';
import '../widgets/lists.dart';
import '../widgets/texts.dart';
@ -37,31 +36,6 @@ class WorkScreen extends StatelessWidget {
tracks.sort((t1, t2) => t1.track.index.compareTo(t2.track.index));
backend.playback.addTracks(tracks);
},
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Edit recording'),
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => RecordingEditor(
recordingInfo: recordingInfo,
),
fullscreenDialog: true,
),
);
},
),
],
);
},
);
},
);
},
),

View file

@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../editors/ensemble.dart';
import '../widgets/lists.dart';
/// A screen to select an ensemble.
///
/// If the user has selected one, it will be returned as an [Ensemble] object
/// using the navigator.
class EnsembleSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Select ensemble'),
),
body: EnsemblesList(
onSelected: (ensemble) {
Navigator.pop(context, ensemble);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final Ensemble ensemble = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EnsembleEditor(),
fullscreenDialog: true,
),
);
if (ensemble != null) {
Navigator.pop(context, ensemble);
}
},
),
);
}
}

View file

@ -1,211 +0,0 @@
import 'package:flutter/material.dart';
import '../backend.dart';
import '../platform.dart';
/// Result of the user's interaction with the files selector.
///
/// This will be given back when popping the navigator.
class FilesSelectorResult {
/// Document ID of the parent directory of the selected files.
///
/// This will be null, if they are in the toplevel directory.
final String parentId;
/// Selected files.
final Set<Document> selection;
FilesSelectorResult(this.parentId, this.selection);
}
/// A screen for selecting files.
///
/// This returns a [FilesSelectorResult] when pooping the navigator. If
/// [chooseDirectory] is true, the user will select a directory instead. In
/// that case, the document ID of the directory will be returned directly.
/// If that value is null, this means that the toplevel directory was selected.
class FilesSelector extends StatefulWidget {
/// Choose a directory instead of multiple files.
final bool chooseDirectory;
FilesSelector({
this.chooseDirectory = false,
});
@override
_FilesSelectorState createState() => _FilesSelectorState();
}
class _FilesSelectorState extends State<FilesSelector> {
final _searchController = TextEditingController();
MusicusBackendState backend;
List<Document> history = [];
List<Document> children = [];
Set<Document> selection = {};
String _search = '';
@override
void initState() {
super.initState();
_searchController.addListener(() {
setState(() {
_search = _searchController.text;
});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
backend = MusicusBackend.of(context);
loadChildren();
}
@override
Widget build(BuildContext context) {
final title = history.isNotEmpty ? history.last.name : 'base path';
final filteredChildren = children
.where((d) => d.name.toLowerCase().contains(_search.toLowerCase()))
.toList();
return WillPopScope(
child: Scaffold(
appBar: AppBar(
title: Text(
widget.chooseDirectory ? 'Choose directory' : 'Choose files'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
Navigator.pop(context);
},
),
actions: <Widget>[
FlatButton(
child: Text(widget.chooseDirectory ? 'SELECT' : 'DONE'),
onPressed: () {
final parentId = history.isNotEmpty ? history.last.id : null;
Navigator.pop(
context,
widget.chooseDirectory
? parentId
: FilesSelectorResult(parentId, selection),
);
},
),
],
),
body: Column(
children: <Widget>[
Material(
elevation: 2.0,
child: ListTile(
leading: IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: history.isNotEmpty ? up : null,
),
title: TextField(
autofocus: true,
controller: _searchController,
decoration: InputDecoration.collapsed(
hintText: 'Search in $title...'),
),
),
),
Expanded(
child: ListView.builder(
itemCount: filteredChildren.length,
itemBuilder: (context, index) {
final document = filteredChildren[index];
if (document.isDirectory) {
return ListTile(
leading: const Icon(Icons.folder),
title: Text(document.name),
onTap: () {
_searchController.text = '';
setState(() {
history.add(document);
});
loadChildren();
},
);
} else {
if (widget.chooseDirectory) {
return ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(document.name),
);
} else {
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
secondary: const Icon(Icons.insert_drive_file),
title: Text(document.name),
value: selection.contains(document),
onChanged: (selected) {
setState(() {
if (selected) {
selection.add(document);
} else {
selection.remove(document);
}
});
},
);
}
}
},
),
),
],
),
),
onWillPop: () => Future.value(up()),
);
}
Future<void> loadChildren() async {
setState(() {
children = [];
// We reset the selection here, because the user should not be able to
// select files from multiple directories for now.
selection = {};
});
final newChildren = await backend.platform
.getChildren(history.isNotEmpty ? history.last.id : null);
newChildren.sort((d1, d2) {
if (d1.isDirectory != d2.isDirectory) {
return d1.isDirectory ? -1 : 1;
} else {
return d1.name.compareTo(d2.name);
}
});
if (mounted) {
setState(() {
children = newChildren;
});
}
}
bool up() {
if (history.isNotEmpty) {
setState(() {
history.removeLast();
});
loadChildren();
return false;
} else {
return true;
}
}
}

View file

@ -1,136 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
import '../editors/instrument.dart';
import '../widgets/lists.dart';
class InstrumentsSelector extends StatefulWidget {
final bool multiple;
final List<Instrument> selection;
InstrumentsSelector({
this.multiple = false,
this.selection,
});
@override
_InstrumentsSelectorState createState() => _InstrumentsSelectorState();
}
class _InstrumentsSelectorState extends State<InstrumentsSelector> {
final _list = GlobalKey<PagedListViewState<Instrument>>();
Set<Instrument> selection = {};
String _search;
@override
void initState() {
super.initState();
if (widget.selection != null) {
selection = widget.selection.toSet();
}
}
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text(widget.multiple
? 'Select instruments/roles'
: 'Select instrument/role'),
actions: widget.multiple
? <Widget>[
FlatButton(
child: Text('DONE'),
onPressed: () => Navigator.pop(context, selection.toList()),
),
]
: null,
),
body: Column(
children: <Widget>[
Material(
elevation: 2.0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
child: TextField(
autofocus: true,
onChanged: (text) {
setState(() {
_search = text;
});
},
decoration: InputDecoration.collapsed(
hintText: 'Search by name...',
),
),
),
),
Expanded(
child: PagedListView<Instrument>(
key: _list,
search: _search,
fetch: (page, search) async {
return await backend.client.getInstruments(page, search);
},
builder: (context, instrument) {
if (widget.multiple) {
return CheckboxListTile(
title: Text(instrument.name),
value: selection.contains(instrument),
checkColor: Colors.black,
onChanged: (selected) {
setState(() {
if (selected) {
selection.add(instrument);
} else {
selection.remove(instrument);
}
});
},
);
} else {
return ListTile(
title: Text(instrument.name),
onTap: () => Navigator.pop(context, instrument),
);
}
},
),
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final Instrument instrument = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InstrumentEditor(),
fullscreenDialog: true,
));
if (instrument != null) {
if (widget.multiple) {
setState(() {
selection.add(instrument);
});
// We need to rebuild the list view, because we added an item.
_list.currentState.update();
} else {
Navigator.pop(context, instrument);
}
}
},
),
);
}
}

View file

@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../editors/person.dart';
import '../widgets/lists.dart';
/// A screen to select a person.
///
/// If the user has selected a person, it will be returned as a [Person] object
/// using the navigator.
class PersonsSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Select person'),
),
body: PersonsList(
onSelected: (person) {
Navigator.pop(context, person);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final Person person = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PersonEditor(),
fullscreenDialog: true,
),
);
if (person != null) {
Navigator.pop(context, person);
}
},
),
);
}
}

View file

@ -1,90 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../editors/recording.dart';
import '../widgets/lists.dart';
class RecordingSelectorResult {
final WorkInfo workInfo;
final RecordingInfo recordingInfo;
RecordingSelectorResult({
this.workInfo,
this.recordingInfo,
});
}
/// A screen to select a recording.
///
/// If the user has selected a recording, a [RecordingSelectorResult] containing
/// the selected recording and the recorded work will be returned using the
/// navigator.
class RecordingSelector extends StatefulWidget {
@override
_RecordingSelectorState createState() => _RecordingSelectorState();
}
class _RecordingSelectorState extends State<RecordingSelector> {
Person person;
WorkInfo workInfo;
@override
Widget build(BuildContext context) {
Widget body;
if (person == null) {
body = PersonsList(
onSelected: (newPerson) {
setState(() {
person = newPerson;
});
},
);
} else if (workInfo == null) {
body = WorksList(
personId: person.id,
onSelected: (newWorkInfo) {
setState(() {
workInfo = newWorkInfo;
});
},
);
} else {
body = RecordingsList(
workId: workInfo.work.id,
onSelected: (recordingInfo) {
Navigator.pop(
context,
RecordingSelectorResult(
workInfo: workInfo,
recordingInfo: recordingInfo,
),
);
},
);
}
return Scaffold(
appBar: AppBar(
title: Text('Select recording'),
),
body: body,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final RecordingSelectorResult result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RecordingEditor(),
fullscreenDialog: true,
),
);
if (result != null) {
Navigator.pop(context, result);
}
},
),
);
}
}

View file

@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../editors/work.dart';
import '../widgets/lists.dart';
/// A screen to select a work.
///
/// If the user has selected a work, a [WorkInfo] will be returned
/// using the navigator.
class WorkSelector extends StatefulWidget {
@override
_WorkSelectorState createState() => _WorkSelectorState();
}
class _WorkSelectorState extends State<WorkSelector> {
Person person;
@override
Widget build(BuildContext context) {
Widget body;
if (person == null) {
body = PersonsList(
onSelected: (newPerson) {
setState(() {
person = newPerson;
});
},
);
} else {
body = WorksList(
personId: person.id,
onSelected: (workInfo) {
setState(() {
Navigator.pop(context, workInfo);
});
},
);
}
return Scaffold(
appBar: AppBar(
title: Text('Select work'),
),
body: body,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
final WorkInfo workInfo = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WorkEditor(),
fullscreenDialog: true,
),
);
if (workInfo != null) {
Navigator.pop(context, workInfo);
}
},
),
);
}
}

View file

@ -1,7 +1,4 @@
import 'dart:convert';
import 'package:meta/meta.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:rxdart/rxdart.dart';
/// Interface for persisting settings.
@ -54,12 +51,6 @@ class MusicusSettings {
/// Android storage access framework.
final musicLibraryPath = BehaviorSubject<String>();
/// Musicus server to connect to.
final server = BehaviorSubject<MusicusServerSettings>();
/// Credentials for the Musicus account to login as.
final account = BehaviorSubject<MusicusAccountCredentials>();
/// Create a settings instance.
MusicusSettings(this.storage);
@ -71,26 +62,6 @@ class MusicusSettings {
if (path != null) {
musicLibraryPath.add(path);
}
final host = await storage.getString('serverHost') ?? defaultHost;
final port = await storage.getInt('serverPort') ?? defaultPort;
final apiPath = await storage.getString('serverApiPath') ?? defaultApiPath;
server.add(MusicusServerSettings(
host: host,
port: port,
apiPath: apiPath,
));
final username = await storage.getString('accountUsername');
final passwordBase64 = await storage.getString('accountPassword');
if (username != null) {
account.add(MusicusAccountCredentials(
username: username,
password: utf8.decode(base64Decode(passwordBase64)),
));
}
}
/// Set a new music library path.
@ -101,54 +72,8 @@ class MusicusSettings {
musicLibraryPath.add(path);
}
/// Update the server settings.
///
/// This will persist the new values and update the stream.
Future<void> setServer(MusicusServerSettings serverSettings) async {
await storage.setString('serverHost', serverSettings.host);
await storage.setInt('serverPort', serverSettings.port);
await storage.setString('severApiPath', serverSettings.apiPath);
server.add(serverSettings);
}
/// Reset the server settings to their defaults.
Future<void> resetServer() async {
await setServer(MusicusServerSettings(
host: defaultHost,
port: defaultPort,
apiPath: defaultApiPath,
));
}
/// Update the account credentials.
///
/// This will persist the new values and update the stream.
Future<void> setAccount(MusicusAccountCredentials credentials) async {
await storage.setString('accountUsername', credentials.username);
// IMPORTANT NOTE: We encode the password using Base64 to defend just the
// simplest of simplest attacks. This provides no additional security
// besides the fact that the password looks a little bit encrypted.
await storage.setString(
'accountPassword',
base64Encode(utf8.encode(credentials.password)),
);
account.add(credentials);
}
/// Delete the current account credentials.
Future<void> clearAccount() async {
await storage.setString('accountUsername', null);
await storage.setString('accountPassword', null);
account.add(null);
}
/// Tidy up.
void dispose() {
musicLibraryPath.close();
server.close();
account.close();
}
}

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import '../backend.dart';
import '../widgets/texts.dart';
@ -97,7 +96,7 @@ class PagedListViewState<T> extends State<PagedListView<T>> {
}
/// Update the content manually.
///
///
/// This will reset the current page to zero and call the provided fetch()
/// method.
void update() {
@ -142,226 +141,3 @@ class PagedListViewState<T> extends State<PagedListView<T>> {
);
}
}
/// A list of persons.
class PersonsList extends StatefulWidget {
/// Called, when the user has selected a person.
final void Function(Person person) onSelected;
PersonsList({
@required this.onSelected,
});
@override
_PersonsListState createState() => _PersonsListState();
}
class _PersonsListState extends State<PersonsList> {
String _search;
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Column(
children: <Widget>[
Material(
elevation: 2.0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
child: TextField(
autofocus: true,
onChanged: (text) {
setState(() {
_search = text;
});
},
decoration: InputDecoration.collapsed(
hintText: 'Search by last name...',
),
),
),
),
Expanded(
child: PagedListView<Person>(
search: _search,
fetch: (page, search) async {
return await backend.client.getPersons(page, search);
},
builder: (context, person) => ListTile(
title: Text('${person.lastName}, ${person.firstName}'),
onTap: () {
widget.onSelected(person);
},
),
),
),
],
);
}
}
/// A list of ensembles.
class EnsemblesList extends StatefulWidget {
/// Called, when the user has selected an ensemble.
final void Function(Ensemble ensemble) onSelected;
EnsemblesList({
@required this.onSelected,
});
@override
_EnsemblesListState createState() => _EnsemblesListState();
}
class _EnsemblesListState extends State<EnsemblesList> {
String _search;
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Column(
children: <Widget>[
Material(
elevation: 2.0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
child: TextField(
autofocus: true,
onChanged: (text) {
setState(() {
_search = text;
});
},
decoration: InputDecoration.collapsed(
hintText: 'Search by name...',
),
),
),
),
Expanded(
child: PagedListView<Ensemble>(
search: _search,
fetch: (page, search) async {
return await backend.client.getEnsembles(page, search);
},
builder: (context, ensemble) => ListTile(
title: Text(ensemble.name),
onTap: () {
widget.onSelected(ensemble);
},
),
),
),
],
);
}
}
/// A list of works by one composer.
class WorksList extends StatefulWidget {
/// The ID of the composer.
final int personId;
/// Called, when the user has selected a work.
final void Function(WorkInfo workInfo) onSelected;
WorksList({
this.personId,
this.onSelected,
});
@override
_WorksListState createState() => _WorksListState();
}
class _WorksListState extends State<WorksList> {
String _search;
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Column(
children: <Widget>[
Material(
elevation: 2.0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
child: TextField(
autofocus: true,
onChanged: (text) {
setState(() {
_search = text;
});
},
decoration: InputDecoration.collapsed(
hintText: 'Search by title...',
),
),
),
),
Expanded(
child: PagedListView<WorkInfo>(
search: _search,
fetch: (page, search) async {
return await backend.client
.getWorks(widget.personId, page, search);
},
builder: (context, workInfo) => ListTile(
title: Text(workInfo.work.title),
onTap: () {
widget.onSelected(workInfo);
},
),
),
),
],
);
}
}
/// A list of recordings of a work.
class RecordingsList extends StatelessWidget {
/// The ID of the work.
final int workId;
/// Called, when the user has selected a recording.
final void Function(RecordingInfo recordingInfo) onSelected;
RecordingsList({
this.workId,
this.onSelected,
});
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return PagedListView<RecordingInfo>(
fetch: (page, _) async {
return await backend.client.getRecordings(workId, page);
},
builder: (context, recordingInfo) => ListTile(
title: PerformancesText(
performanceInfos: recordingInfo.performances,
),
onTap: () {
if (onSelected != null) {
onSelected(recordingInfo);
}
},
),
);
}
}

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import '../backend.dart';
import '../library.dart';

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
import 'texts.dart';

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_database/musicus_database.dart';
/// A widget showing information on a list of performances.
class PerformancesText extends StatelessWidget {
@ -34,7 +34,7 @@ class PerformancesText extends StatelessWidget {
performanceTexts.add(buffer.toString());
}
return Text(performanceTexts.join(', '));
}
}

View file

@ -6,16 +6,15 @@ environment:
sdk: ">=2.3.0 <3.0.0"
dependencies:
drift: ^1.0.0
flutter:
sdk: flutter
flutter_markdown:
meta:
moor:
moor_ffi:
musicus_client:
path: ../client
musicus_database:
path: ../database
rxdart:
url_launcher:
url_launcher: ^6.1.0
flutter:
uses-material-design: true
@ -31,4 +30,4 @@ flutter:
style: italic
- family: Musicus Icons
fonts:
- asset: fonts/musicus_icons.ttf
- asset: fonts/musicus_icons.ttf