Merge branch 'feature/atv' into 'master'
Android TV support See merge request freezer/freezer!2
This commit is contained in:
commit
ef9ae6e2ad
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -37,7 +37,7 @@ android/local.properties
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
.gradle
|
.gradle/
|
||||||
|
|
||||||
# Web related
|
# Web related
|
||||||
lib/generated_plugin_registrant.dart
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-feature android:name="android.software.LEANBACK" android:required="true"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
|
@ -35,6 +36,9 @@
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:banner="@mipmap/ic_launcher"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:logo="@mipmap/ic_launcher"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
@ -90,6 +94,11 @@
|
||||||
android:scheme="https"
|
android:scheme="https"
|
||||||
android:host="deezer.page.link" />
|
android:host="deezer.page.link" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!--
|
<!--
|
||||||
Don't delete the meta-data below.
|
Don't delete the meta-data below.
|
||||||
|
|
181
lib/main.dart
181
lib/main.dart
|
@ -84,6 +84,10 @@ class _FreezerAppState extends State<FreezerApp> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Freezer',
|
title: 'Freezer',
|
||||||
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
|
...WidgetsApp.defaultShortcuts,
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), // DPAD center key, for remote controls
|
||||||
|
},
|
||||||
theme: settings.themeData,
|
theme: settings.themeData,
|
||||||
localizationsDelegates: [
|
localizationsDelegates: [
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
@ -159,10 +163,12 @@ class MainScreen extends StatefulWidget {
|
||||||
_MainScreenState createState() => _MainScreenState();
|
_MainScreenState createState() => _MainScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin{
|
class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
|
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
|
||||||
int _selected = 0;
|
int _selected = 0;
|
||||||
StreamSubscription _urlLinkStream;
|
StreamSubscription _urlLinkStream;
|
||||||
|
int _keyPressed = 0;
|
||||||
|
bool textFieldVisited = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -181,6 +187,7 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
||||||
});
|
});
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startStreamingServer() async {
|
void _startStreamingServer() async {
|
||||||
|
@ -227,9 +234,19 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (_urlLinkStream != null)
|
if (_urlLinkStream != null)
|
||||||
_urlLinkStream.cancel();
|
_urlLinkStream.cancel();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
setState(() {
|
||||||
|
textFieldVisited = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _setupUniLinks() async {
|
void _setupUniLinks() async {
|
||||||
//Listen to URLs
|
//Listen to URLs
|
||||||
_urlLinkStream = getUriLinksStream().listen((Uri uri) {
|
_urlLinkStream = getUriLinksStream().listen((Uri uri) {
|
||||||
|
@ -243,49 +260,133 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValueChanged<RawKeyEvent> _handleKey(FocusScopeNode navigationBarFocusNode, FocusNode screenFocusNode){
|
||||||
|
return (event) {
|
||||||
|
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')) {
|
||||||
|
int keyCode = (event.data as RawKeyEventDataAndroid).keyCode;
|
||||||
|
switch (keyCode) {
|
||||||
|
case 127: // Menu on Android TV
|
||||||
|
case 327: // EPG on Hisense TV
|
||||||
|
focusToNavbar(navigationBarFocusNode);
|
||||||
|
break;
|
||||||
|
case 22: // LEFT + RIGHT
|
||||||
|
case 21:
|
||||||
|
if (_keyPressed == 21 && keyCode == 22 || _keyPressed == 22 && keyCode == 21) {
|
||||||
|
focusToNavbar(navigationBarFocusNode);
|
||||||
|
}
|
||||||
|
_keyPressed = keyCode;
|
||||||
|
Future.delayed(Duration(milliseconds: 100), () => {
|
||||||
|
_keyPressed = 0
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 20: // DOWN
|
||||||
|
// If it's bottom row, go to navigation bar
|
||||||
|
var row = primaryFocus.parent;
|
||||||
|
if (row != null) {
|
||||||
|
var column = row.parent;
|
||||||
|
if (column.children.last == row) {
|
||||||
|
focusToNavbar(navigationBarFocusNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 19: // UP
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
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') {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
FocusScopeNode navigationBarFocusNode = FocusScopeNode(); // for bottom navigation bar
|
||||||
bottomNavigationBar: Column(
|
FocusNode screenFocusNode = FocusNode(); // for CustomNavigator
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
return RawKeyboardListener(
|
||||||
PlayerBar(),
|
focusNode: FocusNode(),
|
||||||
BottomNavigationBar(
|
onKey: _handleKey(navigationBarFocusNode, screenFocusNode),
|
||||||
backgroundColor: Theme.of(context).bottomAppBarColor,
|
child: Scaffold(
|
||||||
currentIndex: _selected,
|
bottomNavigationBar:
|
||||||
onTap: (int s) async {
|
FocusScope(
|
||||||
|
node: navigationBarFocusNode,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
//Pop all routes until home screen
|
await navigatorKey.currentState.maybePop();
|
||||||
while (navigatorKey.currentState.canPop()) {
|
setState(() {
|
||||||
await navigatorKey.currentState.maybePop();
|
_selected = s;
|
||||||
}
|
});
|
||||||
|
},
|
||||||
await navigatorKey.currentState.maybePop();
|
selectedItemColor: Theme.of(context).primaryColor,
|
||||||
setState(() {
|
items: <BottomNavigationBarItem>[
|
||||||
_selected = s;
|
BottomNavigationBarItem(
|
||||||
});
|
icon: Icon(Icons.home),
|
||||||
},
|
title: Text('Home'.i18n)),
|
||||||
selectedItemColor: Theme.of(context).primaryColor,
|
BottomNavigationBarItem(
|
||||||
items: <BottomNavigationBarItem>[
|
icon: Icon(Icons.search),
|
||||||
BottomNavigationBarItem(
|
title: Text('Search'.i18n),
|
||||||
icon: Icon(Icons.home), title: Text('Home'.i18n)),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.search),
|
icon: Icon(Icons.library_music),
|
||||||
title: Text('Search'.i18n),
|
title: Text('Library'.i18n))
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
body: AudioServiceWidget(
|
||||||
|
child: CustomNavigator(
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
|
home: Focus(
|
||||||
|
focusNode: screenFocusNode,
|
||||||
|
skipTraversal: true,
|
||||||
|
canRequestFocus: false,
|
||||||
|
child: _screens[_selected]
|
||||||
|
),
|
||||||
|
pageRoute: PageRoutes.materialPageRoute
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
)));
|
||||||
icon: Icon(Icons.library_music), title: Text('Library'.i18n))
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: AudioServiceWidget(
|
|
||||||
child: CustomNavigator(
|
|
||||||
navigatorKey: navigatorKey,
|
|
||||||
home: _screens[_selected],
|
|
||||||
pageRoute: PageRoutes.materialPageRoute,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,14 +143,11 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
||||||
));
|
));
|
||||||
if (_error)
|
if (_error)
|
||||||
return ErrorScreen();
|
return ErrorScreen();
|
||||||
return ListView.builder(
|
return Column(
|
||||||
shrinkWrap: true,
|
children: List.generate(_homePage.sections.length, (i) {
|
||||||
physics: NeverScrollableScrollPhysics(),
|
return HomepageSectionWidget(_homePage.sections[i]);
|
||||||
itemCount: _homePage.sections.length,
|
},
|
||||||
itemBuilder: (context, i) {
|
));
|
||||||
return HomepageSectionWidget(_homePage.sections[i]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,62 +158,54 @@ class HomepageSectionWidget extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return ListTile(
|
||||||
mainAxisSize: MainAxisSize.min,
|
title: Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
section.title??'',
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
child: Text(
|
|
||||||
section.title,
|
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20.0,
|
fontSize: 20.0,
|
||||||
fontWeight: FontWeight.w900
|
fontWeight: FontWeight.w900
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0)
|
subtitle: SingleChildScrollView(
|
||||||
),
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
SingleChildScrollView(
|
children: List.generate(section.items.length + 1, (j) {
|
||||||
scrollDirection: Axis.horizontal,
|
//Has more items
|
||||||
child: Row(
|
if (j == section.items.length) {
|
||||||
children: List.generate(section.items.length + 1, (i) {
|
if (section.hasMore ?? false) {
|
||||||
//Has more items
|
return FlatButton(
|
||||||
if (i == section.items.length) {
|
child: Text(
|
||||||
if (section.hasMore??false) {
|
'Show more'.i18n,
|
||||||
return FlatButton(
|
textAlign: TextAlign.center,
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
'Show more'.i18n,
|
fontSize: 20.0
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
builder: (context) => Scaffold(
|
|
||||||
appBar: FreezerAppBar(section.title),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: HomePageScreen(
|
|
||||||
channel: DeezerChannel(target: section.pagePath)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
),
|
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
)),
|
builder: (context) => Scaffold(
|
||||||
);
|
appBar: FreezerAppBar(section.title),
|
||||||
}
|
body: SingleChildScrollView(
|
||||||
return Container(height: 0, width: 0);
|
child: HomePageScreen(
|
||||||
}
|
channel: DeezerChannel(target: section.pagePath)
|
||||||
//Show item
|
)
|
||||||
HomePageItem item = section.items[i];
|
),
|
||||||
return HomePageItemWidget(item);
|
),
|
||||||
}),
|
)),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
Container(height: 8.0),
|
return Container(height: 0, width: 0);
|
||||||
],
|
}
|
||||||
);
|
|
||||||
|
//Show item
|
||||||
|
HomePageItem item = section.items[j];
|
||||||
|
return HomePageItemWidget(item);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/player.dart';
|
import 'package:freezer/api/player.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
@ -111,6 +112,17 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
_start();
|
_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARL auth: called on "Save" click, Enter and DPAD_Center press
|
||||||
|
void goARL(FocusNode node, TextEditingController _controller) {
|
||||||
|
if (node != null) {
|
||||||
|
node.unfocus();
|
||||||
|
}
|
||||||
|
_controller.clear();
|
||||||
|
settings.arl = _arl.trim();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_update();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
@ -121,7 +133,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
TextEditingController _controller = new TextEditingController();
|
||||||
|
// For "DPAD center" key handling on remote controls
|
||||||
|
FocusNode focusNode = FocusNode(skipTraversal: true,descendantsAreFocusable: false,onKey: (node, event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.select) {
|
||||||
|
goARL(node, _controller);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
if (settings.arl == null)
|
if (settings.arl == null)
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Padding(
|
body: Padding(
|
||||||
|
@ -165,6 +184,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
Future.delayed(Duration(seconds: 1), () => {focusNode.requestFocus()}); // autofocus doesn't work - it's replacement
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Enter ARL'.i18n),
|
title: Text('Enter ARL'.i18n),
|
||||||
content: Container(
|
content: Container(
|
||||||
|
@ -173,16 +193,17 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Token (ARL)'.i18n
|
labelText: 'Token (ARL)'.i18n
|
||||||
),
|
),
|
||||||
|
focusNode: focusNode,
|
||||||
|
controller: _controller,
|
||||||
|
onSubmitted: (String s) {
|
||||||
|
goARL(focusNode, _controller);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text('Save'.i18n),
|
child: Text('Save'.i18n),
|
||||||
onPressed: () {
|
onPressed: () => goARL(null, _controller),
|
||||||
settings.arl = _arl.trim();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_update();
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ class PlayerBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var focusNode = FocusNode();
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onHorizontalDragUpdate: (details) async {
|
onHorizontalDragUpdate: (details) async {
|
||||||
if (_gestureRegistered) return;
|
if (_gestureRegistered) return;
|
||||||
|
@ -46,9 +47,11 @@ class PlayerBar extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).bottomAppBarColor,
|
// For Android TV: indicate focus by grey
|
||||||
|
color: focusNode.hasFocus ? Colors.black26 : Theme.of(context).bottomAppBarColor,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
|
focusNode: focusNode,
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
|
|
@ -57,7 +57,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||||
setState(() => _bgGradient = LinearGradient(
|
setState(() => _bgGradient = LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [palette.dominantColor.color.withOpacity(0.5), Theme.of(context).bottomAppBarColor],
|
colors: [palette.dominantColor.color.withOpacity(0.5), Color.fromARGB(0, 0, 0, 0)],
|
||||||
stops: [
|
stops: [
|
||||||
0.0,
|
0.0,
|
||||||
0.4
|
0.4
|
||||||
|
@ -687,6 +687,7 @@ class _SeekBarState extends State<SeekBar> {
|
||||||
Container(
|
Container(
|
||||||
height: 32.0,
|
height: 32.0,
|
||||||
child: Slider(
|
child: Slider(
|
||||||
|
focusNode: FocusNode(canRequestFocus: false, skipTraversal: true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||||
value: position,
|
value: position,
|
||||||
max: duration,
|
max: duration,
|
||||||
onChangeStart: (double d) {
|
onChangeStart: (double d) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
import 'package:fluttericon/typicons_icons.dart';
|
import 'package:fluttericon/typicons_icons.dart';
|
||||||
|
import 'package:flutter/src/services/keyboard_key.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/download.dart';
|
import 'package:freezer/api/download.dart';
|
||||||
import 'package:freezer/api/player.dart';
|
import 'package:freezer/api/player.dart';
|
||||||
|
@ -127,9 +128,11 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var textFielFocusNode = FocusNode();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: FreezerAppBar('Search'.i18n),
|
appBar: FreezerAppBar('Search'.i18n),
|
||||||
body: ListView(
|
body: FocusScope(
|
||||||
|
child: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(height: 4.0),
|
Container(height: 4.0),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -140,53 +143,66 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment(1.0, 0.0),
|
alignment: Alignment(1.0, 0.0),
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
RawKeyboardListener(
|
||||||
onChanged: (String s) {
|
focusNode: FocusNode(),
|
||||||
setState(() => _query = s);
|
onKey: (event) { // For Android TV: quit search textfield
|
||||||
_loadSuggestions();
|
if (event.runtimeType.toString() == 'RawKeyUpEvent') {
|
||||||
},
|
LogicalKeyboardKey key = event.data.logicalKey;
|
||||||
onTap: () {
|
if (key == LogicalKeyboardKey.arrowDown) {
|
||||||
setState(() => _showCards = false);
|
textFielFocusNode.unfocus();
|
||||||
},
|
}
|
||||||
focusNode: _focus,
|
}
|
||||||
decoration: InputDecoration(
|
},
|
||||||
labelText: 'Search or paste URL'.i18n,
|
child: TextField(
|
||||||
fillColor: Theme.of(context).bottomAppBarColor,
|
onChanged: (String s) {
|
||||||
filled: true,
|
setState(() => _query = s);
|
||||||
focusedBorder: OutlineInputBorder(
|
_loadSuggestions();
|
||||||
borderSide: BorderSide(color: Colors.grey)
|
},
|
||||||
),
|
onTap: () {
|
||||||
enabledBorder: OutlineInputBorder(
|
setState(() => _showCards = false);
|
||||||
borderSide: BorderSide(color: Colors.grey)
|
},
|
||||||
),
|
focusNode: textFielFocusNode,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
controller: _controller,
|
labelText: 'Search or paste URL'.i18n,
|
||||||
onSubmitted: (String s) => _submit(context, query: s),
|
fillColor: Theme.of(context).bottomAppBarColor,
|
||||||
),
|
filled: true,
|
||||||
Row(
|
focusedBorder: OutlineInputBorder(
|
||||||
mainAxisSize: MainAxisSize.min,
|
borderSide: BorderSide(color: Colors.grey)
|
||||||
children: [
|
),
|
||||||
Container(
|
enabledBorder: OutlineInputBorder(
|
||||||
width: 40.0,
|
borderSide: BorderSide(color: Colors.grey)
|
||||||
child: IconButton(
|
),
|
||||||
splashRadius: 20.0,
|
|
||||||
icon: Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_suggestions = [];
|
|
||||||
_query = '';
|
|
||||||
});
|
|
||||||
_controller.clear();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
controller: _controller,
|
||||||
],
|
onSubmitted: (String s) => _submit(context, query: s),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Focus(
|
||||||
|
canRequestFocus: false, // Focus is moving to cross, and hangs out there,
|
||||||
|
descendantsAreFocusable: false, // so we disable focusing on it at all
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40.0,
|
||||||
|
child: IconButton(
|
||||||
|
splashRadius: 20.0,
|
||||||
|
icon: Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_suggestions = [];
|
||||||
|
_query = '';
|
||||||
|
});
|
||||||
|
_controller.clear();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -374,6 +390,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,7 +135,6 @@ class ArtistTile extends StatelessWidget {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 150,
|
width: 150,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: onHold,
|
||||||
|
@ -246,7 +245,6 @@ class PlaylistCardTile extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
height: 180.0,
|
height: 180.0,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
@ -293,7 +291,6 @@ class SmartTrackListTile extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 200.0,
|
height: 200.0,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: onHold,
|
||||||
|
@ -365,7 +362,6 @@ class AlbumCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: onHold,
|
||||||
|
|
Loading…
Reference in a new issue