freezer/lib/main.dart

495 lines
16 KiB
Dart
Raw Normal View History

import 'dart:async';
2020-06-23 19:23:12 +00:00
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
2020-07-16 20:25:30 +00:00
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
2021-03-21 21:46:44 +00:00
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
2021-08-29 22:25:18 +00:00
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/definitions.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/ui/library.dart';
import 'package:freezer/ui/login_screen.dart';
import 'package:freezer/ui/search.dart';
import 'package:freezer/ui/updater.dart';
import 'package:i18n_extension/i18n_widget.dart';
2020-06-23 19:23:12 +00:00
import 'package:move_to_background/move_to_background.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:uni_links/uni_links.dart';
2020-06-23 19:23:12 +00:00
import 'api/deezer.dart';
import 'api/download.dart';
import 'api/player.dart';
import 'settings.dart';
2020-06-23 19:23:12 +00:00
import 'ui/home_screen.dart';
import 'ui/player_bar.dart';
2020-07-16 20:25:30 +00:00
2021-09-01 12:38:32 +00:00
late Function updateTheme;
late Function logOut;
2020-06-23 19:23:12 +00:00
GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
2021-09-01 12:38:32 +00:00
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
2021-08-30 12:51:51 +00:00
void main() async {
2020-06-23 19:23:12 +00:00
WidgetsFlutterBinding.ensureInitialized();
//Initialize globals
settings = await Settings().loadSettings();
await downloadManager.init();
cache = await Cache.load();
2020-06-23 19:23:12 +00:00
//Do on BG
playerHelper.authorizeLastFM();
2021-11-01 16:41:25 +00:00
await playerHelper.initAudioHandler();
2021-09-01 12:38:32 +00:00
2020-06-23 19:23:12 +00:00
runApp(FreezerApp());
}
class FreezerApp extends StatefulWidget {
@override
_FreezerAppState createState() => _FreezerAppState();
}
class _FreezerAppState extends State<FreezerApp> {
2021-11-01 16:41:25 +00:00
late StreamSubscription _playbackStateSub;
2020-06-23 19:23:12 +00:00
@override
void initState() {
2021-11-01 16:41:25 +00:00
_initStateAsync();
2020-06-23 19:23:12 +00:00
//Make update theme global
updateTheme = _updateTheme;
super.initState();
}
2021-11-01 16:41:25 +00:00
Future<void> _initStateAsync() async {
_playbackStateChanged(audioHandler.playbackState.value);
_playbackStateSub =
audioHandler.playbackState.listen(_playbackStateChanged);
}
Future<void> _playbackStateChanged(PlaybackState playbackState) async {
if (playbackState.processingState == AudioProcessingState.idle ||
playbackState.processingState == AudioProcessingState.error) {
// reconnect maybe?
return;
}
}
@override
void dispose() {
2021-11-01 16:41:25 +00:00
_playbackStateSub.cancel();
super.dispose();
}
2020-06-23 19:23:12 +00:00
void _updateTheme() {
setState(() {
settings.themeData;
});
}
2021-09-01 12:38:32 +00:00
Locale? _locale() {
if (settings.language == null || settings.language!.split('_').length < 2)
2021-04-05 21:27:15 +00:00
return null;
return Locale(
2021-09-01 12:38:32 +00:00
settings.language!.split('_')[0], settings.language!.split('_')[1]);
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
2021-08-29 22:25:18 +00:00
return ScreenUtilInit(
designSize: Size(1080, 720),
builder: () => MaterialApp(
title: 'Freezer',
shortcuts: <ShortcutActivator, Intent>{
...WidgetsApp.defaultShortcuts,
LogicalKeySet(LogicalKeyboardKey.select):
const ActivateIntent(), // DPAD center key, for remote controls
2020-06-23 19:23:12 +00:00
},
2021-08-29 22:25:18 +00:00
theme: settings.themeData,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: WillPopScope(
onWillPop: () async {
2021-09-01 12:38:32 +00:00
if (navigatorKey.currentState!.canPop()) {
await navigatorKey.currentState!.maybePop();
2021-08-29 22:25:18 +00:00
return false;
}
await MoveToBackground.moveTaskToBack();
return false;
},
child: I18n(
initialLocale: _locale(),
child: LoginMainWrapper(),
),
),
2021-08-29 22:25:18 +00:00
navigatorKey: mainNavigatorKey,
2020-06-23 19:23:12 +00:00
),
);
}
}
//Wrapper for login and main screen.
class LoginMainWrapper extends StatefulWidget {
@override
_LoginMainWrapperState createState() => _LoginMainWrapperState();
}
class _LoginMainWrapperState extends State<LoginMainWrapper> {
@override
void initState() {
if (settings.arl != null) {
playerHelper.start();
//Load token on background
deezerAPI.arl = settings.arl;
settings.offlineMode = true;
2021-11-01 16:41:25 +00:00
deezerAPI.authorize().then((b) async {
2020-06-23 19:23:12 +00:00
if (b) setState(() => settings.offlineMode = false);
});
}
2020-07-16 20:25:30 +00:00
//Global logOut function
logOut = _logOut;
2020-06-23 19:23:12 +00:00
super.initState();
}
2020-07-16 20:25:30 +00:00
Future _logOut() async {
setState(() {
settings.arl = null;
settings.offlineMode = false;
2020-07-16 20:25:30 +00:00
deezerAPI = new DeezerAPI();
});
await settings.save();
await Cache.wipe();
2020-07-16 20:25:30 +00:00
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
if (settings.arl == null)
2021-09-02 20:45:14 +00:00
return AnnotatedRegion<SystemUiOverlayStyle>(
value: (Theme.of(context).brightness == Brightness.dark
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light)
.copyWith(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
statusBarIconBrightness: Brightness.light,
),
child: LoginWidget(
callback: () => setState(() => {}),
),
);
2020-06-23 19:23:12 +00:00
return MainScreen();
}
}
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
2021-04-05 21:27:15 +00:00
class _MainScreenState extends State<MainScreen>
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
2021-09-01 12:38:32 +00:00
final _selected = ValueNotifier<int>(0);
StreamSubscription? _urlLinkStream;
2020-10-31 20:52:23 +00:00
int _keyPressed = 0;
bool textFieldVisited = false;
2021-09-02 20:45:14 +00:00
final _slideTween = Tween<Offset>(
begin: const Offset(0.0, 0.025), end: const Offset(0.0, 0.0));
final _scaleTween = Tween(begin: 0.975, end: 1.0);
2020-06-23 19:23:12 +00:00
@override
void initState() {
2021-03-21 21:46:44 +00:00
//Set display mode
2021-09-01 12:38:32 +00:00
if (settings.displayMode != null && settings.displayMode! >= 0) {
2021-03-21 21:46:44 +00:00
FlutterDisplayMode.supported.then((modes) async {
2021-09-01 12:38:32 +00:00
if (modes.length - 1 >= settings.displayMode!)
FlutterDisplayMode.setPreferredMode(modes[settings.displayMode!]);
2021-03-21 21:46:44 +00:00
});
}
2020-11-28 21:32:17 +00:00
_startStreamingServer();
//Start with parameters
_setupUniLinks();
_loadPreloadInfo();
_prepareQuickActions();
//Check for updates on background
Future.delayed(Duration(seconds: 5), () {
FreezerVersions.checkUpdate();
});
2020-06-23 19:23:12 +00:00
super.initState();
2021-09-01 12:38:32 +00:00
WidgetsBinding.instance!.addObserver(this);
2020-06-23 19:23:12 +00:00
}
2020-11-28 21:32:17 +00:00
void _startStreamingServer() async {
2021-04-05 21:27:15 +00:00
await DownloadManager.platform
.invokeMethod("startServer", {"arl": settings.arl});
2020-11-28 21:32:17 +00:00
}
void _prepareQuickActions() {
final QuickActions quickActions = QuickActions();
quickActions.initialize((type) {
2021-09-01 12:38:32 +00:00
_startPreload(type);
});
//Actions
quickActions.setShortcutItems([
2021-04-05 21:27:15 +00:00
ShortcutItem(
type: 'favorites',
localizedTitle: 'Favorites'.i18n,
icon: 'ic_favorites'),
ShortcutItem(type: 'flow', localizedTitle: 'Flow'.i18n, icon: 'ic_flow'),
]);
}
void _startPreload(String type) async {
await deezerAPI.authorize();
if (type == 'flow') {
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow'));
return;
}
if (type == 'favorites') {
Playlist p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
2021-09-01 12:38:32 +00:00
playerHelper.playFromPlaylist(p, p.tracks![0]!.id);
}
}
void _loadPreloadInfo() async {
2021-09-01 12:38:32 +00:00
String? info =
await DownloadManager.platform.invokeMethod('getPreloadInfo');
if (info != null) {
//Used if started from android auto
await deezerAPI.authorize();
_startPreload(info);
}
}
@override
void dispose() {
2021-09-01 12:38:32 +00:00
_urlLinkStream?.cancel();
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
textFieldVisited = false;
});
}
}
void _setupUniLinks() async {
//Listen to URLs
2021-09-01 12:38:32 +00:00
_urlLinkStream = linkStream.listen((String? link) {
if (link == null) return;
openScreenByURL(context, link);
}, onError: (err) {});
//Get initial link on cold start
try {
2021-09-01 12:38:32 +00:00
String? link = await getInitialLink();
2021-04-05 21:27:15 +00:00
if (link != null && link.length > 4) openScreenByURL(context, link);
} catch (e) {}
}
2021-04-05 21:27:15 +00:00
ValueChanged<RawKeyEvent> _handleKey(
FocusScopeNode navigationBarFocusNode, FocusNode screenFocusNode) {
2020-10-31 20:52:23 +00:00
return (event) {
2021-09-01 12:38:32 +00:00
FocusNode primaryFocus = FocusManager.instance.primaryFocus!;
// After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s.
// So, set this flag to indicate a transition to other "mode"
2021-09-01 12:38:32 +00:00
if (primaryFocus.context!.widget.runtimeType.toString() ==
2021-04-05 21:27:15 +00:00
'EditableText') {
setState(() {
textFieldVisited = true;
});
}
// Movement to navigation bar and back
2021-04-05 21:27:15 +00:00
if (event.runtimeType.toString() ==
(textFieldVisited ? 'RawKeyUpEvent' : 'RawKeyDownEvent')) {
2020-10-31 20:52:23 +00:00
int keyCode = (event.data as RawKeyEventDataAndroid).keyCode;
switch (keyCode) {
case 127: // Menu on Android TV
case 327: // EPG on Hisense TV
focusToNavbar(navigationBarFocusNode);
2020-10-31 20:52:23 +00:00
break;
case 22: // LEFT + RIGHT
case 21:
2021-04-05 21:27:15 +00:00
if (_keyPressed == 21 && keyCode == 22 ||
_keyPressed == 22 && keyCode == 21) {
focusToNavbar(navigationBarFocusNode);
2020-10-31 20:52:23 +00:00
}
_keyPressed = keyCode;
2021-04-05 21:27:15 +00:00
Future.delayed(
Duration(milliseconds: 100), () => {_keyPressed = 0});
2020-10-31 20:52:23 +00:00
break;
case 20: // DOWN
// If it's bottom row, go to navigation bar
var row = primaryFocus.parent;
if (row != null) {
2021-09-01 12:38:32 +00:00
var column = row.parent!;
if (column.children.last == row) {
focusToNavbar(navigationBarFocusNode);
}
}
break;
2020-10-31 20:52:23 +00:00
case 19: // UP
if (navigationBarFocusNode.hasFocus) {
2021-09-01 12:38:32 +00:00
screenFocusNode.parent!.parent!.children
2021-04-05 21:27:15 +00:00
.last // children.last is used for handling "playlists" screen in library. Under CustomNavigator 2 screens appears.
.nextFocus(); // nextFocus is used instead of requestFocus because it focuses on last, bottom, non-visible tile of main page
2020-10-31 20:52:23 +00:00
}
break;
}
}
// After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s.
// Focus moving works only on KeyDown events, so here we simulate keys handling as it's done in Flutter
if (textFieldVisited && event.runtimeType.toString() == 'RawKeyUpEvent') {
2021-09-01 12:38:32 +00:00
Map<LogicalKeySet, Intent> shortcuts =
Shortcuts.of(context).shortcuts as Map<LogicalKeySet, Intent>;
final BuildContext? primaryContext = primaryFocus.context;
Intent? intent = shortcuts[LogicalKeySet(event.logicalKey)];
if (intent != null) {
2021-09-01 12:38:32 +00:00
Actions.invoke(primaryContext!, intent);
}
// WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging.
2021-09-01 12:38:32 +00:00
FocusNode? newFocus = FocusManager.instance.primaryFocus;
if (newFocus is FocusScopeNode) {
navigationBarFocusNode.requestFocus();
2020-10-31 20:52:23 +00:00
}
}
};
}
void focusToNavbar(FocusScopeNode navigatorFocusNode) {
navigatorFocusNode.requestFocus();
2021-04-05 21:27:15 +00:00
navigatorFocusNode.focusInDirection(TraversalDirection
.down); // If player bar is hidden, focus won't be visible, so go down once more
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
2021-04-05 21:27:15 +00:00
FocusScopeNode navigationBarFocusNode =
FocusScopeNode(); // for bottom navigation bar
FocusNode screenFocusNode = FocusNode(); // for CustomNavigator
2020-10-31 20:52:23 +00:00
return RawKeyboardListener(
focusNode: FocusNode(),
onKey: _handleKey(navigationBarFocusNode, screenFocusNode),
2020-10-31 20:52:23 +00:00
child: Scaffold(
2021-09-01 12:38:32 +00:00
bottomNavigationBar: FocusScope(
node: navigationBarFocusNode,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PlayerBar(),
ValueListenableBuilder<int>(
valueListenable: _selected,
builder: (context, value, _) {
return BottomNavigationBar(
backgroundColor: Theme.of(context).bottomAppBarColor,
currentIndex: value,
onTap: (int s) async {
//Pop all routes until home screen
while (navigatorKey.currentState!.canPop()) {
await navigatorKey.currentState!.maybePop();
}
await navigatorKey.currentState!.maybePop();
2021-09-02 20:45:14 +00:00
if (_selected.value != s) _selected.value = s;
2021-09-01 12:38:32 +00:00
},
selectedItemColor: Theme.of(context).primaryColor,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), label: 'Home'.i18n),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search'.i18n,
),
BottomNavigationBarItem(
icon: Icon(Icons.library_music),
label: 'Library'.i18n)
],
);
})
],
)),
body: _MainRouteNavigator(
navigatorKey: navigatorKey,
home: Focus(
focusNode: screenFocusNode,
skipTraversal: true,
canRequestFocus: false,
child: ValueListenableBuilder<int>(
valueListenable: _selected,
2021-09-02 20:45:14 +00:00
builder: (context, value, _) => AnimatedSwitcher(
duration: Duration(milliseconds: 250),
transitionBuilder: (child, animation) =>
SlideTransition(
position: _slideTween.animate(animation),
child: ScaleTransition(
scale: _scaleTween.animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
)),
layoutBuilder: (currentChild, previousChildren) =>
currentChild!,
child: _screens[value],
)))),
2021-09-01 12:38:32 +00:00
));
2020-06-23 19:23:12 +00:00
}
}
2021-08-30 12:51:51 +00:00
2021-09-01 12:38:32 +00:00
// hella simple null-safe reimplementation of custom_navigator, which is NOT null-safe
2021-08-30 12:51:51 +00:00
class _MainRouteNavigator extends StatelessWidget with WidgetsBindingObserver {
final Widget home;
final GlobalKey<NavigatorState> navigatorKey;
2021-09-01 12:38:32 +00:00
const _MainRouteNavigator(
{Key? key, required this.home, required this.navigatorKey})
2021-08-30 12:51:51 +00:00
: super(key: key);
// A system method that get invoked when user press back button on Android or back slide on iOS
@override
Future<bool> didPopRoute() async {
2021-09-01 12:38:32 +00:00
final NavigatorState? navigator = navigatorKey.currentState;
2021-08-30 12:51:51 +00:00
if (navigator == null) return false;
return await navigator.maybePop();
}
@override
Future<bool> didPushRoute(String route) async {
2021-09-01 12:38:32 +00:00
final NavigatorState? navigator = navigatorKey.currentState;
2021-08-30 12:51:51 +00:00
if (navigator == null) return false;
navigator.pushNamed(route);
return true;
}
@override
Widget build(BuildContext context) {
return Navigator(
2021-09-01 12:38:32 +00:00
key: navigatorKey,
2021-08-30 12:51:51 +00:00
initialRoute: Navigator.defaultRouteName,
onGenerateRoute: _onGenerateRoute,
);
}
2021-09-01 12:38:32 +00:00
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
2021-08-30 12:51:51 +00:00
if (settings.name == Navigator.defaultRouteName) {
return MaterialPageRoute(builder: (context) => home, settings: settings);
}
return null;
}
}