This commit is contained in:
Thibault Deckers 2023-03-01 18:59:48 +01:00
parent 4fdaf23a0e
commit 7797c03170
15 changed files with 236 additions and 251 deletions

View file

@ -1,7 +1,4 @@
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:flutter/foundation.dart';
class Durations {
// Flutter animations (with margin)
@ -72,26 +69,6 @@ class Durations {
static const mapIdleDebounceDelay = Duration(milliseconds: 100);
}
class DurationsProvider extends StatelessWidget {
final Widget child;
const DurationsProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return ProxyProvider<Settings, DurationsData>(
update: (context, settings, __) {
final enabled = settings.accessibilityAnimations.animate;
return enabled ? DurationsData() : DurationsData.noAnimation();
},
child: child,
);
}
}
@immutable
class DurationsData {
// common animations

View file

@ -33,6 +33,7 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/route_tracker.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/durations_provider.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/home_page.dart';
@ -190,95 +191,87 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Widget build(BuildContext context) {
// place the settings provider above `MaterialApp`
// so it can be used during navigation transitions
return Provider<AppFlavor>.value(
value: widget.flavor,
child: ChangeNotifierProvider<Settings>.value(
value: settings,
child: ListenableProvider<ValueNotifier<AppMode>>.value(
value: _appModeNotifier,
child: Provider<CollectionSource>.value(
value: _mediaStoreSource,
child: Provider<TvRailController>.value(
value: _tvRailController,
child: DurationsProvider(
child: HighlightInfoProvider(
child: OverlaySupport(
child: FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
if (initialized) {
AvesApp.showSystemUI();
}
final home = initialized
? _getFirstPage()
: AvesScaffold(
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
);
return Selector<Settings, Tuple3<Locale?, AvesThemeBrightness, bool>>(
selector: (context, s) => Tuple3(
s.locale,
s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final themeBrightness = s.item2;
final enableDynamicColor = s.item3;
Constants.updateStylesForLocale(settings.appliedLocale);
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return Shortcuts(
shortcuts: {
// handle Android TV remote `select` button
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => _decorateAppChild(
context: context,
initialized: initialized,
child: child,
),
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
),
);
},
);
},
);
},
),
),
),
return MultiProvider(
providers: [
Provider<AppFlavor>.value(value: widget.flavor),
ChangeNotifierProvider<Settings>.value(value: settings),
ListenableProvider<ValueNotifier<AppMode>>.value(value: _appModeNotifier),
Provider<CollectionSource>.value(value: _mediaStoreSource),
Provider<TvRailController>.value(value: _tvRailController),
DurationsProvider(),
HighlightInfoProvider(),
],
child: OverlaySupport(
child: FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
if (initialized) {
AvesApp.showSystemUI();
}
final home = initialized
? _getFirstPage()
: AvesScaffold(
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
);
return Selector<Settings, Tuple3<Locale?, AvesThemeBrightness, bool>>(
selector: (context, s) => Tuple3(
s.locale,
s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
),
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final themeBrightness = s.item2;
final enableDynamicColor = s.item3;
Constants.updateStylesForLocale(settings.appliedLocale);
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return Shortcuts(
shortcuts: {
// handle Android TV remote `select` button
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => _decorateAppChild(
context: context,
initialized: initialized,
child: child,
),
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
),
);
},
);
},
);
},
),
),
);

View file

@ -19,7 +19,7 @@ import 'package:aves/widgets/collection/draggable_thumb_label.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/tile.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';

View file

@ -14,7 +14,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/durations.dart';
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/notifications.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = [];
late CollectionLens _collection;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
@ -146,7 +146,7 @@ class _CollectionPageState extends State<CollectionPage> {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>(
return NotificationListener<DraggableScrollbarNotification>(
onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event);
return false;

View file

@ -0,0 +1,38 @@
import 'package:flutter/rendering.dart';
class ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0.0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0.0);
path.lineTo(0.0, 0.0);
path.close();
const arrowWidth = 8.0;
final startPointX = (size.width - arrowWidth) / 2;
var startPointY = size.height / 2 - arrowWidth / 2;
path.moveTo(startPointX, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
path.lineTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
path.lineTo(startPointX, startPointY + 1.0);
path.close();
startPointY = size.height / 2 + arrowWidth / 2;
path.moveTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
path.lineTo(startPointX, startPointY);
path.lineTo(startPointX, startPointY - 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

View file

@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
@immutable
class DraggableScrollbarNotification extends Notification {
final DraggableScrollbarEvent event;
const DraggableScrollbarNotification(this.event);
}
enum DraggableScrollbarEvent { dragStart, dragEnd }

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class ScrollLabel extends StatelessWidget {
final Animation<double> animation;
final Color backgroundColor;
final Widget child;
const ScrollLabel({
super.key,
required this.child,
required this.animation,
required this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: animation,
child: Container(
margin: const EdgeInsetsDirectional.only(end: 12.0),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: child,
),
),
);
}
}

View file

@ -1,7 +1,9 @@
import 'dart:async';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scroll_label.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/transition.dart';
import 'package:flutter/widgets.dart';
/*
adapted from package `draggable_scrollbar` v0.0.4:
@ -109,35 +111,6 @@ class DraggableScrollbar extends StatefulWidget {
}
}
class ScrollLabel extends StatelessWidget {
final Animation<double> animation;
final Color backgroundColor;
final Widget child;
const ScrollLabel({
super.key,
required this.child,
required this.animation,
required this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: animation,
child: Container(
margin: const EdgeInsetsDirectional.only(end: 12.0),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: child,
),
),
);
}
}
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
bool _isDragInProcess = false;
@ -304,7 +277,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
}
void _onVerticalDragStart() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragStart).dispatch(context);
const DraggableScrollbarNotification(DraggableScrollbarEvent.dragStart).dispatch(context);
_labelAnimationController.forward();
_fadeoutTimer?.cancel();
_showThumb();
@ -326,7 +299,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
}
void _onVerticalDragEnd() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context);
const DraggableScrollbarNotification(DraggableScrollbarEvent.dragEnd).dispatch(context);
_scheduleFadeout();
setState(() => _isDragInProcess = false);
}
@ -373,79 +346,3 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
}
}
}
///This cut 2 lines in arrow shape
class ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0.0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0.0);
path.lineTo(0.0, 0.0);
path.close();
const arrowWidth = 8.0;
final startPointX = (size.width - arrowWidth) / 2;
var startPointY = size.height / 2 - arrowWidth / 2;
path.moveTo(startPointX, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
path.lineTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
path.lineTo(startPointX, startPointY + 1.0);
path.close();
startPointY = size.height / 2 + arrowWidth / 2;
path.moveTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
path.lineTo(startPointX, startPointY);
path.lineTo(startPointX, startPointY - 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
class SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
child: SlideTransition(
position: Tween(
begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
),
);
}
}
@immutable
class DraggableScrollBarNotification extends Notification {
final DraggableScrollBarEvent event;
const DraggableScrollBarNotification(this.event);
}
enum DraggableScrollBarEvent { dragStart, dragEnd }

View file

@ -0,0 +1,31 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
class SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
child: SlideTransition(
position: Tween(
begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
),
);
}
}

View file

@ -1,4 +1,5 @@
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/arrow_clipper.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:flutter/material.dart';
const double avesScrollThumbHeight = 48;

View file

@ -0,0 +1,16 @@
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:provider/provider.dart';
class DurationsProvider extends ProxyProvider<Settings, DurationsData> {
DurationsProvider({
super.key,
super.child,
}) : super(
update: (context, settings, __) {
final enabled = settings.accessibilityAnimations.animate;
return enabled ? DurationsData() : DurationsData.noAnimation();
},
);
}

View file

@ -1,20 +1,11 @@
import 'package:aves/model/highlight.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class HighlightInfoProvider extends StatelessWidget {
final Widget child;
const HighlightInfoProvider({
class HighlightInfoProvider extends ChangeNotifierProvider<HighlightInfo> {
HighlightInfoProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<HighlightInfo>(
create: (context) => HighlightInfo(),
child: child,
);
}
super.child,
}) : super(
create: (context) => HighlightInfo(),
);
}

View file

@ -12,7 +12,8 @@ import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
@ -61,7 +62,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final QueryTest<T> applyQuery;
final Widget Function() emptyBuilder;
final HeroType heroType;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
FilterGridPage({
super.key,
@ -145,7 +146,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>(
return NotificationListener<DraggableScrollbarNotification>(
onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event);
return false;

View file

@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:flutter/material.dart';
class FloatingNavBar extends StatefulWidget {
final ScrollController? scrollController;
final Stream<DraggableScrollBarEvent> events;
final Stream<DraggableScrollbarEvent> events;
final double childHeight;
final Widget child;
@ -109,12 +109,12 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
}
}
void _onDraggableScrollBarEvent(DraggableScrollBarEvent event) {
void _onDraggableScrollBarEvent(DraggableScrollbarEvent event) {
switch (event) {
case DraggableScrollBarEvent.dragStart:
case DraggableScrollbarEvent.dragStart:
_isDragging = true;
break;
case DraggableScrollBarEvent.dragEnd:
case DraggableScrollbarEvent.dragEnd:
_isDragging = false;
break;
}

View file

@ -5,7 +5,7 @@ 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/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
@ -17,7 +17,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppBottomNavBar extends StatefulWidget {
final Stream<DraggableScrollBarEvent> events;
final Stream<DraggableScrollbarEvent> events;
// collection loaded in the `CollectionPage`, if any
final CollectionLens? currentCollection;