2020-10-14 19:09:16 +00:00
import ' dart:async ' ;
2020-06-23 19:23:12 +00:00
import ' package:audio_service/audio_service.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:custom_navigator/custom_navigator.dart ' ;
2020-06-23 19:23:12 +00:00
import ' package:flutter/material.dart ' ;
2020-07-16 20:25:30 +00:00
import ' package:flutter/rendering.dart ' ;
2020-10-09 18:52:45 +00:00
import ' package:flutter/services.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:flutter_localizations/flutter_localizations.dart ' ;
2020-10-09 18:52:45 +00:00
import ' package:freezer/api/cache.dart ' ;
2020-10-15 18:37:36 +00:00
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 ' ;
2020-11-15 19:25:28 +00:00
import ' package:freezer/ui/updater.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:i18n_extension/i18n_widget.dart ' ;
2020-06-23 19:23:12 +00:00
import ' package:move_to_background/move_to_background.dart ' ;
2020-09-18 17:36:41 +00:00
import ' package:freezer/translations.i18n.dart ' ;
2020-10-16 18:54:04 +00:00
import ' package:quick_actions/quick_actions.dart ' ;
2020-10-14 19:09:16 +00:00
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 ' ;
2020-09-18 17:36:41 +00:00
import ' settings.dart ' ;
2020-06-23 19:23:12 +00:00
import ' ui/home_screen.dart ' ;
2020-09-18 17:36:41 +00:00
import ' ui/player_bar.dart ' ;
2020-07-16 20:25:30 +00:00
2020-06-23 19:23:12 +00:00
Function updateTheme ;
2020-07-16 20:25:30 +00:00
Function logOut ;
2020-06-23 19:23:12 +00:00
GlobalKey < NavigatorState > mainNavigatorKey = GlobalKey < NavigatorState > ( ) ;
GlobalKey < NavigatorState > navigatorKey ;
2020-09-18 17:36:41 +00:00
void main ( ) async {
2020-06-23 19:23:12 +00:00
WidgetsFlutterBinding . ensureInitialized ( ) ;
//Initialize globals
settings = await Settings ( ) . loadSettings ( ) ;
await downloadManager . init ( ) ;
2020-10-09 18:52:45 +00:00
cache = await Cache . load ( ) ;
2020-06-23 19:23:12 +00:00
2020-11-09 21:05:47 +00:00
//Do on BG
playerHelper . authorizeLastFM ( ) ;
2020-06-23 19:23:12 +00:00
runApp ( FreezerApp ( ) ) ;
}
class FreezerApp extends StatefulWidget {
@ override
_FreezerAppState createState ( ) = > _FreezerAppState ( ) ;
}
class _FreezerAppState extends State < FreezerApp > {
2020-10-14 19:09:16 +00:00
2020-06-23 19:23:12 +00:00
@ override
void initState ( ) {
//Make update theme global
updateTheme = _updateTheme ;
2020-10-16 18:54:04 +00:00
_updateTheme ( ) ;
2020-06-23 19:23:12 +00:00
super . initState ( ) ;
}
2020-08-16 20:17:22 +00:00
@ override
void dispose ( ) {
super . dispose ( ) ;
}
2020-06-23 19:23:12 +00:00
void _updateTheme ( ) {
setState ( ( ) {
settings . themeData ;
} ) ;
2020-10-16 18:54:04 +00:00
SystemChrome . setSystemUIOverlayStyle ( SystemUiOverlayStyle (
systemNavigationBarColor: settings . themeData . bottomAppBarColor ,
2020-10-20 19:55:14 +00:00
systemNavigationBarIconBrightness: settings . isDark ? Brightness . light : Brightness . dark
2020-10-16 18:54:04 +00:00
) ) ;
2020-06-23 19:23:12 +00:00
}
2020-09-18 17:36:41 +00:00
Locale _locale ( ) {
if ( settings . language = = null | | settings . language . split ( ' _ ' ) . length < 2 ) return null ;
return Locale ( settings . language . split ( ' _ ' ) [ 0 ] , settings . language . split ( ' _ ' ) [ 1 ] ) ;
}
2020-06-23 19:23:12 +00:00
@ override
Widget build ( BuildContext context ) {
return MaterialApp (
2020-09-18 17:36:41 +00:00
title: ' Freezer ' ,
2020-10-31 20:52:23 +00:00
shortcuts: < LogicalKeySet , Intent > {
. . . WidgetsApp . defaultShortcuts ,
LogicalKeySet ( LogicalKeyboardKey . select ) : const ActivateIntent ( ) , // DPAD center key, for remote controls
} ,
2020-06-23 19:23:12 +00:00
theme: settings . themeData ,
2020-09-18 17:36:41 +00:00
localizationsDelegates: [
GlobalMaterialLocalizations . delegate ,
GlobalWidgetsLocalizations . delegate ,
GlobalCupertinoLocalizations . delegate ,
] ,
supportedLocales: supportedLocales ,
2020-06-23 19:23:12 +00:00
home: WillPopScope (
onWillPop: ( ) async {
//For some reason AudioServiceWidget caused the app to freeze after 2 back button presses. "fix"
if ( navigatorKey . currentState . canPop ( ) ) {
await navigatorKey . currentState . maybePop ( ) ;
return false ;
}
await MoveToBackground . moveTaskToBack ( ) ;
return false ;
} ,
2020-09-18 17:36:41 +00:00
child: I18n (
initialLocale: _locale ( ) ,
child: LoginMainWrapper ( ) ,
) ,
2020-06-23 19:23:12 +00:00
) ,
navigatorKey: mainNavigatorKey ,
) ;
}
}
//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 ;
2020-10-09 18:52:45 +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 = true ;
deezerAPI = new DeezerAPI ( ) ;
} ) ;
await settings . save ( ) ;
}
2020-06-23 19:23:12 +00:00
@ override
Widget build ( BuildContext context ) {
if ( settings . arl = = null )
2020-09-18 17:36:41 +00:00
return LoginWidget (
callback: ( ) = > setState ( ( ) = > { } ) ,
) ;
2020-06-23 19:23:12 +00:00
return MainScreen ( ) ;
}
}
class MainScreen extends StatefulWidget {
@ override
_MainScreenState createState ( ) = > _MainScreenState ( ) ;
}
2020-11-22 10:17:37 +00:00
class _MainScreenState extends State < MainScreen > with SingleTickerProviderStateMixin , WidgetsBindingObserver {
2020-09-18 17:36:41 +00:00
List < Widget > _screens = [ HomeScreen ( ) , SearchScreen ( ) , LibraryScreen ( ) ] ;
2020-06-23 19:23:12 +00:00
int _selected = 0 ;
2020-10-14 19:09:16 +00:00
StreamSubscription _urlLinkStream ;
2020-10-31 20:52:23 +00:00
int _keyPressed = 0 ;
2020-11-22 10:17:37 +00:00
bool textFieldVisited = false ;
2020-06-23 19:23:12 +00:00
@ override
void initState ( ) {
navigatorKey = GlobalKey < NavigatorState > ( ) ;
2020-10-14 19:09:16 +00:00
2020-10-16 18:54:04 +00:00
//Start with parameters
_setupUniLinks ( ) ;
2020-10-15 18:37:36 +00:00
_loadPreloadInfo ( ) ;
2020-10-16 18:54:04 +00:00
_prepareQuickActions ( ) ;
2020-10-15 18:37:36 +00:00
2020-11-15 19:25:28 +00:00
//Check for updates on background
Future . delayed ( Duration ( seconds: 5 ) , ( ) {
FreezerVersions . checkUpdate ( ) ;
} ) ;
2020-06-23 19:23:12 +00:00
super . initState ( ) ;
2020-11-22 10:17:37 +00:00
WidgetsBinding . instance . addObserver ( this ) ;
2020-06-23 19:23:12 +00:00
}
2020-10-16 18:54:04 +00:00
void _prepareQuickActions ( ) {
final QuickActions quickActions = QuickActions ( ) ;
quickActions . initialize ( ( type ) {
if ( type ! = null )
_startPreload ( type ) ;
} ) ;
//Actions
quickActions . setShortcutItems ( [
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 ) ;
playerHelper . playFromPlaylist ( p , p . tracks [ 0 ] . id ) ;
}
}
2020-10-15 18:37:36 +00:00
void _loadPreloadInfo ( ) async {
String info = await DownloadManager . platform . invokeMethod ( ' getPreloadInfo ' ) ;
if ( info ! = null ) {
//Used if started from android auto
await deezerAPI . authorize ( ) ;
2020-10-16 18:54:04 +00:00
_startPreload ( info ) ;
2020-10-15 18:37:36 +00:00
}
}
2020-10-14 19:09:16 +00:00
@ override
void dispose ( ) {
if ( _urlLinkStream ! = null )
_urlLinkStream . cancel ( ) ;
2020-11-22 10:17:37 +00:00
WidgetsBinding . instance . removeObserver ( this ) ;
2020-10-14 19:09:16 +00:00
super . dispose ( ) ;
}
2020-11-22 10:17:37 +00:00
@ override
void didChangeAppLifecycleState ( AppLifecycleState state ) {
if ( state = = AppLifecycleState . resumed ) {
setState ( ( ) {
textFieldVisited = false ;
} ) ;
}
}
2020-10-16 18:54:04 +00:00
void _setupUniLinks ( ) async {
2020-10-14 19:09:16 +00:00
//Listen to URLs
_urlLinkStream = getUriLinksStream ( ) . listen ( ( Uri uri ) {
openScreenByURL ( context , uri . toString ( ) ) ;
} , onError: ( err ) { } ) ;
//Get initial link on cold start
try {
String link = await getInitialLink ( ) ;
if ( link ! = null & & link . length > 4 )
openScreenByURL ( context , link ) ;
} catch ( e ) { }
}
2020-11-22 10:17:37 +00:00
ValueChanged < RawKeyEvent > _handleKey ( FocusScopeNode navigationBarFocusNode , FocusNode screenFocusNode ) {
2020-10-31 20:52:23 +00:00
return ( event ) {
2020-11-22 10:17:37 +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"
if ( primaryFocus . context . widget . runtimeType . toString ( ) = = ' EditableText ' ) {
setState ( ( ) {
textFieldVisited = true ;
} ) ;
}
// Movement to navigation bar and back
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
2020-11-22 10:17:37 +00:00
focusToNavbar ( navigationBarFocusNode ) ;
2020-10-31 20:52:23 +00:00
break ;
case 22 : // LEFT + RIGHT
case 21 :
if ( _keyPressed = = 21 & & keyCode = = 22 | | _keyPressed = = 22 & & keyCode = = 21 ) {
2020-11-22 10:17:37 +00:00
focusToNavbar ( navigationBarFocusNode ) ;
2020-10-31 20:52:23 +00:00
}
_keyPressed = keyCode ;
Future . delayed ( Duration ( milliseconds: 100 ) , ( ) = > {
_keyPressed = 0
} ) ;
break ;
2020-11-03 15:26:24 +00:00
case 20 : // DOWN
// If it's bottom row, go to navigation bar
2020-11-22 10:17:37 +00:00
var row = primaryFocus . parent ;
if ( row ! = null ) {
var column = row . parent ;
if ( column . children . last = = row ) {
focusToNavbar ( navigationBarFocusNode ) ;
}
2020-11-03 15:26:24 +00:00
}
break ;
2020-10-31 20:52:23 +00:00
case 19 : // UP
2020-11-22 10:17:37 +00:00
if ( navigationBarFocusNode . hasFocus ) {
screenFocusNode . parent . parent . children . 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 ;
}
}
2020-11-22 10:17:37 +00:00
// 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 ' ) {
Map < LogicalKeySet , Intent > shortcuts = Shortcuts . of ( context ) . shortcuts ;
final BuildContext primaryContext = primaryFocus ? . context ;
Intent intent = shortcuts [ LogicalKeySet ( event . logicalKey ) ] ;
if ( intent ! = null ) {
Actions . invoke ( primaryContext , intent , nullOk: true ) ;
}
// WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging.
FocusNode newFocus = FocusManager . instance . primaryFocus ;
if ( newFocus is FocusScopeNode ) {
navigationBarFocusNode . requestFocus ( ) ;
2020-10-31 20:52:23 +00:00
}
}
} ;
}
2020-10-14 19:09:16 +00:00
2020-11-03 15:26:24 +00:00
void focusToNavbar ( FocusScopeNode navigatorFocusNode ) {
navigatorFocusNode . requestFocus ( ) ;
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 ) {
2020-11-22 10:17:37 +00:00
FocusScopeNode navigationBarFocusNode = FocusScopeNode ( ) ; // for bottom navigation bar
FocusNode screenFocusNode = FocusNode ( ) ; // for CustomNavigator
2020-10-31 20:52:23 +00:00
return RawKeyboardListener (
2020-11-22 10:17:37 +00:00
focusNode: FocusNode ( ) ,
onKey: _handleKey ( navigationBarFocusNode , screenFocusNode ) ,
2020-10-31 20:52:23 +00:00
child: Scaffold (
bottomNavigationBar:
FocusScope (
2020-11-22 10:17:37 +00:00
node: navigationBarFocusNode ,
2020-10-31 20:52:23 +00:00
child: Column (
mainAxisSize: MainAxisSize . min ,
children: < Widget > [
PlayerBar ( ) ,
BottomNavigationBar (
backgroundColor: Theme . of ( context ) . bottomAppBarColor ,
currentIndex: _selected ,
onTap: ( int s ) async {
//Pop all routes until home screen
while ( navigatorKey . currentState . canPop ( ) ) {
await navigatorKey . currentState . maybePop ( ) ;
}
await navigatorKey . currentState . maybePop ( ) ;
setState ( ( ) {
_selected = s ;
} ) ;
} ,
selectedItemColor: Theme . of ( context ) . primaryColor ,
items: < BottomNavigationBarItem > [
BottomNavigationBarItem (
icon: Icon ( Icons . home ) ,
title: Text ( ' Home ' . i18n ) ) ,
BottomNavigationBarItem (
icon: Icon ( Icons . search ) ,
title: Text ( ' Search ' . i18n ) ,
) ,
BottomNavigationBarItem (
icon: Icon ( Icons . library_music ) ,
title: Text ( ' Library ' . i18n ) )
] ,
)
] ,
) ) ,
body: AudioServiceWidget (
child: CustomNavigator (
navigatorKey: navigatorKey ,
2020-11-22 10:17:37 +00:00
home: Focus (
focusNode: screenFocusNode ,
skipTraversal: true ,
canRequestFocus: false ,
child: _screens [ _selected ]
) ,
pageRoute: PageRoutes . materialPageRoute
2020-10-19 19:28:45 +00:00
) ,
2020-10-31 20:52:23 +00:00
) ) ) ;
2020-06-23 19:23:12 +00:00
}
}