diff --git a/lib/main.dart b/lib/main.dart index d66259c8d..8b6066bae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; +import 'package:aves/widgets/welcome.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -26,9 +27,22 @@ void main() { enum AppMode { main, pick, view } -class AvesApp extends StatelessWidget { +class AvesApp extends StatefulWidget { static AppMode mode = AppMode.main; + @override + _AvesAppState createState() => _AvesAppState(); +} + +class _AvesAppState extends State { + Future _appSetup; + + @override + void initState() { + super.initState(); + _appSetup = settings.init(); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -50,7 +64,14 @@ class AvesApp extends StatelessWidget { ), ), ), - home: const HomePage(), + home: FutureBuilder( + future: _appSetup, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasError) return const Icon(AIcons.error); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + return settings.hasAcceptedTerms ? const HomePage() : const WelcomePage(); + }, + ), ); } } @@ -69,7 +90,6 @@ class _HomePageState extends State { @override void initState() { - debugPrint('$runtimeType initState'); super.initState(); _appSetup = _setup(); imageCache.maximumSizeBytes = 512 * (1 << 20); @@ -93,8 +113,6 @@ class _HomePageState extends State { // TODO notify when icons are ready for drawer and section header refresh await androidFileUtils.init(); // 170ms - await settings.init(); // <20ms - final intentData = await ViewerService.getIntentData(); if (intentData != null) { final action = intentData['action']; diff --git a/lib/model/settings.dart b/lib/model/settings.dart index c95f70559..2ac161756 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -20,6 +20,7 @@ class Settings { static const collectionTileExtentKey = 'collection_tile_extent'; static const infoMapZoomKey = 'info_map_zoom'; static const catalogTimeZoneKey = 'catalog_time_zone'; + static const hasAcceptedTermsKey = 'has_accepted_terms'; Future init() async { _prefs = await SharedPreferences.getInstance(); @@ -69,6 +70,10 @@ class Settings { set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue); + bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false); + + set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); + // convenience methods bool getBoolOrDefault(String key, bool defaultValue) => _prefs.getKeys().contains(key) ? _prefs.getBool(key) : defaultValue; diff --git a/lib/model/terms.dart b/lib/model/terms.dart new file mode 100644 index 000000000..7260700be --- /dev/null +++ b/lib/model/terms.dart @@ -0,0 +1,15 @@ +const String termsAndConditions = ''' +# Terms of Service +Aves is an open-source gallery and metadata explorer app allowing you to access and manage your local photos. + +You must use the app for legal, authorized and acceptable purposes. + +# 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. + +__We collect anonymous data to improve the app.__ We use Google Firebase for Analytics and Crash Reporting, and the anonymous analytics data are stored on their servers. Please note that those are anonymous data, there is absolutely nothing personal about those data. + +## Links +[Sources](https://github.com/deckerst/aves) +'''; + diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 1c2f2652a..42743c4cb 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -107,6 +107,12 @@ class Constants { licenseUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer/blob/master/LICENSE', sourceUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer', ), + Dependency( + name: 'Flutter Markdown', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/flutter_markdown/blob/master/LICENSE', + sourceUrl: 'https://github.com/flutter/flutter_markdown', + ), Dependency( name: 'Flutter Native Timezone', license: 'Apache 2.0', diff --git a/lib/widgets/welcome.dart b/lib/widgets/welcome.dart new file mode 100644 index 000000000..2fc485bbc --- /dev/null +++ b/lib/widgets/welcome.dart @@ -0,0 +1,100 @@ +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:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class WelcomePage extends StatefulWidget { + const WelcomePage(); + + @override + _WelcomePageState createState() => _WelcomePageState(); +} + +class _WelcomePageState extends State { + bool _hasAcceptedTerms = false; + + @override + Widget build(BuildContext context) { + final accentColor = Theme.of(context).accentColor; + return Scaffold( + body: SafeArea( + child: Container( + alignment: Alignment.center, + 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', + ), + ), + 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, + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a5381b6ca..08b2f62ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,6 +125,13 @@ packages: relative: true source: path version: "0.3.6" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1" flutter_native_timezone: dependency: "direct main" description: @@ -212,6 +219,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.11.4" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" matcher: dependency: transitive description: @@ -555,4 +569,4 @@ packages: version: "3.6.1" sdks: dart: ">=2.7.2 <3.0.0" - flutter: ">=1.16.3 <2.0.0" + flutter: ">=1.17.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8394b5e7e..ffd651f04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: path: ../flutter_ijkplayer # git: # url: git://github.com/deckerst/flutter_ijkplayer.git + flutter_markdown: flutter_native_timezone: flutter_staggered_animations: flutter_svg: