diff --git a/app/lib/components/app_page.dart b/app/lib/components/app_page.dart new file mode 100644 index 0000000..4839f8f --- /dev/null +++ b/app/lib/components/app_page.dart @@ -0,0 +1,94 @@ +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class AppPage extends StatelessWidget { + final String? title; + final Widget? titleWidget; + final List? actions; + final Widget? leading; + final Widget? floatingActionButton; + final Widget? body; + final bool automaticallyImplyLeading; + final bool? resizeToAvoidBottomInset; + + const AppPage({ + super.key, + this.title, + this.titleWidget, + this.actions, + this.leading, + this.body, + this.floatingActionButton, + this.resizeToAvoidBottomInset, + this.automaticallyImplyLeading = true, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isIOS || Platform.isMacOS) { + return _buildCupertinoScaffold(context); + } else { + return _buildMaterialScaffold(context); + } + } + + Widget _buildCupertinoScaffold(BuildContext context) => CupertinoPageScaffold( + navigationBar: (title == null && titleWidget == null) && + actions == null && + leading == null + ? null + : CupertinoNavigationBar( + leading: leading, + middle: titleWidget ?? _title(context), + border: null, + trailing: actions == null + ? null + : actions!.length == 1 + ? actions!.first + : Row( + mainAxisSize: MainAxisSize.min, + children: actions!, + ), + automaticallyImplyLeading: automaticallyImplyLeading, + previousPageTitle: automaticallyImplyLeading + ? MaterialLocalizations.of(context).backButtonTooltip + : null, + ), + resizeToAvoidBottomInset: resizeToAvoidBottomInset ?? true, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + body ?? const SizedBox(), + SafeArea( + child: Padding( + padding: const EdgeInsets.only(right: 16, bottom: 16), + child: floatingActionButton ?? const SizedBox(), + ), + ), + ], + ), + ); + + Widget _buildMaterialScaffold(BuildContext context) => Scaffold( + appBar: (title == null && titleWidget == null) && + actions == null && + leading == null + ? null + : AppBar( + title: titleWidget ?? _title(context), + actions: actions, + leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, + ), + body: body, + floatingActionButton: floatingActionButton, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + ); + + Widget _title(BuildContext context) => Text( + title ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); +} diff --git a/style/lib/animations/parallex_effect.dart b/style/lib/animations/parallex_effect.dart new file mode 100644 index 0000000..935a373 --- /dev/null +++ b/style/lib/animations/parallex_effect.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class ParallaxFlowDelegate extends FlowDelegate { + ParallaxFlowDelegate({ + required this.scrollable, + required this.listItemContext, + required this.backgroundImageKey, + }) : super(repaint: scrollable.position); + + final ScrollableState scrollable; + final BuildContext listItemContext; + final GlobalKey backgroundImageKey; + + @override + BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) { + return BoxConstraints.tightFor( + width: constraints.maxWidth, + ); + } + + @override + void paintChildren(FlowPaintingContext context) { + // Ensure necessary objects are not null before proceeding + final backgroundImageContext = backgroundImageKey.currentContext; + if (backgroundImageContext == null) { + return; + } + + // Calculate the position of this list item within the viewport. + final scrollableBox = scrollable.context.findRenderObject() as RenderBox; + final listItemBox = listItemContext.findRenderObject() as RenderBox; + final listItemOffset = listItemBox.localToGlobal( + listItemBox.size.centerLeft(Offset.zero), + ancestor: scrollableBox, + ); + + // Determine the percent position of this list item within the scrollable area. + final viewportDimension = scrollable.position.viewportDimension; + final scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0); + + // Calculate the vertical alignment of the background based on the scroll percent. + final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1); + + // Convert the background alignment into a pixel offset for painting purposes. + final backgroundSize = (backgroundImageContext.findRenderObject() as RenderBox).size; + final listItemSize = context.size; + final childRect = verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize); + + // Paint the background. + context.paintChild( + 0, + transform: Transform.translate(offset: Offset(0.0, childRect.top)).transform, + ); + } + + @override + bool shouldRepaint(ParallaxFlowDelegate oldDelegate) { + return scrollable != oldDelegate.scrollable || + listItemContext != oldDelegate.listItemContext || + backgroundImageKey != oldDelegate.backgroundImageKey; + } +}