mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-25 19:27:24 +02:00
common: Adapt to dependencies and remove editing
This commit is contained in:
parent
0ddf0ff84a
commit
777c89fed4
32 changed files with 45 additions and 3217 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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('');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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(', '));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue