#437 tv: back navigation
This commit is contained in:
parent
e8bb1a77f0
commit
c0fd75777e
17 changed files with 377 additions and 202 deletions
|
@ -243,6 +243,7 @@ class Settings extends ChangeNotifier {
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
themeBrightness = AvesThemeBrightness.dark;
|
themeBrightness = AvesThemeBrightness.dark;
|
||||||
mustBackTwiceToExit = false;
|
mustBackTwiceToExit = false;
|
||||||
|
// address `TV-BU` / `TV-BY` requirements from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
keepScreenOn = KeepScreenOn.videoPlayback;
|
keepScreenOn = KeepScreenOn.videoPlayback;
|
||||||
enableBottomNavigationBar = false;
|
enableBottomNavigationBar = false;
|
||||||
drawerTypeBookmarks = [
|
drawerTypeBookmarks = [
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/widgets/about/credits.dart';
|
||||||
import 'package:aves/widgets/about/licenses.dart';
|
import 'package:aves/widgets/about/licenses.dart';
|
||||||
import 'package:aves/widgets/about/translators.dart';
|
import 'package:aves/widgets/about/translators.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -46,13 +47,15 @@ class AboutPage extends StatelessWidget {
|
||||||
|
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Row(
|
body: TvPopScope(
|
||||||
children: [
|
child: Row(
|
||||||
TvRail(
|
children: [
|
||||||
controller: context.read<TvRailController>(),
|
TvRail(
|
||||||
),
|
controller: context.read<TvRailController>(),
|
||||||
Expanded(child: body),
|
),
|
||||||
],
|
Expanded(child: body),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -112,8 +112,10 @@ class AvesApp extends StatefulWidget {
|
||||||
if (urlString != null) {
|
if (urlString != null) {
|
||||||
final url = Uri.parse(urlString);
|
final url = Uri.parse(urlString);
|
||||||
if (await ul.canLaunchUrl(url)) {
|
if (await ul.canLaunchUrl(url)) {
|
||||||
|
// address `TV-WB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
|
final mode = device.isTelevision ? ul.LaunchMode.inAppWebView : ul.LaunchMode.externalApplication;
|
||||||
try {
|
try {
|
||||||
await ul.launchUrl(url, mode: device.isTelevision ? ul.LaunchMode.inAppWebView : ul.LaunchMode.externalApplication);
|
await ul.launchUrl(url, mode: mode);
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
debugPrint('failed to open url=$urlString with error=$error\n$stack');
|
debugPrint('failed to open url=$urlString with error=$error\n$stack');
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_fab.dart';
|
import 'package:aves/widgets/common/identity/aves_fab.dart';
|
||||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||||
|
@ -97,18 +98,15 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
}
|
}
|
||||||
return SynchronousFuture(true);
|
return SynchronousFuture(true);
|
||||||
},
|
},
|
||||||
child: DoubleBackPopScope(
|
child: const DoubleBackPopScope(
|
||||||
child: GestureAreaProtectorStack(
|
child: GestureAreaProtectorStack(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: ChangeNotifierProvider<CollectionLens>.value(
|
child: CollectionGrid(
|
||||||
value: _collection,
|
// key is expected by test driver
|
||||||
child: const CollectionGrid(
|
key: Key('collection-grid'),
|
||||||
// key is expected by test driver
|
settingsRouteKey: CollectionPage.routeName,
|
||||||
key: Key('collection-grid'),
|
|
||||||
settingsRouteKey: CollectionPage.routeName,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -117,22 +115,25 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget page;
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
return Scaffold(
|
page = TvPopScope(
|
||||||
body: Row(
|
child: Scaffold(
|
||||||
children: [
|
body: Row(
|
||||||
TvRail(
|
children: [
|
||||||
controller: context.read<TvRailController>(),
|
TvRail(
|
||||||
currentCollection: _collection,
|
controller: context.read<TvRailController>(),
|
||||||
),
|
currentCollection: _collection,
|
||||||
Expanded(child: body),
|
),
|
||||||
],
|
Expanded(child: body),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
extendBody: true,
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
extendBody: true,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Selector<Settings, bool>(
|
page = Selector<Settings, bool>(
|
||||||
selector: (context, s) => s.enableBottomNavigationBar,
|
selector: (context, s) => s.enableBottomNavigationBar,
|
||||||
builder: (context, enableBottomNavigationBar, child) {
|
builder: (context, enableBottomNavigationBar, child) {
|
||||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||||
|
@ -160,6 +161,11 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// this provider should be above `TvRail`
|
||||||
|
return ChangeNotifierProvider<CollectionLens>.value(
|
||||||
|
value: _collection,
|
||||||
|
child: page,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
73
lib/widgets/common/behaviour/tv_pop.dart
Normal file
73
lib/widgets/common/behaviour/tv_pop.dart
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
import 'package:aves/model/settings/enums/home_page.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
|
class TvPopScope extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const TvPopScope({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () {
|
||||||
|
if (!device.isTelevision || _isHome(context)) {
|
||||||
|
return SynchronousFuture(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
_getHomeRoute(),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
return SynchronousFuture(false);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isHome(BuildContext context) {
|
||||||
|
final homePage = settings.homePage;
|
||||||
|
final currentRoute = context.currentRouteName;
|
||||||
|
|
||||||
|
if (currentRoute != homePage.routeName) return false;
|
||||||
|
|
||||||
|
switch (homePage) {
|
||||||
|
case HomePageSetting.collection:
|
||||||
|
return context.read<CollectionLens>().filters.isEmpty;
|
||||||
|
case HomePageSetting.albums:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Route _getHomeRoute() {
|
||||||
|
switch (settings.homePage) {
|
||||||
|
case HomePageSetting.collection:
|
||||||
|
return MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||||
|
builder: (context) => CollectionPage(
|
||||||
|
source: context.read<CollectionSource>(),
|
||||||
|
filters: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case HomePageSetting.albums:
|
||||||
|
return MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: AlbumListPage.routeName),
|
||||||
|
builder: (context) => const AlbumListPage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/settings/enums/map_style.dart';
|
import 'package:aves/model/settings/enums/map_style.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
@ -35,11 +36,13 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
Widget? navigationButton;
|
Widget? navigationButton;
|
||||||
switch (context.select<MapThemeData, MapNavigationButton>((v) => v.navigationButton)) {
|
switch (context.select<MapThemeData, MapNavigationButton>((v) => v.navigationButton)) {
|
||||||
case MapNavigationButton.back:
|
case MapNavigationButton.back:
|
||||||
navigationButton = MapOverlayButton(
|
if (!device.isTelevision) {
|
||||||
icon: const BackButtonIcon(),
|
navigationButton = MapOverlayButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
icon: const BackButtonIcon(),
|
||||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
onPressed: () => Navigator.pop(context),
|
||||||
);
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case MapNavigationButton.map:
|
case MapNavigationButton.map:
|
||||||
if (openMapPage != null) {
|
if (openMapPage != null) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/search/route.dart';
|
import 'package:aves/widgets/common/search/route.dart';
|
||||||
|
@ -19,6 +20,10 @@ abstract class AvesSearchDelegate extends SearchDelegate {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? buildLeading(BuildContext context) {
|
Widget? buildLeading(BuildContext context) {
|
||||||
|
if (device.isTelevision) {
|
||||||
|
return const Icon(AIcons.search);
|
||||||
|
}
|
||||||
|
|
||||||
// use a property instead of checking `Navigator.canPop(context)`
|
// use a property instead of checking `Navigator.canPop(context)`
|
||||||
// because the navigator state changes as soon as we press back
|
// because the navigator state changes as soon as we press back
|
||||||
// so the leading may mistakenly switch to the close button
|
// so the leading may mistakenly switch to the close button
|
||||||
|
|
|
@ -2,6 +2,8 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
import 'package:aves/utils/debouncer.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||||
import 'package:aves/widgets/common/search/delegate.dart';
|
import 'package:aves/widgets/common/search/delegate.dart';
|
||||||
import 'package:aves/widgets/common/search/route.dart';
|
import 'package:aves/widgets/common/search/route.dart';
|
||||||
|
@ -145,9 +147,13 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
),
|
),
|
||||||
actions: widget.delegate.buildActions(context),
|
actions: widget.delegate.buildActions(context),
|
||||||
),
|
),
|
||||||
body: AnimatedSwitcher(
|
body: TvPopScope(
|
||||||
duration: const Duration(milliseconds: 300),
|
child: DoubleBackPopScope(
|
||||||
child: body,
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/services/analysis_service.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||||
import 'package:aves/widgets/debug/android_apps.dart';
|
import 'package:aves/widgets/debug/android_apps.dart';
|
||||||
import 'package:aves/widgets/debug/android_codecs.dart';
|
import 'package:aves/widgets/debug/android_codecs.dart';
|
||||||
|
@ -40,48 +41,50 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Directionality(
|
return TvPopScope(
|
||||||
textDirection: TextDirection.ltr,
|
child: Directionality(
|
||||||
child: Scaffold(
|
textDirection: TextDirection.ltr,
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: const Text('Debug'),
|
appBar: AppBar(
|
||||||
actions: [
|
title: const Text('Debug'),
|
||||||
MenuIconTheme(
|
actions: [
|
||||||
child: PopupMenuButton<AppDebugAction>(
|
MenuIconTheme(
|
||||||
// key is expected by test driver
|
child: PopupMenuButton<AppDebugAction>(
|
||||||
key: const Key('appbar-menu-button'),
|
// key is expected by test driver
|
||||||
itemBuilder: (context) => AppDebugAction.values
|
key: const Key('appbar-menu-button'),
|
||||||
.map((v) => PopupMenuItem(
|
itemBuilder: (context) => AppDebugAction.values
|
||||||
// key is expected by test driver
|
.map((v) => PopupMenuItem(
|
||||||
key: Key('menu-${v.name}'),
|
// key is expected by test driver
|
||||||
value: v,
|
key: Key('menu-${v.name}'),
|
||||||
child: MenuRow(text: v.name),
|
value: v,
|
||||||
))
|
child: MenuRow(text: v.name),
|
||||||
.toList(),
|
))
|
||||||
onSelected: (action) async {
|
.toList(),
|
||||||
// wait for the popup menu to hide before proceeding with the action
|
onSelected: (action) async {
|
||||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
unawaited(_onActionSelected(action));
|
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||||
},
|
unawaited(_onActionSelected(action));
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
children: [
|
|
||||||
_buildGeneralTabView(),
|
|
||||||
const DebugAndroidAppSection(),
|
|
||||||
const DebugAndroidCodecSection(),
|
|
||||||
const DebugAndroidDirSection(),
|
|
||||||
const DebugCacheSection(),
|
|
||||||
const DebugAppDatabaseSection(),
|
|
||||||
const DebugErrorReportingSection(),
|
|
||||||
const DebugSettingsSection(),
|
|
||||||
const DebugStorageSection(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
children: [
|
||||||
|
_buildGeneralTabView(),
|
||||||
|
const DebugAndroidAppSection(),
|
||||||
|
const DebugAndroidCodecSection(),
|
||||||
|
const DebugAndroidDirSection(),
|
||||||
|
const DebugCacheSection(),
|
||||||
|
const DebugAppDatabaseSection(),
|
||||||
|
const DebugErrorReportingSection(),
|
||||||
|
const DebugSettingsSection(),
|
||||||
|
const DebugStorageSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||||
|
@ -88,35 +89,37 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
}
|
}
|
||||||
return SynchronousFuture(true);
|
return SynchronousFuture(true);
|
||||||
},
|
},
|
||||||
child: DoubleBackPopScope(
|
child: TvPopScope(
|
||||||
child: GestureAreaProtectorStack(
|
child: DoubleBackPopScope(
|
||||||
child: SafeArea(
|
child: GestureAreaProtectorStack(
|
||||||
top: false,
|
child: SafeArea(
|
||||||
bottom: false,
|
top: false,
|
||||||
child: Selector<MediaQueryData, double>(
|
bottom: false,
|
||||||
selector: (context, mq) => mq.padding.top,
|
child: Selector<MediaQueryData, double>(
|
||||||
builder: (context, mqPaddingTop, child) {
|
selector: (context, mq) => mq.padding.top,
|
||||||
return ValueListenableBuilder<double>(
|
builder: (context, mqPaddingTop, child) {
|
||||||
valueListenable: appBarHeightNotifier,
|
return ValueListenableBuilder<double>(
|
||||||
builder: (context, appBarHeight, child) {
|
valueListenable: appBarHeightNotifier,
|
||||||
return FilterGrid<T>(
|
builder: (context, appBarHeight, child) {
|
||||||
// key is expected by test driver
|
return FilterGrid<T>(
|
||||||
key: const Key('filter-grid'),
|
// key is expected by test driver
|
||||||
settingsRouteKey: settingsRouteKey,
|
key: const Key('filter-grid'),
|
||||||
appBar: appBar,
|
settingsRouteKey: settingsRouteKey,
|
||||||
appBarHeight: mqPaddingTop + appBarHeight,
|
appBar: appBar,
|
||||||
sections: sections,
|
appBarHeight: mqPaddingTop + appBarHeight,
|
||||||
newFilters: newFilters,
|
sections: sections,
|
||||||
sortFactor: sortFactor,
|
newFilters: newFilters,
|
||||||
showHeaders: showHeaders,
|
sortFactor: sortFactor,
|
||||||
selectable: selectable,
|
showHeaders: showHeaders,
|
||||||
applyQuery: applyQuery,
|
selectable: selectable,
|
||||||
emptyBuilder: emptyBuilder,
|
applyQuery: applyQuery,
|
||||||
heroType: heroType,
|
emptyBuilder: emptyBuilder,
|
||||||
);
|
heroType: heroType,
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/widgets/about/about_page.dart';
|
import 'package:aves/widgets/about/about_page.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -66,10 +67,12 @@ class PageNavTile extends StatelessWidget {
|
||||||
static Route routeBuilder(BuildContext context, String routeName) {
|
static Route routeBuilder(BuildContext context, String routeName) {
|
||||||
switch (routeName) {
|
switch (routeName) {
|
||||||
case SearchPage.routeName:
|
case SearchPage.routeName:
|
||||||
|
final currentCollection = context.read<CollectionLens?>();
|
||||||
return SearchPageRoute(
|
return SearchPageRoute(
|
||||||
delegate: CollectionSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
searchFieldLabel: context.l10n.searchCollectionFieldHint,
|
||||||
source: context.read<CollectionSource>(),
|
source: context.read<CollectionSource>(),
|
||||||
|
parentCollection: currentCollection?.copyWith(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -192,16 +192,19 @@ class _TvRailState extends State<TvRail> {
|
||||||
return pageBookmarks.map(_routeNavEntry).toList();
|
return pageBookmarks.map(_routeNavEntry).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_NavEntry _routeNavEntry(String route) => _NavEntry(
|
_NavEntry _routeNavEntry(String routeName) => _NavEntry(
|
||||||
icon: DrawerPageIcon(route: route),
|
icon: DrawerPageIcon(route: routeName),
|
||||||
label: DrawerPageTitle(route: route),
|
label: DrawerPageTitle(route: routeName),
|
||||||
isSelected: context.currentRouteName == route,
|
isSelected: context.currentRouteName == routeName,
|
||||||
onSelection: () => _goTo(route),
|
onSelection: () => _goTo(routeName),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _goTo(String routeName) async {
|
void _goTo(String routeName) {
|
||||||
// TODO TLAD [tv] check `topLevel` / `Navigator.pushAndRemoveUntil`
|
Navigator.pushAndRemoveUntil(
|
||||||
await Navigator.push(context, PageNavTile.routeBuilder(context, routeName));
|
context,
|
||||||
|
PageNavTile.routeBuilder(context, routeName),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _goToCollection(BuildContext context, CollectionFilter? filter) {
|
void _goToCollection(BuildContext context, CollectionFilter? filter) {
|
||||||
|
|
|
@ -279,21 +279,25 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
|
||||||
if (parentCollection != null) {
|
if (parentCollection != null) {
|
||||||
_applyToParentCollectionPage(context, filter);
|
_applyToParentCollectionPage(context, filter);
|
||||||
} else {
|
} else {
|
||||||
_jumpToCollectionPage(context, filter);
|
_jumpToCollectionPage(context, {filter});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) {
|
void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) {
|
||||||
parentCollection!.addFilter(filter);
|
parentCollection!.addFilter(filter);
|
||||||
// We delay closing the current page after applying the filter selection
|
if (Navigator.canPop(context)) {
|
||||||
// so that hero animation target is ready in the `FilterBar`,
|
// We delay closing the current page after applying the filter selection
|
||||||
// even when the target is a child of an `AnimatedList`.
|
// so that hero animation target is ready in the `FilterBar`,
|
||||||
// Do not use `WidgetsBinding.instance.addPostFrameCallback`,
|
// even when the target is a child of an `AnimatedList`.
|
||||||
// as it may not trigger if there is no subsequent build.
|
// Do not use `WidgetsBinding.instance.addPostFrameCallback`,
|
||||||
Future.delayed(const Duration(milliseconds: 100), () => goBack(context));
|
// as it may not trigger if there is no subsequent build.
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () => goBack(context));
|
||||||
|
} else {
|
||||||
|
_jumpToCollectionPage(context, parentCollection!.filters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) {
|
void _jumpToCollectionPage(BuildContext context, Set<CollectionFilter> filters) {
|
||||||
clean();
|
clean();
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
|
@ -301,7 +305,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
|
||||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||||
builder: (context) => CollectionPage(
|
builder: (context) => CollectionPage(
|
||||||
source: source,
|
source: source,
|
||||||
filters: {filter},
|
filters: filters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(route) => false,
|
(route) => false,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/menu.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/search/route.dart';
|
import 'package:aves/widgets/common/search/route.dart';
|
||||||
|
@ -73,62 +74,64 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
|
||||||
|
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Row(
|
body: TvPopScope(
|
||||||
children: [
|
child: Row(
|
||||||
TvRail(
|
children: [
|
||||||
controller: context.read<TvRailController>(),
|
TvRail(
|
||||||
),
|
controller: context.read<TvRailController>(),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
AppBar(
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
title: appBarTitle,
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ValueListenableBuilder<int>(
|
|
||||||
valueListenable: _tvSelectedIndexNotifier,
|
|
||||||
builder: (context, selectedIndex, child) {
|
|
||||||
final rail = NavigationRail(
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
extended: true,
|
|
||||||
destinations: sections
|
|
||||||
.map((section) => NavigationRailDestination(
|
|
||||||
icon: section.icon(context),
|
|
||||||
label: Text(section.title(context)),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
selectedIndex: selectedIndex,
|
|
||||||
onDestinationSelected: (index) => _tvSelectedIndexNotifier.value = index,
|
|
||||||
);
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
|
||||||
child: IntrinsicHeight(child: rail),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _SettingsSectionBody(
|
|
||||||
loader: Future.value(sections[selectedIndex].tiles(context)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
],
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: appBarTitle,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ValueListenableBuilder<int>(
|
||||||
|
valueListenable: _tvSelectedIndexNotifier,
|
||||||
|
builder: (context, selectedIndex, child) {
|
||||||
|
final rail = NavigationRail(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
extended: true,
|
||||||
|
destinations: sections
|
||||||
|
.map((section) => NavigationRailDestination(
|
||||||
|
icon: section.icon(context),
|
||||||
|
label: Text(section.title(context)),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
selectedIndex: selectedIndex,
|
||||||
|
onDestinationSelected: (index) => _tvSelectedIndexNotifier.value = index,
|
||||||
|
);
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
|
child: IntrinsicHeight(child: rail),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _SettingsSectionBody(
|
||||||
|
loader: Future.value(sections[selectedIndex].tiles(context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
@ -225,6 +226,7 @@ class _StatsPageState extends State<StatsPage> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: !device.isTelevision,
|
||||||
title: Text(l10n.statsPageTitle),
|
title: Text(l10n.statsPageTitle),
|
||||||
),
|
),
|
||||||
body: GestureAreaProtectorStack(
|
body: GestureAreaProtectorStack(
|
||||||
|
@ -354,6 +356,7 @@ class StatsTopPage extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: !device.isTelevision,
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
),
|
),
|
||||||
body: GestureAreaProtectorStack(
|
body: GestureAreaProtectorStack(
|
||||||
|
|
|
@ -185,14 +185,17 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
||||||
|
|
||||||
Widget? child;
|
Widget? child;
|
||||||
Map<ShortcutActivator, Intent>? shortcuts = {
|
Map<ShortcutActivator, Intent>? shortcuts = {
|
||||||
const SingleActivator(LogicalKeyboardKey.arrowUp): isTelevision ? const TvShowLessInfoIntent() : const LeaveIntent(),
|
const SingleActivator(LogicalKeyboardKey.arrowUp): isTelevision ? const TvShowLessInfoIntent() : const _LeaveIntent(),
|
||||||
const SingleActivator(LogicalKeyboardKey.arrowDown): isTelevision ? const TvShowMoreInfoIntent() : const ShowInfoIntent(),
|
const SingleActivator(LogicalKeyboardKey.arrowDown): isTelevision ? const _TvShowMoreInfoIntent() : const _ShowInfoIntent(),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.mediaPause): const _PlayPauseIntent.pause(),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.mediaPlay): const _PlayPauseIntent.play(),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.mediaPlayPause): const _PlayPauseIntent.toggle(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasCollection) {
|
if (hasCollection) {
|
||||||
shortcuts.addAll(const {
|
shortcuts.addAll(const {
|
||||||
SingleActivator(LogicalKeyboardKey.arrowLeft): ShowPreviousIntent(),
|
SingleActivator(LogicalKeyboardKey.arrowLeft): _ShowPreviousIntent(),
|
||||||
SingleActivator(LogicalKeyboardKey.arrowRight): ShowNextIntent(),
|
SingleActivator(LogicalKeyboardKey.arrowRight): _ShowNextIntent(),
|
||||||
});
|
});
|
||||||
child = MultiEntryScroller(
|
child = MultiEntryScroller(
|
||||||
collection: collection!,
|
collection: collection!,
|
||||||
|
@ -227,16 +230,18 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
shortcuts: shortcuts,
|
shortcuts: shortcuts,
|
||||||
actions: {
|
actions: {
|
||||||
ShowPreviousIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(-1, animate: false)),
|
_ShowPreviousIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(-1, animate: false)),
|
||||||
ShowNextIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(1, animate: false)),
|
_ShowNextIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(1, animate: false)),
|
||||||
LeaveIntent: CallbackAction<Intent>(onInvoke: (intent) => Navigator.pop(context)),
|
_LeaveIntent: CallbackAction<Intent>(onInvoke: (intent) => Navigator.pop(context)),
|
||||||
ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoPageNotification().dispatch(context)),
|
_ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoPageNotification().dispatch(context)),
|
||||||
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
|
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
|
||||||
TvShowMoreInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)),
|
_TvShowMoreInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)),
|
||||||
|
_PlayPauseIntent: CallbackAction<_PlayPauseIntent>(onInvoke: (intent) => _onPlayPauseIntent(intent, entry)),
|
||||||
ActivateIntent: CallbackAction<Intent>(onInvoke: (intent) {
|
ActivateIntent: CallbackAction<Intent>(onInvoke: (intent) {
|
||||||
if (isTelevision) {
|
if (isTelevision) {
|
||||||
final _entry = entry;
|
final _entry = entry;
|
||||||
if (_entry != null && _entry.isVideo) {
|
if (_entry != null && _entry.isVideo) {
|
||||||
|
// address `TV-PC` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
final controller = context.read<VideoConductor>().getController(_entry);
|
final controller = context.read<VideoConductor>().getController(_entry);
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
VideoActionNotification(controller: controller, action: EntryAction.videoTogglePlay).dispatch(context);
|
VideoActionNotification(controller: controller, action: EntryAction.videoTogglePlay).dispatch(context);
|
||||||
|
@ -330,30 +335,75 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onPlayPauseIntent(_PlayPauseIntent intent, entry) {
|
||||||
|
// address `TV-PP` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
|
final _entry = entry;
|
||||||
|
if (_entry != null && _entry.isVideo) {
|
||||||
|
final controller = context.read<VideoConductor>().getController(_entry);
|
||||||
|
if (controller != null) {
|
||||||
|
bool toggle;
|
||||||
|
switch (intent.type) {
|
||||||
|
case _TvPlayPauseType.play:
|
||||||
|
toggle = !controller.isPlaying;
|
||||||
|
break;
|
||||||
|
case _TvPlayPauseType.pause:
|
||||||
|
toggle = controller.isPlaying;
|
||||||
|
break;
|
||||||
|
case _TvPlayPauseType.toggle:
|
||||||
|
toggle = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (toggle) {
|
||||||
|
VideoActionNotification(controller: controller, action: EntryAction.videoTogglePlay).dispatch(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyboard shortcut intents
|
// keyboard shortcut intents
|
||||||
|
|
||||||
class ShowPreviousIntent extends Intent {
|
class _ShowPreviousIntent extends Intent {
|
||||||
const ShowPreviousIntent();
|
const _ShowPreviousIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShowNextIntent extends Intent {
|
class _ShowNextIntent extends Intent {
|
||||||
const ShowNextIntent();
|
const _ShowNextIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class LeaveIntent extends Intent {
|
class _LeaveIntent extends Intent {
|
||||||
const LeaveIntent();
|
const _LeaveIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShowInfoIntent extends Intent {
|
class _ShowInfoIntent extends Intent {
|
||||||
const ShowInfoIntent();
|
const _ShowInfoIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TvShowLessInfoIntent extends Intent {
|
class TvShowLessInfoIntent extends Intent {
|
||||||
const TvShowLessInfoIntent();
|
const TvShowLessInfoIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TvShowMoreInfoIntent extends Intent {
|
class _TvShowMoreInfoIntent extends Intent {
|
||||||
const TvShowMoreInfoIntent();
|
const _TvShowMoreInfoIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayPauseIntent extends Intent {
|
||||||
|
const _PlayPauseIntent({
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const _PlayPauseIntent.play() : type = _TvPlayPauseType.play;
|
||||||
|
|
||||||
|
const _PlayPauseIntent.pause() : type = _TvPlayPauseType.pause;
|
||||||
|
|
||||||
|
const _PlayPauseIntent.toggle() : type = _TvPlayPauseType.toggle;
|
||||||
|
|
||||||
|
final _TvPlayPauseType type;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _TvPlayPauseType {
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
toggle,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
@ -36,13 +37,16 @@ class InfoAppBar extends StatelessWidget {
|
||||||
final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v));
|
final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v));
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
leading: IconButton(
|
leading: device.isTelevision
|
||||||
// key is expected by test driver
|
? null
|
||||||
key: const Key('back-button'),
|
: IconButton(
|
||||||
icon: const Icon(AIcons.goUp),
|
// key is expected by test driver
|
||||||
onPressed: onBackPressed,
|
key: const Key('back-button'),
|
||||||
tooltip: context.l10n.viewerInfoBackToViewerTooltip,
|
icon: const Icon(AIcons.goUp),
|
||||||
),
|
onPressed: onBackPressed,
|
||||||
|
tooltip: context.l10n.viewerInfoBackToViewerTooltip,
|
||||||
|
),
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
title: SliverAppBarTitleWrapper(
|
title: SliverAppBarTitleWrapper(
|
||||||
child: InteractiveAppBarTitle(
|
child: InteractiveAppBarTitle(
|
||||||
onTap: () => _goToSearch(context),
|
onTap: () => _goToSearch(context),
|
||||||
|
|
Loading…
Reference in a new issue