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",
"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",

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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": "УДАЛИТЬ",

View file

@ -17,10 +17,13 @@ class PolicyPage extends StatefulWidget {
class _PolicyPageState extends State<PolicyPage> {
late Future<String> _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<PolicyPage> {
final terms = snapshot.data!;
return Padding(
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 selector = GridSelectionGestureDetector(
scrollableKey: scrollableKey,
selectable: isMainMode,
items: collection.sortedEntries,
scrollController: scrollController,

View file

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

View file

@ -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<MediaQueryData, double>((mq) => mq.systemGestureInsets.left),

View file

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

View file

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

View file

@ -30,37 +30,6 @@ class SectionHeader<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final spans = [
WidgetSpan(
alignment: widgetSpanAlignment,
child: _SectionSelectableLeading<T>(
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<T> 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<T>(
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,
),
),
);

View file

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

View file

@ -191,6 +191,7 @@ class SectionedListLayout<T> {
required this.sectionLayouts,
});
// return tile rectangle in layout space, i.e. x=0 is start
Rect? getTileRect(T item) {
final MapEntry<SectionKey?, List<T>>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item));
if (section == null) return null;
@ -211,6 +212,7 @@ class SectionedListLayout<T> {
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);

View file

@ -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<T> extends StatefulWidget {
final GlobalKey scrollableKey;
final bool selectable;
final List<T> items;
final ScrollController scrollController;
@ -17,6 +19,7 @@ class GridSelectionGestureDetector<T> extends StatefulWidget {
const GridSelectionGestureDetector({
Key? key,
required this.scrollableKey,
this.selectable = true,
required this.items,
required this.scrollController,
@ -42,6 +45,13 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
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 scrollMaxPixelPerSecond = 600.0;
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.
final offset = Offset(0, scrollController.offset - appBarHeight) + localPosition;
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) {

View file

@ -283,20 +283,26 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
),
),
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(),
),
),
),
),
);
}),
);
},
),
],
),
);

View file

@ -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',

View file

@ -46,46 +46,49 @@ class _AppDebugPageState extends State<AppDebugPage> {
@override
Widget build(BuildContext context) {
return MediaQueryDataProvider(
child: Scaffold(
appBar: AppBar(
title: const Text('Debug'),
actions: [
MenuIconTheme(
child: PopupMenuButton<AppDebugAction>(
// 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<AppDebugAction>(
// 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(),
],
),
),
),
),
);

View file

@ -131,7 +131,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
final use24hour = context.select<MediaQueryData, bool>((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<EditEntryDateDialog> {
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: [

View file

@ -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<RenameEntryDialog> {
@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(),

View file

@ -131,9 +131,8 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> 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(

View file

@ -168,7 +168,7 @@ class CoveredFilterChip<T extends CollectionFilter> 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<T extends CollectionFilter> 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,

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 selector = GridSelectionGestureDetector<FilterGridItem<T>>(
scrollableKey: scrollableKey,
selectable: isMainMode && widget.selectable,
items: visibleSections.values.expand((v) => v).toList(),
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/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<T extends CollectionFilter> 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,
);

View file

@ -51,7 +51,7 @@ class FilterListDetails<T extends CollectionFilter> 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<T extends CollectionFilter> 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)

View file

@ -174,14 +174,14 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
children: [
Positioned.fill(
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
alignment: AlignmentDirectional.centerStart,
widthFactor: .5,
child: header,
),
),
Positioned.fill(
child: FractionallySizedBox(
alignment: Alignment.centerRight,
alignment: AlignmentDirectional.centerEnd,
widthFactor: .5,
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/icons.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/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons.dart';
@ -179,7 +180,7 @@ class _FilePickerState extends State<FilePicker> {
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(() {});

View file

@ -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<T extends Comparable> 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<T extends Comparable> 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),

View file

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

View file

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

View file

@ -25,11 +25,14 @@ class _WelcomePageState extends State<WelcomePage> {
bool _hasAcceptedTerms = false;
late Future<String> _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<WelcomePage> {
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<WelcomePage> {
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),
),
)
),
],
),
)