about: link to privacy policy
This commit is contained in:
parent
941288b5fc
commit
f6ac8f5e37
9 changed files with 153 additions and 56 deletions
|
@ -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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -451,6 +451,8 @@
|
||||||
"@aboutLinkSources": {},
|
"@aboutLinkSources": {},
|
||||||
"aboutLinkLicense": "License",
|
"aboutLinkLicense": "License",
|
||||||
"@aboutLinkLicense": {},
|
"@aboutLinkLicense": {},
|
||||||
|
"aboutLinkPolicy": "Privacy Policy",
|
||||||
|
"@aboutLinkPolicy": {},
|
||||||
|
|
||||||
"aboutUpdate": "New Version Available",
|
"aboutUpdate": "New Version Available",
|
||||||
"@aboutUpdate": {},
|
"@aboutUpdate": {},
|
||||||
|
@ -504,6 +506,9 @@
|
||||||
"aboutLicensesShowAllButtonLabel": "Show All Licenses",
|
"aboutLicensesShowAllButtonLabel": "Show All Licenses",
|
||||||
"@aboutLicensesShowAllButtonLabel": {},
|
"@aboutLicensesShowAllButtonLabel": {},
|
||||||
|
|
||||||
|
"policyPageTitle": "Privacy Policy",
|
||||||
|
"@policyPageTitle": {},
|
||||||
|
|
||||||
"collectionPageTitle": "Collection",
|
"collectionPageTitle": "Collection",
|
||||||
"@collectionPageTitle": {},
|
"@collectionPageTitle": {},
|
||||||
"collectionPickPageTitle": "Pick",
|
"collectionPickPageTitle": "Pick",
|
||||||
|
|
|
@ -203,6 +203,7 @@
|
||||||
"aboutPageTitle": "앱 정보",
|
"aboutPageTitle": "앱 정보",
|
||||||
"aboutLinkSources": "소스 코드",
|
"aboutLinkSources": "소스 코드",
|
||||||
"aboutLinkLicense": "라이선스",
|
"aboutLinkLicense": "라이선스",
|
||||||
|
"aboutLinkPolicy": "개인정보 보호정책",
|
||||||
|
|
||||||
"aboutUpdate": "업데이트 사용 가능",
|
"aboutUpdate": "업데이트 사용 가능",
|
||||||
"aboutUpdateLinks1": "앱의 최신 버전을",
|
"aboutUpdateLinks1": "앱의 최신 버전을",
|
||||||
|
@ -232,6 +233,8 @@
|
||||||
"aboutLicensesDartPackages": "다트 패키지",
|
"aboutLicensesDartPackages": "다트 패키지",
|
||||||
"aboutLicensesShowAllButtonLabel": "라이선스 모두 보기",
|
"aboutLicensesShowAllButtonLabel": "라이선스 모두 보기",
|
||||||
|
|
||||||
|
"policyPageTitle": "개인정보 보호정책",
|
||||||
|
|
||||||
"collectionPageTitle": "미디어",
|
"collectionPageTitle": "미디어",
|
||||||
"collectionPickPageTitle": "항목 선택",
|
"collectionPickPageTitle": "항목 선택",
|
||||||
"collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}",
|
"collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.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/basic/link_chip.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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/aves_logo.dart';
|
||||||
|
@ -66,16 +67,18 @@ class _AppReferenceState extends State<AppReference> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLinks() {
|
Widget _buildLinks() {
|
||||||
|
final l10n = context.l10n;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
LinkChip(
|
LinkChip(
|
||||||
leading: const Icon(
|
leading: const Icon(
|
||||||
AIcons.github,
|
AIcons.github,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
text: context.l10n.aboutLinkSources,
|
text: l10n.aboutLinkSources,
|
||||||
url: Constants.avesGithub,
|
url: Constants.avesGithub,
|
||||||
),
|
),
|
||||||
LinkChip(
|
LinkChip(
|
||||||
|
@ -83,10 +86,28 @@ class _AppReferenceState extends State<AppReference> {
|
||||||
AIcons.legal,
|
AIcons.legal,
|
||||||
size: 22,
|
size: 22,
|
||||||
),
|
),
|
||||||
text: context.l10n.aboutLinkLicense,
|
text: l10n.aboutLinkLicense,
|
||||||
url: '${Constants.avesGithub}/blob/main/LICENSE',
|
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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
49
lib/widgets/about/policy_page.dart
Normal file
49
lib/widgets/about/policy_page.dart
Normal file
|
@ -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<PolicyPage> {
|
||||||
|
late Future<String> _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<String>(
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,10 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
class LinkChip extends StatelessWidget {
|
class LinkChip extends StatelessWidget {
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final String text;
|
final String text;
|
||||||
final String url;
|
final String? url;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
static const borderRadius = BorderRadius.all(Radius.circular(8));
|
static const borderRadius = BorderRadius.all(Radius.circular(8));
|
||||||
|
|
||||||
|
@ -15,22 +16,25 @@ class LinkChip extends StatelessWidget {
|
||||||
Key? key,
|
Key? key,
|
||||||
this.leading,
|
this.leading,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.url,
|
this.url,
|
||||||
this.color,
|
this.color,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
|
this.onTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final _url = url;
|
||||||
return DefaultTextStyle.merge(
|
return DefaultTextStyle.merge(
|
||||||
style: (textStyle ?? const TextStyle()).copyWith(color: color),
|
style: (textStyle ?? const TextStyle()).copyWith(color: color),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
onTap: () async {
|
onTap: onTap ??
|
||||||
if (await canLaunch(url)) {
|
() async {
|
||||||
await launch(url);
|
if (_url != null && await canLaunch(_url)) {
|
||||||
}
|
await launch(_url);
|
||||||
},
|
}
|
||||||
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
53
lib/widgets/common/basic/markdown_container.dart
Normal file
53
lib/widgets/common/basic/markdown_container.dart
Normal file
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/app_flavor.dart';
|
import 'package:aves/app_flavor.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/durations.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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons.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/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class WelcomePage extends StatefulWidget {
|
class WelcomePage extends StatefulWidget {
|
||||||
const WelcomePage({Key? key}) : super(key: key);
|
const WelcomePage({Key? key}) : super(key: key);
|
||||||
|
@ -26,8 +25,6 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
bool _hasAcceptedTerms = false;
|
bool _hasAcceptedTerms = false;
|
||||||
late Future<String> _termsLoader;
|
late Future<String> _termsLoader;
|
||||||
|
|
||||||
static const double maxWidth = 460;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -44,7 +41,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
child: FutureBuilder<String>(
|
child: FutureBuilder<String>(
|
||||||
future: _termsLoader,
|
future: _termsLoader,
|
||||||
builder: (context, snapshot) {
|
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 terms = snapshot.data!;
|
||||||
final durations = context.watch<DurationsData>();
|
final durations = context.watch<DurationsData>();
|
||||||
final isPortrait = context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait;
|
final isPortrait = context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait;
|
||||||
|
@ -62,7 +59,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
children: [
|
children: [
|
||||||
..._buildHeader(context, isPortrait: isPortrait),
|
..._buildHeader(context, isPortrait: isPortrait),
|
||||||
if (isPortrait) ...[
|
if (isPortrait) ...[
|
||||||
Flexible(child: _buildTerms(terms)),
|
Flexible(child: MarkdownContainer(data: terms)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
..._buildControls(context),
|
..._buildControls(context),
|
||||||
] else
|
] else
|
||||||
|
@ -72,7 +69,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: _buildTerms(terms),
|
child: MarkdownContainer(data: terms),
|
||||||
)),
|
)),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
@ -127,7 +124,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
final canEnableErrorReporting = context.select<AppFlavor, bool>((v) => v.canEnableErrorReporting);
|
final canEnableErrorReporting = context.select<AppFlavor, bool>((v) => v.canEnableErrorReporting);
|
||||||
const contentPadding = EdgeInsets.symmetric(horizontal: 8);
|
const contentPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||||
final switches = ConstrainedBox(
|
final switches = ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: maxWidth),
|
constraints: const BoxConstraints(maxWidth: MarkdownContainer.maxWidth),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -183,43 +180,6 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// as of flutter_staggered_animations v0.1.2, `AnimationConfiguration.toStaggeredList` does not handle `Flexible` widgets
|
||||||
// so we use this workaround instead
|
// so we use this workaround instead
|
||||||
static List<Widget> _toStaggeredList({
|
static List<Widget> _toStaggeredList({
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
"unsupportedTypeDialogTitle",
|
"unsupportedTypeDialogTitle",
|
||||||
"unsupportedTypeDialogMessage",
|
"unsupportedTypeDialogMessage",
|
||||||
"editEntryDateDialogExtractFromTitle",
|
"editEntryDateDialogExtractFromTitle",
|
||||||
|
"aboutLinkPolicy",
|
||||||
"aboutCreditsTranslators",
|
"aboutCreditsTranslators",
|
||||||
|
"policyPageTitle",
|
||||||
"collectionActionEdit",
|
"collectionActionEdit",
|
||||||
"collectionEditFailureFeedback",
|
"collectionEditFailureFeedback",
|
||||||
"collectionEditSuccessFeedback",
|
"collectionEditSuccessFeedback",
|
||||||
|
|
Loading…
Reference in a new issue