diff --git a/mobile/lib/screens/account_settings.dart b/mobile/lib/screens/account_settings.dart new file mode 100644 index 0000000..3815171 --- /dev/null +++ b/mobile/lib/screens/account_settings.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:musicus_common/musicus_common.dart'; + +import 'register.dart'; + +/// A screen for logging in using a Musicus account. +class AccountSettingsScreen extends StatefulWidget { + @override + _AccountSettingsScreenState createState() => _AccountSettingsScreenState(); +} + +class _AccountSettingsScreenState extends State { + final nameController = TextEditingController(); + final passwordController = TextEditingController(); + + MusicusBackendState backend; + StreamSubscription accountSubscription; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + backend = MusicusBackend.of(context); + + if (accountSubscription != null) { + accountSubscription.cancel(); + } + + _settingsChanged(backend.settings.account.value); + accountSubscription = backend.settings.account.listen((settings) { + _settingsChanged(settings); + }); + } + + void _settingsChanged(MusicusAccountSettings settings) { + nameController.text = settings?.username ?? ''; + passwordController.text = settings?.password ?? ''; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Account settings'), + actions: [ + FlatButton( + onPressed: () async { + await backend.settings.setAccount(MusicusAccountSettings( + username: nameController.text, + password: passwordController.text, + )); + + Navigator.pop(context); + }, + child: Text('LOGIN'), + ), + ], + ), + body: ListView( + children: [ + 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: passwordController, + obscureText: true, + decoration: InputDecoration( + labelText: 'Password', + ), + ), + ), + ListTile( + title: Text('Create a new account'), + onTap: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => RegisterScreen(), + ), + ); + }, + ), + ListTile( + title: Text('Don\'t use an account'), + onTap: () { + backend.settings.clearAccount(); + Navigator.pop(context); + }, + ), + ], + ), + ); + } + + @override + void dispose() { + super.dispose(); + accountSubscription.cancel(); + } +} diff --git a/mobile/lib/screens/register.dart b/mobile/lib/screens/register.dart new file mode 100644 index 0000000..056465d --- /dev/null +++ b/mobile/lib/screens/register.dart @@ -0,0 +1,142 @@ +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 { + @override + _RegisterScreenState createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final nameController = TextEditingController(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final repeatController = TextEditingController(); + + bool _loading = false; + + @override + Widget build(BuildContext context) { + final backend = MusicusBackend.of(context); + + return Scaffold( + appBar: AppBar( + title: Text('Create account'), + actions: [ + Builder( + builder: (context) { + if (!_loading) { + return FlatButton( + onPressed: () async { + if (_verify()) { + setState(() { + _loading = true; + }); + + final success = await backend.client.register(User( + name: nameController.text, + email: emailController.text, + password: passwordController.text, + )); + + setState(() { + _loading = false; + }); + + if (success) { + await backend.settings + .setAccount(MusicusAccountSettings( + username: nameController.text, + email: emailController.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: [ + 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; + } +} diff --git a/mobile/lib/screens/settings.dart b/mobile/lib/screens/settings.dart index 176e117..227c5ef 100644 --- a/mobile/lib/screens/settings.dart +++ b/mobile/lib/screens/settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:musicus_common/musicus_common.dart'; +import 'account_settings.dart'; import 'server_settings.dart'; class SettingsScreen extends StatelessWidget { @@ -19,46 +20,67 @@ class SettingsScreen extends StatelessWidget { body: ListView( children: [ StreamBuilder( - 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('openTree'); + 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('openTree'); - if (uri != null) { - settings.setMusicLibraryPath(uri); - } - }, - ); - }), + if (uri != null) { + settings.setMusicLibraryPath(uri); + } + }, + ); + }, + ), StreamBuilder( - stream: settings.server, - builder: (context, snapshot) { - final s = snapshot.data; + 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(), - ), - ); + 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); - } - }, - ); - }), + if (result != null) { + settings.setServer(result); + } + }, + ); + }, + ), + StreamBuilder( + stream: settings.account, + builder: (context, snapshot) { + final a = snapshot.data; + + return ListTile( + title: Text('Account settings'), + subtitle: Text(a != null ? a.username : 'No account'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AccountSettingsScreen(), + ), + ); + }, + ); + }, + ), ], ), );