accessibility: time to take action

This commit is contained in:
Thibault Deckers 2021-09-27 17:04:07 +09:00
parent ff1af89ce1
commit b720f65754
23 changed files with 317 additions and 30 deletions

View file

@ -5,11 +5,13 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Map: show items for bounds, open items in viewer, tap gesture to toggle fullscreen - Map: show items for bounds, open items in viewer, tap gesture to toggle fullscreen
- Info: remove metadata (Exif, XMP, etc.) - Info: remove metadata (Exif, XMP, etc.)
- Accessibility: support "time to take action" settings
### Changed ### Changed
- upgraded Flutter to stable v2.5.1 - upgraded Flutter to stable v2.5.1
- faster collection loading when launching the app - faster collection loading when launching the app
- Collection: changed color & scale of thumbnail icons to match text - Collection: changed color & scale of thumbnail icons to match text
- Albums / Countries / Tags: changed layout, with label below cover
### Fixed ### Fixed
- album bookmarks & pins were reset when rescanning items - album bookmarks & pins were reset when rescanning items

View file

@ -52,6 +52,7 @@ class MainActivity : FlutterActivity() {
val messenger = flutterEngine!!.dartExecutor.binaryMessenger val messenger = flutterEngine!!.dartExecutor.binaryMessenger
// dart -> platform -> dart // dart -> platform -> dart
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this)) MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler())

View file

@ -0,0 +1,64 @@
package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.content.Context
import android.os.Build
import android.view.accessibility.AccessibilityManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class AccessibilityHandler(private val context: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts)
"getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis)
else -> result.notImplemented()
}
}
private fun hasRecommendedTimeouts(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
}
private fun getRecommendedTimeoutMillis(call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.error("getRecommendedTimeoutMillis-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return
}
val originalTimeoutMillis = call.argument<Int>("originalTimeoutMillis")
val content = call.argument<List<String>>("content")
if (originalTimeoutMillis == null || content == null) {
result.error("getRecommendedTimeoutMillis-args", "failed because of missing arguments", null)
return
}
var uiContentFlags = 0
content.forEach {
uiContentFlags = when (it) {
"controls" -> uiContentFlags or AccessibilityManager.FLAG_CONTENT_CONTROLS
"icons" -> uiContentFlags or AccessibilityManager.FLAG_CONTENT_ICONS
"text" -> uiContentFlags or AccessibilityManager.FLAG_CONTENT_TEXT
else -> {
result.error("getRecommendedTimeoutMillis-flag", "unsupported UI content flag=$it", null)
return
}
}
}
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
if (am == null) {
result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null)
return
}
val millis = am.getRecommendedTimeoutMillis(originalTimeoutMillis, uiContentFlags)
result.success(millis)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/a11y"
}
}

View file

@ -102,7 +102,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
private suspend fun getAppIcon(call: MethodCall, result: MethodChannel.Result) { private suspend fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
val packageName = call.argument<String>("packageName") val packageName = call.argument<String>("packageName")
val sizeDip = call.argument<Double>("sizeDip") val sizeDip = call.argument<Number>("sizeDip")?.toDouble()
if (packageName == null || sizeDip == null) { if (packageName == null || sizeDip == null) {
result.error("getAppIcon-args", "failed because of missing arguments", null) result.error("getAppIcon-args", "failed because of missing arguments", null)
return return

View file

@ -66,10 +66,10 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong() val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong()
val rotationDegrees = call.argument<Int>("rotationDegrees") val rotationDegrees = call.argument<Int>("rotationDegrees")
val isFlipped = call.argument<Boolean>("isFlipped") val isFlipped = call.argument<Boolean>("isFlipped")
val widthDip = call.argument<Double>("widthDip") val widthDip = call.argument<Number>("widthDip")?.toDouble()
val heightDip = call.argument<Double>("heightDip") val heightDip = call.argument<Number>("heightDip")?.toDouble()
val pageId = call.argument<Int>("pageId") val pageId = call.argument<Int>("pageId")
val defaultSizeDip = call.argument<Double>("defaultSizeDip") val defaultSizeDip = call.argument<Number>("defaultSizeDip")?.toDouble()
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
result.error("getThumbnail-args", "failed because of missing arguments", null) result.error("getThumbnail-args", "failed because of missing arguments", null)

View file

@ -14,6 +14,19 @@
} }
}, },
"timeSeconds": "{seconds, plural, =1{1 second} other{{seconds} seconds}}",
"@timeSeconds": {
"placeholders": {
"seconds": {}
}
},
"timeMinutes": "{minutes, plural, =1{1 minute} other{{minutes} minutes}}",
"@timeMinutes": {
"placeholders": {
"minutes": {}
}
},
"applyButtonLabel": "APPLY", "applyButtonLabel": "APPLY",
"@applyButtonLabel": {}, "@applyButtonLabel": {},
"deleteButtonLabel": "DELETE", "deleteButtonLabel": "DELETE",
@ -616,6 +629,8 @@
"@settingsPageTitle": {}, "@settingsPageTitle": {},
"settingsSystemDefault": "System", "settingsSystemDefault": "System",
"@settingsSystemDefault": {}, "@settingsSystemDefault": {},
"settingsDefault": "Default",
"@settingsDefault": {},
"settingsActionExport": "Export", "settingsActionExport": "Export",
"@settingsActionExport": {}, "@settingsActionExport": {},
@ -782,6 +797,13 @@
"settingsStorageAccessRevokeTooltip": "Revoke", "settingsStorageAccessRevokeTooltip": "Revoke",
"@settingsStorageAccessRevokeTooltip": {}, "@settingsStorageAccessRevokeTooltip": {},
"settingsSectionAccessibility": "Accessibility",
"@settingsSectionAccessibility": {},
"settingsTimeToTakeActionTile": "Time to take action",
"@settingsTimeToTakeActionTile": {},
"settingsTimeToTakeActionTitle": "Time to Take Action",
"@settingsTimeToTakeActionTitle": {},
"settingsSectionLanguage": "Language & Formats", "settingsSectionLanguage": "Language & Formats",
"@settingsSectionLanguage": {}, "@settingsSectionLanguage": {},
"settingsLanguage": "Language", "settingsLanguage": "Language",

View file

@ -5,6 +5,9 @@
"welcomeTermsToggle": "이용약관에 동의합니다", "welcomeTermsToggle": "이용약관에 동의합니다",
"itemCount": "{count, plural, other{{count}개}}", "itemCount": "{count, plural, other{{count}개}}",
"timeSeconds": "{seconds, plural, other{{seconds}초}}",
"timeMinutes": "{minutes, plural, other{{minutes}분}}",
"applyButtonLabel": "확인", "applyButtonLabel": "확인",
"deleteButtonLabel": "삭제", "deleteButtonLabel": "삭제",
"nextButtonLabel": "다음", "nextButtonLabel": "다음",
@ -294,6 +297,7 @@
"settingsPageTitle": "설정", "settingsPageTitle": "설정",
"settingsSystemDefault": "시스템", "settingsSystemDefault": "시스템",
"settingsDefault": "기본",
"settingsActionExport": "내보내기", "settingsActionExport": "내보내기",
"settingsActionImport": "가져오기", "settingsActionImport": "가져오기",
@ -384,6 +388,10 @@
"settingsStorageAccessEmpty": "접근 허용이 없습니다", "settingsStorageAccessEmpty": "접근 허용이 없습니다",
"settingsStorageAccessRevokeTooltip": "취소", "settingsStorageAccessRevokeTooltip": "취소",
"settingsSectionAccessibility": "접근성",
"settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간",
"settingsTimeToTakeActionTitle": "액션 취하기 전 대기 시간",
"settingsSectionLanguage": "언어 및 표시 형식", "settingsSectionLanguage": "언어 및 표시 형식",
"settingsLanguage": "언어", "settingsLanguage": "언어",
"settingsCoordinateFormatTile": "좌표 표현", "settingsCoordinateFormatTile": "좌표 표현",

View file

@ -0,0 +1,23 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraAccessibilityTimeout on AccessibilityTimeout {
String getName(BuildContext context) {
switch (this) {
case AccessibilityTimeout.system:
return context.l10n.settingsSystemDefault;
case AccessibilityTimeout.appDefault:
return context.l10n.settingsDefault;
case AccessibilityTimeout.s10:
return context.l10n.timeSeconds(10);
case AccessibilityTimeout.s30:
return context.l10n.timeSeconds(30);
case AccessibilityTimeout.s60:
return context.l10n.timeMinutes(1);
case AccessibilityTimeout.s120:
return context.l10n.timeMinutes(2);
}
}
}

View file

@ -12,8 +12,6 @@ extension ExtraCoordinateFormat on CoordinateFormat {
return context.l10n.coordinateFormatDms; return context.l10n.coordinateFormatDms;
case CoordinateFormat.decimal: case CoordinateFormat.decimal:
return context.l10n.coordinateFormatDecimal; return context.l10n.coordinateFormatDecimal;
default:
return toString();
} }
} }
@ -23,8 +21,6 @@ extension ExtraCoordinateFormat on CoordinateFormat {
return toDMS(latLng).join(', '); return toDMS(latLng).join(', ');
case CoordinateFormat.decimal: case CoordinateFormat.decimal:
return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', '); return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', ');
default:
return toString();
} }
} }
} }

View file

@ -57,6 +57,8 @@ class SettingsDefaults {
static const showOverlayMinimap = false; static const showOverlayMinimap = false;
static const showOverlayInfo = true; static const showOverlayInfo = true;
static const showOverlayShootingDetails = false; static const showOverlayShootingDetails = false;
// `enableOverlayBlurEffect` has a contextual default value
static const enableOverlayBlurEffect = true; static const enableOverlayBlurEffect = true;
static const viewerUseCutout = true; static const viewerUseCutout = true;
@ -78,6 +80,7 @@ class SettingsDefaults {
static const subtitleBackgroundColor = Colors.transparent; static const subtitleBackgroundColor = Colors.transparent;
// info // info
// `infoMapStyle` has a contextual default value
static const infoMapStyle = EntryMapStyle.stamenWatercolor; static const infoMapStyle = EntryMapStyle.stamenWatercolor;
static const infoMapZoom = 12.0; static const infoMapZoom = 12.0;
static const coordinateFormat = CoordinateFormat.dms; static const coordinateFormat = CoordinateFormat.dms;
@ -87,4 +90,8 @@ class SettingsDefaults {
// search // search
static const saveSearchHistory = true; static const saveSearchHistory = true;
// accessibility
// `timeToTakeAction` has a contextual default value
static const timeToTakeAction = AccessibilityTimeout.appDefault;
} }

View file

@ -1,5 +1,7 @@
enum CoordinateFormat { dms, decimal } enum CoordinateFormat { dms, decimal }
enum AccessibilityTimeout { system, appDefault, s10, s30, s60, s120 }
enum EntryBackground { black, white, checkered } enum EntryBackground { black, white, checkered }
enum HomePageSetting { collection, albums } enum HomePageSetting { collection, albums }

View file

@ -12,8 +12,6 @@ extension ExtraHomePageSetting on HomePageSetting {
return context.l10n.collectionPageTitle; return context.l10n.collectionPageTitle;
case HomePageSetting.albums: case HomePageSetting.albums:
return context.l10n.albumPageTitle; return context.l10n.albumPageTitle;
default:
return toString();
} }
} }
@ -23,8 +21,6 @@ extension ExtraHomePageSetting on HomePageSetting {
return CollectionPage.routeName; return CollectionPage.routeName;
case HomePageSetting.albums: case HomePageSetting.albums:
return AlbumListPage.routeName; return AlbumListPage.routeName;
default:
return toString();
} }
} }
} }

View file

@ -18,8 +18,6 @@ extension ExtraEntryMapStyle on EntryMapStyle {
return context.l10n.mapStyleStamenToner; return context.l10n.mapStyleStamenToner;
case EntryMapStyle.stamenWatercolor: case EntryMapStyle.stamenWatercolor:
return context.l10n.mapStyleStamenWatercolor; return context.l10n.mapStyleStamenWatercolor;
default:
return toString();
} }
} }

View file

@ -13,8 +13,6 @@ extension ExtraKeepScreenOn on KeepScreenOn {
return context.l10n.keepScreenOnViewerOnly; return context.l10n.keepScreenOnViewerOnly;
case KeepScreenOn.always: case KeepScreenOn.always:
return context.l10n.keepScreenOnAlways; return context.l10n.keepScreenOnAlways;
default:
return toString();
} }
} }

View file

@ -10,6 +10,7 @@ import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/map_style.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/services/a11y_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -104,6 +105,9 @@ class Settings extends ChangeNotifier {
static const saveSearchHistoryKey = 'save_search_history'; static const saveSearchHistoryKey = 'save_search_history';
static const searchHistoryKey = 'search_history'; static const searchHistoryKey = 'search_history';
// accessibility
static const timeToTakeActionKey = 'time_to_take_action';
// version // version
static const lastVersionCheckDateKey = 'last_version_check_date'; static const lastVersionCheckDateKey = 'last_version_check_date';
@ -137,6 +141,10 @@ class Settings extends ChangeNotifier {
final styles = EntryMapStyle.values.whereNot((v) => v.isGoogleMaps).toList(); final styles = EntryMapStyle.values.whereNot((v) => v.isGoogleMaps).toList();
infoMapStyle = styles[Random().nextInt(styles.length)]; infoMapStyle = styles[Random().nextInt(styles.length)];
} }
// accessibility
final hasRecommendedTimeouts = await AccessibilityService.hasRecommendedTimeouts();
timeToTakeAction = hasRecommendedTimeouts ? AccessibilityTimeout.system : AccessibilityTimeout.appDefault;
} }
// app // app
@ -372,6 +380,12 @@ class Settings extends ChangeNotifier {
set searchHistory(List<CollectionFilter> newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); set searchHistory(List<CollectionFilter> newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList());
// accessibility
AccessibilityTimeout get timeToTakeAction => getEnumOrDefault(timeToTakeActionKey, SettingsDefaults.timeToTakeAction, AccessibilityTimeout.values);
set timeToTakeAction(AccessibilityTimeout newValue) => setAndNotify(timeToTakeActionKey, newValue.toString());
// version // version
DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs!.getInt(lastVersionCheckDateKey) ?? 0); DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs!.getInt(lastVersionCheckDateKey) ?? 0);
@ -524,6 +538,7 @@ class Settings extends ChangeNotifier {
case infoMapStyleKey: case infoMapStyleKey:
case coordinateFormatKey: case coordinateFormatKey:
case imageBackgroundKey: case imageBackgroundKey:
case timeToTakeActionKey:
if (value is String) { if (value is String) {
_prefs!.setString(key, value); _prefs!.setString(key, value);
} else { } else {

View file

@ -13,8 +13,6 @@ extension ExtraVideoLoopMode on VideoLoopMode {
return context.l10n.videoLoopModeShortOnly; return context.l10n.videoLoopModeShortOnly;
case VideoLoopMode.always: case VideoLoopMode.always:
return context.l10n.videoLoopModeAlways; return context.l10n.videoLoopModeAlways;
default:
return toString();
} }
} }

View file

@ -0,0 +1,42 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class AccessibilityService {
static const platform = MethodChannel('deckers.thibault/aves/a11y');
static Future<bool> hasRecommendedTimeouts() async {
try {
final result = await platform.invokeMethod('hasRecommendedTimeouts');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
static Future<int> getRecommendedTimeToRead(int originalTimeoutMillis) async {
try {
final result = await platform.invokeMethod('getRecommendedTimeoutMillis', <String, dynamic>{
'originalTimeoutMillis': originalTimeoutMillis,
'content': ['icons', 'text']
});
if (result != null) return result as int;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return originalTimeoutMillis;
}
static Future<int> getRecommendedTimeToTakeAction(int originalTimeoutMillis) async {
try {
final result = await platform.invokeMethod('getRecommendedTimeoutMillis', <String, dynamic>{
'originalTimeoutMillis': originalTimeoutMillis,
'content': ['controls', 'icons', 'text']
});
if (result != null) return result as int;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return originalTimeoutMillis;
}
}

View file

@ -56,7 +56,7 @@ class Durations {
static const quickActionHighlightAnimation = Duration(milliseconds: 200); static const quickActionHighlightAnimation = Duration(milliseconds: 200);
// delays & refresh intervals // delays & refresh intervals
static const opToastDisplay = Duration(seconds: 3); static const opToastTextDisplay = Duration(seconds: 3);
static const opToastActionDisplay = Duration(seconds: 5); static const opToastActionDisplay = Duration(seconds: 5);
static const infoScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const infoScrollMonitoringTimerDelay = Duration(milliseconds: 100);
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);

View file

@ -7,6 +7,7 @@ class AIcons {
static const IconData video = Icons.movie_outlined; static const IconData video = Icons.movie_outlined;
static const IconData vector = Icons.code_outlined; static const IconData vector = Icons.code_outlined;
static const IconData a11y = Icons.accessibility_new_outlined;
static const IconData android = Icons.android; static const IconData android = Icons.android;
static const IconData broken = Icons.broken_image_outlined; static const IconData broken = Icons.broken_image_outlined;
static const IconData checked = Icons.done_outlined; static const IconData checked = Icons.done_outlined;

View file

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/a11y_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -15,7 +18,7 @@ mixin FeedbackMixin {
// provide the messenger if feedback happens as the widget is disposed // provide the messenger if feedback happens as the widget is disposed
void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) { void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) {
final duration = action != null ? Durations.opToastActionDisplay : Durations.opToastDisplay; _getSnackBarDuration(action != null).then((duration) {
final progressColor = Theme.of(context).colorScheme.secondary; final progressColor = Theme.of(context).colorScheme.secondary;
messenger.showSnackBar(SnackBar( messenger.showSnackBar(SnackBar(
content: _FeedbackMessage( content: _FeedbackMessage(
@ -26,6 +29,27 @@ mixin FeedbackMixin {
action: action, action: action,
duration: duration, duration: duration,
)); ));
});
}
Future<Duration> _getSnackBarDuration(bool hasAction) async {
final appDefaultDuration = hasAction ? Durations.opToastActionDisplay : Durations.opToastTextDisplay;
switch (settings.timeToTakeAction) {
case AccessibilityTimeout.system:
final original = appDefaultDuration.inMilliseconds;
final millis = await (hasAction ? AccessibilityService.getRecommendedTimeToTakeAction(original) : AccessibilityService.getRecommendedTimeToRead(original));
return Duration(milliseconds: millis);
case AccessibilityTimeout.appDefault:
return appDefaultDuration;
case AccessibilityTimeout.s10:
return const Duration(seconds: 10);
case AccessibilityTimeout.s30:
return const Duration(seconds: 30);
case AccessibilityTimeout.s60:
return const Duration(minutes: 1);
case AccessibilityTimeout.s120:
return const Duration(minutes: 2);
}
} }
// report overlay for multiple operations // report overlay for multiple operations

View file

@ -0,0 +1,32 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/settings/a11y/time_to_take_action.dart';
import 'package:aves/widgets/settings/common/tile_leading.dart';
import 'package:flutter/material.dart';
class AccessibilitySection extends StatelessWidget {
final ValueNotifier<String?> expandedNotifier;
const AccessibilitySection({
Key? key,
required this.expandedNotifier,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AvesExpansionTile(
leading: SettingsTileLeading(
icon: AIcons.a11y,
color: stringToColor('Accessibility'),
),
title: context.l10n.settingsSectionAccessibility,
expandedNotifier: expandedNotifier,
showHighlight: false,
children: const [
TimeToTakeActionTile(),
],
);
}
}

View file

@ -0,0 +1,56 @@
import 'package:aves/model/settings/a11y_timeout.dart';
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/a11y_service.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class TimeToTakeActionTile extends StatefulWidget {
const TimeToTakeActionTile({Key? key}) : super(key: key);
@override
_TimeToTakeActionTileState createState() => _TimeToTakeActionTileState();
}
class _TimeToTakeActionTileState extends State<TimeToTakeActionTile> {
late Future<bool> _hasSystemOptionLoader;
@override
void initState() {
super.initState();
_hasSystemOptionLoader = AccessibilityService.hasRecommendedTimeouts();
}
@override
Widget build(BuildContext context) {
final currentTimeToTakeAction = context.select<Settings, AccessibilityTimeout>((s) => s.timeToTakeAction);
return FutureBuilder<bool>(
future: _hasSystemOptionLoader,
builder: (context, snapshot) {
if (snapshot.hasError || !snapshot.hasData) return const SizedBox.shrink();
final hasSystemOption = snapshot.data!;
final optionValues = hasSystemOption ? AccessibilityTimeout.values : AccessibilityTimeout.values.where((v) => v != AccessibilityTimeout.system).toList();
return ListTile(
title: Text(context.l10n.settingsTimeToTakeActionTile),
subtitle: Text(currentTimeToTakeAction.getName(context)),
onTap: () async {
final value = await showDialog<AccessibilityTimeout>(
context: context,
builder: (context) => AvesSelectionDialog<AccessibilityTimeout>(
initialValue: currentTimeToTakeAction,
options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))),
title: context.l10n.settingsTimeToTakeActionTitle,
),
);
if (value != null) {
settings.timeToTakeAction = value;
}
},
);
},
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.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/settings/a11y/a11y.dart';
import 'package:aves/widgets/settings/language/language.dart'; import 'package:aves/widgets/settings/language/language.dart';
import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/navigation/navigation.dart';
import 'package:aves/widgets/settings/privacy/privacy.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart';
@ -91,6 +92,7 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
ViewerSection(expandedNotifier: _expandedNotifier), ViewerSection(expandedNotifier: _expandedNotifier),
VideoSection(expandedNotifier: _expandedNotifier), VideoSection(expandedNotifier: _expandedNotifier),
PrivacySection(expandedNotifier: _expandedNotifier), PrivacySection(expandedNotifier: _expandedNotifier),
AccessibilitySection(expandedNotifier: _expandedNotifier),
LanguageSection(expandedNotifier: _expandedNotifier), LanguageSection(expandedNotifier: _expandedNotifier),
], ],
), ),