Move more code from mobile to common

This commit is contained in:
Elias Projahn 2020-07-18 11:54:49 +02:00
parent 2e4f69a178
commit 5312bad52d
28 changed files with 258 additions and 215 deletions

View file

@ -1,156 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:musicus_common/musicus_common.dart';
import 'screens/home.dart';
import 'widgets/player_bar.dart';
class App extends StatelessWidget {
static const _platform = MethodChannel('de.johrpan.musicus/platform');
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return MaterialApp(
title: 'Musicus',
theme: ThemeData(
brightness: Brightness.dark,
accentColor: Colors.amber,
textSelectionColor: Colors.grey[600],
cursorColor: Colors.amber,
textSelectionHandleColor: Colors.amber,
toggleableActiveColor: Colors.amber,
// Added for sliders and FABs. Not everything seems to obey this.
colorScheme: ColorScheme.dark(
primary: Colors.amber,
secondary: Colors.amber,
),
snackBarTheme: SnackBarThemeData(
backgroundColor: Colors.grey[800],
contentTextStyle: TextStyle(
color: Colors.white,
),
behavior: SnackBarBehavior.floating,
),
fontFamily: 'Libertinus Sans',
),
home: Builder(
builder: (context) {
if (backend.status == MusicusBackendStatus.loading) {
return Material(
color: Theme.of(context).scaffoldBackgroundColor,
);
} else if (backend.status == MusicusBackendStatus.setup) {
return Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Choose the base path for\nyour music library.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline6,
),
SizedBox(
height: 16.0,
),
ListTile(
leading: const Icon(Icons.folder_open),
title: Text('Choose path'),
onTap: () async {
final uri =
await _platform.invokeMethod<String>('openTree');
if (uri != null) {
backend.settings.setMusicLibraryPath(uri);
}
},
),
],
),
);
} else {
return Content();
}
},
),
);
}
}
class Content extends StatefulWidget {
@override
_ContentState createState() => _ContentState();
}
class _ContentState extends State<Content> with SingleTickerProviderStateMixin {
final nestedNavigator = GlobalKey<NavigatorState>();
AnimationController playerBarAnimation;
MusicusBackendState backend;
StreamSubscription<bool> playerActiveSubscription;
@override
void initState() {
super.initState();
playerBarAnimation = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
backend = MusicusBackend.of(context);
playerBarAnimation.value = backend.playback.active.value ? 1.0 : 0.0;
if (playerActiveSubscription != null) {
playerActiveSubscription.cancel();
}
playerActiveSubscription = backend.playback.active.listen((active) =>
active ? playerBarAnimation.forward() : playerBarAnimation.reverse());
}
@override
Widget build(BuildContext context) {
// The nested Navigator is for every screen from which the player bar at
// the bottom should be accessible. The WillPopScope widget intercepts
// taps on the system back button and redirects them to the nested
// navigator.
return WillPopScope(
onWillPop: () async => !(await nestedNavigator.currentState.maybePop()),
child: Scaffold(
body: Navigator(
key: nestedNavigator,
onGenerateRoute: (settings) => settings.name == '/'
? MaterialPageRoute(
builder: (context) => HomeScreen(),
)
: null,
initialRoute: '/',
),
bottomNavigationBar: SizeTransition(
sizeFactor: CurvedAnimation(
curve: Curves.easeOut,
parent: playerBarAnimation,
),
axisAlignment: -1.0,
child: PlayerBar(),
),
),
);
}
@override
void dispose() {
super.dispose();
playerActiveSubscription.cancel();
}
}

View file

@ -1,14 +0,0 @@
import 'package:flutter/widgets.dart';
/// Custom icons.
///
/// This was generated using https://fluttericon.com/.
class MusicusIcons {
MusicusIcons._();
static const _kFontFam = 'Musicus Icons';
static const _kFontPkg = null;
static const IconData musicus =
IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View file

@ -4,7 +4,6 @@ import 'package:musicus_common/musicus_common.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart' as pp;
import 'app.dart';
import 'settings.dart';
import 'platform.dart';
import 'playback.dart';
@ -16,12 +15,11 @@ Future<void> main() async {
final dbPath = p.join(dir.path, 'db.sqlite');
runApp(AudioServiceWidget(
child: MusicusBackend(
child: MusicusApp(
dbPath: dbPath,
settingsStorage: SettingsStorage(),
platform: MusicusAndroidPlatform(),
playback: Playback(),
child: App(),
),
));
}

View file

@ -4,6 +4,11 @@ import 'package:musicus_common/musicus_common.dart';
class MusicusAndroidPlatform extends MusicusPlatform {
static const _platform = MethodChannel('de.johrpan.musicus/platform');
@override
Future<String> chooseBasePath() async {
return await _platform.invokeMethod<String>('openTree');
}
@override
Future<List<Document>> getChildren(String parentId) async {
final List<Map<dynamic, dynamic>> childrenJson =

View file

@ -1,40 +0,0 @@
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;
class AboutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
return Scaffold(
appBar: AppBar(
title: Text('About'),
),
body: FutureBuilder<String>(
future: rootBundle.loadString('assets/about.md'),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Markdown(
data: snapshot.data,
styleSheet: MarkdownStyleSheet(
h1: textTheme.headline6.copyWith(
height: 2.0,
),
a: textTheme.bodyText1.copyWith(
color: theme.accentColor,
decoration: TextDecoration.underline,
),
),
onTapLink: (link) => url.launch(link),
);
} else {
return Container();
}
},
),
);
}
}

View file

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

View file

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

View file

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

View file

@ -1,129 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_common/musicus_common.dart';
import '../icons.dart';
import 'about.dart';
import 'person.dart';
import 'settings.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String _search;
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
leading: Icon(
MusicusIcons.musicus,
color: Colors.amber,
),
title: TextField(
autofocus: true,
onChanged: (text) {
setState(() {
_search = text;
});
},
decoration: InputDecoration.collapsed(
hintText: 'Composers',
),
),
actions: <Widget>[
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text('Add tracks'),
),
PopupMenuItem(
value: 2,
child: Text('Settings'),
),
PopupMenuItem(
value: 3,
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) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AboutScreen(),
),
);
}
},
),
],
),
body: PagedListView<Person>(
search: _search,
fetch: (page, search) async {
return await backend.db.getPersons(page, search);
},
builder: (context, person) => ListTile(
title: Text('${person.lastName}, ${person.firstName}'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PersonScreen(
person: person,
),
),
),
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Edit person'),
onTap: () async {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => PersonEditor(
person: person,
),
fullscreenDialog: true,
),
);
},
),
],
);
},
);
},
),
),
);
}
}

View file

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

View file

@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_common/musicus_common.dart';
import 'work.dart';
class PersonScreen extends StatefulWidget {
final Person person;
PersonScreen({
this.person,
});
@override
_PersonScreenState createState() => _PersonScreenState();
}
class _PersonScreenState extends State<PersonScreen> {
String _search;
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: TextField(
autofocus: true,
onChanged: (text) {
setState(() {
_search = text;
});
},
decoration: InputDecoration.collapsed(
hintText:
'Works by ${widget.person.firstName} ${widget.person.lastName}',
),
),
),
body: PagedListView<WorkInfo>(
search: _search,
fetch: (page, search) async {
return await backend.db.getWorks(widget.person.id, page, search);
},
builder: (context, workInfo) => ListTile(
title: Text(workInfo.work.title),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WorkScreen(
workInfo: workInfo,
),
),
),
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Edit work'),
onTap: () async {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => WorkEditor(
workInfo: workInfo,
),
fullscreenDialog: true,
),
);
},
),
],
);
},
);
},
),
),
);
}
}

View file

@ -1,318 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_common/musicus_common.dart';
import '../widgets/play_pause_button.dart';
class ProgramScreen extends StatefulWidget {
@override
_ProgramScreenState createState() => _ProgramScreenState();
}
class _ProgramScreenState extends State<ProgramScreen> {
MusicusBackendState backend;
StreamSubscription<bool> playerActiveSubscription;
StreamSubscription<List<InternalTrack>> playlistSubscription;
List<Widget> widgets = [];
StreamSubscription<double> positionSubscription;
double position = 0.0;
bool seeking = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
backend = MusicusBackend.of(context);
if (playerActiveSubscription != null) {
playerActiveSubscription.cancel();
}
// Close the program screen, if the player is no longer active.
playerActiveSubscription = backend.playback.active.listen((active) {
if (!active) {
Navigator.pop(context);
}
});
if (playlistSubscription != null) {
playlistSubscription.cancel();
}
playlistSubscription = backend.playback.playlist.listen((playlist) {
updateProgram(playlist);
});
if (positionSubscription != null) {
positionSubscription.cancel();
}
positionSubscription = backend.playback.normalizedPosition.listen((pos) {
if (!seeking) {
setState(() {
position = pos;
});
}
});
}
/// Go through the tracks of [playlist] and preprocess them for displaying.
Future<void> updateProgram(List<InternalTrack> playlist) async {
List<Widget> newWidgets = [];
// The following variables exist to adapt the resulting ProgramItem to its
// predecessor.
// If the previous recording was the same, we won't need to include the
// recording data again.
int lastRecordingId;
// If the previous work was the same, we won't need to retrieve its parts
// from the database again.
int lastWorkId;
// This will contain information on the last new work.
WorkInfo workInfo;
// The index of the last displayed section.
int lastSectionIndex;
for (var i = 0; i < playlist.length; i++) {
// The widgets displayed for this track.
List<Widget> children = [];
final track = playlist[i];
final recordingId = track.track.recordingId;
final partIds = track.track.partIds;
// If the recording is the same, the work will also be the same, so
// workInfo doesn't have to be updated either.
if (recordingId != lastRecordingId) {
lastRecordingId = recordingId;
final recordingInfo = await backend.db.getRecording(recordingId);
if (recordingInfo.recording.work != lastWorkId) {
lastWorkId = recordingInfo.recording.work;
workInfo = await backend.db.getWork(lastWorkId);
lastSectionIndex = null;
}
children.addAll([
RecordingTile(
workInfo: workInfo,
recordingInfo: recordingInfo,
),
SizedBox(
height: 8.0,
),
]);
}
for (final partId in partIds) {
final partInfo = workInfo.parts[partId];
final sectionIndex = workInfo.sections
.lastIndexWhere((s) => s.beforePartIndex <= partId);
if (sectionIndex != lastSectionIndex && sectionIndex >= 0) {
lastSectionIndex = sectionIndex;
children.add(Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
),
child: Text(workInfo.sections[sectionIndex].title),
));
}
children.add(Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
child: Text(
partInfo.part.title,
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
));
}
newWidgets.add(Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
));
}
// Check, whether we are still a part of the widget tree, because this
// function might take some time.
if (mounted) {
setState(() {
widgets = newWidgets;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => Navigator.pop(context),
),
title: Text('Program'),
),
body: StreamBuilder<int>(
stream: backend.playback.currentIndex,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, index) {
return InkWell(
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(4.0),
child: index == snapshot.data
? const Icon(Icons.play_arrow)
: SizedBox(
width: 24.0,
height: 24.0,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: widgets[index],
),
),
],
),
onTap: () {
backend.playback.skipTo(index);
},
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Remove from playlist'),
onTap: () {
backend.playback.removeTrack(index);
Navigator.pop(context);
},
),
],
);
});
},
);
},
);
} else {
return Container();
}
},
),
bottomNavigationBar: BottomAppBar(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
value: position,
onChangeStart: (_) {
seeking = true;
},
onChangeEnd: (pos) {
seeking = false;
backend.playback.seekTo(pos);
},
onChanged: (pos) {
setState(() {
position = pos;
});
},
),
Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 24.0),
child: StreamBuilder<Duration>(
stream: backend.playback.position,
builder: (context, snapshot) {
if (snapshot.hasData) {
return DurationText(snapshot.data);
} else {
return Container();
}
},
),
),
Spacer(),
IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: () {
backend.playback.skipToPrevious();
},
),
PlayPauseButton(),
IconButton(
icon: const Icon(Icons.skip_next),
onPressed: () {
backend.playback.skipToNext();
},
),
Spacer(),
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: StreamBuilder<Duration>(
stream: backend.playback.duration,
builder: (context, snapshot) {
if (snapshot.hasData) {
return DurationText(snapshot.data);
} else {
return Container();
}
},
),
),
],
),
],
),
),
);
}
@override
void dispose() {
super.dispose();
playerActiveSubscription.cancel();
playlistSubscription.cancel();
positionSubscription.cancel();
}
}
class DurationText extends StatelessWidget {
final Duration duration;
DurationText(this.duration);
@override
Widget build(BuildContext context) {
final minutes = duration.inMinutes;
final seconds = (duration - Duration(minutes: minutes)).inSeconds;
final secondsString = seconds >= 10 ? seconds.toString() : '0$seconds';
return Text('$minutes:$secondsString');
}
}

View file

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

View file

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

View file

@ -1,90 +0,0 @@
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 'account_settings.dart';
import 'server_settings.dart';
class SettingsScreen extends StatelessWidget {
static const _platform = MethodChannel('de.johrpan.musicus/platform');
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
final settings = backend.settings;
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: ListView(
children: <Widget>[
StreamBuilder<String>(
stream: settings.musicLibraryPath,
builder: (context, snapshot) {
return ListTile(
title: Text('Music library path'),
subtitle: Text(snapshot.data ?? 'Choose folder'),
isThreeLine: snapshot.hasData,
onTap: () async {
final uri = await _platform.invokeMethod<String>('openTree');
if (uri != null) {
settings.setMusicLibraryPath(uri);
}
},
);
},
),
StreamBuilder<MusicusServerSettings>(
stream: settings.server,
builder: (context, snapshot) {
final s = snapshot.data;
return ListTile(
title: Text('Musicus server'),
subtitle:
Text(s != null ? '${s.host}:${s.port}${s.apiPath}' : '...'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final MusicusServerSettings result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ServerSettingsScreen(),
),
);
if (result != null) {
settings.setServer(result);
}
},
);
},
),
StreamBuilder<MusicusAccountCredentials>(
stream: settings.account,
builder: (context, snapshot) {
final credentials = snapshot.data;
return ListTile(
title: Text('Account settings'),
subtitle: Text(
credentials != null ? credentials.username : 'No account'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AccountSettingsScreen(),
),
);
},
);
},
),
],
),
);
}
}

View file

@ -1,66 +0,0 @@
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_common/musicus_common.dart';
class WorkScreen extends StatelessWidget {
final WorkInfo workInfo;
WorkScreen({
this.workInfo,
});
@override
Widget build(BuildContext context) {
final backend = MusicusBackend.of(context);
return Scaffold(
appBar: AppBar(
title: Text(workInfo.work.title),
),
body: PagedListView<RecordingInfo>(
fetch: (page, _) async {
return await backend.db.getRecordings(workInfo.work.id, page);
},
builder: (context, recordingInfo) {
final recordingId = recordingInfo.recording.id;
return ListTile(
title: PerformancesText(
performanceInfos: recordingInfo.performances,
),
onTap: () {
final tracks = backend.library.tracks[recordingId];
tracks.sort((t1, t2) => t1.track.index.compareTo(t2.track.index));
backend.playback.addTracks(tracks);
},
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
children: <Widget>[
ListTile(
title: Text('Edit recording'),
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => RecordingEditor(
recordingInfo: recordingInfo,
),
fullscreenDialog: true,
),
);
},
),
],
);
},
);
},
);
},
),
);
}
}

View file

@ -1,58 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_common/musicus_common.dart';
class PlayPauseButton extends StatefulWidget {
@override
_PlayPauseButtonState createState() => _PlayPauseButtonState();
}
class _PlayPauseButtonState extends State<PlayPauseButton>
with SingleTickerProviderStateMixin {
AnimationController playPauseAnimation;
MusicusBackendState backend;
StreamSubscription<bool> playingSubscription;
@override
void initState() {
super.initState();
playPauseAnimation = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
backend = MusicusBackend.of(context);
playPauseAnimation.value = backend.playback.playing.value ? 1.0 : 0.0;
if (playingSubscription != null) {
playingSubscription.cancel();
}
playingSubscription = backend.playback.playing.listen((playing) =>
playing ? playPauseAnimation.forward() : playPauseAnimation.reverse());
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: playPauseAnimation,
),
onPressed: backend.playback.playPause,
);
}
@override
void dispose() {
super.dispose();
playingSubscription.cancel();
}
}

View file

@ -1,134 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:musicus_client/musicus_client.dart';
import 'package:musicus_common/musicus_common.dart';
import '../screens/program.dart';
import 'play_pause_button.dart';
class PlayerBar extends StatefulWidget {
@override
_PlayerBarState createState() => _PlayerBarState();
}
class _PlayerBarState extends State<PlayerBar> {
MusicusBackendState _backend;
StreamSubscription<InternalTrack> _currentTrackSubscribtion;
WorkInfo _workInfo;
List<int> _partIds;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_backend = MusicusBackend.of(context);
_currentTrackSubscribtion?.cancel();
_currentTrackSubscribtion = _backend.playback.currentTrack.listen((track) {
if (track != null) {
_setTrack(track.track);
}
});
}
Future<void> _setTrack(Track track) async {
final recording =
await _backend.db.recordingById(track.recordingId).getSingle();
final workInfo = await _backend.db.getWork(recording.work);
final partIds = track.partIds;
if (mounted) {
setState(() {
_workInfo = workInfo;
_partIds = partIds;
});
}
}
@override
Widget build(BuildContext context) {
String title;
String subtitle;
if (_workInfo != null) {
title = _workInfo.composers
.map((p) => '${p.firstName} ${p.lastName}')
.join(', ');
final subtitleBuffer = StringBuffer(_workInfo.work.title);
if (_partIds.isNotEmpty) {
subtitleBuffer.write(': ');
final section = _workInfo.sections.lastWhere(
(s) => s.beforePartIndex <= _partIds[0],
orElse: () => null,
);
if (section != null) {
subtitleBuffer.write(section.title);
subtitleBuffer.write(': ');
}
subtitleBuffer.write(
_partIds.map((i) => _workInfo.parts[i].part.title).join(', '));
}
subtitle = subtitleBuffer.toString();
} else {
title = '...';
subtitle = '...';
}
return BottomAppBar(
child: InkWell(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
StreamBuilder(
stream: _backend.playback.normalizedPosition,
builder: (context, snapshot) => LinearProgressIndicator(
value: snapshot.data,
),
),
Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.keyboard_arrow_up),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DefaultTextStyle.merge(
style: TextStyle(fontWeight: FontWeight.bold),
child: Text(title),
),
Text(subtitle),
],
),
),
PlayPauseButton(),
],
),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProgramScreen(),
),
),
),
);
}
@override
void dispose() {
super.dispose();
_currentTrackSubscribtion?.cancel();
}
}