diff --git a/lib/main.dart b/lib/main.dart index 8b6066bae..f30bf99c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,6 +37,8 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State { Future _appSetup; + static const accentColor = Colors.indigoAccent; + @override void initState() { super.initState(); @@ -49,8 +51,10 @@ class _AvesAppState extends State { title: 'Aves', theme: ThemeData( brightness: Brightness.dark, - accentColor: Colors.indigoAccent, + accentColor: accentColor, scaffoldBackgroundColor: Colors.grey[900], + buttonColor: accentColor, + toggleableActiveColor: accentColor, tooltipTheme: const TooltipThemeData( verticalOffset: 32, ), diff --git a/lib/model/terms.dart b/lib/model/terms.dart index 7260700be..ba2f9ed66 100644 --- a/lib/model/terms.dart +++ b/lib/model/terms.dart @@ -4,6 +4,9 @@ Aves is an open-source gallery and metadata explorer app allowing you to access You must use the app for legal, authorized and acceptable purposes. +# Disclaimer +This app is released "as-is", without any warranty, responsibility or liability. Use of the app is at your own risk. + # Privacy policy Aves does not collect any personal data in its standard use. We never have access to your photos and videos. This also means that we cannot get them back for you if you delete them without backing them up. diff --git a/lib/widgets/common/labeled_checkbox.dart b/lib/widgets/common/labeled_checkbox.dart new file mode 100644 index 000000000..be10a3cb5 --- /dev/null +++ b/lib/widgets/common/labeled_checkbox.dart @@ -0,0 +1,59 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class LabeledCheckbox extends StatefulWidget { + final bool value; + final ValueChanged onChanged; + final String text; + + const LabeledCheckbox({ + Key key, + @required this.value, + @required this.onChanged, + @required this.text, + }) : super(key: key); + + @override + _LabeledCheckboxState createState() => _LabeledCheckboxState(); +} + +class _LabeledCheckboxState extends State { + TapGestureRecognizer _tapRecognizer; + + @override + void initState() { + super.initState(); + _tapRecognizer = TapGestureRecognizer() + ..onTap = () { + debugPrint('tapped'); + widget.onChanged(!widget.value); + }; + } + + @override + void dispose() { + _tapRecognizer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Checkbox( + value: widget.value, + onChanged: widget.onChanged, + ), + ), + TextSpan( + text: widget.text, + recognizer: _tapRecognizer, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/welcome.dart b/lib/widgets/welcome.dart index 2fc485bbc..02ed615d7 100644 --- a/lib/widgets/welcome.dart +++ b/lib/widgets/welcome.dart @@ -2,8 +2,10 @@ import 'package:aves/main.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/terms.dart'; import 'package:aves/widgets/common/aves_logo.dart'; +import 'package:aves/widgets/common/labeled_checkbox.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:url_launcher/url_launcher.dart'; class WelcomePage extends StatefulWidget { @@ -18,7 +20,6 @@ class _WelcomePageState extends State { @override Widget build(BuildContext context) { - final accentColor = Theme.of(context).accentColor; return Scaffold( body: SafeArea( child: Container( @@ -26,75 +27,144 @@ class _WelcomePageState extends State { padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, - children: [ - const AvesLogo(size: 64), - const SizedBox(height: 16), - const Text( - 'Welcome to Aves', - style: TextStyle( - fontSize: 22, - fontFamily: 'Concourse', + children: _toStaggeredList( + duration: const Duration(milliseconds: 375), + childAnimationBuilder: (child) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, ), ), - const SizedBox(height: 16), - Flexible( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Colors.white10, - ), - child: SingleChildScrollView( - child: Container( - child: MarkdownBody( - data: termsAndConditions, - onTapLink: (url) async { - if (await canLaunch(url)) { - await launch(url); - } - }, - ), // const Text('Terms terms terms'), - ), - ), - ), - ), - Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Checkbox( - value: _hasAcceptedTerms, - onChanged: (v) => setState(() => _hasAcceptedTerms = v), - activeColor: accentColor, - ), - ), - const TextSpan( - text: 'I accept the Terms of Service', - ), - ], - ), - ), - RaisedButton( - color: accentColor, - child: const Text('Continue'), - onPressed: _hasAcceptedTerms - ? () { - settings.hasAcceptedTerms = true; - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => const HomePage(), - ), - (route) => false, - ); - } - : null, - ), - ], + children: _buildChildren(context), + ), ), ), ), ); } + + List _buildChildren(BuildContext context) { + return [ + ..._buildTop(context), + Flexible(child: _buildTerms()), + ..._buildBottomControls(context), + ]; + } + + List _buildTop(BuildContext context) { + const message = Text( + 'Welcome to Aves', + style: TextStyle( + fontSize: 22, + fontFamily: 'Concourse', + ), + ); + return [ + ...(MediaQuery.of(context).orientation == Orientation.portrait + ? [ + const AvesLogo(size: 64), + const SizedBox(height: 16), + message, + ] + : [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const AvesLogo(size: 48), + const SizedBox(width: 16), + message, + ], + ) + ]), + const SizedBox(height: 16), + ]; + } + + List _buildBottomControls(BuildContext context) { + final checkbox = LabeledCheckbox( + value: _hasAcceptedTerms, + onChanged: (v) => setState(() => _hasAcceptedTerms = v), + text: 'I agree to the terms and conditions', + ); + final button = RaisedButton( + child: const Text('Continue'), + onPressed: _hasAcceptedTerms + ? () { + settings.hasAcceptedTerms = true; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const HomePage(), + ), + (route) => false, + ); + } + : null, + ); + return MediaQuery.of(context).orientation == Orientation.portrait + ? [ + checkbox, + button, + ] + : [ + const SizedBox(height: 16), + Row( + children: [ + checkbox, + const Spacer(), + button, + ], + ), + ]; + } + + Widget _buildTerms() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white10, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Markdown( + data: termsAndConditions, + // TODO TLAD make it selectable when this fix (in 1.18.0-6.0.pre) lands on stable: https://github.com/flutter/flutter/pull/54479 + selectable: false, + onTapLink: (url) async { + if (await canLaunch(url)) { + await launch(url); + } + }, + shrinkWrap: true, + ), + ), + ); + } + + // workaround to handle `Flexible` widgets, + // because `AnimationConfiguration.toStaggeredList` does not, + // as of flutter_staggered_animations v0.1.2, + static List _toStaggeredList({ + Duration duration, + Duration delay, + @required Widget Function(Widget) childAnimationBuilder, + @required List children, + }) => + children + .asMap() + .map((index, widget) { + var child = widget is Flexible ? widget.child : widget; + child = AnimationConfiguration.staggeredList( + position: index, + duration: duration, + child: childAnimationBuilder(child), + ); + child = widget is Flexible ? Flexible(child: child) : child; + return MapEntry( + index, + child, + ); + }) + .values + .toList(); }