mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 10:47:25 +01:00
This introduces a new state of the backend called "setup". If the music library path is not set, the backend goes into that state and the app widget can show the UI to set it up. This also introduces a new dependency on shared_preferences.
185 lines
5.7 KiB
Dart
185 lines
5.7 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'backend.dart';
|
|
import 'screens/home.dart';
|
|
import 'selectors/files.dart';
|
|
import 'widgets/player_bar.dart';
|
|
|
|
class App extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final backend = Backend.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,
|
|
),
|
|
fontFamily: 'Libertinus Sans',
|
|
),
|
|
home: Builder(
|
|
builder: (context) {
|
|
if (backend.status == BackendStatus.loading) {
|
|
return Material(
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
);
|
|
} else if (backend.status == BackendStatus.needsPermissions) {
|
|
return Material(
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
Text(
|
|
'Musicus needs permissions\nto access your files.',
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headline6,
|
|
),
|
|
SizedBox(
|
|
height: 16.0,
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.done),
|
|
title: Text('Grant permissions'),
|
|
onTap: () {
|
|
backend.requestPermissions();
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.settings),
|
|
title: Text('Open system\'s app settings'),
|
|
onTap: () {
|
|
backend.openAppSettings();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} else if (backend.status == BackendStatus.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 path = await Navigator.push<String>(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => FilesSelector(
|
|
mode: FilesSelectorMode.directory,
|
|
),
|
|
fullscreenDialog: true,
|
|
),
|
|
);
|
|
|
|
if (path != null) {
|
|
backend.setMusicLibraryPath(path);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} else {
|
|
return Content();
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class Content extends StatefulWidget {
|
|
@override
|
|
_ContentState createState() => _ContentState();
|
|
}
|
|
|
|
class _ContentState extends State<Content> with SingleTickerProviderStateMixin {
|
|
final nestedNavigator = GlobalKey<NavigatorState>();
|
|
|
|
AnimationController playerBarAnimation;
|
|
BackendState backend;
|
|
StreamSubscription<bool> playerActiveSubscription;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
playerBarAnimation = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 300),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
|
|
backend = Backend.of(context);
|
|
playerBarAnimation.value = backend.playerActive.value ? 1.0 : 0.0;
|
|
|
|
if (playerActiveSubscription != null) {
|
|
playerActiveSubscription.cancel();
|
|
}
|
|
|
|
playerActiveSubscription = backend.playerActive.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();
|
|
}
|
|
}
|