diff --git a/assets/terms.md b/assets/terms.md index e00aca5a1..50e0dc078 100644 --- a/assets/terms.md +++ b/assets/terms.md @@ -8,7 +8,7 @@ You must use the app for legal, authorized and acceptable purposes. The app is released “as-is”, without any warranty, responsibility or liability. Use of the app is at your own risk. -## Privacy policy +## Privacy Policy The app does not collect any personal data. 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/l10n/app_en.arb b/lib/l10n/app_en.arb index dfaf26beb..12c8b9d7c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -451,6 +451,8 @@ "@aboutLinkSources": {}, "aboutLinkLicense": "License", "@aboutLinkLicense": {}, + "aboutLinkPolicy": "Privacy Policy", + "@aboutLinkPolicy": {}, "aboutUpdate": "New Version Available", "@aboutUpdate": {}, @@ -504,6 +506,9 @@ "aboutLicensesShowAllButtonLabel": "Show All Licenses", "@aboutLicensesShowAllButtonLabel": {}, + "policyPageTitle": "Privacy Policy", + "@policyPageTitle": {}, + "collectionPageTitle": "Collection", "@collectionPageTitle": {}, "collectionPickPageTitle": "Pick", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index e325885c2..4b3374459 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -203,6 +203,7 @@ "aboutPageTitle": "앱 정보", "aboutLinkSources": "소스 코드", "aboutLinkLicense": "라이선스", + "aboutLinkPolicy": "개인정보 보호정책", "aboutUpdate": "업데이트 사용 가능", "aboutUpdateLinks1": "앱의 최신 버전을", @@ -232,6 +233,8 @@ "aboutLicensesDartPackages": "다트 패키지", "aboutLicensesShowAllButtonLabel": "라이선스 모두 보기", + "policyPageTitle": "개인정보 보호정책", + "collectionPageTitle": "미디어", "collectionPickPageTitle": "항목 선택", "collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}", diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 77f8d0b5f..cf632b400 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/about/policy_page.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; @@ -66,16 +67,18 @@ class _AppReferenceState extends State { } Widget _buildLinks() { + final l10n = context.l10n; return Wrap( - crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, spacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, children: [ LinkChip( leading: const Icon( AIcons.github, size: 24, ), - text: context.l10n.aboutLinkSources, + text: l10n.aboutLinkSources, url: Constants.avesGithub, ), LinkChip( @@ -83,10 +86,28 @@ class _AppReferenceState extends State { AIcons.legal, size: 22, ), - text: context.l10n.aboutLinkLicense, + text: l10n.aboutLinkLicense, url: '${Constants.avesGithub}/blob/main/LICENSE', ), + LinkChip( + leading: const Icon( + AIcons.privacy, + size: 22, + ), + text: l10n.aboutLinkPolicy, + onTap: _goToPolicyPage, + ), ], ); } + + void _goToPolicyPage() { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: PolicyPage.routeName), + builder: (context) => const PolicyPage(), + ), + ); + } } diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart new file mode 100644 index 000000000..fbfa3adcc --- /dev/null +++ b/lib/widgets/about/policy_page.dart @@ -0,0 +1,49 @@ +import 'package:aves/widgets/common/basic/markdown_container.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PolicyPage extends StatefulWidget { + static const routeName = '/about/policy'; + + const PolicyPage({ + Key? key, + }) : super(key: key); + + @override + _PolicyPageState createState() => _PolicyPageState(); +} + +class _PolicyPageState extends State { + late Future _termsLoader; + + @override + void initState() { + super.initState(); + _termsLoader = rootBundle.loadString('assets/terms.md'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.policyPageTitle), + ), + body: SafeArea( + child: Center( + child: FutureBuilder( + future: _termsLoader, + builder: (context, snapshot) { + if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); + final terms = snapshot.data!; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: MarkdownContainer(data: terms), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/basic/link_chip.dart b/lib/widgets/common/basic/link_chip.dart index d1437531f..63929a809 100644 --- a/lib/widgets/common/basic/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -5,9 +5,10 @@ import 'package:url_launcher/url_launcher.dart'; class LinkChip extends StatelessWidget { final Widget? leading; final String text; - final String url; + final String? url; final Color? color; final TextStyle? textStyle; + final VoidCallback? onTap; static const borderRadius = BorderRadius.all(Radius.circular(8)); @@ -15,22 +16,25 @@ class LinkChip extends StatelessWidget { Key? key, this.leading, required this.text, - required this.url, + this.url, this.color, this.textStyle, + this.onTap, }) : super(key: key); @override Widget build(BuildContext context) { + final _url = url; return DefaultTextStyle.merge( style: (textStyle ?? const TextStyle()).copyWith(color: color), child: InkWell( borderRadius: borderRadius, - onTap: () async { - if (await canLaunch(url)) { - await launch(url); - } - }, + onTap: onTap ?? + () async { + if (_url != null && await canLaunch(_url)) { + await launch(_url); + } + }, child: Padding( padding: const EdgeInsets.all(8.0), child: Row( diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart new file mode 100644 index 000000000..521daa510 --- /dev/null +++ b/lib/widgets/common/basic/markdown_container.dart @@ -0,0 +1,53 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MarkdownContainer extends StatelessWidget { + final String data; + + const MarkdownContainer({ + Key? key, + required this.data, + }) : super(key: key); + + static const double maxWidth = 460; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(16)), + color: Colors.white10, + ), + constraints: const BoxConstraints(maxWidth: maxWidth), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Theme( + data: Theme.of(context).copyWith( + scrollbarTheme: const ScrollbarThemeData( + isAlwaysShown: true, + radius: Radius.circular(16), + crossAxisMargin: 6, + mainAxisMargin: 16, + interactive: true, + ), + ), + child: Scrollbar( + child: Markdown( + data: data, + selectable: true, + onTapLink: (text, href, title) async { + if (href != null && await canLaunch(href)) { + await launch(href); + } + }, + shrinkWrap: true, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 6fbed43dc..ba3ed3018 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -1,6 +1,7 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/basic/markdown_container.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/common/identity/buttons.dart'; @@ -10,10 +11,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; class WelcomePage extends StatefulWidget { const WelcomePage({Key? key}) : super(key: key); @@ -26,8 +25,6 @@ class _WelcomePageState extends State { bool _hasAcceptedTerms = false; late Future _termsLoader; - static const double maxWidth = 460; - @override void initState() { super.initState(); @@ -44,7 +41,7 @@ class _WelcomePageState extends State { child: FutureBuilder( future: _termsLoader, builder: (context, snapshot) { - if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); final terms = snapshot.data!; final durations = context.watch(); final isPortrait = context.select((mq) => mq.orientation) == Orientation.portrait; @@ -62,7 +59,7 @@ class _WelcomePageState extends State { children: [ ..._buildHeader(context, isPortrait: isPortrait), if (isPortrait) ...[ - Flexible(child: _buildTerms(terms)), + Flexible(child: MarkdownContainer(data: terms)), const SizedBox(height: 16), ..._buildControls(context), ] else @@ -72,7 +69,7 @@ class _WelcomePageState extends State { Flexible( child: Padding( padding: const EdgeInsets.only(bottom: 8), - child: _buildTerms(terms), + child: MarkdownContainer(data: terms), )), Flexible( child: ListView( @@ -127,7 +124,7 @@ class _WelcomePageState extends State { final canEnableErrorReporting = context.select((v) => v.canEnableErrorReporting); const contentPadding = EdgeInsets.symmetric(horizontal: 8); final switches = ConstrainedBox( - constraints: const BoxConstraints(maxWidth: maxWidth), + constraints: const BoxConstraints(maxWidth: MarkdownContainer.maxWidth), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -183,43 +180,6 @@ class _WelcomePageState extends State { ]; } - Widget _buildTerms(String terms) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(16)), - color: Colors.white10, - ), - constraints: const BoxConstraints(maxWidth: maxWidth), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: Theme( - data: Theme.of(context).copyWith( - scrollbarTheme: const ScrollbarThemeData( - isAlwaysShown: true, - radius: Radius.circular(16), - crossAxisMargin: 6, - mainAxisMargin: 16, - interactive: true, - ), - ), - child: Scrollbar( - child: Markdown( - data: terms, - selectable: true, - onTapLink: (text, href, title) async { - if (href != null && await canLaunch(href)) { - await launch(href); - } - }, - shrinkWrap: true, - ), - ), - ), - ), - ); - } - // as of flutter_staggered_animations v0.1.2, `AnimationConfiguration.toStaggeredList` does not handle `Flexible` widgets // so we use this workaround instead static List _toStaggeredList({ diff --git a/untranslated.json b/untranslated.json index e6b83a860..dfaff10b2 100644 --- a/untranslated.json +++ b/untranslated.json @@ -17,7 +17,9 @@ "unsupportedTypeDialogTitle", "unsupportedTypeDialogMessage", "editEntryDateDialogExtractFromTitle", + "aboutLinkPolicy", "aboutCreditsTranslators", + "policyPageTitle", "collectionActionEdit", "collectionEditFailureFeedback", "collectionEditSuccessFeedback",