mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 02:37:25 +01:00
Move more code from mobile to common
This commit is contained in:
parent
2e4f69a178
commit
5312bad52d
28 changed files with 258 additions and 215 deletions
|
|
@ -1,25 +0,0 @@
|
|||
# Introduction
|
||||
|
||||
Musicus is a classical music player and organizer.
|
||||
|
||||
# Contact
|
||||
|
||||
Please contact me [via e-mail](mailto:johrpan@gmail.com?subject=Musicus), if
|
||||
you have any questions or need help. I'm also open to ideas for the future of
|
||||
Musicus! Musicus is free and open source software. You can study the source
|
||||
code and contribute to it on [GitHub](https://github.com/johrpan/musicus).
|
||||
|
||||
# License
|
||||
|
||||
© 2019–2020 Elias Projahn
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option) any
|
||||
later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. See the
|
||||
[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html)
|
||||
for more details.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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('');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,34 +12,13 @@ dependencies:
|
|||
audio_service:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_markdown:
|
||||
meta:
|
||||
moor:
|
||||
moor_ffi:
|
||||
musicus_client:
|
||||
path: ../client
|
||||
musicus_common:
|
||||
path: ../common
|
||||
musicus_player:
|
||||
path: ../player
|
||||
path:
|
||||
path_provider:
|
||||
rxdart:
|
||||
shared_preferences:
|
||||
url_launcher:
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/about.md
|
||||
fonts:
|
||||
- family: Libertinus Sans
|
||||
fonts:
|
||||
- asset: fonts/libertinussans_regular.otf
|
||||
- asset: fonts/libertinussans_bold.otf
|
||||
weight: 700
|
||||
- asset: fonts/libertinussans_italic.otf
|
||||
style: italic
|
||||
- family: Musicus Icons
|
||||
fonts:
|
||||
- asset: fonts/musicus_icons.ttf
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue