privacy: reviewed policy, welcome & settings pages for app inventory access
This commit is contained in:
parent
b5c25656b8
commit
941288b5fc
15 changed files with 166 additions and 145 deletions
|
@ -1,25 +1,25 @@
|
||||||
# Terms of Service
|
## Terms of Service
|
||||||
|
|
||||||
Aves is an open-source gallery and metadata explorer app allowing you to access and manage your local photos.
|
“Aves Gallery” is an open-source gallery and metadata explorer app allowing you to access and manage your local photos and videos.
|
||||||
|
|
||||||
You must use the app for legal, authorized and acceptable purposes.
|
You must use the app for legal, authorized and acceptable purposes.
|
||||||
|
|
||||||
# Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This 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
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
In the “Play” edition of Aves, __with the user's consent, anonymous data is collected to improve the app.__ We use Firebase Crashlytics, and the anonymous data are stored on their servers. Please note that those are anonymous data, there is absolutely nothing personal about those data.
|
__Optionally, with your consent, the app accesses the inventory of installed apps__ to improve album display.
|
||||||
|
|
||||||
|
__Optionally, with your consent, the app collects anonymous error and diagnostic data__ to improve the app quality. We use Firebase Crashlytics, and the anonymous data are stored on their servers. Please note that those are anonymous data, there is absolutely nothing personal about those data.
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
[gallery.aves@gmail.com](mailto:gallery.aves@gmail.com)
|
Developer: Thibault Deckers
|
||||||
|
|
||||||
## Links
|
Email: [gallery.aves@gmail.com](mailto:gallery.aves@gmail.com)
|
||||||
|
|
||||||
[Sources](https://github.com/deckerst/aves)
|
Website: [https://github.com/deckerst/aves](https://github.com/deckerst/aves)
|
||||||
|
|
||||||
[License](https://github.com/deckerst/aves/blob/main/LICENSE)
|
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
"@appName": {},
|
"@appName": {},
|
||||||
"welcomeMessage": "Welcome to Aves",
|
"welcomeMessage": "Welcome to Aves",
|
||||||
"@welcomeMessage": {},
|
"@welcomeMessage": {},
|
||||||
"welcomeCrashReportToggle": "Allow anonymous error reporting (optional)",
|
"welcomeOptional": "Optional",
|
||||||
"@welcomeCrashReportToggle": {},
|
|
||||||
"welcomeTermsToggle": "I agree to the terms and conditions",
|
"welcomeTermsToggle": "I agree to the terms and conditions",
|
||||||
"@welcomeTermsToggle": {},
|
"@welcomeTermsToggle": {},
|
||||||
"itemCount": "{count, plural, =1{1 item} other{{count} items}}",
|
"itemCount": "{count, plural, =1{1 item} other{{count} items}}",
|
||||||
|
@ -850,8 +849,12 @@
|
||||||
|
|
||||||
"settingsSectionPrivacy": "Privacy",
|
"settingsSectionPrivacy": "Privacy",
|
||||||
"@settingsSectionPrivacy": {},
|
"@settingsSectionPrivacy": {},
|
||||||
"settingsEnableErrorReporting": "Allow anonymous error reporting",
|
"settingsAllowInstalledAppAccess": "Allow access to app inventory",
|
||||||
"@settingsEnableErrorReporting": {},
|
"@settingsAllowInstalledAppAccess": {},
|
||||||
|
"settingsAllowInstalledAppAccessSubtitle": "Used to improve album display",
|
||||||
|
"@settingsAllowInstalledAppAccessSubtitle": {},
|
||||||
|
"settingsAllowErrorReporting": "Allow anonymous error reporting",
|
||||||
|
"@settingsAllowErrorReporting": {},
|
||||||
"settingsSaveSearchHistory": "Save search history",
|
"settingsSaveSearchHistory": "Save search history",
|
||||||
"@settingsSaveSearchHistory": {},
|
"@settingsSaveSearchHistory": {},
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"appName": "아베스",
|
"appName": "아베스",
|
||||||
"welcomeMessage": "아베스 사용을 환영합니다",
|
"welcomeMessage": "아베스 사용을 환영합니다",
|
||||||
"welcomeCrashReportToggle": "오류 보고서를 보내는 것에 동의합니다 (선택)",
|
"welcomeOptional": "선택",
|
||||||
"welcomeTermsToggle": "이용약관에 동의합니다",
|
"welcomeTermsToggle": "이용약관에 동의합니다",
|
||||||
"itemCount": "{count, plural, other{{count}개}}",
|
"itemCount": "{count, plural, other{{count}개}}",
|
||||||
|
|
||||||
|
@ -393,7 +393,7 @@
|
||||||
"settingsSubtitleThemeTextAlignmentRight": "오른쪽",
|
"settingsSubtitleThemeTextAlignmentRight": "오른쪽",
|
||||||
|
|
||||||
"settingsSectionPrivacy": "개인정보 보호",
|
"settingsSectionPrivacy": "개인정보 보호",
|
||||||
"settingsEnableErrorReporting": "오류 보고서 보내기",
|
"settingsAllowErrorReporting": "오류 보고서 보내기",
|
||||||
"settingsSaveSearchHistory": "검색기록",
|
"settingsSaveSearchHistory": "검색기록",
|
||||||
|
|
||||||
"settingsHiddenFiltersTile": "숨겨진 필터",
|
"settingsHiddenFiltersTile": "숨겨진 필터",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"appName": "Aves",
|
"appName": "Aves",
|
||||||
"welcomeMessage": "Добро пожаловать в Aves",
|
"welcomeMessage": "Добро пожаловать в Aves",
|
||||||
"welcomeCrashReportToggle": "Разрешить анонимную отправку ошибок (опционально)",
|
"welcomeOptional": "Опционально",
|
||||||
"welcomeTermsToggle": "Я согласен с условиями и положениями",
|
"welcomeTermsToggle": "Я согласен с условиями и положениями",
|
||||||
"itemCount": "{count, plural, =1{1 объект} few{{count} объекта} other{{count} объектов}}",
|
"itemCount": "{count, plural, =1{1 объект} few{{count} объекта} other{{count} объектов}}",
|
||||||
|
|
||||||
|
@ -392,7 +392,7 @@
|
||||||
"settingsSubtitleThemeTextAlignmentRight": "По правой стороне",
|
"settingsSubtitleThemeTextAlignmentRight": "По правой стороне",
|
||||||
|
|
||||||
"settingsSectionPrivacy": "Конфиденциальность",
|
"settingsSectionPrivacy": "Конфиденциальность",
|
||||||
"settingsEnableErrorReporting": "Разрешить анонимную отправку логов",
|
"settingsAllowErrorReporting": "Разрешить анонимную отправку логов",
|
||||||
"settingsSaveSearchHistory": "Сохранять историю поиска",
|
"settingsSaveSearchHistory": "Сохранять историю поиска",
|
||||||
|
|
||||||
"settingsHiddenFiltersTile": "Скрытые фильтры",
|
"settingsHiddenFiltersTile": "Скрытые фильтры",
|
||||||
|
|
|
@ -14,7 +14,9 @@ class SettingsDefaults {
|
||||||
// app
|
// app
|
||||||
static const hasAcceptedTerms = false;
|
static const hasAcceptedTerms = false;
|
||||||
static const canUseAnalysisService = true;
|
static const canUseAnalysisService = true;
|
||||||
static const isErrorReportingEnabled = false;
|
// TODO TLAD currently opt-out for transition (v1.5.4 -> vNext), should make it opt-in for vNext+1
|
||||||
|
static const isInstalledAppAccessAllowed = true;
|
||||||
|
static const isErrorReportingAllowed = false;
|
||||||
static const mustBackTwiceToExit = true;
|
static const mustBackTwiceToExit = true;
|
||||||
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
||||||
static const homePage = HomePageSetting.collection;
|
static const homePage = HomePageSetting.collection;
|
||||||
|
|
|
@ -41,7 +41,8 @@ class Settings extends ChangeNotifier {
|
||||||
// app
|
// app
|
||||||
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
||||||
static const canUseAnalysisServiceKey = 'can_use_analysis_service';
|
static const canUseAnalysisServiceKey = 'can_use_analysis_service';
|
||||||
static const isErrorReportingEnabledKey = 'is_crashlytics_enabled';
|
static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed';
|
||||||
|
static const isErrorReportingAllowedKey = 'is_crashlytics_enabled';
|
||||||
static const localeKey = 'locale';
|
static const localeKey = 'locale';
|
||||||
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
||||||
static const keepScreenOnKey = 'keep_screen_on';
|
static const keepScreenOnKey = 'keep_screen_on';
|
||||||
|
@ -174,9 +175,13 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set canUseAnalysisService(bool newValue) => setAndNotify(canUseAnalysisServiceKey, newValue);
|
set canUseAnalysisService(bool newValue) => setAndNotify(canUseAnalysisServiceKey, newValue);
|
||||||
|
|
||||||
bool get isErrorReportingEnabled => getBoolOrDefault(isErrorReportingEnabledKey, SettingsDefaults.isErrorReportingEnabled);
|
bool get isInstalledAppAccessAllowed => getBoolOrDefault(isInstalledAppAccessAllowedKey, SettingsDefaults.isInstalledAppAccessAllowed);
|
||||||
|
|
||||||
set isErrorReportingEnabled(bool newValue) => setAndNotify(isErrorReportingEnabledKey, newValue);
|
set isInstalledAppAccessAllowed(bool newValue) => setAndNotify(isInstalledAppAccessAllowedKey, newValue);
|
||||||
|
|
||||||
|
bool get isErrorReportingAllowed => getBoolOrDefault(isErrorReportingAllowedKey, SettingsDefaults.isErrorReportingAllowed);
|
||||||
|
|
||||||
|
set isErrorReportingAllowed(bool newValue) => setAndNotify(isErrorReportingAllowedKey, newValue);
|
||||||
|
|
||||||
static const localeSeparator = '-';
|
static const localeSeparator = '-';
|
||||||
|
|
||||||
|
@ -568,7 +573,8 @@ class Settings extends ChangeNotifier {
|
||||||
debugPrint('failed to import key=$key, value=$value is not a double');
|
debugPrint('failed to import key=$key, value=$value is not a double');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case isErrorReportingEnabledKey:
|
case isInstalledAppAccessAllowedKey:
|
||||||
|
case isErrorReportingAllowedKey:
|
||||||
case mustBackTwiceToExitKey:
|
case mustBackTwiceToExitKey:
|
||||||
case showThumbnailLocationKey:
|
case showThumbnailLocationKey:
|
||||||
case showThumbnailMotionPhotoKey:
|
case showThumbnailMotionPhotoKey:
|
||||||
|
|
|
@ -39,12 +39,19 @@ class AndroidFileUtils {
|
||||||
|
|
||||||
Future<void> initAppNames() async {
|
Future<void> initAppNames() async {
|
||||||
if (_packages.isEmpty) {
|
if (_packages.isEmpty) {
|
||||||
|
debugPrint('Access installed app inventory');
|
||||||
_packages = await androidAppService.getPackages();
|
_packages = await androidAppService.getPackages();
|
||||||
_potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList();
|
_potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList();
|
||||||
areAppNamesReadyNotifier.value = true;
|
areAppNamesReadyNotifier.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> resetAppNames() async {
|
||||||
|
_packages.clear();
|
||||||
|
_potentialAppDirs.clear();
|
||||||
|
areAppNamesReadyNotifier.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO'));
|
bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO'));
|
||||||
|
|
||||||
bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('${separator}Screenshots');
|
bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('${separator}Screenshots');
|
||||||
|
@ -181,9 +188,9 @@ class VolumeRelativeDirectory extends Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
'volumePath': volumePath,
|
'volumePath': volumePath,
|
||||||
'relativeDir': relativeDir,
|
'relativeDir': relativeDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
// prefer static method over a null returning factory constructor
|
// prefer static method over a null returning factory constructor
|
||||||
static VolumeRelativeDirectory? fromPath(String dirPath) {
|
static VolumeRelativeDirectory? fromPath(String dirPath) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
import 'package:aves/utils/debouncer.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/route_tracker.dart';
|
import 'package:aves/widgets/common/behaviour/route_tracker.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
|
@ -168,12 +169,23 @@ class _AvesAppState extends State<AvesApp> {
|
||||||
);
|
);
|
||||||
settings.keepScreenOn.apply();
|
settings.keepScreenOn.apply();
|
||||||
|
|
||||||
|
// installed app access
|
||||||
|
settings.updateStream.where((key) => key == Settings.isInstalledAppAccessAllowedKey).listen(
|
||||||
|
(_) {
|
||||||
|
if (settings.isInstalledAppAccessAllowed) {
|
||||||
|
androidFileUtils.initAppNames();
|
||||||
|
} else {
|
||||||
|
androidFileUtils.resetAppNames();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// error reporting
|
// error reporting
|
||||||
await reportService.init();
|
await reportService.init();
|
||||||
settings.updateStream.where((key) => key == Settings.isErrorReportingEnabledKey).listen(
|
settings.updateStream.where((key) => key == Settings.isErrorReportingAllowedKey).listen(
|
||||||
(_) => reportService.setCollectionEnabled(settings.isErrorReportingEnabled),
|
(_) => reportService.setCollectionEnabled(settings.isErrorReportingAllowed),
|
||||||
);
|
);
|
||||||
await reportService.setCollectionEnabled(settings.isErrorReportingEnabled);
|
await reportService.setCollectionEnabled(settings.isErrorReportingAllowed);
|
||||||
|
|
||||||
FlutterError.onError = reportService.recordFlutterError;
|
FlutterError.onError = reportService.recordFlutterError;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LabeledCheckbox extends StatefulWidget {
|
|
||||||
final bool value;
|
|
||||||
final ValueChanged<bool?> 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<LabeledCheckbox> {
|
|
||||||
late TapGestureRecognizer _tapRecognizer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_tapRecognizer = TapGestureRecognizer()..onTap = () => 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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,9 +14,16 @@ class AvesOutlinedButton extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
final style = ButtonStyle(
|
final style = ButtonStyle(
|
||||||
side: MaterialStateProperty.all<BorderSide>(BorderSide(color: Theme.of(context).colorScheme.secondary)),
|
side: MaterialStateProperty.resolveWith<BorderSide>((states) {
|
||||||
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
return BorderSide(
|
||||||
|
color: states.contains(MaterialState.disabled) ? theme.disabledColor : theme.colorScheme.secondary,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||||||
|
return states.contains(MaterialState.disabled) ? theme.disabledColor : Colors.white;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return icon != null
|
return icon != null
|
||||||
? OutlinedButton.icon(
|
? OutlinedButton.icon(
|
||||||
|
|
|
@ -68,7 +68,9 @@ class _HomePageState extends State<HomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await androidFileUtils.init();
|
await androidFileUtils.init();
|
||||||
unawaited(androidFileUtils.initAppNames());
|
if (settings.isInstalledAppAccessAllowed) {
|
||||||
|
unawaited(androidFileUtils.initAppNames());
|
||||||
|
}
|
||||||
|
|
||||||
var appMode = AppMode.main;
|
var appMode = AppMode.main;
|
||||||
final intentData = widget.intentData ?? await ViewerService.getIntentData();
|
final intentData = widget.intentData ?? await ViewerService.getIntentData();
|
||||||
|
|
|
@ -32,13 +32,22 @@ class PrivacySection extends StatelessWidget {
|
||||||
expandedNotifier: expandedNotifier,
|
expandedNotifier: expandedNotifier,
|
||||||
showHighlight: false,
|
showHighlight: false,
|
||||||
children: [
|
children: [
|
||||||
|
Selector<Settings, bool>(
|
||||||
|
selector: (context, s) => s.isInstalledAppAccessAllowed,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
|
onChanged: (v) => settings.isInstalledAppAccessAllowed = v,
|
||||||
|
title: Text(context.l10n.settingsAllowInstalledAppAccess),
|
||||||
|
subtitle: Text(context.l10n.settingsAllowInstalledAppAccessSubtitle),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (canEnableErrorReporting)
|
if (canEnableErrorReporting)
|
||||||
Selector<Settings, bool>(
|
Selector<Settings, bool>(
|
||||||
selector: (context, s) => s.isErrorReportingEnabled,
|
selector: (context, s) => s.isErrorReportingAllowed,
|
||||||
builder: (context, current, child) => SwitchListTile(
|
builder: (context, current, child) => SwitchListTile(
|
||||||
value: current,
|
value: current,
|
||||||
onChanged: (v) => settings.isErrorReportingEnabled = v,
|
onChanged: (v) => settings.isErrorReportingAllowed = v,
|
||||||
title: Text(context.l10n.settingsEnableErrorReporting),
|
title: Text(context.l10n.settingsAllowErrorReporting),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Selector<Settings, bool>(
|
Selector<Settings, bool>(
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
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/labeled_checkbox.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/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/home_page.dart';
|
import 'package:aves/widgets/home_page.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -26,6 +26,8 @@ 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();
|
||||||
|
@ -38,15 +40,14 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Container(
|
child: Center(
|
||||||
alignment: Alignment.center,
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
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.shrink();
|
||||||
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;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: _toStaggeredList(
|
children: _toStaggeredList(
|
||||||
|
@ -59,10 +60,29 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
..._buildTop(context),
|
..._buildHeader(context, isPortrait: isPortrait),
|
||||||
Flexible(child: _buildTerms(terms)),
|
if (isPortrait) ...[
|
||||||
const SizedBox(height: 16),
|
Flexible(child: _buildTerms(terms)),
|
||||||
..._buildBottomControls(context),
|
const SizedBox(height: 16),
|
||||||
|
..._buildControls(context),
|
||||||
|
] else
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: _buildTerms(terms),
|
||||||
|
)),
|
||||||
|
Flexible(
|
||||||
|
child: ListView(
|
||||||
|
// shrinkWrap: true,
|
||||||
|
children: _buildControls(context),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -74,13 +94,15 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildTop(BuildContext context) {
|
List<Widget> _buildHeader(BuildContext context, {required bool isPortrait}) {
|
||||||
final message = Text(
|
final message = Text(
|
||||||
context.l10n.welcomeMessage,
|
context.l10n.welcomeMessage,
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
);
|
);
|
||||||
|
final padding = isPortrait ? 16.0 : 8.0;
|
||||||
return [
|
return [
|
||||||
...(context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait
|
SizedBox(height: padding),
|
||||||
|
...(isPortrait
|
||||||
? [
|
? [
|
||||||
const AvesLogo(size: 64),
|
const AvesLogo(size: 64),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
@ -96,38 +118,50 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: padding),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildBottomControls(BuildContext context) {
|
List<Widget> _buildControls(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
final canEnableErrorReporting = context.select<AppFlavor, bool>((v) => v.canEnableErrorReporting);
|
final canEnableErrorReporting = context.select<AppFlavor, bool>((v) => v.canEnableErrorReporting);
|
||||||
final checkboxes = Column(
|
const contentPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final switches = ConstrainedBox(
|
||||||
children: [
|
constraints: const BoxConstraints(maxWidth: maxWidth),
|
||||||
if (canEnableErrorReporting)
|
child: Column(
|
||||||
LabeledCheckbox(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
value: settings.isErrorReportingEnabled,
|
children: [
|
||||||
onChanged: (v) {
|
SwitchListTile(
|
||||||
if (v != null) setState(() => settings.isErrorReportingEnabled = v);
|
value: settings.isInstalledAppAccessAllowed,
|
||||||
},
|
onChanged: (v) => setState(() => settings.isInstalledAppAccessAllowed = v),
|
||||||
text: context.l10n.welcomeCrashReportToggle,
|
title: Text(l10n.settingsAllowInstalledAppAccess),
|
||||||
|
subtitle: Text([l10n.welcomeOptional, l10n.settingsAllowInstalledAppAccessSubtitle].join(' • ')),
|
||||||
|
contentPadding: contentPadding,
|
||||||
),
|
),
|
||||||
LabeledCheckbox(
|
if (canEnableErrorReporting)
|
||||||
// key is expected by test driver
|
SwitchListTile(
|
||||||
key: const Key('agree-checkbox'),
|
value: settings.isErrorReportingAllowed,
|
||||||
value: _hasAcceptedTerms,
|
onChanged: (v) => setState(() => settings.isErrorReportingAllowed = v),
|
||||||
onChanged: (v) {
|
title: Text(l10n.settingsAllowErrorReporting),
|
||||||
if (v != null) setState(() => _hasAcceptedTerms = v);
|
subtitle: Text(l10n.welcomeOptional),
|
||||||
},
|
contentPadding: contentPadding,
|
||||||
text: context.l10n.welcomeTermsToggle,
|
),
|
||||||
),
|
SwitchListTile(
|
||||||
],
|
// key is expected by test driver
|
||||||
|
key: const Key('agree-checkbox'),
|
||||||
|
value: _hasAcceptedTerms,
|
||||||
|
onChanged: (v) => setState(() => _hasAcceptedTerms = v),
|
||||||
|
title: Text(l10n.welcomeTermsToggle),
|
||||||
|
contentPadding: contentPadding,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final button = ElevatedButton(
|
final button = AvesOutlinedButton(
|
||||||
// key is expected by test driver
|
// key is expected by test driver
|
||||||
key: const Key('continue-button'),
|
key: const Key('continue-button'),
|
||||||
|
label: context.l10n.continueButtonLabel,
|
||||||
onPressed: _hasAcceptedTerms
|
onPressed: _hasAcceptedTerms
|
||||||
? () {
|
? () {
|
||||||
settings.hasAcceptedTerms = true;
|
settings.hasAcceptedTerms = true;
|
||||||
|
@ -140,33 +174,23 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(context.l10n.continueButtonLabel),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait
|
return [
|
||||||
? [
|
switches,
|
||||||
checkboxes,
|
Center(child: button),
|
||||||
button,
|
const SizedBox(height: 8),
|
||||||
]
|
];
|
||||||
: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
checkboxes,
|
|
||||||
const Spacer(),
|
|
||||||
button,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTerms(String terms) {
|
Widget _buildTerms(String terms) {
|
||||||
return Container(
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
color: Colors.white10,
|
color: Colors.white10,
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints(maxWidth: 460),
|
constraints: const BoxConstraints(maxWidth: maxWidth),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
child: Theme(
|
child: Theme(
|
||||||
|
|
|
@ -30,7 +30,7 @@ Future<void> configureAndLaunch() async {
|
||||||
settings
|
settings
|
||||||
..keepScreenOn = KeepScreenOn.always
|
..keepScreenOn = KeepScreenOn.always
|
||||||
..hasAcceptedTerms = false
|
..hasAcceptedTerms = false
|
||||||
..isErrorReportingEnabled = false
|
..isErrorReportingAllowed = false
|
||||||
..locale = const Locale('en')
|
..locale = const Locale('en')
|
||||||
..homePage = HomePageSetting.collection
|
..homePage = HomePageSetting.collection
|
||||||
..imageBackground = EntryBackground.checkered;
|
..imageBackground = EntryBackground.checkered;
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
"collectionEditSuccessFeedback",
|
"collectionEditSuccessFeedback",
|
||||||
"settingsCollectionBrowsingQuickActionsTile",
|
"settingsCollectionBrowsingQuickActionsTile",
|
||||||
"settingsCollectionBrowsingQuickActionEditorTitle",
|
"settingsCollectionBrowsingQuickActionEditorTitle",
|
||||||
"settingsCollectionBrowsingQuickActionEditorBanner"
|
"settingsCollectionBrowsingQuickActionEditorBanner",
|
||||||
|
"settingsAllowInstalledAppAccess",
|
||||||
|
"settingsAllowInstalledAppAccessSubtitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -21,6 +23,8 @@
|
||||||
"collectionEditSuccessFeedback",
|
"collectionEditSuccessFeedback",
|
||||||
"settingsCollectionBrowsingQuickActionsTile",
|
"settingsCollectionBrowsingQuickActionsTile",
|
||||||
"settingsCollectionBrowsingQuickActionEditorTitle",
|
"settingsCollectionBrowsingQuickActionEditorTitle",
|
||||||
"settingsCollectionBrowsingQuickActionEditorBanner"
|
"settingsCollectionBrowsingQuickActionEditorBanner",
|
||||||
|
"settingsAllowInstalledAppAccess",
|
||||||
|
"settingsAllowInstalledAppAccessSubtitle"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue