diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c9d0eff..8d28ec570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,13 @@ All notable changes to this project will be documented in this file. ### Added - Map: show items for bounds, open items in viewer, tap gesture to toggle fullscreen - Info: remove metadata (Exif, XMP, etc.) +- Accessibility: support "time to take action" settings ### Changed - upgraded Flutter to stable v2.5.1 - faster collection loading when launching the app - Collection: changed color & scale of thumbnail icons to match text +- Albums / Countries / Tags: changed layout, with label below cover ### Fixed - album bookmarks & pins were reset when rescanning items diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 0a2dfb4ee..35c72a618 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -52,6 +52,7 @@ class MainActivity : FlutterActivity() { val messenger = flutterEngine!!.dartExecutor.binaryMessenger // dart -> platform -> dart + MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt new file mode 100644 index 000000000..3c0957182 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt @@ -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("originalTimeoutMillis") + val content = call.argument>("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" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 50d2be701..f2a161440 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -102,7 +102,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { private suspend fun getAppIcon(call: MethodCall, result: MethodChannel.Result) { val packageName = call.argument("packageName") - val sizeDip = call.argument("sizeDip") + val sizeDip = call.argument("sizeDip")?.toDouble() if (packageName == null || sizeDip == null) { result.error("getAppIcon-args", "failed because of missing arguments", null) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt index 908d87450..6ee746c10 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt @@ -66,10 +66,10 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler { val dateModifiedSecs = call.argument("dateModifiedSecs")?.toLong() val rotationDegrees = call.argument("rotationDegrees") val isFlipped = call.argument("isFlipped") - val widthDip = call.argument("widthDip") - val heightDip = call.argument("heightDip") + val widthDip = call.argument("widthDip")?.toDouble() + val heightDip = call.argument("heightDip")?.toDouble() val pageId = call.argument("pageId") - val defaultSizeDip = call.argument("defaultSizeDip") + val defaultSizeDip = call.argument("defaultSizeDip")?.toDouble() 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) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b98607d1a..803b849f3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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": {}, "deleteButtonLabel": "DELETE", @@ -616,6 +629,8 @@ "@settingsPageTitle": {}, "settingsSystemDefault": "System", "@settingsSystemDefault": {}, + "settingsDefault": "Default", + "@settingsDefault": {}, "settingsActionExport": "Export", "@settingsActionExport": {}, @@ -782,6 +797,13 @@ "settingsStorageAccessRevokeTooltip": "Revoke", "@settingsStorageAccessRevokeTooltip": {}, + "settingsSectionAccessibility": "Accessibility", + "@settingsSectionAccessibility": {}, + "settingsTimeToTakeActionTile": "Time to take action", + "@settingsTimeToTakeActionTile": {}, + "settingsTimeToTakeActionTitle": "Time to Take Action", + "@settingsTimeToTakeActionTitle": {}, + "settingsSectionLanguage": "Language & Formats", "@settingsSectionLanguage": {}, "settingsLanguage": "Language", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index fd397a490..887e4787a 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -5,6 +5,9 @@ "welcomeTermsToggle": "이용약관에 동의합니다", "itemCount": "{count, plural, other{{count}개}}", + "timeSeconds": "{seconds, plural, other{{seconds}초}}", + "timeMinutes": "{minutes, plural, other{{minutes}분}}", + "applyButtonLabel": "확인", "deleteButtonLabel": "삭제", "nextButtonLabel": "다음", @@ -294,6 +297,7 @@ "settingsPageTitle": "설정", "settingsSystemDefault": "시스템", + "settingsDefault": "기본", "settingsActionExport": "내보내기", "settingsActionImport": "가져오기", @@ -384,6 +388,10 @@ "settingsStorageAccessEmpty": "접근 허용이 없습니다", "settingsStorageAccessRevokeTooltip": "취소", + "settingsSectionAccessibility": "접근성", + "settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간", + "settingsTimeToTakeActionTitle": "액션 취하기 전 대기 시간", + "settingsSectionLanguage": "언어 및 표시 형식", "settingsLanguage": "언어", "settingsCoordinateFormatTile": "좌표 표현", diff --git a/lib/model/settings/a11y_timeout.dart b/lib/model/settings/a11y_timeout.dart new file mode 100644 index 000000000..b5ef55d78 --- /dev/null +++ b/lib/model/settings/a11y_timeout.dart @@ -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); + } + } +} diff --git a/lib/model/settings/coordinate_format.dart b/lib/model/settings/coordinate_format.dart index a417ad056..788b95197 100644 --- a/lib/model/settings/coordinate_format.dart +++ b/lib/model/settings/coordinate_format.dart @@ -12,8 +12,6 @@ extension ExtraCoordinateFormat on CoordinateFormat { return context.l10n.coordinateFormatDms; case CoordinateFormat.decimal: return context.l10n.coordinateFormatDecimal; - default: - return toString(); } } @@ -23,8 +21,6 @@ extension ExtraCoordinateFormat on CoordinateFormat { return toDMS(latLng).join(', '); case CoordinateFormat.decimal: return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', '); - default: - return toString(); } } } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 9de2ddf4c..343ed3dcf 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -57,6 +57,8 @@ class SettingsDefaults { static const showOverlayMinimap = false; static const showOverlayInfo = true; static const showOverlayShootingDetails = false; + + // `enableOverlayBlurEffect` has a contextual default value static const enableOverlayBlurEffect = true; static const viewerUseCutout = true; @@ -78,6 +80,7 @@ class SettingsDefaults { static const subtitleBackgroundColor = Colors.transparent; // info + // `infoMapStyle` has a contextual default value static const infoMapStyle = EntryMapStyle.stamenWatercolor; static const infoMapZoom = 12.0; static const coordinateFormat = CoordinateFormat.dms; @@ -87,4 +90,8 @@ class SettingsDefaults { // search static const saveSearchHistory = true; + + // accessibility + // `timeToTakeAction` has a contextual default value + static const timeToTakeAction = AccessibilityTimeout.appDefault; } diff --git a/lib/model/settings/enums.dart b/lib/model/settings/enums.dart index c7f9a5e98..5a64623b5 100644 --- a/lib/model/settings/enums.dart +++ b/lib/model/settings/enums.dart @@ -1,5 +1,7 @@ enum CoordinateFormat { dms, decimal } +enum AccessibilityTimeout { system, appDefault, s10, s30, s60, s120 } + enum EntryBackground { black, white, checkered } enum HomePageSetting { collection, albums } diff --git a/lib/model/settings/home_page.dart b/lib/model/settings/home_page.dart index 8c4aa09a6..8dac921c6 100644 --- a/lib/model/settings/home_page.dart +++ b/lib/model/settings/home_page.dart @@ -12,8 +12,6 @@ extension ExtraHomePageSetting on HomePageSetting { return context.l10n.collectionPageTitle; case HomePageSetting.albums: return context.l10n.albumPageTitle; - default: - return toString(); } } @@ -23,8 +21,6 @@ extension ExtraHomePageSetting on HomePageSetting { return CollectionPage.routeName; case HomePageSetting.albums: return AlbumListPage.routeName; - default: - return toString(); } } } diff --git a/lib/model/settings/map_style.dart b/lib/model/settings/map_style.dart index 954edb2f2..fa5f8eedb 100644 --- a/lib/model/settings/map_style.dart +++ b/lib/model/settings/map_style.dart @@ -18,8 +18,6 @@ extension ExtraEntryMapStyle on EntryMapStyle { return context.l10n.mapStyleStamenToner; case EntryMapStyle.stamenWatercolor: return context.l10n.mapStyleStamenWatercolor; - default: - return toString(); } } diff --git a/lib/model/settings/screen_on.dart b/lib/model/settings/screen_on.dart index 2fb7dfeef..3341a05f8 100644 --- a/lib/model/settings/screen_on.dart +++ b/lib/model/settings/screen_on.dart @@ -13,8 +13,6 @@ extension ExtraKeepScreenOn on KeepScreenOn { return context.l10n.keepScreenOnViewerOnly; case KeepScreenOn.always: return context.l10n.keepScreenOnAlways; - default: - return toString(); } } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index a09875411..10f4edbd3 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -10,6 +10,7 @@ import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/services/a11y_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -104,6 +105,9 @@ class Settings extends ChangeNotifier { static const saveSearchHistoryKey = 'save_search_history'; static const searchHistoryKey = 'search_history'; + // accessibility + static const timeToTakeActionKey = 'time_to_take_action'; + // version static const lastVersionCheckDateKey = 'last_version_check_date'; @@ -137,6 +141,10 @@ class Settings extends ChangeNotifier { final styles = EntryMapStyle.values.whereNot((v) => v.isGoogleMaps).toList(); infoMapStyle = styles[Random().nextInt(styles.length)]; } + + // accessibility + final hasRecommendedTimeouts = await AccessibilityService.hasRecommendedTimeouts(); + timeToTakeAction = hasRecommendedTimeouts ? AccessibilityTimeout.system : AccessibilityTimeout.appDefault; } // app @@ -372,6 +380,12 @@ class Settings extends ChangeNotifier { set searchHistory(List 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 DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs!.getInt(lastVersionCheckDateKey) ?? 0); @@ -524,6 +538,7 @@ class Settings extends ChangeNotifier { case infoMapStyleKey: case coordinateFormatKey: case imageBackgroundKey: + case timeToTakeActionKey: if (value is String) { _prefs!.setString(key, value); } else { diff --git a/lib/model/settings/video_loop_mode.dart b/lib/model/settings/video_loop_mode.dart index 352b1d911..0a6cba0a9 100644 --- a/lib/model/settings/video_loop_mode.dart +++ b/lib/model/settings/video_loop_mode.dart @@ -13,8 +13,6 @@ extension ExtraVideoLoopMode on VideoLoopMode { return context.l10n.videoLoopModeShortOnly; case VideoLoopMode.always: return context.l10n.videoLoopModeAlways; - default: - return toString(); } } diff --git a/lib/services/a11y_service.dart b/lib/services/a11y_service.dart new file mode 100644 index 000000000..cccde4741 --- /dev/null +++ b/lib/services/a11y_service.dart @@ -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 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 getRecommendedTimeToRead(int originalTimeoutMillis) async { + try { + final result = await platform.invokeMethod('getRecommendedTimeoutMillis', { + '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 getRecommendedTimeToTakeAction(int originalTimeoutMillis) async { + try { + final result = await platform.invokeMethod('getRecommendedTimeoutMillis', { + '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; + } +} diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 868f069ca..a27af7c77 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -56,7 +56,7 @@ class Durations { static const quickActionHighlightAnimation = Duration(milliseconds: 200); // delays & refresh intervals - static const opToastDisplay = Duration(seconds: 3); + static const opToastTextDisplay = Duration(seconds: 3); static const opToastActionDisplay = Duration(seconds: 5); static const infoScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100); diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index ffe0201b0..d00c22c10 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -7,6 +7,7 @@ class AIcons { static const IconData video = Icons.movie_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 broken = Icons.broken_image_outlined; static const IconData checked = Icons.done_outlined; diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index ef6f170b9..e9d8cf6e8 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,6 +1,9 @@ import 'dart:async'; 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:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -15,17 +18,38 @@ mixin FeedbackMixin { // provide the messenger if feedback happens as the widget is disposed void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) { - final duration = action != null ? Durations.opToastActionDisplay : Durations.opToastDisplay; - final progressColor = Theme.of(context).colorScheme.secondary; - messenger.showSnackBar(SnackBar( - content: _FeedbackMessage( - message: message, - progressColor: progressColor, - duration: action != null ? duration : null, - ), - action: action, - duration: duration, - )); + _getSnackBarDuration(action != null).then((duration) { + final progressColor = Theme.of(context).colorScheme.secondary; + messenger.showSnackBar(SnackBar( + content: _FeedbackMessage( + message: message, + progressColor: progressColor, + duration: action != null ? duration : null, + ), + action: action, + duration: duration, + )); + }); + } + + Future _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 diff --git a/lib/widgets/settings/a11y/a11y.dart b/lib/widgets/settings/a11y/a11y.dart new file mode 100644 index 000000000..5b2705d8c --- /dev/null +++ b/lib/widgets/settings/a11y/a11y.dart @@ -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 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(), + ], + ); + } +} diff --git a/lib/widgets/settings/a11y/time_to_take_action.dart b/lib/widgets/settings/a11y/time_to_take_action.dart new file mode 100644 index 000000000..a1b3cc2a0 --- /dev/null +++ b/lib/widgets/settings/a11y/time_to_take_action.dart @@ -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 { + late Future _hasSystemOptionLoader; + + @override + void initState() { + super.initState(); + _hasSystemOptionLoader = AccessibilityService.hasRecommendedTimeouts(); + } + + @override + Widget build(BuildContext context) { + final currentTimeToTakeAction = context.select((s) => s.timeToTakeAction); + + return FutureBuilder( + 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( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentTimeToTakeAction, + options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsTimeToTakeActionTitle, + ), + ); + if (value != null) { + settings.timeToTakeAction = value; + } + }, + ); + }, + ); + } +} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index a2bd060f8..368b6fce5 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -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/extensions/build_context.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/navigation/navigation.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; @@ -91,6 +92,7 @@ class _SettingsPageState extends State with FeedbackMixin { ViewerSection(expandedNotifier: _expandedNotifier), VideoSection(expandedNotifier: _expandedNotifier), PrivacySection(expandedNotifier: _expandedNotifier), + AccessibilitySection(expandedNotifier: _expandedNotifier), LanguageSection(expandedNotifier: _expandedNotifier), ], ),