diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 727801e59..565f74f8d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -3,10 +3,11 @@ "welcomeMessage": "Willkommen bei Aves", "welcomeOptional": "Optional", "welcomeTermsToggle": "Ich stimme den Bedingungen und Konditionen zu", - "itemCount": " {count, plural, =1{1 Element} other{{count} Elemente}}", + "itemCount": "{count, plural, =1{1 Element} other{{count} Elemente}}", - "timeSeconds": " {seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}", - "timeMinutes": " {minutes, plural, =1{1 Minute} other{{minutes} Minuten}}", + "timeSeconds": "{seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}", + "timeMinutes": "{minutes, plural, =1{1 Minute} other{{minutes} Minuten}}", + "focalLength": "{length} mm", "applyButtonLabel": "ANWENDEN", "deleteButtonLabel": "LÖSCHEN", @@ -93,7 +94,7 @@ "coordinateFormatDms": "GMS", "coordinateFormatDecimal": "Dezimalgrad", - "coordinateDms": " {coordinate} {direction}", + "coordinateDms": "{coordinate} {direction}", "coordinateDmsNorth": "N", "coordinateDmsSouth": "s", "coordinateDmsEast": "O", @@ -144,7 +145,7 @@ "missingSystemFilePickerDialogMessage": "Der System-Dateiauswahldialog fehlt oder ist deaktiviert. Bitte aktivieren Sie ihn und versuchen Sie es erneut.", "unsupportedTypeDialogTitle": "Nicht unterstützte Typen", - "unsupportedTypeDialogMessage": " {count, plural, =1{Dieser Vorgang wird für Elemente des folgenden Typs nicht unterstützt: {types}.} other{Dieser Vorgang wird für Elemente der folgenden Typen nicht unterstützt: {types}.}}", + "unsupportedTypeDialogMessage": "{count, plural, =1{Dieser Vorgang wird für Elemente des folgenden Typs nicht unterstützt: {types}.} other{Dieser Vorgang wird für Elemente der folgenden Typen nicht unterstützt: {types}.}}", "nameConflictDialogSingleSourceMessage": "Einige Dateien im Zielordner haben den gleichen Namen.", "nameConflictDialogMultipleSourceMessage": "Einige Dateien haben denselben Namen.", @@ -155,7 +156,7 @@ "noMatchingAppDialogTitle": "Keine passende App", "noMatchingAppDialogMessage": "Es gibt keine Anwendungen, die dies bewältigen können.", - "deleteEntriesConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie dieses Element löschen möchten?} other{Sind Sie sicher, dass Sie diese {count} Elemente löschen möchten?}}", + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Sind Sie sicher, dass Sie dieses Element löschen möchten?} other{Sind Sie sicher, dass Sie diese {count} Elemente löschen möchten?}}", "videoResumeDialogMessage": "Möchten Sie bei {time} weiter abspielen?", "videoStartOverButtonLabel": "NEU BEGINNEN", @@ -175,8 +176,8 @@ "renameAlbumDialogLabel": "Neuer Name", "renameAlbumDialogLabelAlreadyExistsHelper": "Verzeichnis existiert bereits", - "deleteSingleAlbumConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie dieses Album und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie dieses Album und deren {count} Elemente löschen möchten?}}", - "deleteMultiAlbumConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie diese Alben und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie diese Alben und deren {count} Elemente löschen möchten?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Sind Sie sicher, dass Sie dieses Album und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie dieses Album und deren {count} Elemente löschen möchten?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Sind Sie sicher, dass Sie diese Alben und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie diese Alben und deren {count} Elemente löschen möchten?}}", "exportEntryDialogFormat": "Format:", @@ -256,7 +257,7 @@ "collectionPageTitle": "Sammlung", "collectionPickPageTitle": "Wähle", - "collectionSelectionPageTitle": " {count, plural, =0{Elemente auswählen} =1{1 Element} other{{count} Elemente}}", + "collectionSelectionPageTitle": "{count, plural, =0{Elemente auswählen} =1{1 Element} other{{count} Elemente}}", "collectionActionShowTitleSearch": "Titelfilter anzeigen", "collectionActionHideTitleSearch": "Titelfilter ausblenden", @@ -282,14 +283,14 @@ "dateToday": "Heute", "dateYesterday": "Gestern", "dateThisMonth": "Diesen Monat", - "collectionDeleteFailureFeedback": " {count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}", - "collectionCopyFailureFeedback": " {count, plural, =1{1 Element konnte nicht kopiert werden} other{{count} Element konnten nicht kopiert werden}}", - "collectionMoveFailureFeedback": " {count, plural, =1{1 Element konnte nicht verschoben werden} other{{count} Elemente konnten nicht verschoben werden}}", - "collectionEditFailureFeedback": " {count, plural, =1{1 Element konnte nicht bearbeitet werden} other{{count} 1 Elemente konnten nicht bearbeitet werden}}", - "collectionExportFailureFeedback": " {count, plural, =1{1 Seite konnte nicht exportiert werden} other{{count} Seiten konnten nicht exportiert werden}}", - "collectionCopySuccessFeedback": " {count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}", - "collectionMoveSuccessFeedback": " {count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}", - "collectionEditSuccessFeedback": " {count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}", + "collectionDeleteFailureFeedback": "{count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}", + "collectionCopyFailureFeedback": "{count, plural, =1{1 Element konnte nicht kopiert werden} other{{count} Element konnten nicht kopiert werden}}", + "collectionMoveFailureFeedback": "{count, plural, =1{1 Element konnte nicht verschoben werden} other{{count} Elemente konnten nicht verschoben werden}}", + "collectionEditFailureFeedback": "{count, plural, =1{1 Element konnte nicht bearbeitet werden} other{{count} 1 Elemente konnten nicht bearbeitet werden}}", + "collectionExportFailureFeedback": "{count, plural, =1{1 Seite konnte nicht exportiert werden} other{{count} Seiten konnten nicht exportiert werden}}", + "collectionCopySuccessFeedback": "{count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}", + "collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}", + "collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}", "collectionEmptyFavourites": "Keine Favoriten", "collectionEmptyVideos": "Keine Videos", @@ -471,7 +472,7 @@ "settingsUnitSystemTitle": "Einheiten", "statsPageTitle": "Statistiken", - "statsWithGps": " {count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}", + "statsWithGps": "{count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}", "statsTopCountries": "Top-Länder", "statsTopPlaces": "Top-Plätze", "statsTopTags": "Top-Tags", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d849f7017..d8eeb588b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -22,6 +22,15 @@ "minutes": {} } }, + "focalLength": "{length} mm", + "@focalLength": { + "placeholders": { + "length": { + "type": "String", + "example": "5.4" + } + } + }, "applyButtonLabel": "APPLY", "deleteButtonLabel": "DELETE", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 7e1995151..232abb8d3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -4,10 +4,11 @@ "welcomeOptional": "Opcional", "welcomeTermsToggle": "Acepto los términos y condiciones", "itemCount": "{count, plural, =1{1 elemento} other{{count} elementos}}", - + "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}", "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}", - + "focalLength": "{length} mm", + "applyButtonLabel": "APLICAR", "deleteButtonLabel": "BORRAR", "nextButtonLabel": "SIGUIENTE", @@ -140,13 +141,13 @@ "restrictedAccessDialogMessage": "Esta aplicación no tiene permiso para modificar archivos de {directory} en «{volume}».\n\nPor favor use un gestor de archivos o la aplicación de galería preinstalada para mover los elementos a otro directorio.", "notEnoughSpaceDialogTitle": "Espacio insuficiente", "notEnoughSpaceDialogMessage": "Esta operación necesita {neededSize} de espacio libre en «{volume}» para completarse, pero sólo hay {freeSize} disponible.", - + "missingSystemFilePickerDialogTitle": "Selector de archivos del sistema no disponible", "missingSystemFilePickerDialogMessage": "El selector de archivos del sistema no se encuentra disponible o fue deshabilitado. Por favor habilítelo e intente nuevamente.", "unsupportedTypeDialogTitle": "Tipos de archivo incompatibles", "unsupportedTypeDialogMessage": "{count, plural, =1{Esta operación no está disponible para un elemento del siguiente tipo: {types}.} other{Esta operación no está disponible para elementos de los siguientes tipos: {types}.}}", - + "nameConflictDialogSingleSourceMessage": "Algunos archivos en el directorio de destino tienen el mismo nombre.", "nameConflictDialogMultipleSourceMessage": "Algunos archivos tienen el mismo nombre.", @@ -244,7 +245,7 @@ "aboutCreditsWorldAtlas2": "bajo licencia ISC.", "aboutCreditsTranslators": "Traductores:", "aboutCreditsTranslatorLine": "{language}: {names}", - + "aboutLicenses": "Licencias de código abierto", "aboutLicensesBanner": "Esta aplicación usa los siguientes paquetes y librerías de código abierto.", "aboutLicensesAndroidLibraries": "Librerías de Android", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1ea5d28c2..9755d745f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 seconde} other{{seconds} secondes}}", "timeMinutes": "{minutes, plural, =1{1 minute} other{{minutes} minutes}}", + "focalLength": "{length} mm", "applyButtonLabel": "ENREGISTRER", "deleteButtonLabel": "SUPPRIMER", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index b9d346a84..60d864d40 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, other{{seconds}초}}", "timeMinutes": "{minutes, plural, other{{minutes}분}}", + "focalLength": "{length} mm", "applyButtonLabel": "확인", "deleteButtonLabel": "삭제", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b530d69d9..496a7807b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}", "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}", + "focalLength": "{length} mm", "applyButtonLabel": "APLIQUE", "deleteButtonLabel": "EXCLUIR", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 508fe8ade..307b3da6a 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 секунда} few{{seconds} секунды} other{{seconds} секунд}}", "timeMinutes": "{minutes, plural, =1{1 минута} few{{minutes} минуты} other{{minutes} минут}}", + "focalLength": "{length} mm", "applyButtonLabel": "ПРИМЕНИТЬ", "deleteButtonLabel": "УДАЛИТЬ", diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart index fbfa3adcc..67c12597c 100644 --- a/lib/widgets/about/policy_page.dart +++ b/lib/widgets/about/policy_page.dart @@ -17,10 +17,13 @@ class PolicyPage extends StatefulWidget { class _PolicyPageState extends State { late Future _termsLoader; + static const termsPath = 'assets/terms.md'; + static const termsDirection = TextDirection.ltr; + @override void initState() { super.initState(); - _termsLoader = rootBundle.loadString('assets/terms.md'); + _termsLoader = rootBundle.loadString(termsPath); } @override @@ -38,7 +41,10 @@ class _PolicyPageState extends State { final terms = snapshot.data!; return Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: MarkdownContainer(data: terms), + child: MarkdownContainer( + data: terms, + textDirection: termsDirection, + ), ); }, ), diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 1a3c13bd0..4041019b6 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -192,6 +192,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); final selector = GridSelectionGestureDetector( + scrollableKey: scrollableKey, selectable: isMainMode, items: collection.sortedEntries, scrollController: scrollController, diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart index d416814a0..fb7bd7b11 100644 --- a/lib/widgets/common/basic/draggable_scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; /* @@ -9,6 +10,8 @@ import 'package:flutter/material.dart'; - allow any `Widget` as label content - moved out constraints responsibility - various extent & thumb positioning fixes + - null safety + - directionality aware */ /// Build the Scroll Thumb and label using the current configuration @@ -350,7 +353,7 @@ class SlideFadeTransition extends StatelessWidget { builder: (context, child) => animation.value == 0.0 ? Container() : child!, child: SlideTransition( position: Tween( - begin: Offset((Directionality.of(context) == TextDirection.ltr ? 1 : -1) * .3, 0), + begin: Offset((context.isRtl ? -1 : 1) * .3, 0), end: Offset.zero, ).animate(animation), child: FadeTransition( diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 2727565e6..4915aea22 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -33,6 +33,8 @@ class SideGestureAreaProtector extends StatelessWidget { Widget build(BuildContext context) { return Positioned.fill( child: Row( + // `systemGestureInsets` are not directional + textDirection: TextDirection.ltr, children: [ SizedBox( width: context.select((mq) => mq.systemGestureInsets.left), diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index 47e28bc4a..76110e64f 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -4,10 +4,12 @@ import 'package:url_launcher/url_launcher.dart'; class MarkdownContainer extends StatelessWidget { final String data; + final TextDirection? textDirection; const MarkdownContainer({ Key? key, required this.data, + this.textDirection, }) : super(key: key); static const double maxWidth = 460; @@ -34,15 +36,18 @@ class MarkdownContainer extends StatelessWidget { ), ), child: Scrollbar( - child: Markdown( - data: data, - selectable: true, - onTapLink: (text, href, title) async { - if (href != null && await canLaunch(href)) { - await launch(href); - } - }, - shrinkWrap: true, + child: Directionality( + textDirection: textDirection ?? Directionality.of(context), + child: Markdown( + data: data, + selectable: true, + onTapLink: (text, href, title) async { + if (href != null && await canLaunch(href)) { + await launch(href); + } + }, + shrinkWrap: true, + ), ), ), ), diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart index cf3a515ee..6a4a86af0 100644 --- a/lib/widgets/common/extensions/build_context.dart +++ b/lib/widgets/common/extensions/build_context.dart @@ -5,4 +5,6 @@ extension ExtraContext on BuildContext { String? get currentRouteName => ModalRoute.of(this)?.settings.name; AppLocalizations get l10n => AppLocalizations.of(this)!; + + bool get isRtl => Directionality.of(this) == TextDirection.rtl; } diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 5d97bb53f..bcb0e40e0 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -30,37 +30,6 @@ class SectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final spans = [ - WidgetSpan( - alignment: widgetSpanAlignment, - child: _SectionSelectableLeading( - selectable: selectable, - sectionKey: sectionKey, - browsingBuilder: leading != null - ? (context) => Container( - padding: const EdgeInsetsDirectional.only(end: 8, bottom: 4), - width: leadingDimension, - height: leadingDimension, - child: leading, - ) - : null, - onPressed: selectable ? () => _toggleSectionSelection(context) : null, - ), - ), - TextSpan( - text: title, - style: Constants.titleTextStyle, - ), - if (trailing != null) - WidgetSpan( - alignment: widgetSpanAlignment, - child: Container( - padding: const EdgeInsetsDirectional.only(start: 8, bottom: 2), - child: trailing, - ), - ), - ]; - return Container( alignment: AlignmentDirectional.centerStart, padding: padding, @@ -68,13 +37,38 @@ class SectionHeader extends StatelessWidget { child: GestureDetector( onTap: selectable ? () => _toggleSectionSelection(context) : null, child: Text.rich( - // bidi with optional surrounding widget spans is tricky - // so we use LTR direction for the rich text itself and reverse the spans if necessary - // TODO TLAD [rtl] revisit this solution as it is failing for multiline headers TextSpan( - children: Directionality.of(context) == TextDirection.ltr ? spans : spans.reversed.toList(), + children: [ + WidgetSpan( + alignment: widgetSpanAlignment, + child: _SectionSelectableLeading( + selectable: selectable, + sectionKey: sectionKey, + browsingBuilder: leading != null + ? (context) => Container( + padding: const EdgeInsetsDirectional.only(end: 8, bottom: 4), + width: leadingDimension, + height: leadingDimension, + child: leading, + ) + : null, + onPressed: selectable ? () => _toggleSectionSelection(context) : null, + ), + ), + TextSpan( + text: title, + style: Constants.titleTextStyle, + ), + if (trailing != null) + WidgetSpan( + alignment: widgetSpanAlignment, + child: Container( + padding: const EdgeInsetsDirectional.only(start: 8, bottom: 2), + child: trailing, + ), + ), + ], ), - textDirection: TextDirection.ltr, ), ), ); diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 8ade2aaf4..15097ba46 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -4,6 +4,7 @@ import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; @@ -304,7 +305,7 @@ class _ScaleOverlayState extends State<_ScaleOverlay> { gradientCenter = center; break; case TileLayout.list: - gradientCenter = Offset(Directionality.of(context) == TextDirection.rtl ? gridWidth : 0, center.dy); + gradientCenter = Offset(context.isRtl ? gridWidth : 0, center.dy); break; } diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index 86c78b267..c30ac4c7d 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -191,6 +191,7 @@ class SectionedListLayout { required this.sectionLayouts, }); + // return tile rectangle in layout space, i.e. x=0 is start Rect? getTileRect(T item) { final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); if (section == null) return null; @@ -211,6 +212,7 @@ class SectionedListLayout { SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); + // `position` in layout space, i.e. x=0 is start T? getItemAt(Offset position) { var dy = position.dy; final sectionLayout = getSectionAt(dy); diff --git a/lib/widgets/common/grid/selector.dart b/lib/widgets/common/grid/selector.dart index 92604fbf4..05f41d58c 100644 --- a/lib/widgets/common/grid/selector.dart +++ b/lib/widgets/common/grid/selector.dart @@ -3,12 +3,14 @@ import 'dart:math'; import 'package:aves/model/selection.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class GridSelectionGestureDetector extends StatefulWidget { + final GlobalKey scrollableKey; final bool selectable; final List items; final ScrollController scrollController; @@ -17,6 +19,7 @@ class GridSelectionGestureDetector extends StatefulWidget { const GridSelectionGestureDetector({ Key? key, + required this.scrollableKey, this.selectable = true, required this.items, required this.scrollController, @@ -42,6 +45,13 @@ class _GridSelectionGestureDetectorState extends State widget.appBarHeightNotifier.value; + double get scrollableWidth { + final scrollableContext = widget.scrollableKey.currentContext!; + final scrollableBox = scrollableContext.findRenderObject() as RenderBox; + // not the same as `MediaQuery.size.width`, because of screen insets/padding + return scrollableBox.size.width; + } + static const double scrollEdgeRatio = .15; static const double scrollMaxPixelPerSecond = 600.0; static const Duration scrollUpdateInterval = Duration(milliseconds: 100); @@ -147,7 +157,7 @@ class _GridSelectionGestureDetectorState extends State>(); - return sectionedListLayout.getItemAt(offset); + return sectionedListLayout.getItemAt(context.isRtl ? Offset(scrollableWidth - offset.dx, offset.dy) : offset); } void _toggleSelectionToIndex(int toIndex) { diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 68bf5285f..b8504df7d 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -283,20 +283,26 @@ class _AvesFilterChipState extends State { ), ), if (banner != null) - LayoutBuilder(builder: (context, constraints) { - return ClipRRect( - borderRadius: borderRadius, - child: Transform( - transform: Matrix4.identity().scaled((constraints.maxHeight / 90 - .4).clamp(.45, 1.0)), - child: Banner( - message: banner.toUpperCase(), - location: BannerLocation.topStart, - color: Theme.of(context).colorScheme.secondary, - child: const SizedBox(), + LayoutBuilder( + builder: (context, constraints) { + return ClipRRect( + borderRadius: borderRadius, + child: Align( + // align to corner the scaled down banner in RTL + alignment: AlignmentDirectional.topStart, + child: Transform( + transform: Matrix4.identity().scaled((constraints.maxHeight / 90 - .4).clamp(.45, 1.0)), + child: Banner( + message: banner.toUpperCase(), + location: BannerLocation.topStart, + color: Theme.of(context).colorScheme.secondary, + child: const SizedBox(), + ), + ), ), - ), - ); - }), + ); + }, + ), ], ), ); diff --git a/lib/widgets/common/map/marker.dart b/lib/widgets/common/map/marker.dart index eca28b008..f8238f888 100644 --- a/lib/widgets/common/map/marker.dart +++ b/lib/widgets/common/map/marker.dart @@ -1,4 +1,5 @@ import 'package:aves/model/entry.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:flutter/material.dart'; @@ -82,18 +83,31 @@ class ImageMarker extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2), decoration: ShapeDecoration( color: Theme.of(context).colorScheme.secondary, - shape: const CustomRoundedRectangleBorder( - leftSide: borderSide, - rightSide: borderSide, - topSide: borderSide, - bottomSide: borderSide, - topLeftCornerSide: borderSide, - bottomRightCornerSide: borderSide, - borderRadius: BorderRadius.only( - topLeft: innerRadius, - bottomRight: innerRadius, - ), - ), + shape: context.isRtl + ? const CustomRoundedRectangleBorder( + leftSide: borderSide, + rightSide: borderSide, + topSide: borderSide, + bottomSide: borderSide, + topRightCornerSide: borderSide, + bottomLeftCornerSide: borderSide, + borderRadius: BorderRadius.only( + topRight: innerRadius, + bottomLeft: innerRadius, + ), + ) + : const CustomRoundedRectangleBorder( + leftSide: borderSide, + rightSide: borderSide, + topSide: borderSide, + bottomSide: borderSide, + topLeftCornerSide: borderSide, + bottomRightCornerSide: borderSide, + borderRadius: BorderRadius.only( + topLeft: innerRadius, + bottomRight: innerRadius, + ), + ), ), child: Text( '$count', diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 05beb7b64..3ad653c3f 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -46,46 +46,49 @@ class _AppDebugPageState extends State { @override Widget build(BuildContext context) { return MediaQueryDataProvider( - child: Scaffold( - appBar: AppBar( - title: const Text('Debug'), - actions: [ - MenuIconTheme( - child: PopupMenuButton( - // key is expected by test driver - key: const Key('appbar-menu-button'), - itemBuilder: (context) => AppDebugAction.values - .map((v) => PopupMenuItem( - // key is expected by test driver - key: Key('menu-${v.name}'), - value: v, - child: MenuRow(text: v.name), - )) - .toList(), - onSelected: (action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - unawaited(_onActionSelected(action)); - }, + child: Directionality( + textDirection: TextDirection.ltr, + child: Scaffold( + appBar: AppBar( + title: const Text('Debug'), + actions: [ + MenuIconTheme( + child: PopupMenuButton( + // key is expected by test driver + key: const Key('appbar-menu-button'), + itemBuilder: (context) => AppDebugAction.values + .map((v) => PopupMenuItem( + // key is expected by test driver + key: Key('menu-${v.name}'), + value: v, + child: MenuRow(text: v.name), + )) + .toList(), + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + unawaited(_onActionSelected(action)); + }, + ), ), - ), - ], - ), - body: SafeArea( - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - _buildGeneralTabView(), - const DebugAndroidAppSection(), - const DebugAndroidCodecSection(), - const DebugAndroidDirSection(), - const DebugCacheSection(), - const DebugAppDatabaseSection(), - const DebugErrorReportingSection(), - const DebugSettingsSection(), - const DebugStorageSection(), ], ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.all(8), + children: [ + _buildGeneralTabView(), + const DebugAndroidAppSection(), + const DebugAndroidCodecSection(), + const DebugAndroidDirSection(), + const DebugCacheSection(), + const DebugAppDatabaseSection(), + const DebugErrorReportingSection(), + const DebugSettingsSection(), + const DebugStorageSection(), + ], + ), + ), ), ), ); diff --git a/lib/widgets/dialogs/entry_editors/edit_entry_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_entry_date_dialog.dart index 639b4bca8..cb4e5657b 100644 --- a/lib/widgets/dialogs/entry_editors/edit_entry_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_entry_date_dialog.dart @@ -131,7 +131,7 @@ class _EditEntryDateDialogState extends State { final use24hour = context.select((v) => v.alwaysUse24HourFormat); return Padding( - padding: const EdgeInsets.only(left: 16, right: 8), + padding: const EdgeInsetsDirectional.only(start: 16, end: 8), child: Row( children: [ Expanded(child: Text(formatDateTime(_setDateTime, locale, use24hour))), @@ -177,6 +177,8 @@ class _EditEntryDateDialogState extends State { const textStyle = TextStyle(fontSize: 34); return Center( child: Table( + // even when ambient direction is RTL, time is displayed in LTR + textDirection: TextDirection.ltr, children: [ TableRow( children: [ diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart index 2213bf598..03c72304f 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:aves/model/entry.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -40,12 +41,17 @@ class _RenameEntryDialogState extends State { @override Widget build(BuildContext context) { + final isRtl = context.isRtl; + final extensionSuffixText = '${Constants.fsi}${entry.extension}${Constants.pdi}'; return AvesDialog( content: TextField( controller: _nameController, decoration: InputDecoration( labelText: context.l10n.renameEntryDialogLabel, - suffixText: entry.extension, + // decoration prefix and suffix follow directionality + // but the file extension should always be on the right + prefixText: isRtl ? extensionSuffixText : null, + suffixText: isRtl ? null : extensionSuffixText, ), autofocus: true, onChanged: (_) => _validate(), diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 60484d1cc..de3a26288 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -131,9 +131,8 @@ class _TileViewDialogState extends State> with // crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Material( - borderRadius: const BorderRadius.only( - topLeft: AvesDialog.cornerRadius, - topRight: AvesDialog.cornerRadius, + borderRadius: const BorderRadius.vertical( + top: AvesDialog.cornerRadius, ), clipBehavior: Clip.antiAlias, child: TabBar( diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index 47451ffb9..8e2769952 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -168,7 +168,7 @@ class CoveredFilterChip extends StatelessWidget { children: [ if (pinned) AnimatedPadding( - padding: EdgeInsets.only(right: padding), + padding: EdgeInsetsDirectional.only(end: padding), duration: Durations.chipDecorationAnimation, child: Icon( AIcons.pin, @@ -178,7 +178,7 @@ class CoveredFilterChip extends StatelessWidget { ), if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) AnimatedPadding( - padding: EdgeInsets.only(right: padding), + padding: EdgeInsetsDirectional.only(end: padding), duration: Durations.chipDecorationAnimation, child: Icon( AIcons.removableStorage, diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index d25ef8ecb..e9e098ddc 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -384,6 +384,7 @@ class _FilterSectionedContentState extends State<_Fi final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); final selector = GridSelectionGestureDetector>( + scrollableKey: scrollableKey, selectable: isMainMode && widget.selectable, items: visibleSections.values.expand((v) => v).toList(), scrollController: scrollController, diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index 3877871e3..a3e01625e 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -6,6 +6,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/scaling.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart'; @@ -178,8 +179,11 @@ class FilterTile extends StatelessWidget { ], ); if (onTap != null) { + // larger than the chip corner radius, so ink effects will be effectively clipped from the leading chip corners + const radius = Radius.circular(123); child = InkWell( - borderRadius: const BorderRadius.only(topLeft: Radius.circular(123), bottomLeft: Radius.circular(123)), + // as of Flutter v2.8.1, `InkWell` does not use `BorderRadiusGeometry` + borderRadius: context.isRtl ? const BorderRadius.only(topRight: radius, bottomRight: radius) : const BorderRadius.only(topLeft: radius, bottomLeft: radius), onTap: onTap, child: child, ); diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index 119d499e7..f638d7847 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -51,7 +51,7 @@ class FilterListDetails extends StatelessWidget { WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( - padding: const EdgeInsets.only(right: FilterListDetailsTheme.titleIconPadding), + padding: const EdgeInsetsDirectional.only(end: FilterListDetailsTheme.titleIconPadding), child: IconTheme( data: IconThemeData(color: detailsTheme.titleStyle.color), child: leading, @@ -127,7 +127,7 @@ class FilterListDetails extends StatelessWidget { children: leadingIcons .mapIndexed((i, child) => i > 0 ? Padding( - padding: const EdgeInsets.only(left: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: child, ) : child) diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index 1b1dc7f74..e3d87797a 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -174,14 +174,14 @@ class _QuickActionEditorBodyState extends State { Widget _buildContentLine(BuildContext context, FileSystemEntity content) { return ListTile( leading: const Icon(AIcons.folder), - title: Text(pContext.split(content.path).last), + title: Text('${Constants.fsi}${pContext.split(content.path).last}${Constants.pdi}'), onTap: () { _goTo(content.path); setState(() {}); diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index d42e1082f..ea05acb8a 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart' as intl; @@ -40,7 +41,7 @@ class FilterTable extends StatelessWidget { final textScaleFactor = MediaQuery.textScaleFactorOf(context); final lineHeight = 16 * textScaleFactor; - final isRTL = Directionality.of(context) == TextDirection.rtl; + final isRtl = context.isRtl; return Padding( padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.outlineWidth / 2 + 6, end: 8), @@ -74,7 +75,7 @@ class FilterTable extends StatelessWidget { backgroundColor: Colors.white24, progressColor: stringToColor(label), animation: true, - isRTL: isRTL, + isRTL: isRtl, padding: EdgeInsets.symmetric(horizontal: lineHeight), center: Text( intl.NumberFormat.percentPattern().format(percent), diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 59312765e..c94663d50 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -109,7 +109,7 @@ class StatsPage extends StatelessWidget { backgroundColor: Colors.white24, progressColor: Theme.of(context).colorScheme.secondary, animation: animate, - isRTL: Directionality.of(context) == TextDirection.rtl, + isRTL: context.isRtl, leading: const Icon(AIcons.location), padding: EdgeInsets.symmetric(horizontal: lineHeight), center: Text( @@ -221,23 +221,25 @@ class StatsPage extends StatelessWidget { children: seriesData .map((d) => GestureDetector( onTap: () => _onFilterSelection(context, MimeFilter(d.mimeType)), - child: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: const EdgeInsetsDirectional.only(end: 8), - child: Icon(AIcons.disc, color: d.color), - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(AIcons.disc, color: d.color), + const SizedBox(width: 8), + Flexible( + child: Text( + d.displayText, + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, ), - TextSpan(text: '${d.displayText} '), - TextSpan(text: '${d.entryCount}', style: const TextStyle(color: Colors.white70)), - ], - ), - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, + ), + const SizedBox(width: 8), + Text( + '${d.entryCount}', + style: const TextStyle(color: Colors.white70), + ), + ], ), )) .toList(), diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index 511a52a2f..75a677123 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -358,7 +358,7 @@ class _PositionTitleRow extends StatelessWidget { [ if (collectionPosition != null) collectionPosition, if (pagePosition != null) pagePosition, - if (title != null) title, + if (title != null) '${Constants.fsi}$title${Constants.pdi}', ].join(separator), strutStyle: Constants.overflowStrutStyle); @@ -430,7 +430,7 @@ class _ShootingRow extends StatelessWidget { final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown; final focalLength = details.focalLength; - final focalLengthText = focalLength != null ? '${NumberFormat('0.#', locale).format(focalLength)} mm' : Constants.overlayUnknown; + final focalLengthText = focalLength != null ? context.l10n.focalLength(NumberFormat('0.#', locale).format(focalLength)) : Constants.overlayUnknown; final iso = details.iso; final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown; diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 0ef8a2028..d21f9c743 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -25,11 +25,14 @@ class _WelcomePageState extends State { bool _hasAcceptedTerms = false; late Future _termsLoader; + static const termsPath = 'assets/terms.md'; + static const termsDirection = TextDirection.ltr; + @override void initState() { super.initState(); settings.setContextualDefaults(); - _termsLoader = rootBundle.loadString('assets/terms.md'); + _termsLoader = rootBundle.loadString(termsPath); WidgetsBinding.instance!.addPostFrameCallback((_) => _initWelcomeSettings()); } @@ -68,7 +71,12 @@ class _WelcomePageState extends State { children: [ ..._buildHeader(context, isPortrait: isPortrait), if (isPortrait) ...[ - Flexible(child: MarkdownContainer(data: terms)), + Flexible( + child: MarkdownContainer( + data: terms, + textDirection: termsDirection, + ), + ), const SizedBox(height: 16), ..._buildControls(context), ] else @@ -76,16 +84,19 @@ class _WelcomePageState extends State { child: Row( children: [ Flexible( - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MarkdownContainer(data: terms), - )), + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MarkdownContainer( + data: terms, + textDirection: termsDirection, + ), + ), + ), Flexible( child: ListView( - // shrinkWrap: true, children: _buildControls(context), ), - ) + ), ], ), )