privacy: reviewed policy, welcome & settings pages for app inventory access

This commit is contained in:
Thibault Deckers 2021-11-02 16:47:17 +09:00
parent b5c25656b8
commit 941288b5fc
15 changed files with 166 additions and 145 deletions

View file

@ -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)

View file

@ -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": {},

View file

@ -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": "숨겨진 필터",

View file

@ -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": "Скрытые фильтры",

View file

@ -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;

View file

@ -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:

View file

@ -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) {

View file

@ -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();

View file

@ -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,
),
],
),
);
}
}

View file

@ -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(

View file

@ -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();

View file

@ -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>(

View file

@ -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(

View file

@ -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;

View file

@ -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"
] ]
} }