This commit is contained in:
Thibault Deckers 2022-01-23 16:58:54 +09:00
parent 2a4c07a657
commit d44b001bb7
33 changed files with 268 additions and 177 deletions

View file

@ -3,10 +3,11 @@
"welcomeMessage": "Willkommen bei Aves", "welcomeMessage": "Willkommen bei Aves",
"welcomeOptional": "Optional", "welcomeOptional": "Optional",
"welcomeTermsToggle": "Ich stimme den Bedingungen und Konditionen zu", "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}}", "timeSeconds": "{seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}",
"timeMinutes": " {minutes, plural, =1{1 Minute} other{{minutes} Minuten}}", "timeMinutes": "{minutes, plural, =1{1 Minute} other{{minutes} Minuten}}",
"focalLength": "{length} mm",
"applyButtonLabel": "ANWENDEN", "applyButtonLabel": "ANWENDEN",
"deleteButtonLabel": "LÖSCHEN", "deleteButtonLabel": "LÖSCHEN",
@ -93,7 +94,7 @@
"coordinateFormatDms": "GMS", "coordinateFormatDms": "GMS",
"coordinateFormatDecimal": "Dezimalgrad", "coordinateFormatDecimal": "Dezimalgrad",
"coordinateDms": " {coordinate} {direction}", "coordinateDms": "{coordinate} {direction}",
"coordinateDmsNorth": "N", "coordinateDmsNorth": "N",
"coordinateDmsSouth": "s", "coordinateDmsSouth": "s",
"coordinateDmsEast": "O", "coordinateDmsEast": "O",
@ -144,7 +145,7 @@
"missingSystemFilePickerDialogMessage": "Der System-Dateiauswahldialog fehlt oder ist deaktiviert. Bitte aktivieren Sie ihn und versuchen Sie es erneut.", "missingSystemFilePickerDialogMessage": "Der System-Dateiauswahldialog fehlt oder ist deaktiviert. Bitte aktivieren Sie ihn und versuchen Sie es erneut.",
"unsupportedTypeDialogTitle": "Nicht unterstützte Typen", "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.", "nameConflictDialogSingleSourceMessage": "Einige Dateien im Zielordner haben den gleichen Namen.",
"nameConflictDialogMultipleSourceMessage": "Einige Dateien haben denselben Namen.", "nameConflictDialogMultipleSourceMessage": "Einige Dateien haben denselben Namen.",
@ -155,7 +156,7 @@
"noMatchingAppDialogTitle": "Keine passende App", "noMatchingAppDialogTitle": "Keine passende App",
"noMatchingAppDialogMessage": "Es gibt keine Anwendungen, die dies bewältigen können.", "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?", "videoResumeDialogMessage": "Möchten Sie bei {time} weiter abspielen?",
"videoStartOverButtonLabel": "NEU BEGINNEN", "videoStartOverButtonLabel": "NEU BEGINNEN",
@ -175,8 +176,8 @@
"renameAlbumDialogLabel": "Neuer Name", "renameAlbumDialogLabel": "Neuer Name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Verzeichnis existiert bereits", "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?}}", "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?}}", "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:", "exportEntryDialogFormat": "Format:",
@ -256,7 +257,7 @@
"collectionPageTitle": "Sammlung", "collectionPageTitle": "Sammlung",
"collectionPickPageTitle": "Wähle", "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", "collectionActionShowTitleSearch": "Titelfilter anzeigen",
"collectionActionHideTitleSearch": "Titelfilter ausblenden", "collectionActionHideTitleSearch": "Titelfilter ausblenden",
@ -282,14 +283,14 @@
"dateToday": "Heute", "dateToday": "Heute",
"dateYesterday": "Gestern", "dateYesterday": "Gestern",
"dateThisMonth": "Diesen Monat", "dateThisMonth": "Diesen Monat",
"collectionDeleteFailureFeedback": " {count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}", "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}}", "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}}", "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}}", "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}}", "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}}", "collectionCopySuccessFeedback": "{count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}",
"collectionMoveSuccessFeedback": " {count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}", "collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
"collectionEditSuccessFeedback": " {count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}", "collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
"collectionEmptyFavourites": "Keine Favoriten", "collectionEmptyFavourites": "Keine Favoriten",
"collectionEmptyVideos": "Keine Videos", "collectionEmptyVideos": "Keine Videos",
@ -471,7 +472,7 @@
"settingsUnitSystemTitle": "Einheiten", "settingsUnitSystemTitle": "Einheiten",
"statsPageTitle": "Statistiken", "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", "statsTopCountries": "Top-Länder",
"statsTopPlaces": "Top-Plätze", "statsTopPlaces": "Top-Plätze",
"statsTopTags": "Top-Tags", "statsTopTags": "Top-Tags",

View file

@ -22,6 +22,15 @@
"minutes": {} "minutes": {}
} }
}, },
"focalLength": "{length} mm",
"@focalLength": {
"placeholders": {
"length": {
"type": "String",
"example": "5.4"
}
}
},
"applyButtonLabel": "APPLY", "applyButtonLabel": "APPLY",
"deleteButtonLabel": "DELETE", "deleteButtonLabel": "DELETE",

View file

@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}", "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}",
"timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}", "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}",
"focalLength": "{length} mm",
"applyButtonLabel": "APLICAR", "applyButtonLabel": "APLICAR",
"deleteButtonLabel": "BORRAR", "deleteButtonLabel": "BORRAR",

View file

@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, =1{1 seconde} other{{seconds} secondes}}", "timeSeconds": "{seconds, plural, =1{1 seconde} other{{seconds} secondes}}",
"timeMinutes": "{minutes, plural, =1{1 minute} other{{minutes} minutes}}", "timeMinutes": "{minutes, plural, =1{1 minute} other{{minutes} minutes}}",
"focalLength": "{length} mm",
"applyButtonLabel": "ENREGISTRER", "applyButtonLabel": "ENREGISTRER",
"deleteButtonLabel": "SUPPRIMER", "deleteButtonLabel": "SUPPRIMER",

View file

@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, other{{seconds}초}}", "timeSeconds": "{seconds, plural, other{{seconds}초}}",
"timeMinutes": "{minutes, plural, other{{minutes}분}}", "timeMinutes": "{minutes, plural, other{{minutes}분}}",
"focalLength": "{length} mm",
"applyButtonLabel": "확인", "applyButtonLabel": "확인",
"deleteButtonLabel": "삭제", "deleteButtonLabel": "삭제",

View file

@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}", "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}",
"timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}", "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}",
"focalLength": "{length} mm",
"applyButtonLabel": "APLIQUE", "applyButtonLabel": "APLIQUE",
"deleteButtonLabel": "EXCLUIR", "deleteButtonLabel": "EXCLUIR",

View file

@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, =1{1 секунда} few{{seconds} секунды} other{{seconds} секунд}}", "timeSeconds": "{seconds, plural, =1{1 секунда} few{{seconds} секунды} other{{seconds} секунд}}",
"timeMinutes": "{minutes, plural, =1{1 минута} few{{minutes} минуты} other{{minutes} минут}}", "timeMinutes": "{minutes, plural, =1{1 минута} few{{minutes} минуты} other{{minutes} минут}}",
"focalLength": "{length} mm",
"applyButtonLabel": "ПРИМЕНИТЬ", "applyButtonLabel": "ПРИМЕНИТЬ",
"deleteButtonLabel": "УДАЛИТЬ", "deleteButtonLabel": "УДАЛИТЬ",

View file

@ -17,10 +17,13 @@ class PolicyPage extends StatefulWidget {
class _PolicyPageState extends State<PolicyPage> { class _PolicyPageState extends State<PolicyPage> {
late Future<String> _termsLoader; late Future<String> _termsLoader;
static const termsPath = 'assets/terms.md';
static const termsDirection = TextDirection.ltr;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_termsLoader = rootBundle.loadString('assets/terms.md'); _termsLoader = rootBundle.loadString(termsPath);
} }
@override @override
@ -38,7 +41,10 @@ class _PolicyPageState extends State<PolicyPage> {
final terms = snapshot.data!; final terms = snapshot.data!;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: MarkdownContainer(data: terms), child: MarkdownContainer(
data: terms,
textDirection: termsDirection,
),
); );
}, },
), ),

View file

@ -192,6 +192,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main); final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
final selector = GridSelectionGestureDetector( final selector = GridSelectionGestureDetector(
scrollableKey: scrollableKey,
selectable: isMainMode, selectable: isMainMode,
items: collection.sortedEntries, items: collection.sortedEntries,
scrollController: scrollController, scrollController: scrollController,

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/* /*
@ -9,6 +10,8 @@ import 'package:flutter/material.dart';
- allow any `Widget` as label content - allow any `Widget` as label content
- moved out constraints responsibility - moved out constraints responsibility
- various extent & thumb positioning fixes - various extent & thumb positioning fixes
- null safety
- directionality aware
*/ */
/// Build the Scroll Thumb and label using the current configuration /// 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!, builder: (context, child) => animation.value == 0.0 ? Container() : child!,
child: SlideTransition( child: SlideTransition(
position: Tween( position: Tween(
begin: Offset((Directionality.of(context) == TextDirection.ltr ? 1 : -1) * .3, 0), begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
end: Offset.zero, end: Offset.zero,
).animate(animation), ).animate(animation),
child: FadeTransition( child: FadeTransition(

View file

@ -33,6 +33,8 @@ class SideGestureAreaProtector extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Positioned.fill( return Positioned.fill(
child: Row( child: Row(
// `systemGestureInsets` are not directional
textDirection: TextDirection.ltr,
children: [ children: [
SizedBox( SizedBox(
width: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.left), width: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.left),

View file

@ -4,10 +4,12 @@ import 'package:url_launcher/url_launcher.dart';
class MarkdownContainer extends StatelessWidget { class MarkdownContainer extends StatelessWidget {
final String data; final String data;
final TextDirection? textDirection;
const MarkdownContainer({ const MarkdownContainer({
Key? key, Key? key,
required this.data, required this.data,
this.textDirection,
}) : super(key: key); }) : super(key: key);
static const double maxWidth = 460; static const double maxWidth = 460;
@ -34,6 +36,8 @@ class MarkdownContainer extends StatelessWidget {
), ),
), ),
child: Scrollbar( child: Scrollbar(
child: Directionality(
textDirection: textDirection ?? Directionality.of(context),
child: Markdown( child: Markdown(
data: data, data: data,
selectable: true, selectable: true,
@ -47,6 +51,7 @@ class MarkdownContainer extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }

View file

@ -5,4 +5,6 @@ extension ExtraContext on BuildContext {
String? get currentRouteName => ModalRoute.of(this)?.settings.name; String? get currentRouteName => ModalRoute.of(this)?.settings.name;
AppLocalizations get l10n => AppLocalizations.of(this)!; AppLocalizations get l10n => AppLocalizations.of(this)!;
bool get isRtl => Directionality.of(this) == TextDirection.rtl;
} }

View file

@ -30,7 +30,15 @@ class SectionHeader<T> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final spans = [ return Container(
alignment: AlignmentDirectional.centerStart,
padding: padding,
constraints: const BoxConstraints(minHeight: leadingDimension),
child: GestureDetector(
onTap: selectable ? () => _toggleSectionSelection(context) : null,
child: Text.rich(
TextSpan(
children: [
WidgetSpan( WidgetSpan(
alignment: widgetSpanAlignment, alignment: widgetSpanAlignment,
child: _SectionSelectableLeading<T>( child: _SectionSelectableLeading<T>(
@ -59,22 +67,8 @@ class SectionHeader<T> extends StatelessWidget {
child: trailing, child: trailing,
), ),
), ),
]; ],
return Container(
alignment: AlignmentDirectional.centerStart,
padding: padding,
constraints: const BoxConstraints(minHeight: leadingDimension),
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(),
), ),
textDirection: TextDirection.ltr,
), ),
), ),
); );

View file

@ -4,6 +4,7 @@ import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.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/grid/theme.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/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
@ -304,7 +305,7 @@ class _ScaleOverlayState extends State<_ScaleOverlay> {
gradientCenter = center; gradientCenter = center;
break; break;
case TileLayout.list: case TileLayout.list:
gradientCenter = Offset(Directionality.of(context) == TextDirection.rtl ? gridWidth : 0, center.dy); gradientCenter = Offset(context.isRtl ? gridWidth : 0, center.dy);
break; break;
} }

View file

@ -191,6 +191,7 @@ class SectionedListLayout<T> {
required this.sectionLayouts, required this.sectionLayouts,
}); });
// return tile rectangle in layout space, i.e. x=0 is start
Rect? getTileRect(T item) { Rect? getTileRect(T item) {
final MapEntry<SectionKey?, List<T>>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); final MapEntry<SectionKey?, List<T>>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item));
if (section == null) return null; if (section == null) return null;
@ -211,6 +212,7 @@ class SectionedListLayout<T> {
SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset);
// `position` in layout space, i.e. x=0 is start
T? getItemAt(Offset position) { T? getItemAt(Offset position) {
var dy = position.dy; var dy = position.dy;
final sectionLayout = getSectionAt(dy); final sectionLayout = getSectionAt(dy);

View file

@ -3,12 +3,14 @@ import 'dart:math';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/utils/math_utils.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/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class GridSelectionGestureDetector<T> extends StatefulWidget { class GridSelectionGestureDetector<T> extends StatefulWidget {
final GlobalKey scrollableKey;
final bool selectable; final bool selectable;
final List<T> items; final List<T> items;
final ScrollController scrollController; final ScrollController scrollController;
@ -17,6 +19,7 @@ class GridSelectionGestureDetector<T> extends StatefulWidget {
const GridSelectionGestureDetector({ const GridSelectionGestureDetector({
Key? key, Key? key,
required this.scrollableKey,
this.selectable = true, this.selectable = true,
required this.items, required this.items,
required this.scrollController, required this.scrollController,
@ -42,6 +45,13 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
double get appBarHeight => widget.appBarHeightNotifier.value; double get appBarHeight => 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 scrollEdgeRatio = .15;
static const double scrollMaxPixelPerSecond = 600.0; static const double scrollMaxPixelPerSecond = 600.0;
static const Duration scrollUpdateInterval = Duration(milliseconds: 100); static const Duration scrollUpdateInterval = Duration(milliseconds: 100);
@ -147,7 +157,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
// so we use custom layout computation instead to find the item. // so we use custom layout computation instead to find the item.
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition; final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
final sectionedListLayout = context.read<SectionedListLayout<T>>(); final sectionedListLayout = context.read<SectionedListLayout<T>>();
return sectionedListLayout.getItemAt(offset); return sectionedListLayout.getItemAt(context.isRtl ? Offset(scrollableWidth - offset.dx, offset.dy) : offset);
} }
void _toggleSelectionToIndex(int toIndex) { void _toggleSelectionToIndex(int toIndex) {

View file

@ -283,9 +283,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
), ),
), ),
if (banner != null) if (banner != null)
LayoutBuilder(builder: (context, constraints) { LayoutBuilder(
builder: (context, constraints) {
return ClipRRect( return ClipRRect(
borderRadius: borderRadius, borderRadius: borderRadius,
child: Align(
// align to corner the scaled down banner in RTL
alignment: AlignmentDirectional.topStart,
child: Transform( child: Transform(
transform: Matrix4.identity().scaled((constraints.maxHeight / 90 - .4).clamp(.45, 1.0)), transform: Matrix4.identity().scaled((constraints.maxHeight / 90 - .4).clamp(.45, 1.0)),
child: Banner( child: Banner(
@ -295,8 +299,10 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
child: const SizedBox(), child: const SizedBox(),
), ),
), ),
),
); );
}), },
),
], ],
), ),
); );

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; 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:aves/widgets/common/thumbnail/image.dart';
import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -82,7 +83,20 @@ class ImageMarker extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2), padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2),
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
shape: const CustomRoundedRectangleBorder( 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, leftSide: borderSide,
rightSide: borderSide, rightSide: borderSide,
topSide: borderSide, topSide: borderSide,

View file

@ -46,6 +46,8 @@ class _AppDebugPageState extends State<AppDebugPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: Directionality(
textDirection: TextDirection.ltr,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Debug'), title: const Text('Debug'),
@ -88,6 +90,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
), ),
), ),
), ),
),
); );
} }

View file

@ -131,7 +131,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
return Padding( return Padding(
padding: const EdgeInsets.only(left: 16, right: 8), padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
child: Row( child: Row(
children: [ children: [
Expanded(child: Text(formatDateTime(_setDateTime, locale, use24hour))), Expanded(child: Text(formatDateTime(_setDateTime, locale, use24hour))),
@ -177,6 +177,8 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
const textStyle = TextStyle(fontSize: 34); const textStyle = TextStyle(fontSize: 34);
return Center( return Center(
child: Table( child: Table(
// even when ambient direction is RTL, time is displayed in LTR
textDirection: TextDirection.ltr,
children: [ children: [
TableRow( TableRow(
children: [ children: [

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/services/common/services.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:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -40,12 +41,17 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isRtl = context.isRtl;
final extensionSuffixText = '${Constants.fsi}${entry.extension}${Constants.pdi}';
return AvesDialog( return AvesDialog(
content: TextField( content: TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: context.l10n.renameEntryDialogLabel, 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, autofocus: true,
onChanged: (_) => _validate(), onChanged: (_) => _validate(),

View file

@ -131,9 +131,8 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
// crossAxisAlignment: CrossAxisAlignment.stretch, // crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Material( Material(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.vertical(
topLeft: AvesDialog.cornerRadius, top: AvesDialog.cornerRadius,
topRight: AvesDialog.cornerRadius,
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: TabBar( child: TabBar(

View file

@ -168,7 +168,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
children: [ children: [
if (pinned) if (pinned)
AnimatedPadding( AnimatedPadding(
padding: EdgeInsets.only(right: padding), padding: EdgeInsetsDirectional.only(end: padding),
duration: Durations.chipDecorationAnimation, duration: Durations.chipDecorationAnimation,
child: Icon( child: Icon(
AIcons.pin, AIcons.pin,
@ -178,7 +178,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
), ),
if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album))
AnimatedPadding( AnimatedPadding(
padding: EdgeInsets.only(right: padding), padding: EdgeInsetsDirectional.only(end: padding),
duration: Durations.chipDecorationAnimation, duration: Durations.chipDecorationAnimation,
child: Icon( child: Icon(
AIcons.removableStorage, AIcons.removableStorage,

View file

@ -384,6 +384,7 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main); final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
final selector = GridSelectionGestureDetector<FilterGridItem<T>>( final selector = GridSelectionGestureDetector<FilterGridItem<T>>(
scrollableKey: scrollableKey,
selectable: isMainMode && widget.selectable, selectable: isMainMode && widget.selectable,
items: visibleSections.values.expand((v) => v).toList(), items: visibleSections.values.expand((v) => v).toList(),
scrollController: scrollController, scrollController: scrollController,

View file

@ -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/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/collection/collection_page.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/grid/scaling.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
@ -178,8 +179,11 @@ class FilterTile<T extends CollectionFilter> extends StatelessWidget {
], ],
); );
if (onTap != null) { 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( 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, onTap: onTap,
child: child, child: child,
); );

View file

@ -51,7 +51,7 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget {
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: FilterListDetailsTheme.titleIconPadding), padding: const EdgeInsetsDirectional.only(end: FilterListDetailsTheme.titleIconPadding),
child: IconTheme( child: IconTheme(
data: IconThemeData(color: detailsTheme.titleStyle.color), data: IconThemeData(color: detailsTheme.titleStyle.color),
child: leading, child: leading,
@ -127,7 +127,7 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget {
children: leadingIcons children: leadingIcons
.mapIndexed((i, child) => i > 0 .mapIndexed((i, child) => i > 0
? Padding( ? Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsetsDirectional.only(start: 8),
child: child, child: child,
) )
: child) : child)

View file

@ -174,14 +174,14 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
children: [ children: [
Positioned.fill( Positioned.fill(
child: FractionallySizedBox( child: FractionallySizedBox(
alignment: Alignment.centerLeft, alignment: AlignmentDirectional.centerStart,
widthFactor: .5, widthFactor: .5,
child: header, child: header,
), ),
), ),
Positioned.fill( Positioned.fill(
child: FractionallySizedBox( child: FractionallySizedBox(
alignment: Alignment.centerRight, alignment: AlignmentDirectional.centerEnd,
widthFactor: .5, widthFactor: .5,
child: footer, child: footer,
), ),

View file

@ -5,6 +5,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.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/identity/buttons.dart'; import 'package:aves/widgets/common/identity/buttons.dart';
@ -179,7 +180,7 @@ class _FilePickerState extends State<FilePicker> {
Widget _buildContentLine(BuildContext context, FileSystemEntity content) { Widget _buildContentLine(BuildContext context, FileSystemEntity content) {
return ListTile( return ListTile(
leading: const Icon(AIcons.folder), leading: const Icon(AIcons.folder),
title: Text(pContext.split(content.path).last), title: Text('${Constants.fsi}${pContext.split(content.path).last}${Constants.pdi}'),
onTap: () { onTap: () {
_goTo(content.path); _goTo(content.path);
setState(() {}); setState(() {});

View file

@ -1,6 +1,7 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/color_utils.dart';
import 'package:aves/utils/constants.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:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl; import 'package:intl/intl.dart' as intl;
@ -40,7 +41,7 @@ class FilterTable<T extends Comparable> extends StatelessWidget {
final textScaleFactor = MediaQuery.textScaleFactorOf(context); final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final lineHeight = 16 * textScaleFactor; final lineHeight = 16 * textScaleFactor;
final isRTL = Directionality.of(context) == TextDirection.rtl; final isRtl = context.isRtl;
return Padding( return Padding(
padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.outlineWidth / 2 + 6, end: 8), padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.outlineWidth / 2 + 6, end: 8),
@ -74,7 +75,7 @@ class FilterTable<T extends Comparable> extends StatelessWidget {
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
progressColor: stringToColor(label), progressColor: stringToColor(label),
animation: true, animation: true,
isRTL: isRTL, isRTL: isRtl,
padding: EdgeInsets.symmetric(horizontal: lineHeight), padding: EdgeInsets.symmetric(horizontal: lineHeight),
center: Text( center: Text(
intl.NumberFormat.percentPattern().format(percent), intl.NumberFormat.percentPattern().format(percent),

View file

@ -109,7 +109,7 @@ class StatsPage extends StatelessWidget {
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
progressColor: Theme.of(context).colorScheme.secondary, progressColor: Theme.of(context).colorScheme.secondary,
animation: animate, animation: animate,
isRTL: Directionality.of(context) == TextDirection.rtl, isRTL: context.isRtl,
leading: const Icon(AIcons.location), leading: const Icon(AIcons.location),
padding: EdgeInsets.symmetric(horizontal: lineHeight), padding: EdgeInsets.symmetric(horizontal: lineHeight),
center: Text( center: Text(
@ -221,24 +221,26 @@ class StatsPage extends StatelessWidget {
children: seriesData children: seriesData
.map((d) => GestureDetector( .map((d) => GestureDetector(
onTap: () => _onFilterSelection(context, MimeFilter(d.mimeType)), onTap: () => _onFilterSelection(context, MimeFilter(d.mimeType)),
child: Text.rich( child: Row(
TextSpan( mainAxisSize: MainAxisSize.min,
children: [ children: [
WidgetSpan( Icon(AIcons.disc, color: d.color),
alignment: PlaceholderAlignment.middle, const SizedBox(width: 8),
child: Padding( Flexible(
padding: const EdgeInsetsDirectional.only(end: 8), child: Text(
child: Icon(AIcons.disc, color: d.color), d.displayText,
),
),
TextSpan(text: '${d.displayText} '),
TextSpan(text: '${d.entryCount}', style: const TextStyle(color: Colors.white70)),
],
),
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
softWrap: false, softWrap: false,
maxLines: 1, maxLines: 1,
), ),
),
const SizedBox(width: 8),
Text(
'${d.entryCount}',
style: const TextStyle(color: Colors.white70),
),
],
),
)) ))
.toList(), .toList(),
), ),

View file

@ -358,7 +358,7 @@ class _PositionTitleRow extends StatelessWidget {
[ [
if (collectionPosition != null) collectionPosition, if (collectionPosition != null) collectionPosition,
if (pagePosition != null) pagePosition, if (pagePosition != null) pagePosition,
if (title != null) title, if (title != null) '${Constants.fsi}$title${Constants.pdi}',
].join(separator), ].join(separator),
strutStyle: Constants.overflowStrutStyle); strutStyle: Constants.overflowStrutStyle);
@ -430,7 +430,7 @@ class _ShootingRow extends StatelessWidget {
final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown; final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown;
final focalLength = details.focalLength; 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 iso = details.iso;
final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown; final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown;

View file

@ -25,11 +25,14 @@ class _WelcomePageState extends State<WelcomePage> {
bool _hasAcceptedTerms = false; bool _hasAcceptedTerms = false;
late Future<String> _termsLoader; late Future<String> _termsLoader;
static const termsPath = 'assets/terms.md';
static const termsDirection = TextDirection.ltr;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
settings.setContextualDefaults(); settings.setContextualDefaults();
_termsLoader = rootBundle.loadString('assets/terms.md'); _termsLoader = rootBundle.loadString(termsPath);
WidgetsBinding.instance!.addPostFrameCallback((_) => _initWelcomeSettings()); WidgetsBinding.instance!.addPostFrameCallback((_) => _initWelcomeSettings());
} }
@ -68,7 +71,12 @@ class _WelcomePageState extends State<WelcomePage> {
children: [ children: [
..._buildHeader(context, isPortrait: isPortrait), ..._buildHeader(context, isPortrait: isPortrait),
if (isPortrait) ...[ if (isPortrait) ...[
Flexible(child: MarkdownContainer(data: terms)), Flexible(
child: MarkdownContainer(
data: terms,
textDirection: termsDirection,
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
..._buildControls(context), ..._buildControls(context),
] else ] else
@ -78,14 +86,17 @@ class _WelcomePageState extends State<WelcomePage> {
Flexible( Flexible(
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: MarkdownContainer(data: terms), child: MarkdownContainer(
)), data: terms,
textDirection: termsDirection,
),
),
),
Flexible( Flexible(
child: ListView( child: ListView(
// shrinkWrap: true,
children: _buildControls(context), children: _buildControls(context),
), ),
) ),
], ],
), ),
) )