diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de3150aa..896ce5ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.7.10] - 2023-01-18 + +### Added + +- Video: optional gestures to adjust brightness/volume +- TV: improved support for Search, About, Privacy Policy + +### Changed + +- Viewer: do not keep max brightness when viewing info + +### Fixed + +- crash when media button events are triggered with no active media session + ## [v1.7.9] - 2023-01-15 ### Added diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 64f920264..5d8bc1ce9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -213,6 +213,15 @@ This change eventually prevents building the app with Flutter v3.3.3. + + + + + + >) { + val children = mutableListOf() + result.sendResult(children) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 715b155b5..d42e5ea50 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -12,7 +12,7 @@ import com.drew.metadata.xmp.XmpDirectory import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.metadata.* -import deckers.thibault.aves.metadata.XMP.doesPropExist +import deckers.thibault.aves.metadata.XMP.doesPropPathExist import deckers.thibault.aves.metadata.XMP.getSafeStructField import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.model.FieldMap @@ -104,7 +104,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { try { container = xmpDirs.firstNotNullOfOrNull { val xmpMeta = it.xmpMeta - if (xmpMeta.doesPropExist(XMP.GDEVICE_DIRECTORY_PROP_NAME)) { + if (xmpMeta.doesPropPathExist(listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) { GoogleDeviceContainer().apply { findItems(xmpMeta) } } else { null diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt index 590b1f65d..a481c94e4 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt @@ -3,7 +3,7 @@ package deckers.thibault.aves.metadata import android.content.Context import android.net.Uri import com.adobe.internal.xmp.XMPMeta -import deckers.thibault.aves.metadata.XMP.countPropArrayItems +import deckers.thibault.aves.metadata.XMP.countPropPathArrayItems import deckers.thibault.aves.metadata.XMP.getSafeStructField import deckers.thibault.aves.utils.indexOfBytes import java.io.DataInputStream @@ -15,11 +15,12 @@ class GoogleDeviceContainer { private val offsets: MutableList = ArrayList() fun findItems(xmpMeta: XMPMeta) { - val count = xmpMeta.countPropArrayItems(XMP.GDEVICE_DIRECTORY_PROP_NAME) + val containerDirectoryPath = listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME) + val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath) for (i in 1 until count + 1) { - val mimeType = xmpMeta.getSafeStructField(listOf(XMP.GDEVICE_DIRECTORY_PROP_NAME, i, XMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value - val length = xmpMeta.getSafeStructField(listOf(XMP.GDEVICE_DIRECTORY_PROP_NAME, i, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull() - val dataUri = xmpMeta.getSafeStructField(listOf(XMP.GDEVICE_DIRECTORY_PROP_NAME, i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value + val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value + val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull() + val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value if (mimeType != null && length != null && dataUri != null) { items.add( GoogleDeviceContainerItem( diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index 737c862da..74a9a9a3d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -49,6 +49,7 @@ object XMP { private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/" private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/" private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/" + private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/" private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/" private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/" private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/" @@ -70,6 +71,7 @@ object XMP { // cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format private val knownDataProps = listOf( XMPPropName(GAUDIO_NS_URI, "Data"), + XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"), XMPPropName(GIMAGE_NS_URI, "Data"), XMPPropName(GDEPTH_NS_URI, "Data"), XMPPropName(GDEPTH_NS_URI, "Confidence"), @@ -79,7 +81,8 @@ object XMP { // google portrait - val GDEVICE_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container/Container:Directory") + val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container") + val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory") val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI") val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length") val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime") @@ -254,10 +257,18 @@ object XMP { return doesPropertyExist(prop.nsUri, prop.toString()) } + fun XMPMeta.doesPropPathExist(props: List): Boolean { + return doesPropertyExist(props.first().nsUri, props.joinToString("/")) + } + fun XMPMeta.countPropArrayItems(prop: XMPPropName): Int { return countArrayItems(prop.nsUri, prop.toString()) } + fun XMPMeta.countPropPathArrayItems(props: List): Int { + return countArrayItems(props.first().nsUri, props.joinToString("/")) + } + fun XMPMeta.getPropArrayItemValues(prop: XMPPropName): List { val schema = prop.nsUri val propName = prop.toString() diff --git a/android/app/src/main/res/values-iw/strings.xml b/android/app/src/main/res/values-iw/strings.xml new file mode 100644 index 000000000..fa2bc3053 --- /dev/null +++ b/android/app/src/main/res/values-iw/strings.xml @@ -0,0 +1,12 @@ + + + אייבז + מסגרת תמונה + טפט + חיפוש + סרטים + סריקת מדיה + סרוק תמונות וסרטים + סורק מדיה + הפסק + \ No newline at end of file diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml index 1e53b15c3..28f72abad 100644 --- a/android/app/src/main/res/values-pl/strings.xml +++ b/android/app/src/main/res/values-pl/strings.xml @@ -2,10 +2,10 @@ Ramka Zdjęcia Szukaj - Filmy - Skan mediów - Skan obrazów & filmów - Skanowanie mediów + Wideo + Przeskanuj multimedia + Przeskanuj obrazy oraz wideo + Skanowanie multimediów Zatrzymaj Aves Tapeta diff --git a/fastlane/metadata/android/en-US/changelogs/90.txt b/fastlane/metadata/android/en-US/changelogs/90.txt new file mode 100644 index 000000000..7683711d3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/90.txt @@ -0,0 +1,5 @@ +In v1.7.10: +- Android TV support (cont'd) +- interact with videos via media session controls +- enjoy the app in Czech & Polish +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/9001.txt b/fastlane/metadata/android/en-US/changelogs/9001.txt new file mode 100644 index 000000000..7683711d3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/9001.txt @@ -0,0 +1,5 @@ +In v1.7.10: +- Android TV support (cont'd) +- interact with videos via media session controls +- enjoy the app in Czech & Polish +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/he/full_description.txt b/fastlane/metadata/android/he/full_description.txt new file mode 100644 index 000000000..6c92748f8 --- /dev/null +++ b/fastlane/metadata/android/he/full_description.txt @@ -0,0 +1,5 @@ +Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files. + +Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc. + +Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker. \ No newline at end of file diff --git a/fastlane/metadata/android/he/short_description.txt b/fastlane/metadata/android/he/short_description.txt new file mode 100644 index 000000000..8c9445bd5 --- /dev/null +++ b/fastlane/metadata/android/he/short_description.txt @@ -0,0 +1 @@ +Gallery and metadata explorer \ No newline at end of file diff --git a/fastlane/metadata/android/pl/full_description.txt b/fastlane/metadata/android/pl/full_description.txt index 2c26d7bb2..c10a1a6c6 100644 --- a/fastlane/metadata/android/pl/full_description.txt +++ b/fastlane/metadata/android/pl/full_description.txt @@ -1,4 +1,4 @@ -Aves obsługuje wszelkiego rodzaju obrazy i filmy, w tym typowe pliki JPEG i MP4 ale także bardziej egzotyczne formaty takie jak wielostronnicowe pliki TIFF, SVG, stare pliki AVI i wiele więcej! Skanuje twoją kolekcję multimediów aby zidentyfikować ruchome zdjęcia, panoramy (inaczej zdjęcia sferyczne), filmy 360°, a także pliki GeoTIFF. +Aves obsługuje wszelkiego rodzaju obrazy i filmy, w tym typowe pliki JPEG i MP4, ale także bardziej egzotyczne formaty, takie jak wielostronicowe pliki TIFF, SVG, stare pliki AVI i wiele innych! Skanuje twoją kolekcję multimediów, aby zidentyfikować ruchome zdjęcia, zdjęcia panoramiczne (inaczej zdjęcia sferyczne), wideo 360°, a także pliki GeoTIFF. Nawigacja i wyszukiwanie jest ważną częścią Aves. Celem jest aby użytkownicy mogli łatwo przechodzić od albumów do zdjęć, tagów, map itd. diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 52c9ab346..1141b9bfb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -781,6 +781,7 @@ "settingsVideoButtonsTile": "Buttons", "settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause", "settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward", + "settingsVideoGestureVerticalDragBrightnessVolume": "Swipe up or down to adjust brightness/volume", "settingsPrivacySectionTitle": "Privacy", "settingsAllowInstalledAppAccess": "Allow access to app inventory", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3cf489059..e4609e5fd 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1206,5 +1206,9 @@ "filterLocatedLabel": "Localizado", "@filterLocatedLabel": {}, "filterTaggedLabel": "Etiquetado", - "@filterTaggedLabel": {} + "@filterTaggedLabel": {}, + "tooManyItemsErrorDialogMessage": "Vuelva a intentarlo con menos elementos.", + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Deslice hacia arriba o hacia abajo para ajustar el brillo o el volumen", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 90c5e9903..c62ac6bb8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1208,5 +1208,7 @@ "filterLocatedLabel": "Localisé", "@filterLocatedLabel": {}, "tooManyItemsErrorDialogMessage": "Réessayez avec moins d’éléments.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Balayer verticalement pour ajuster la luminosité et le volume", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_he.arb b/lib/l10n/app_he.arb new file mode 100644 index 000000000..d73ba56f5 --- /dev/null +++ b/lib/l10n/app_he.arb @@ -0,0 +1,10 @@ +{ + "appName": "אייבז", + "@appName": {}, + "welcomeMessage": "ברוך הבא לאייבז", + "@welcomeMessage": {}, + "welcomeOptional": "אופציונלי", + "@welcomeOptional": {}, + "welcomeTermsToggle": "אני מסכימ/ה לתנאים", + "@welcomeTermsToggle": {} +} diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 9b1209d1e..1d9639b19 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1208,5 +1208,7 @@ "filterTaggedLabel": "Dilabel", "@filterTaggedLabel": {}, "tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Usap ke atas atau bawah untuk mengatur kecerahan/volume", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 5a62c46eb..4a3c0ee2a 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1208,5 +1208,7 @@ "filterLocatedLabel": "위치 있음", "@filterLocatedLabel": {}, "tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_nb.arb b/lib/l10n/app_nb.arb index aaedbab80..0c98e35ba 100644 --- a/lib/l10n/app_nb.arb +++ b/lib/l10n/app_nb.arb @@ -1265,7 +1265,7 @@ "count": {} } }, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett disse albumene og deres element?} other{Slett disse albumene og deres {count} elementer?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett disse albumene og elementet i demt?} other{Slett disse albumene og de {count} elementene i dem?}}", "@deleteMultiAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -1338,5 +1338,33 @@ "albumTierSpecial": "Ofte åpnet", "@albumTierSpecial": {}, "editEntryLocationDialogTitle": "Plassering", - "@editEntryLocationDialogTitle": {} + "@editEntryLocationDialogTitle": {}, + "filterLocatedLabel": "Posisjonert", + "@filterLocatedLabel": {}, + "filterTaggedLabel": "Etikettmerket", + "@filterTaggedLabel": {}, + "settingsDisplayUseTvInterface": "Android TV-grensesnitt", + "@settingsDisplayUseTvInterface": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Vis multi-trykkhåndvendingsalternativer", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "columnCount": "{count, plural, =1{1 kolonne} other{{count} kolonner}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "keepScreenOnVideoPlayback": "Under videoavspilling", + "@keepScreenOnVideoPlayback": {}, + "entryActionShareImageOnly": "Del kun bilde", + "@entryActionShareImageOnly": {}, + "entryActionShareVideoOnly": "Del kun video", + "@entryActionShareVideoOnly": {}, + "entryInfoActionRemoveLocation": "Fjern posisjon", + "@entryInfoActionRemoveLocation": {}, + "settingsViewerShowDescription": "Vis beskrivelse", + "@settingsViewerShowDescription": {}, + "settingsModificationWarningDialogMessage": "Andre innstillinger vil bli endret.", + "@settingsModificationWarningDialogMessage": {}, + "tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.", + "@tooManyItemsErrorDialogMessage": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d8bc6fbe4..2ef13f337 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -13,7 +13,7 @@ "@resetTooltip": {}, "pickTooltip": "Wybierz", "@pickTooltip": {}, - "doubleBackExitMessage": "Tapnij ponownie „wstecz” aby wyjść.", + "doubleBackExitMessage": "Dotknij ponownie „wstecz”, aby wyjść.", "@doubleBackExitMessage": {}, "saveTooltip": "Zapisz", "@saveTooltip": {}, @@ -29,7 +29,7 @@ "@appName": {}, "welcomeMessage": "Witaj w Aves", "@welcomeMessage": {}, - "welcomeOptional": "Opcjonalny", + "welcomeOptional": "Opcjonalnie", "@welcomeOptional": {}, "welcomeTermsToggle": "Akceptuję warunki i zasady", "@welcomeTermsToggle": {}, @@ -99,9 +99,9 @@ "@entryInfoActionEditTitleDescription": {}, "entryInfoActionEditRating": "Edytuj ocenę", "@entryInfoActionEditRating": {}, - "entryInfoActionEditTags": "Edytuj tagi", + "entryInfoActionEditTags": "Edytuj znaczniki", "@entryInfoActionEditTags": {}, - "entryInfoActionRemoveMetadata": "Usuń metadatę", + "entryInfoActionRemoveMetadata": "Usuń metadane", "@entryInfoActionRemoveMetadata": {}, "filterBinLabel": "Kosz", "@filterBinLabel": {}, @@ -111,17 +111,17 @@ "@filterNoDateLabel": {}, "filterNoRatingLabel": "Nieoceniony", "@filterNoRatingLabel": {}, - "filterNoTagLabel": "Nieoznakowany", + "filterNoTagLabel": "Nieoznaczone", "@filterNoTagLabel": {}, "filterNoTitleLabel": "Bez tytułu", "@filterNoTitleLabel": {}, "filterOnThisDayLabel": "Tego dnia", "@filterOnThisDayLabel": {}, - "filterRecentlyAddedLabel": "Ostatnio dodany", + "filterRecentlyAddedLabel": "Ostatnio dodane", "@filterRecentlyAddedLabel": {}, "filterTypeMotionPhotoLabel": "Ruchome Zdjęcie", "@filterTypeMotionPhotoLabel": {}, - "filterTypePanoramaLabel": "Zdjęcie sferyczne", + "filterTypePanoramaLabel": "Zdjęcie panoramiczne", "@filterTypePanoramaLabel": {}, "entryActionFlip": "Obróć w poziomie", "@entryActionFlip": {}, @@ -171,7 +171,7 @@ "@entryActionSetAs": {}, "entryActionAddFavourite": "Dodaj do ulubionych", "@entryActionAddFavourite": {}, - "filterNoLocationLabel": "Nieumiejscowiony", + "filterNoLocationLabel": "Nieumiejscowione", "@filterNoLocationLabel": {}, "filterRatingRejectedLabel": "Odrzucony", "@filterRatingRejectedLabel": {}, @@ -193,9 +193,9 @@ "@nameConflictStrategySkip": {}, "videoLoopModeAlways": "Zawsze", "@videoLoopModeAlways": {}, - "filterLocatedLabel": "Usytuowany", + "filterLocatedLabel": "Umiejscowione", "@filterLocatedLabel": {}, - "filterTaggedLabel": "Oznaczony", + "filterTaggedLabel": "Oznaczone", "@filterTaggedLabel": {}, "nameConflictStrategyReplace": "Zastąp", "@nameConflictStrategyReplace": {}, @@ -211,9 +211,9 @@ "@filterAspectRatioPortraitLabel": {}, "filterNoAddressLabel": "Brak adresu", "@filterNoAddressLabel": {}, - "videoControlsPlaySeek": "Odtwórz i szukaj do przodu/do tyłu", + "videoControlsPlaySeek": "Odtwórz oraz przeszukuj", "@videoControlsPlaySeek": {}, - "videoControlsPlayOutside": "Otwórz w innym odtwarzaczu", + "videoControlsPlayOutside": "Odtwórz innym odtwarzaczem", "@videoControlsPlayOutside": {}, "mapStyleGoogleNormal": "Mapy Google", "@mapStyleGoogleNormal": {}, @@ -229,7 +229,7 @@ "@nameConflictStrategyRename": {}, "mapStyleOsmHot": "Humanitarny OSM", "@mapStyleOsmHot": {}, - "keepScreenOnVideoPlayback": "Podczas odtwarzania wideo", + "keepScreenOnVideoPlayback": "Przy odtwarzaniu wideo", "@keepScreenOnVideoPlayback": {}, "displayRefreshRatePreferLowest": "Najniższa", "@displayRefreshRatePreferLowest": {}, @@ -327,11 +327,11 @@ "@filterTypeGeotiffLabel": {}, "filterMimeImageLabel": "Obraz", "@filterMimeImageLabel": {}, - "unitSystemImperial": "Imperialny", + "unitSystemImperial": "Imperialne", "@unitSystemImperial": {}, "videoLoopModeNever": "Nigdy", "@videoLoopModeNever": {}, - "videoControlsNone": "Nic", + "videoControlsNone": "Brak", "@videoControlsNone": {}, "accessibilityAnimationsRemove": "Zapobiegaj efektom ekranu", "@accessibilityAnimationsRemove": {}, @@ -341,7 +341,7 @@ "@displayRefreshRatePreferHighest": {}, "keepScreenOnNever": "Nigdy", "@keepScreenOnNever": {}, - "keepScreenOnViewerOnly": "Tylko na stronie przeglądarki", + "keepScreenOnViewerOnly": "Na stronie przeglądarki", "@keepScreenOnViewerOnly": {}, "videoPlaybackWithSound": "Odtwarzaj z dźwiękiem", "@videoPlaybackWithSound": {}, @@ -359,7 +359,7 @@ "@coordinateDmsEast": {}, "coordinateDmsWest": "Z", "@coordinateDmsWest": {}, - "unitSystemMetric": "Metryczny", + "unitSystemMetric": "Metryczne", "@unitSystemMetric": {}, "videoControlsPlay": "Odtwórz", "@videoControlsPlay": {}, @@ -375,7 +375,7 @@ "@widgetOpenPageViewer": {}, "albumTierNew": "Nowy", "@albumTierNew": {}, - "albumTierSpecial": "Wspólny", + "albumTierSpecial": "Wspólne", "@albumTierSpecial": {}, "albumTierApps": "Aplikacje", "@albumTierApps": {}, @@ -617,7 +617,7 @@ }, "collectionEmptyFavourites": "Brak ulubionych", "@collectionEmptyFavourites": {}, - "collectionEmptyVideos": "Brak filmów", + "collectionEmptyVideos": "Brak wideo", "@collectionEmptyVideos": {}, "sortByDate": "Według daty", "@sortByDate": {}, @@ -775,7 +775,7 @@ "@albumPickPageTitleCopy": {}, "albumPickPageTitleExport": "Wyeksportuj do albumu", "@albumPickPageTitleExport": {}, - "tagEmpty": "Bez znaczników", + "tagEmpty": "Brak znaczników", "@tagEmpty": {}, "searchCountriesSectionTitle": "Kraje", "@searchCountriesSectionTitle": {}, @@ -1003,7 +1003,7 @@ "@settingsPrivacySectionTitle": {}, "settingsAllowInstalledAppAccess": "Zezwól na dostęp do spisu aplikacji", "@settingsAllowInstalledAppAccess": {}, - "settingsAllowErrorReporting": "Pozwól na anonimowe zgłaszanie błędów", + "settingsAllowErrorReporting": "Zezwól na anonimowe zgłaszanie błędów", "@settingsAllowErrorReporting": {}, "settingsSaveSearchHistory": "Zapisz historię wyszukiwania", "@settingsSaveSearchHistory": {}, @@ -1045,7 +1045,7 @@ "@filePickerUseThisFolder": {}, "mapEmptyRegion": "Brak obrazów w tym regionie", "@mapEmptyRegion": {}, - "settingsKeepScreenOnTile": "Pozostaw ekran załączony", + "settingsKeepScreenOnTile": "Pozostaw ekran włączony", "@settingsKeepScreenOnTile": {}, "filePickerOpenFrom": "Otwórz z", "@filePickerOpenFrom": {}, @@ -1057,7 +1057,7 @@ "@filePickerDoNotShowHiddenFiles": {}, "settingsActionImportDialogTitle": "Zaimportuj", "@settingsActionImportDialogTitle": {}, - "settingsKeepScreenOnDialogTitle": "Pozostaw ekran załączony", + "settingsKeepScreenOnDialogTitle": "Pozostaw ekran włączony", "@settingsKeepScreenOnDialogTitle": {}, "settingsNavigationDrawerTile": "Menu nawigacyjne", "@settingsNavigationDrawerTile": {}, @@ -1083,7 +1083,7 @@ "@settingsVideoEnableHardwareAcceleration": {}, "settingsVideoAutoPlay": "Odtwarzaj automatycznie", "@settingsVideoAutoPlay": {}, - "settingsSubtitleThemeSample": "To jest próbka.", + "settingsSubtitleThemeSample": "Przykładowy napis.", "@settingsSubtitleThemeSample": {}, "settingsSubtitleThemeTextAlignmentDialogTitle": "Dopasowanie tekstu", "@settingsSubtitleThemeTextAlignmentDialogTitle": {}, @@ -1131,7 +1131,7 @@ "@mapStyleTooltip": {}, "mapStyleDialogTitle": "Styl mapy", "@mapStyleDialogTitle": {}, - "wallpaperUseScrollEffect": "Użyj efektu przewijania na ekranie głównym", + "wallpaperUseScrollEffect": "Używaj efektu przewijania na ekranie głównym", "@wallpaperUseScrollEffect": {}, "tagEditorSectionRecent": "Ostatnie", "@tagEditorSectionRecent": {}, @@ -1231,7 +1231,7 @@ "@settingsUnitSystemTile": {}, "addPathTooltip": "Dodaj ścieżkę", "@addPathTooltip": {}, - "settingsHiddenPathsBanner": "Zdjęcia i wideo w tych folderach ani w żadnym z ich podfolderów nie pojawią się w kolekcji.", + "settingsHiddenPathsBanner": "Zdjęcia i wideo w tych katalogach, ani w żadnym z ich podkatalogów nie pojawią się w kolekcji.", "@settingsHiddenPathsBanner": {}, "viewerInfoLabelOwner": "Właściciel", "@viewerInfoLabelOwner": {}, @@ -1307,7 +1307,7 @@ "@settingsVideoButtonsTile": {}, "settingsAllowInstalledAppAccessSubtitle": "Używane do poprawy wyświetlania albumu", "@settingsAllowInstalledAppAccessSubtitle": {}, - "settingsEnableBin": "Użyj kosza", + "settingsEnableBin": "Używaj kosza", "@settingsEnableBin": {}, "settingsWidgetShowOutline": "Zarys", "@settingsWidgetShowOutline": {}, @@ -1364,5 +1364,9 @@ "settingsSubtitleThemeTile": "Napisy", "@settingsSubtitleThemeTile": {}, "openMapPageTooltip": "Wyświetl na mapie", - "@openMapPageTooltip": {} + "@openMapPageTooltip": {}, + "tooManyItemsErrorDialogMessage": "Spróbuj ponownie z mniejszą ilością elementów.", + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Przesuń palcem w górę lub w dół, aby dostosować jasność/głośność", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index d0ffb46ed..4fe017638 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -432,13 +432,13 @@ "@renameProcessorCounter": {}, "renameProcessorName": "Nume", "@renameProcessorName": {}, - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Ștergeți acest album și articolul său?} other{Ștergeți acest album și {count} articole ale acestuia?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Ștergi acest album și articolul din el?} other{Ștergi acest album și {count} articolele din el?}}", "@deleteSingleAlbumConfirmationDialogMessage": { "placeholders": { "count": {} } }, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Ștergeți aceste albume și articolele lor?} other{Ștergeți aceste albume și {count} articole ale lor?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Ștergi aceste albume și articolul din ele?} other{Ștergi aceste albume și {count} articolele din ele?}}", "@deleteMultiAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -1364,5 +1364,7 @@ "settingsModificationWarningDialogMessage": "Alte setări vor fi modificate.", "@settingsModificationWarningDialogMessage": {}, "settingsDisplayUseTvInterface": "Interfață Android TV", - "@settingsDisplayUseTvInterface": {} + "@settingsDisplayUseTvInterface": {}, + "tooManyItemsErrorDialogMessage": "Încearcă din nou cu mai puține elemente.", + "@tooManyItemsErrorDialogMessage": {} } diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index e79a80551..25e93a4c7 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -569,5 +569,87 @@ "viewDialogLayoutSectionTitle": "เค้าโครง", "@viewDialogLayoutSectionTitle": {}, "aboutLinkPolicy": "นโยบายความเป็นส่วนตัว", - "@aboutLinkPolicy": {} + "@aboutLinkPolicy": {}, + "filterLocatedLabel": "ระบุสถานที่", + "@filterLocatedLabel": {}, + "filterTaggedLabel": "ระบุแท็ก", + "@filterTaggedLabel": {}, + "keepScreenOnVideoPlayback": "ระหว่างการเล่นวิดีโอ", + "@keepScreenOnVideoPlayback": {}, + "keepScreenOnViewerOnly": "หน้า Viewer เท่านั้น", + "@keepScreenOnViewerOnly": {}, + "accessibilityAnimationsRemove": "ปิดการเคลื่อนไหว", + "@accessibilityAnimationsRemove": {}, + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{ลบอัลบั้มเหล่านี้และรายการในอัลบั้มทั้งหมด?} other{ลบอัลบั้มเหล่านี้และ {count} รายการในอัลบั้ม?}}", + "@deleteMultiAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "entryActionShareVideoOnly": "แชร์วิดีโอเท่านั้น", + "@entryActionShareVideoOnly": {}, + "entryActionShowGeoTiffOnMap": "แสดงแผนที่ซ้อน", + "@entryActionShowGeoTiffOnMap": {}, + "videoActionCaptureFrame": "จับภาพเฟรม", + "@videoActionCaptureFrame": {}, + "entryInfoActionRemoveLocation": "ลบสถานที่", + "@entryInfoActionRemoveLocation": {}, + "filterAspectRatioLandscapeLabel": "ภาพแนวนอน", + "@filterAspectRatioLandscapeLabel": {}, + "filterAspectRatioPortraitLabel": "ภาพแนวตั้ง", + "@filterAspectRatioPortraitLabel": {}, + "filterNoAddressLabel": "ไม่มีที่อยู่", + "@filterNoAddressLabel": {}, + "widgetOpenPageViewer": "เปิดหน้า Viewer", + "@widgetOpenPageViewer": {}, + "coordinateDms": "{coordinate} {direction}", + "@coordinateDms": { + "placeholders": { + "coordinate": { + "type": "String", + "example": "38° 41′ 47.72″" + }, + "direction": { + "type": "String", + "example": "S" + } + } + }, + "missingSystemFilePickerDialogMessage": "ตัวเลือกไฟล์ระบบหายไป หรือปิดใช้งาน โปรดเปิดใช้งานและลองอีกครั้ง", + "@missingSystemFilePickerDialogMessage": {}, + "addShortcutDialogLabel": "ป้ายทางลัด", + "@addShortcutDialogLabel": {}, + "binEntriesConfirmationDialogMessage": "{count, plural, =1{ย้ายรายการนี้ไปยังถังขยะ?} other{ย้าย {count} รายการนี้ไปยังถังขยะ?}}", + "@binEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{ลบรายการนี้?} other{ลบ {count} รายการนี้?}}", + "@deleteEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{ลบอัลบั้มนี้และรายการในอัลบั้มทั้งหมด?} other{ลบอัลบั้มนี้และ {count} รายการในอัลบั้ม?}}", + "@deleteSingleAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "entryActionShareImageOnly": "แชร์รูปภาพเท่านั้น", + "@entryActionShareImageOnly": {}, + "accessibilityAnimationsKeep": "เปิดการเคลื่อนไหว", + "@accessibilityAnimationsKeep": {}, + "unsupportedTypeDialogMessage": "{count, plural, =1{การดำเนินการนี้ไม่รองรับรายการประเภทต่อไปนี้: {types}.} other{การดำเนินการนี้ไม่รองรับรายการประเภทต่อไปนี้: {types}.}}", + "@unsupportedTypeDialogMessage": { + "placeholders": { + "count": {}, + "types": { + "type": "String", + "example": "GIF, TIFF, MP4", + "description": "a list of unsupported types" + } + } + } } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 84018f06c..4f1945b97 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -1208,5 +1208,7 @@ "filterTaggedLabel": "Etiketli", "@filterTaggedLabel": {}, "tooManyItemsErrorDialogMessage": "Daha az ögeyle tekrar deneyin.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Parlaklığı/ses seviyesini ayarlamak için yukarı veya aşağı kaydırın", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 37a0c5579..3f0f8c3d4 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1364,5 +1364,9 @@ "filterLocatedLabel": "Розташований", "@filterLocatedLabel": {}, "filterTaggedLabel": "Позначений тегом", - "@filterTaggedLabel": {} + "@filterTaggedLabel": {}, + "tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.", + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index 7aa91fae6..cd592f02b 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -463,7 +463,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { modified |= XMP.removeElements( descriptions, XMP.containerDirectory, - Namespaces.container, + Namespaces.gContainer, ); modified |= [ diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 1dbcc1a18..fb2273e6d 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -97,6 +97,7 @@ class SettingsDefaults { static const videoControls = VideoControls.play; static const videoGestureDoubleTapTogglePlay = false; static const videoGestureSideDoubleTapSeek = true; + static const videoGestureVerticalDragBrightnessVolume = false; // subtitles static const subtitleFontSize = 20.0; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 27c944d82..f2595ffae 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -41,7 +41,6 @@ class Settings extends ChangeNotifier { static const Set _internalKeys = { hasAcceptedTermsKey, catalogTimeZoneKey, - videoShowRawTimedTextKey, searchHistoryKey, platformAccelerometerRotationKey, platformTransitionAnimationScaleKey, @@ -131,10 +130,10 @@ class Settings extends ChangeNotifier { static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec'; static const videoAutoPlayModeKey = 'video_auto_play_mode'; static const videoLoopModeKey = 'video_loop'; - static const videoShowRawTimedTextKey = 'video_show_raw_timed_text'; static const videoControlsKey = 'video_controls'; static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play'; static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip'; + static const videoGestureVerticalDragBrightnessVolumeKey = 'video_gesture_vertical_drag_brightness_volume'; // subtitles static const subtitleFontSizeKey = 'subtitle_font_size'; @@ -637,10 +636,6 @@ class Settings extends ChangeNotifier { set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString()); - bool get videoShowRawTimedText => getBool(videoShowRawTimedTextKey) ?? SettingsDefaults.videoShowRawTimedText; - - set videoShowRawTimedText(bool newValue) => _set(videoShowRawTimedTextKey, newValue); - VideoControls get videoControls => getEnumOrDefault(videoControlsKey, SettingsDefaults.videoControls, VideoControls.values); set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString()); @@ -653,6 +648,10 @@ class Settings extends ChangeNotifier { set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue); + bool get videoGestureVerticalDragBrightnessVolume => getBool(videoGestureVerticalDragBrightnessVolumeKey) ?? SettingsDefaults.videoGestureVerticalDragBrightnessVolume; + + set videoGestureVerticalDragBrightnessVolume(bool newValue) => _set(videoGestureVerticalDragBrightnessVolumeKey, newValue); + // subtitles double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize; @@ -1039,6 +1038,7 @@ class Settings extends ChangeNotifier { case enableVideoHardwareAccelerationKey: case videoGestureDoubleTapTogglePlayKey: case videoGestureSideDoubleTapSeekKey: + case videoGestureVerticalDragBrightnessVolumeKey: case subtitleShowOutlineKey: case tagEditorCurrentFilterSectionExpandedKey: case saveSearchHistoryKey: diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 50185d2b2..cd05e0c89 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -14,6 +14,8 @@ class AIcons { static const IconData aspectRatio = Icons.aspect_ratio_outlined; static const IconData bin = Icons.delete_outlined; static const IconData broken = Icons.broken_image_outlined; + static const IconData brightnessMin = Icons.brightness_low_outlined; + static const IconData brightnessMax = Icons.brightness_high_outlined; static const IconData checked = Icons.done_outlined; static const IconData count = MdiIcons.counter; static const IconData counter = Icons.plus_one_outlined; @@ -52,6 +54,8 @@ class AIcons { static const IconData text = Icons.format_quote_outlined; static const IconData tag = Icons.local_offer_outlined; static const IconData tagUntagged = MdiIcons.tagOffOutline; + static const IconData volumeMin = Icons.volume_mute_outlined; + static const IconData volumeMax = Icons.volume_up_outlined; // view static const IconData group = Icons.group_work_outlined; diff --git a/lib/utils/dependencies.dart b/lib/utils/dependencies.dart index 9bb4babd0..27a0c94c9 100644 --- a/lib/utils/dependencies.dart +++ b/lib/utils/dependencies.dart @@ -123,6 +123,11 @@ class Dependencies { licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher', ), + Dependency( + name: 'Volume Controller', + license: mit, + sourceUrl: 'https://github.com/kurenai7968/volume_controller', + ), ]; static const List _googleMobileServices = [ diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index 72f2b3515..f660e51d6 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -7,7 +7,6 @@ class Namespaces { static const avm = 'http://www.communicatingastronomy.org/avm/1.0/'; static const camera = 'http://pix4d.com/camera/1.0/'; static const cc = 'http://creativecommons.org/ns#'; - static const container = 'http://ns.google.com/photos/1.0/container/'; static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/'; static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/'; static const crlcp = 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/'; @@ -26,9 +25,13 @@ class Namespaces { static const exifEx = 'http://cipa.jp/exif/1.0/'; static const gAudio = 'http://ns.google.com/photos/1.0/audio/'; static const gCamera = 'http://ns.google.com/photos/1.0/camera/'; + static const gContainer = 'http://ns.google.com/photos/1.0/container/'; static const gCreations = 'http://ns.google.com/photos/1.0/creations/'; static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/'; static const gDevice = 'http://ns.google.com/photos/dd/1.0/device/'; + static const gDeviceCamera = 'http://ns.google.com/photos/dd/1.0/camera/'; + static const gDeviceContainer = 'http://ns.google.com/photos/dd/1.0/container/'; + static const gDeviceItem = 'http://ns.google.com/photos/dd/1.0/item/'; static const gFocus = 'http://ns.google.com/photos/1.0/focus/'; static const gImage = 'http://ns.google.com/photos/1.0/image/'; static const gPano = 'http://ns.google.com/photos/1.0/panorama/'; @@ -83,7 +86,6 @@ class Namespaces { avm: 'Astronomy Visualization', camera: 'Pix4D Camera', cc: 'Creative Commons', - container: 'Container', crd: 'Camera Raw Defaults', creatorAtom: 'After Effects', crs: 'Camera Raw Settings', @@ -97,6 +99,7 @@ class Namespaces { exifEx: 'Exif Ex', gAudio: 'Google Audio', gCamera: 'Google Camera', + gContainer: 'Google Container', gCreations: 'Google Creations', gDepth: 'Google Depth', gDevice: 'Google Device', @@ -138,7 +141,7 @@ class Namespaces { }; static final defaultPrefixes = { - container: 'Container', + gContainer: 'Container', dc: 'dc', gCamera: 'GCamera', microsoftPhoto: 'MicrosoftPhoto', diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index c5c1e654b..4888d01bc 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -5,6 +5,7 @@ import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/translators.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -28,7 +29,8 @@ class AboutPage extends StatelessWidget { sliver: SliverList( delegate: SliverChildListDelegate( [ - AppReference(showLogo: !useTvLayout), + const TvEdgeFocus(), + const AppReference(), if (!settings.useTvLayout) ...[ const Divider(), const BugReport(), diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index ce4a94ba4..fbab83d89 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -10,12 +10,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class AppReference extends StatefulWidget { - final bool showLogo; - - const AppReference({ - super.key, - required this.showLogo, - }); + const AppReference({super.key}); @override State createState() => _AppReferenceState(); @@ -24,6 +19,13 @@ class AppReference extends StatefulWidget { class _AppReferenceState extends State { late Future _packageInfoLoader; + static const _appTitleStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + letterSpacing: 1.0, + fontFeatures: [FontFeature.enable('smcp')], + ); + @override void initState() { super.initState(); @@ -44,28 +46,19 @@ class _AppReferenceState extends State { } Widget _buildAvesLine() { - const style = TextStyle( - fontSize: 20, - fontWeight: FontWeight.normal, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], - ); - return FutureBuilder( future: _packageInfoLoader, builder: (context, snapshot) { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (widget.showLogo) ...[ - AvesLogo( - size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, - ), - const SizedBox(width: 8), - ], + AvesLogo( + size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, + ), + const SizedBox(width: 8), Text( '${context.l10n.appName} ${snapshot.data?.version}', - style: style, + style: _appTitleStyle, ), ], ); diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index e4422cce3..2fd508712 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -1,4 +1,4 @@ -import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -14,13 +14,7 @@ class AboutCredits extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(l10n.aboutCreditsSectionTitle, style: Constants.knownTitleTextStyle), - ), - ), + AboutSectionTitle(text: l10n.aboutCreditsSectionTitle), const SizedBox(height: 8), Text.rich( TextSpan( diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 813776474..50b376ce3 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,8 +1,9 @@ import 'package:aves/app_flavor.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/dependencies.dart'; +import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -50,30 +51,32 @@ class _LicensesState extends State { [ _buildHeader(), const SizedBox(height: 16), - AvesExpansionTile( - title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.android), - expandedNotifier: _expandedNotifier, - children: _platform.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesDartPackagesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), - ), + if (!settings.useTvLayout) ...[ + AvesExpansionTile( + title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.android), + expandedNotifier: _expandedNotifier, + children: _platform.map((package) => LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesDartPackagesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), + ), + ], Center( child: AvesOutlinedButton( label: context.l10n.aboutLicensesShowAllButtonLabel, @@ -104,13 +107,7 @@ class _LicensesState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(context.l10n.aboutLicensesSectionTitle, style: Constants.knownTitleTextStyle), - ), - ), + AboutSectionTitle(text: context.l10n.aboutLicensesSectionTitle), const SizedBox(height: 8), Text(context.l10n.aboutLicensesBanner), ], diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart index ea4a1d113..3803bae2f 100644 --- a/lib/widgets/about/policy_page.dart +++ b/lib/widgets/about/policy_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -14,6 +15,7 @@ class PolicyPage extends StatefulWidget { class _PolicyPageState extends State { late Future _termsLoader; + final ScrollController _scrollController = ScrollController(); static const termsPath = 'assets/terms.md'; static const termsDirection = TextDirection.ltr; @@ -28,26 +30,72 @@ class _PolicyPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.policyPageTitle), ), body: SafeArea( - child: Center( - child: FutureBuilder( - future: _termsLoader, - builder: (context, snapshot) { - if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); - final terms = snapshot.data!; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: MarkdownContainer( - data: terms, - textDirection: termsDirection, - ), - ); - }, + child: FocusableActionDetector( + autofocus: true, + shortcuts: const { + SingleActivator(LogicalKeyboardKey.arrowUp): _ScrollIntent.up(), + SingleActivator(LogicalKeyboardKey.arrowDown): _ScrollIntent.down(), + }, + actions: { + _ScrollIntent: CallbackAction<_ScrollIntent>(onInvoke: _onScrollIntent), + }, + child: Center( + child: FutureBuilder( + future: _termsLoader, + builder: (context, snapshot) { + if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); + final terms = snapshot.data!; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: MarkdownContainer( + scrollController: _scrollController, + data: terms, + textDirection: termsDirection, + ), + ); + }, + ), ), ), ), ); } + + void _onScrollIntent(_ScrollIntent intent) { + late int factor; + switch (intent.type) { + case _ScrollDirection.up: + factor = -1; + break; + case _ScrollDirection.down: + factor = 1; + break; + } + _scrollController.animateTo( + _scrollController.offset + factor * 150, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + ); + } +} + +class _ScrollIntent extends Intent { + const _ScrollIntent({ + required this.type, + }); + + const _ScrollIntent.up() : type = _ScrollDirection.up; + + const _ScrollIntent.down() : type = _ScrollDirection.down; + + final _ScrollDirection type; +} + +enum _ScrollDirection { + up, + down, } diff --git a/lib/widgets/about/title.dart b/lib/widgets/about/title.dart new file mode 100644 index 000000000..bfb63b27d --- /dev/null +++ b/lib/widgets/about/title.dart @@ -0,0 +1,37 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:flutter/material.dart'; + +class AboutSectionTitle extends StatelessWidget { + final String text; + + const AboutSectionTitle({ + super.key, + required this.text, + }); + + @override + Widget build(BuildContext context) { + Widget child = Container( + alignment: AlignmentDirectional.centerStart, + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), + child: Text(text, style: Constants.knownTitleTextStyle), + ); + + if (settings.useTvLayout) { + child = InkWell( + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ], + ), + ), + ); + } + return child; + } +} diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index d83094991..7d172bbaa 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/text/change_highlight.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:collection/collection.dart'; @@ -50,27 +51,21 @@ class AboutTranslators extends StatelessWidget { // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk // Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai + // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew }; @override Widget build(BuildContext context) { - final l10n = context.l10n; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(l10n.aboutTranslatorsSectionTitle, style: Constants.knownTitleTextStyle), - ), - ), + AboutSectionTitle(text: context.l10n.aboutTranslatorsSectionTitle), const SizedBox(height: 8), _RandomTextSpanHighlighter( spans: translators.map((v) => v.name).toList(), - highlightColor: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimary, ), const SizedBox(height: 16), ], @@ -81,11 +76,11 @@ class AboutTranslators extends StatelessWidget { class _RandomTextSpanHighlighter extends StatefulWidget { final List spans; - final Color highlightColor; + final Color color; const _RandomTextSpanHighlighter({ required this.spans, - required this.highlightColor, + required this.color, }); @override @@ -102,18 +97,21 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> void initState() { super.initState(); + final color = widget.color; _baseStyle = TextStyle( + color: color.withOpacity(.7), shadows: [ Shadow( - color: widget.highlightColor.withOpacity(0), + color: color.withOpacity(0), blurRadius: 0, ) ], ); final highlightStyle = TextStyle( + color: color.withOpacity(1), shadows: [ Shadow( - color: widget.highlightColor, + color: color.withOpacity(1), blurRadius: 3, ) ], @@ -132,7 +130,7 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> ..repeat(reverse: true); _animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(CurvedAnimation( parent: _controller, - curve: Curves.linear, + curve: Curves.easeInOutCubic, )); } diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 445fae01c..8bcdf131b 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -54,7 +54,7 @@ class AvesApp extends StatefulWidget { final AppFlavor flavor; // temporary exclude locales not ready yet for prime time - static final _unsupportedLocales = {'ar', 'fa', 'gl', 'nn', 'th'}.map(Locale.new).toSet(); + static final _unsupportedLocales = {'ar', 'fa', 'gl', 'he', 'nn', 'th'}.map(Locale.new).toSet(); static final List supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); static final ValueNotifier cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); static final GlobalKey navigatorKey = GlobalKey(debugLabel: 'app-navigator'); @@ -540,6 +540,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { ? 'profile' : 'debug', 'has_mobile_services': mobileServices.isServiceAvailable, + 'is_television': device.isTelevision, 'locales': WidgetsBinding.instance.window.locales.join(', '), 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', }); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 3198fd9f1..e23286a9b 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -58,6 +58,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware }) { final canWrite = !settings.isReadOnly; final isMain = appMode == AppMode.main; + final useTvLayout = settings.useTvLayout; switch (action) { // general case EntrySetAction.configureView: @@ -70,9 +71,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware return isSelecting && selectedItemCount == itemCount; // browsing case EntrySetAction.searchCollection: - return !settings.useTvLayout && appMode.canNavigate && !isSelecting; + return !useTvLayout && appMode.canNavigate && !isSelecting; case EntrySetAction.toggleTitleSearch: - return !isSelecting; + return !useTvLayout && !isSelecting; case EntrySetAction.addShortcut: return isMain && !isSelecting && device.canPinShortcut && !isTrash; case EntrySetAction.emptyBin: @@ -83,7 +84,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.stats: return isMain; case EntrySetAction.rescan: - return !settings.useTvLayout && isMain && !isTrash; + return !useTvLayout && isMain && !isTrash; // selecting case EntrySetAction.share: case EntrySetAction.toggleFavourite: diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index e99c52296..defc70ecf 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -27,7 +27,7 @@ import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index a01fd3cb6..a8174795b 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -6,11 +6,13 @@ import 'package:flutter_markdown/flutter_markdown.dart'; class MarkdownContainer extends StatelessWidget { final String data; final TextDirection? textDirection; + final ScrollController? scrollController; const MarkdownContainer({ super.key, required this.data, this.textDirection, + this.scrollController, }); static const double maxWidth = 460; @@ -44,6 +46,7 @@ class MarkdownContainer extends StatelessWidget { data: data, selectable: true, onTapLink: (text, href, title) => AvesApp.launchUrl(href), + controller: scrollController, shrinkWrap: true, ), ), diff --git a/lib/widgets/common/basic/tv_edge_focus.dart b/lib/widgets/common/basic/tv_edge_focus.dart new file mode 100644 index 000000000..9e624a030 --- /dev/null +++ b/lib/widgets/common/basic/tv_edge_focus.dart @@ -0,0 +1,25 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +// to be placed at the edges of lists and grids, +// so that TV can reach them with D-pad +class TvEdgeFocus extends StatelessWidget { + final FocusNode? focusNode; + + const TvEdgeFocus({ + super.key, + this.focusNode, + }); + + @override + Widget build(BuildContext context) { + final useTvLayout = context.select((s) => s.useTvLayout); + return useTvLayout + ? Focus( + focusNode: focusNode, + child: const SizedBox(), + ) + : const SizedBox(); + } +} diff --git a/lib/widgets/common/expandable_filter_row.dart b/lib/widgets/common/expandable_filter_row.dart index 54d0ebf63..3557e651c 100644 --- a/lib/widgets/common/expandable_filter_row.dart +++ b/lib/widgets/common/expandable_filter_row.dart @@ -1,4 +1,5 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; @@ -31,26 +32,49 @@ class TitledExpandableFilterRow extends StatelessWidget { final isExpanded = expandedNotifier.value == title; + Widget header = Text( + title, + style: Constants.knownTitleTextStyle, + ); + void toggle() => expandedNotifier.value = isExpanded ? null : title; + if (settings.useTvLayout) { + header = Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: InkWell( + onTap: toggle, + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + header, + ], + ), + ), + ), + ); + } else { + header = Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + header, + const Spacer(), + IconButton( + icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand), + onPressed: toggle, + tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint, + ), + ], + ), + ); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Text( - title, - style: Constants.knownTitleTextStyle, - ), - const Spacer(), - IconButton( - icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand), - onPressed: () => expandedNotifier.value = isExpanded ? null : title, - tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint, - ), - ], - ), - ), + header, ExpandableFilterRow( filters: filters, isExpanded: isExpanded, diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index bc941d63b..d6da38773 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -34,18 +34,10 @@ class SectionHeader extends StatelessWidget { Widget build(BuildContext context) { Widget child = _buildContent(context); if (settings.useTvLayout) { - final primaryColor = Theme.of(context).colorScheme.primary; - child = Material( - type: MaterialType.transparency, - child: InkResponse( - onTap: _onTap(context), - containedInkWell: true, - highlightShape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(123)), - hoverColor: primaryColor.withOpacity(0.04), - splashColor: primaryColor.withOpacity(0.12), - child: child, - ), + child = InkWell( + onTap: _onTap(context), + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: child, ); } return Container( diff --git a/lib/widgets/common/identity/buttons/captioned_button.dart b/lib/widgets/common/identity/buttons/captioned_button.dart index dda3e7787..10cc09602 100644 --- a/lib/widgets/common/identity/buttons/captioned_button.dart +++ b/lib/widgets/common/identity/buttons/captioned_button.dart @@ -8,7 +8,7 @@ class CaptionedButton extends StatefulWidget { final Animation scale; final Widget captionText; final CaptionedIconButtonBuilder iconButtonBuilder; - final bool showCaption; + final bool autofocus, showCaption; final VoidCallback? onPressed; static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); @@ -21,6 +21,7 @@ class CaptionedButton extends StatefulWidget { CaptionedIconButtonBuilder? iconButtonBuilder, String? caption, Widget? captionText, + this.autofocus = false, this.showCaption = true, required this.onPressed, }) : assert(icon != null || iconButtonBuilder != null), @@ -57,6 +58,7 @@ class CaptionedButton extends StatefulWidget { class _CaptionedButtonState extends State { final FocusNode _focusNode = FocusNode(); final ValueNotifier _focusedNotifier = ValueNotifier(false); + bool _didAutofocus = false; @override void initState() { @@ -65,12 +67,21 @@ class _CaptionedButtonState extends State { _focusNode.addListener(_onFocusChanged); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _handleAutofocus(); + } + @override void didUpdateWidget(covariant CaptionedButton oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.onPressed != widget.onPressed) { _updateTraversal(); } + if (oldWidget.autofocus != widget.autofocus) { + _handleAutofocus(); + } } @override @@ -120,6 +131,13 @@ class _CaptionedButtonState extends State { ); } + void _handleAutofocus() { + if (!_didAutofocus && widget.autofocus) { + FocusScope.of(context).autofocus(_focusNode); + _didAutofocus = true; + } + } + void _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus; void _updateTraversal() { diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart index 282c38aa1..26dd06c57 100644 --- a/lib/widgets/common/search/delegate.dart +++ b/lib/widgets/common/search/delegate.dart @@ -18,6 +18,9 @@ abstract class AvesSearchDelegate extends SearchDelegate { query = initialQuery ?? ''; } + @mustCallSuper + void dispose() {} + @override Widget? buildLeading(BuildContext context) { if (settings.useTvLayout) { @@ -44,7 +47,7 @@ abstract class AvesSearchDelegate extends SearchDelegate { @override List? buildActions(BuildContext context) { return [ - if (query.isNotEmpty) + if (!settings.useTvLayout && query.isNotEmpty) IconButton( icon: const Icon(AIcons.clear), onPressed: () { @@ -63,28 +66,40 @@ abstract class AvesSearchDelegate extends SearchDelegate { void clean() { currentBody = null; - focusNode?.unfocus(); + searchFieldFocusNode?.unfocus(); } // adapted from Flutter `SearchDelegate` in `/material/search.dart` @override void showResults(BuildContext context) { - focusNode?.unfocus(); - currentBody = SearchBody.results; + if (settings.useTvLayout) { + suggestionsScrollController?.jumpTo(0); + WidgetsBinding.instance.addPostFrameCallback((_) { + suggestionsFocusNode?.requestFocus(); + FocusScope.of(context).nextFocus(); + }); + } else { + searchFieldFocusNode?.unfocus(); + currentBody = SearchBody.results; + } } @override void showSuggestions(BuildContext context) { - assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); - focusNode!.requestFocus(); + assert(searchFieldFocusNode != null, '_focusNode must be set by route before showSuggestions is called.'); + searchFieldFocusNode!.requestFocus(); currentBody = SearchBody.suggestions; } @override Animation get transitionAnimation => proxyAnimation; - FocusNode? focusNode; + FocusNode? searchFieldFocusNode; + + FocusNode? get suggestionsFocusNode => null; + + ScrollController? get suggestionsScrollController => null; final TextEditingController queryTextController = TextEditingController(); diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 4e31065fb..58ebf7e9d 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -29,7 +29,7 @@ class SearchPage extends StatefulWidget { class _SearchPageState extends State { final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); - final FocusNode _focusNode = FocusNode(); + final FocusNode _searchFieldFocusNode = FocusNode(); final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); @override @@ -37,7 +37,7 @@ class _SearchPageState extends State { super.initState(); _registerWidget(widget); widget.animation.addStatusListener(_onAnimationStatusChanged); - _focusNode.addListener(_onFocusChanged); + _searchFieldFocusNode.addListener(_onFocusChanged); } @override @@ -53,21 +53,22 @@ class _SearchPageState extends State { void dispose() { _unregisterWidget(widget); widget.animation.removeStatusListener(_onAnimationStatusChanged); - _focusNode.dispose(); + _searchFieldFocusNode.dispose(); _doubleBackPopHandler.dispose(); + widget.delegate.dispose(); super.dispose(); } void _registerWidget(SearchPage widget) { widget.delegate.queryTextController.addListener(_onQueryChanged); widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); - widget.delegate.focusNode = _focusNode; + widget.delegate.searchFieldFocusNode = _searchFieldFocusNode; } void _unregisterWidget(SearchPage widget) { widget.delegate.queryTextController.removeListener(_onQueryChanged); widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); - widget.delegate.focusNode = null; + widget.delegate.searchFieldFocusNode = null; } void _onAnimationStatusChanged(AnimationStatus status) { @@ -77,12 +78,12 @@ class _SearchPageState extends State { widget.animation.removeStatusListener(_onAnimationStatusChanged); Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { if (!mounted) return; - _focusNode.requestFocus(); + _searchFieldFocusNode.requestFocus(); }); } void _onFocusChanged() { - if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) { + if (_searchFieldFocusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) { widget.delegate.showSuggestions(context); } } @@ -136,7 +137,7 @@ class _SearchPageState extends State { style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), child: TextField( controller: widget.delegate.queryTextController, - focusNode: _focusNode, + focusNode: _searchFieldFocusNode, decoration: InputDecoration( border: InputBorder.none, hintText: widget.delegate.searchFieldLabel, diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index a4bbe7c98..d4fc6dc75 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -43,11 +43,6 @@ class DebugSettingsSection extends StatelessWidget { onChanged: (v) => settings.canUseAnalysisService = v, title: const Text('canUseAnalysisService'), ), - SwitchListTile( - value: settings.videoShowRawTimedText, - onChanged: (v) => settings.videoShowRawTimedText = v, - title: const Text('videoShowRawTimedText'), - ), Padding( padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index 95352649d..836d8f48a 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -135,16 +135,14 @@ class SelectionRadioListTile extends StatelessWidget { reselectable: true, title: Text( title, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, + overflow: TextOverflow.ellipsis, + maxLines: 2, ), subtitle: subtitle != null ? Text( subtitle, softWrap: false, overflow: TextOverflow.fade, - maxLines: 1, ) : null, dense: dense, diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart similarity index 79% rename from lib/widgets/filter_grids/album_pick.dart rename to lib/widgets/dialogs/pick_dialogs/album_pick_page.dart index 3a19327cd..7039ccfe4 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart @@ -13,6 +13,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu.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/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; @@ -128,6 +129,53 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { Selection> selection, AlbumChipSetActionDelegate actionDelegate, ) { + final itemCount = actionDelegate.allItems.length; + final isSelecting = selection.isSelecting; + final selectedItems = selection.selectedItems; + final selectedFilters = selectedItems.map((v) => v.filter).toSet(); + + bool isVisible(ChipSetAction action) => actionDelegate.isVisible( + action, + appMode: appMode, + isSelecting: isSelecting, + itemCount: itemCount, + selectedFilters: selectedFilters, + ); + + return settings.useTvLayout + ? _buildTelevisionActions( + context: context, + isVisible: isVisible, + actionDelegate: actionDelegate, + ) + : _buildMobileActions( + context: context, + isVisible: isVisible, + actionDelegate: actionDelegate, + ); + } + + List _buildTelevisionActions({ + required BuildContext context, + required bool Function(ChipSetAction action) isVisible, + required AlbumChipSetActionDelegate actionDelegate, + }) { + return [ + ...ChipSetActions.general, + ].where(isVisible).map((action) { + return CaptionedButton( + icon: action.getIcon(), + caption: action.getText(context), + onPressed: () => actionDelegate.onActionSelected(context, {}, action), + ); + }).toList(); + } + + List _buildMobileActions({ + required BuildContext context, + required bool Function(ChipSetAction action) isVisible, + required AlbumChipSetActionDelegate actionDelegate, + }) { return [ if (widget.moveType != null) IconButton( @@ -149,7 +197,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { child: PopupMenuButton( itemBuilder: (context) { return [ - FilterGridAppBar.toMenuItem(context, ChipSetAction.configureView, enabled: true), + ...ChipSetActions.general.where(isVisible).map((action) => FilterGridAppBar.toMenuItem(context, action, enabled: true)), const PopupMenuDivider(), FilterGridAppBar.toMenuItem(context, ChipSetAction.toggleTitleSearch, enabled: true), ]; diff --git a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart index 9cc912c76..418edc63f 100644 --- a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart @@ -1,4 +1,5 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; @@ -37,8 +38,10 @@ class _AppPickPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.appPickDialogTitle), ), body: SafeArea( @@ -57,7 +60,7 @@ class _AppPickPageState extends State { final packages = allPackages.where((package) => package.categoryLauncher).toList()..sort((a, b) => compareAsciiUpperCase(_displayName(a), _displayName(b))); return Column( children: [ - QueryBar(queryNotifier: _queryNotifier), + if (!useTvLayout) QueryBar(queryNotifier: _queryNotifier), ValueListenableBuilder( valueListenable: _queryNotifier, builder: (context, query, child) { diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 310d2b382..0a01f5ff1 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -70,6 +70,7 @@ abstract class ChipSetActionDelegate with FeedbackMi final selectedItemCount = selectedFilters.length; final hasSelection = selectedFilters.isNotEmpty; final isMain = appMode == AppMode.main; + final useTvLayout = settings.useTvLayout; switch (action) { // general case ChipSetAction.configureView: @@ -82,9 +83,9 @@ abstract class ChipSetActionDelegate with FeedbackMi return isSelecting && selectedItemCount == itemCount; // browsing case ChipSetAction.search: - return !settings.useTvLayout && appMode.canNavigate && !isSelecting; + return !useTvLayout && appMode.canNavigate && !isSelecting; case ChipSetAction.toggleTitleSearch: - return !isSelecting; + return !useTvLayout && !isSelecting; case ChipSetAction.createAlbum: return false; // browsing or selecting diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index beb3114f3..7f0e61c5b 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -115,15 +115,23 @@ class FilterGridPage extends StatelessWidget { ); if (useTvLayout) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); return Scaffold( - body: Row( - children: [ - TvRail( - controller: context.read(), - ), - Expanded(child: body), - ], - ), + body: canNavigate + ? Row( + children: [ + TvRail( + controller: context.read(), + ), + Expanded(child: body), + ], + ) + : DirectionalSafeArea( + top: false, + end: false, + bottom: false, + child: body, + ), resizeToAvoidBottomInset: false, extendBody: true, ); diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 25a94f89e..ed5108c40 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -277,11 +277,12 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin MapAction.zoomIn, MapAction.zoomOut, ] - .map((action) => Padding( + .mapIndexed((i, action) => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: CaptionedButton( icon: action.getIcon(), caption: action.getText(context), + autofocus: i == 0, onPressed: () => MapActionDelegate(_mapController).onActionSelected(context, action), ), )) diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 9ce560a16..ab6f21f13 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -19,6 +19,7 @@ import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/expandable_filter_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -33,6 +34,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate { final CollectionSource source; final CollectionLens? parentCollection; final ValueNotifier _expandedSectionNotifier = ValueNotifier(null); + final FocusNode _suggestionsTopFocusNode = FocusNode(); + final ScrollController _suggestionsScrollController = ScrollController(); + + @override + FocusNode? get suggestionsFocusNode => _suggestionsTopFocusNode; + + @override + ScrollController get suggestionsScrollController => _suggestionsScrollController; static const int searchHistoryCount = 10; static final typeFilters = [ @@ -64,6 +73,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate { query = initialQuery ?? ''; } + @override + void dispose() { + _expandedSectionNotifier.dispose(); + _suggestionsTopFocusNode.dispose(); + _suggestionsScrollController.dispose(); + super.dispose(); + } + @override Widget buildSuggestions(BuildContext context) { final upQuery = query.trim().toUpperCase(); @@ -91,8 +108,12 @@ class CollectionSearchDelegate extends AvesSearchDelegate { final history = settings.searchHistory.where(notHidden).toList(); return ListView( + controller: _suggestionsScrollController, padding: const EdgeInsets.only(top: 8), children: [ + TvEdgeFocus( + focusNode: _suggestionsTopFocusNode, + ), _buildFilterRow( context: context, filters: [ diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart index faf8916d8..1b62dcd2a 100644 --- a/lib/widgets/settings/navigation/drawer_tab_albums.dart +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -1,9 +1,10 @@ import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; import 'package:flutter/material.dart'; @@ -29,8 +30,10 @@ class _DrawerAlbumTabState extends State { final source = context.read(); return Column( children: [ - const DrawerEditorBanner(), - const Divider(height: 0), + if (!settings.useTvLayout) ...[ + const DrawerEditorBanner(), + const Divider(height: 0), + ], Flexible( child: ReorderableListView.builder( itemBuilder: (context, index) { diff --git a/lib/widgets/settings/navigation/drawer_tab_fixed.dart b/lib/widgets/settings/navigation/drawer_tab_fixed.dart index 0ee54de7b..1069693a3 100644 --- a/lib/widgets/settings/navigation/drawer_tab_fixed.dart +++ b/lib/widgets/settings/navigation/drawer_tab_fixed.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; @@ -30,8 +31,10 @@ class _DrawerFixedListTabState extends State> { Widget build(BuildContext context) { return Column( children: [ - const DrawerEditorBanner(), - const Divider(height: 0), + if (!settings.useTvLayout) ...[ + const DrawerEditorBanner(), + const Divider(height: 0), + ], Flexible( child: ReorderableListView.builder( itemBuilder: (context, index) { diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index e57b8ef18..d7743500c 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -36,6 +36,11 @@ class VideoControlsPage extends StatelessWidget { onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v, title: context.l10n.settingsVideoGestureSideDoubleTapSeek, ), + SettingsSwitchListTile( + selector: (context, s) => s.videoGestureVerticalDragBrightnessVolume, + onChanged: (v) => settings.videoGestureVerticalDragBrightnessVolume = v, + title: context.l10n.settingsVideoGestureVerticalDragBrightnessVolume, + ), ], ), ), diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart index e41ab0848..c8ae248e4 100644 --- a/lib/widgets/settings/video/subtitle_sample.dart +++ b/lib/widgets/settings/video/subtitle_sample.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/common/basic/text/background_painter.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/stats/mime_donut.dart b/lib/widgets/stats/mime_donut.dart index f7739612e..4aee3ade5 100644 --- a/lib/widgets/stats/mime_donut.dart +++ b/lib/widgets/stats/mime_donut.dart @@ -111,45 +111,37 @@ class _MimeDonutState extends State with AutomaticKeepAliveClientMixi ], ), ); - final primaryColor = Theme.of(context).colorScheme.primary; final legend = SizedBox( width: dim, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: seriesData - .map((d) => Material( - type: MaterialType.transparency, - child: InkResponse( - onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), - containedInkWell: true, - highlightShape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(123)), - hoverColor: primaryColor.withOpacity(0.04), - splashColor: primaryColor.withOpacity(0.12), - 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, - ), + .map((d) => InkWell( + onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), + borderRadius: const BorderRadius.all(Radius.circular(123)), + 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, ), - const SizedBox(width: 8), - Text( - numberFormat.format(d.entryCount), - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall!.color, - ), + ), + const SizedBox(width: 8), + Text( + numberFormat.format(d.entryCount), + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall!.color, ), - const SizedBox(width: 4), - ], - ), + ), + const SizedBox(width: 4), + ], ), )) .toList(), diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 1edaad08a..85af4999f 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -15,6 +15,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -96,6 +97,7 @@ class _StatsPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return ValueListenableBuilder( valueListenable: _isPageAnimatingNotifier, builder: (context, animating, child) { @@ -196,6 +198,7 @@ class _StatsPageState extends State { ), ), children: [ + const TvEdgeFocus(), mimeDonuts, Histogram( entries: entries, @@ -218,7 +221,7 @@ class _StatsPageState extends State { return Scaffold( appBar: AppBar( - automaticallyImplyLeading: !settings.useTvLayout, + automaticallyImplyLeading: !useTvLayout, title: Text(l10n.statsPageTitle), ), body: GestureAreaProtectorStack( @@ -274,23 +277,15 @@ class _StatsPageState extends State { style: Constants.knownTitleTextStyle, ); if (settings.useTvLayout) { - final primaryColor = Theme.of(context).colorScheme.primary; header = Container( padding: const EdgeInsets.symmetric(vertical: 12), alignment: AlignmentDirectional.centerStart, - child: Material( - type: MaterialType.transparency, - child: InkResponse( - onTap: onHeaderPressed, - containedInkWell: true, - highlightShape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(123)), - hoverColor: primaryColor.withOpacity(0.04), - splashColor: primaryColor.withOpacity(0.12), - child: Padding( - padding: const EdgeInsets.all(16), - child: header, - ), + child: InkWell( + onTap: onHeaderPressed, + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Padding( + padding: const EdgeInsets.all(16), + child: header, ), ), ); diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 61577aa3b..1cd1d0a79 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -31,7 +31,7 @@ import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/rename_entry_dialog.dart'; import 'package:aves/widgets/dialogs/export_entry_dialog.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/action/printer.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart'; diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 76c11f35a..e19a65eaf 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -138,10 +138,12 @@ class _ViewerVerticalPageViewState extends State { child: child!, ); }, - child: InfoPage( - collection: collection, - entryNotifier: widget.entryNotifier, - isScrollingNotifier: _isVerticallyScrollingNotifier, + child: FocusScope( + child: InfoPage( + collection: collection, + entryNotifier: widget.entryNotifier, + isScrollingNotifier: _isVerticallyScrollingNotifier, + ), ), ), ); @@ -286,10 +288,10 @@ class _ViewerVerticalPageViewState extends State { final opacity = min(1.0, page); _backgroundOpacityNotifier.value = opacity * opacity; - if (page <= 1 && settings.viewerMaxBrightness) { + if (settings.viewerMaxBrightness) { _systemBrightness?.then((system) { - final transition = max(system, lerpDouble(system, maximumBrightness, page / 2)!); - ScreenBrightness().setScreenBrightness(transition); + final value = lerpDouble(maximumBrightness, system, ((1 - page).abs() * 2).clamp(0, 1))!; + ScreenBrightness().setScreenBrightness(value); }); } diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 5dc2c3d74..871f1175f 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -680,9 +680,7 @@ class _EntryViewerStackState extends State with EntryViewContr } Future _onLeave() async { - if (settings.viewerMaxBrightness) { - await ScreenBrightness().resetScreenBrightness(); - } + await ScreenBrightness().resetScreenBrightness(); if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { await windowService.keepScreenOn(false); } diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index e5b2b5c43..58e56006f 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -7,6 +7,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart'; @@ -281,6 +282,9 @@ class _InfoPageContentState extends State<_InfoPageContent> { child: CustomScrollView( controller: widget.scrollController, slivers: [ + const SliverToBoxAdapter( + child: TvEdgeFocus(), + ), InfoAppBar( entry: entry, collection: collection, diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 1a92214f0..7264a8c40 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -22,56 +22,64 @@ import 'package:tuple/tuple.dart'; @immutable class XmpNamespace extends Equatable { + final Map schemaRegistryPrefixes; final String nsUri, nsPrefix; final Map rawProps; @override List get props => [nsUri, nsPrefix]; - const XmpNamespace(this.nsUri, this.nsPrefix, this.rawProps); + XmpNamespace({ + required this.nsUri, + required this.schemaRegistryPrefixes, + required this.rawProps, + }) : nsPrefix = prefixForUri(schemaRegistryPrefixes, nsUri); - factory XmpNamespace.create(String nsUri, String nsPrefix, Map rawProps) { + factory XmpNamespace.create(Map schemaRegistryPrefixes, String nsPrefix, Map rawProps) { + final nsUri = schemaRegistryPrefixes[nsPrefix] ?? ''; switch (nsUri) { - case Namespaces.container: - return XmpContainer(nsPrefix, rawProps); case Namespaces.creatorAtom: - return XmpCreatorAtom(nsPrefix, rawProps); + return XmpCreatorAtom(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.crs: - return XmpCrsNamespace(nsPrefix, rawProps); + return XmpCrsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.darktable: - return XmpDarktableNamespace(nsPrefix, rawProps); + return XmpDarktableNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.dwc: - return XmpDwcNamespace(nsPrefix, rawProps); + return XmpDwcNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.exif: - return XmpExifNamespace(nsPrefix, rawProps); + return XmpExifNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.gAudio: - return XmpGAudioNamespace(nsPrefix, rawProps); + return XmpGAudioNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); + case Namespaces.gCamera: + return XmpGCameraNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); + case Namespaces.gContainer: + return XmpGContainer(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.gDepth: - return XmpGDepthNamespace(nsPrefix, rawProps); + return XmpGDepthNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.gDevice: - return XmpGDeviceNamespace(nsPrefix, rawProps); + return XmpGDeviceNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.gImage: - return XmpGImageNamespace(nsPrefix, rawProps); + return XmpGImageNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.iptc4xmpCore: - return XmpIptcCoreNamespace(nsPrefix, rawProps); + return XmpIptcCoreNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.iptc4xmpExt: - return XmpIptc4xmpExtNamespace(nsPrefix, rawProps); + return XmpIptc4xmpExtNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.mwgrs: - return XmpMgwRegionsNamespace(nsPrefix, rawProps); + return XmpMgwRegionsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.mp: - return XmpMPNamespace(nsPrefix, rawProps); + return XmpMPNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.photoshop: - return XmpPhotoshopNamespace(nsPrefix, rawProps); + return XmpPhotoshopNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.plus: - return XmpPlusNamespace(nsPrefix, rawProps); + return XmpPlusNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.tiff: - return XmpTiffNamespace(nsPrefix, rawProps); + return XmpTiffNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.xmp: - return XmpBasicNamespace(nsPrefix, rawProps); + return XmpBasicNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); case Namespaces.xmpMM: - return XmpMMNamespace(nsPrefix, rawProps); + return XmpMMNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); default: - return XmpNamespace(nsUri, nsPrefix, rawProps); + return XmpNamespace(nsUri: nsUri, schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); } } @@ -130,6 +138,8 @@ class XmpNamespace extends Equatable { String formatValue(XmpProp prop) => prop.value; Map linkifyValues(List props) => {}; + + static String prefixForUri(Map schemaRegistryPrefixes, String nsUri) => schemaRegistryPrefixes.entries.firstWhereOrNull((kv) => kv.value == nsUri)?.key ?? ''; } class XmpProp implements Comparable { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart b/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart index adc24490e..cae67a6d6 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart @@ -2,7 +2,7 @@ import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpCrsNamespace extends XmpNamespace { - XmpCrsNamespace(String nsPrefix, Map rawProps) : super(Namespaces.crs, nsPrefix, rawProps); + XmpCrsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.crs); @override late final List cards = [ diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart b/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart index 991908f0b..d0778f570 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart @@ -4,68 +4,69 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md class XmpExifNamespace extends XmpNamespace { - const XmpExifNamespace(String nsPrefix, Map rawProps) : super(Namespaces.exif, nsPrefix, rawProps); + XmpExifNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.exif); @override String formatValue(XmpProp prop) { final v = prop.value; - switch (prop.path) { - case 'exif:ColorSpace': + final field = prop.path.replaceAll(nsPrefix, ''); + switch (field) { + case 'ColorSpace': return Exif.getColorSpaceDescription(v); - case 'exif:Contrast': + case 'Contrast': return Exif.getContrastDescription(v); - case 'exif:CustomRendered': + case 'CustomRendered': return Exif.getCustomRenderedDescription(v); - case 'exif:ExifVersion': - case 'exif:FlashpixVersion': + case 'ExifVersion': + case 'FlashpixVersion': return Exif.getExifVersionDescription(v); - case 'exif:ExposureMode': + case 'ExposureMode': return Exif.getExposureModeDescription(v); - case 'exif:ExposureProgram': + case 'ExposureProgram': return Exif.getExposureProgramDescription(v); - case 'exif:FileSource': + case 'FileSource': return Exif.getFileSourceDescription(v); - case 'exif:Flash/exif:Mode': + case 'Flash/Mode': return Exif.getFlashModeDescription(v); - case 'exif:Flash/exif:Return': + case 'Flash/Return': return Exif.getFlashReturnDescription(v); - case 'exif:FocalPlaneResolutionUnit': + case 'FocalPlaneResolutionUnit': return Exif.getResolutionUnitDescription(v); - case 'exif:GainControl': + case 'GainControl': return Exif.getGainControlDescription(v); - case 'exif:LightSource': + case 'LightSource': return Exif.getLightSourceDescription(v); - case 'exif:MeteringMode': + case 'MeteringMode': return Exif.getMeteringModeDescription(v); - case 'exif:Saturation': + case 'Saturation': return Exif.getSaturationDescription(v); - case 'exif:SceneCaptureType': + case 'SceneCaptureType': return Exif.getSceneCaptureTypeDescription(v); - case 'exif:SceneType': + case 'SceneType': return Exif.getSceneTypeDescription(v); - case 'exif:SensingMethod': + case 'SensingMethod': return Exif.getSensingMethodDescription(v); - case 'exif:Sharpness': + case 'Sharpness': return Exif.getSharpnessDescription(v); - case 'exif:SubjectDistanceRange': + case 'SubjectDistanceRange': return Exif.getSubjectDistanceRangeDescription(v); - case 'exif:WhiteBalance': + case 'WhiteBalance': return Exif.getWhiteBalanceDescription(v); - case 'exif:GPSAltitudeRef': + case 'GPSAltitudeRef': return Exif.getGPSAltitudeRefDescription(v); - case 'exif:GPSDestBearingRef': - case 'exif:GPSImgDirectionRef': - case 'exif:GPSTrackRef': + case 'GPSDestBearingRef': + case 'GPSImgDirectionRef': + case 'GPSTrackRef': return Exif.getGPSDirectionRefDescription(v); - case 'exif:GPSDestDistanceRef': + case 'GPSDestDistanceRef': return Exif.getGPSDestDistanceRefDescription(v); - case 'exif:GPSDifferential': + case 'GPSDifferential': return Exif.getGPSDifferentialDescription(v); - case 'exif:GPSMeasureMode': + case 'GPSMeasureMode': return Exif.getGPSMeasureModeDescription(v); - case 'exif:GPSSpeedRef': + case 'GPSSpeedRef': return Exif.getGPSSpeedRefDescription(v); - case 'exif:GPSStatus': + case 'GPSStatus': return Exif.getGPSStatusDescription(v); default: return v; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 53833c81b..e33140105 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -7,7 +7,11 @@ import 'package:collection/collection.dart'; import 'package:tuple/tuple.dart'; abstract class XmpGoogleNamespace extends XmpNamespace { - const XmpGoogleNamespace(String nsUri, String nsPrefix, Map rawProps) : super(nsUri, nsPrefix, rawProps); + XmpGoogleNamespace({ + required super.nsUri, + required super.schemaRegistryPrefixes, + required super.rawProps, + }); List> get dataProps; @@ -53,14 +57,34 @@ abstract class XmpGoogleNamespace extends XmpNamespace { } class XmpGAudioNamespace extends XmpGoogleNamespace { - const XmpGAudioNamespace(String nsPrefix, Map rawProps) : super(Namespaces.gAudio, nsPrefix, rawProps); + XmpGAudioNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gAudio); @override - List> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')]; + List> get dataProps => [ + Tuple2('${nsPrefix}Data', '${nsPrefix}Mime'), + ]; +} + +class XmpGCameraNamespace extends XmpGoogleNamespace { + XmpGCameraNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gCamera); + + @override + List> get dataProps => [ + Tuple2('${nsPrefix}RelitInputImageData', '${nsPrefix}RelitInputImageMime'), + ]; +} + +class XmpGContainer extends XmpNamespace { + XmpGContainer({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gContainer); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'Directory\[(\d+)\]/' + nsPrefix + r'Item/(.*)'), title: 'Directory Item'), + ]; } class XmpGDepthNamespace extends XmpGoogleNamespace { - const XmpGDepthNamespace(String nsPrefix, Map rawProps) : super(Namespaces.gDepth, nsPrefix, rawProps); + XmpGDepthNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gDepth); @override List> get dataProps => [ @@ -70,8 +94,16 @@ class XmpGDepthNamespace extends XmpGoogleNamespace { } class XmpGDeviceNamespace extends XmpNamespace { - XmpGDeviceNamespace(String nsPrefix, Map rawProps) : super(Namespaces.gDevice, nsPrefix, rawProps) { - final mimePattern = RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/Item:Mime'); + late final String _cameraNsPrefix; + late final String _containerNsPrefix; + late final String _itemNsPrefix; + + XmpGDeviceNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gDevice) { + _cameraNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, Namespaces.gDeviceCamera); + _containerNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, Namespaces.gDeviceContainer); + _itemNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, Namespaces.gDeviceItem); + + final mimePattern = RegExp(nsPrefix + r'Container/' + _containerNsPrefix + r'Directory\[(\d+)\]/' + _itemNsPrefix + r'Mime'); final originalProps = rawProps.entries.toList(); originalProps.forEach((kv) { final path = kv.key; @@ -81,7 +113,7 @@ class XmpGDeviceNamespace extends XmpNamespace { if (indexString != null) { final index = int.tryParse(indexString); if (index != null) { - final dataPath = '${nsPrefix}Container/Container:Directory[$index]/Item:Data'; + final dataPath = '${nsPrefix}Container/${_containerNsPrefix}Directory[$index]/${_itemNsPrefix}Data'; rawProps[dataPath] = '[skipped]'; } } @@ -94,16 +126,16 @@ class XmpGDeviceNamespace extends XmpNamespace { XmpCardData( RegExp(nsPrefix + r'Cameras\[(\d+)\]/(.*)'), cards: [ - XmpCardData(RegExp(r'Camera:DepthMap/(.*)')), - XmpCardData(RegExp(r'Camera:Image/(.*)')), - XmpCardData(RegExp(r'Camera:ImagingModel/(.*)')), + XmpCardData(RegExp(_cameraNsPrefix + r'DepthMap/(.*)')), + XmpCardData(RegExp(_cameraNsPrefix + r'Image/(.*)')), + XmpCardData(RegExp(_cameraNsPrefix + r'ImagingModel/(.*)')), ], ), XmpCardData( - RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/(.*)'), + RegExp(nsPrefix + r'Container/' + _containerNsPrefix + r'Directory\[(\d+)\]/(.*)'), spanBuilders: (index, struct) { - if (struct.containsKey('Item:Data') && struct.containsKey('Item:DataURI')) { - final dataUriProp = struct['Item:DataURI']; + if (struct.containsKey('${_itemNsPrefix}Data') && struct.containsKey('${_itemNsPrefix}DataURI')) { + final dataUriProp = struct['${_itemNsPrefix}DataURI']; if (dataUriProp != null) { return { 'Data': InfoRowGroup.linkSpanBuilder( @@ -121,17 +153,10 @@ class XmpGDeviceNamespace extends XmpNamespace { } class XmpGImageNamespace extends XmpGoogleNamespace { - const XmpGImageNamespace(String nsPrefix, Map rawProps) : super(Namespaces.gImage, nsPrefix, rawProps); + XmpGImageNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gImage); @override - List> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')]; -} - -class XmpContainer extends XmpNamespace { - XmpContainer(String nsPrefix, Map rawProps) : super(Namespaces.container, nsPrefix, rawProps); - - @override - late final List cards = [ - XmpCardData(RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)'), title: 'Directory Item'), - ]; + List> get dataProps => [ + Tuple2('${nsPrefix}Data', '${nsPrefix}Mime'), + ]; } diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart b/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart index 17c31f8a2..a47ffccd3 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart @@ -2,7 +2,7 @@ import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpCreatorAtom extends XmpNamespace { - XmpCreatorAtom(String nsPrefix, Map rawProps) : super(Namespaces.creatorAtom, nsPrefix, rawProps); + XmpCreatorAtom({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.creatorAtom); @override late final List cards = [ @@ -11,7 +11,7 @@ class XmpCreatorAtom extends XmpNamespace { } class XmpDarktableNamespace extends XmpNamespace { - XmpDarktableNamespace(String nsPrefix, Map rawProps) : super(Namespaces.darktable, nsPrefix, rawProps); + XmpDarktableNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.darktable); @override late final List cards = [ @@ -20,7 +20,7 @@ class XmpDarktableNamespace extends XmpNamespace { } class XmpDwcNamespace extends XmpNamespace { - XmpDwcNamespace(String nsPrefix, Map rawProps) : super(Namespaces.dwc, nsPrefix, rawProps); + XmpDwcNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.dwc); @override late final List cards = [ @@ -37,7 +37,7 @@ class XmpDwcNamespace extends XmpNamespace { } class XmpIptcCoreNamespace extends XmpNamespace { - XmpIptcCoreNamespace(String nsPrefix, Map rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps); + XmpIptcCoreNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.iptc4xmpCore); @override late final List cards = [ @@ -46,7 +46,7 @@ class XmpIptcCoreNamespace extends XmpNamespace { } class XmpIptc4xmpExtNamespace extends XmpNamespace { - XmpIptc4xmpExtNamespace(String nsPrefix, Map rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps); + XmpIptc4xmpExtNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.iptc4xmpExt); @override late final List cards = [ @@ -55,7 +55,7 @@ class XmpIptc4xmpExtNamespace extends XmpNamespace { } class XmpMPNamespace extends XmpNamespace { - XmpMPNamespace(String nsPrefix, Map rawProps) : super(Namespaces.mp, nsPrefix, rawProps); + XmpMPNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.mp); @override late final List cards = [ @@ -65,7 +65,7 @@ class XmpMPNamespace extends XmpNamespace { // cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15) class XmpMgwRegionsNamespace extends XmpNamespace { - XmpMgwRegionsNamespace(String nsPrefix, Map rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps); + XmpMgwRegionsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.mwgrs); @override late final List cards = [ @@ -75,7 +75,7 @@ class XmpMgwRegionsNamespace extends XmpNamespace { } class XmpPlusNamespace extends XmpNamespace { - XmpPlusNamespace(String nsPrefix, Map rawProps) : super(Namespaces.plus, nsPrefix, rawProps); + XmpPlusNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.plus); @override late final List cards = [ @@ -86,7 +86,7 @@ class XmpPlusNamespace extends XmpNamespace { } class XmpMMNamespace extends XmpNamespace { - XmpMMNamespace(String nsPrefix, Map rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps); + XmpMMNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.xmpMM); @override late final List cards = [ diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart index e6a749ec7..da01fd4f3 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart @@ -3,7 +3,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md class XmpPhotoshopNamespace extends XmpNamespace { - XmpPhotoshopNamespace(String nsPrefix, Map rawProps) : super(Namespaces.photoshop, nsPrefix, rawProps); + XmpPhotoshopNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.photoshop); @override late final List cards = [ diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart b/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart index bf027ccf4..5ff5ed4eb 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart @@ -4,7 +4,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md class XmpTiffNamespace extends XmpNamespace { - const XmpTiffNamespace(String nsPrefix, Map rawProps) : super(Namespaces.tiff, nsPrefix, rawProps); + XmpTiffNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.tiff); @override String formatValue(XmpProp prop) { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index fceb37783..7d497e1ac 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpBasicNamespace extends XmpNamespace { - XmpBasicNamespace(String nsPrefix, Map rawProps) : super(Namespaces.xmp, nsPrefix, rawProps); + XmpBasicNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.xmp); @override late final List cards = [ diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 3d0ac7a23..ff71933e6 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -56,9 +56,8 @@ class _XmpDirTileState extends State { return nsPrefix; }).entries.map((kv) { final nsPrefix = kv.key; - final nsUri = _schemaRegistryPrefixes[nsPrefix] ?? ''; final rawProps = Map.fromEntries(kv.value); - return XmpNamespace.create(nsUri, nsPrefix, rawProps); + return XmpNamespace.create(_schemaRegistryPrefixes, nsPrefix, rawProps); }).toList() ..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle)); return AvesExpansionTile( diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 287a8caea..2257879b9 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -3,30 +3,27 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/media_session_service.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/basic/insets.dart'; -import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/error.dart'; import 'package:aves/widgets/viewer/visual/raster.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart'; import 'package:aves/widgets/viewer/visual/vector.dart'; -import 'package:aves/widgets/viewer/visual/video.dart'; +import 'package:aves/widgets/viewer/visual/video/cover.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart'; +import 'package:aves/widgets/viewer/visual/video/swipe_action.dart'; +import 'package:aves/widgets/viewer/visual/video/video_view.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; -import 'package:collection/collection.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -55,17 +52,8 @@ class _EntryPageViewState extends State with SingleTickerProvider late ValueNotifier _viewStateNotifier; late AvesMagnifierController _magnifierController; final List _subscriptions = []; - ImageStream? _videoCoverStream; - late ImageStreamListener _videoCoverStreamListener; - final ValueNotifier _videoCoverInfoNotifier = ValueNotifier(null); final ValueNotifier _actionFeedbackChildNotifier = ValueNotifier(null); - - AvesMagnifierController? _dismissedCoverMagnifierController; - - AvesMagnifierController get dismissedCoverMagnifierController { - _dismissedCoverMagnifierController ??= AvesMagnifierController(); - return _dismissedCoverMagnifierController!; - } + OverlayEntry? _actionFeedbackOverlayEntry; AvesEntry get mainEntry => widget.mainEntry; @@ -73,9 +61,6 @@ class _EntryPageViewState extends State with SingleTickerProvider ViewerController get viewerController => widget.viewerController; - // use the high res photo as cover for the video part of a motion photo - ImageProvider get videoCoverUriImage => mainEntry.isMotionPhoto ? mainEntry.uriImage : entry.uriImage; - static const rasterMaxScale = ScaleLevel(factor: 5); static const vectorMaxScale = ScaleLevel(factor: 25); @@ -110,9 +95,6 @@ class _EntryPageViewState extends State with SingleTickerProvider _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); if (entry.isVideo) { _subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand)); - _videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image); - _videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty); - _videoCoverStream!.addListener(_videoCoverStreamListener); } viewerController.startAutopilotAnimation( vsync: this, @@ -127,9 +109,6 @@ class _EntryPageViewState extends State with SingleTickerProvider void _unregisterWidget(EntryPageView oldWidget) { viewerController.stopAutopilotAnimation(vsync: this); - _videoCoverStream?.removeListener(_videoCoverStreamListener); - _videoCoverStream = null; - _videoCoverInfoNotifier.value = null; _magnifierController.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) @@ -222,169 +201,169 @@ class _EntryPageViewState extends State with SingleTickerProvider builder: (context, sar, child) { final videoDisplaySize = entry.videoDisplaySize(sar); - return Selector>( - selector: (context, s) => Tuple2(s.videoGestureDoubleTapTogglePlay, s.videoGestureSideDoubleTapSeek), + return Selector>( + selector: (context, s) => Tuple3( + s.videoGestureDoubleTapTogglePlay, + s.videoGestureSideDoubleTapSeek, + s.videoGestureVerticalDragBrightnessVolume, + ), builder: (context, s, child) { final playGesture = s.item1; final seekGesture = s.item2; - final useActionGesture = playGesture || seekGesture; + final useVerticalDragGesture = s.item3; + final useTapGesture = playGesture || seekGesture; - void _applyAction(EntryAction action, {IconData? Function()? icon}) { - _actionFeedbackChildNotifier.value = DecoratedIcon( - icon?.call() ?? action.getIconData(), - size: 48, - color: Colors.white, - shadows: const [ - Shadow( - color: Colors.black, - blurRadius: 4, - ) - ], - ); - VideoActionNotification( - controller: videoController, - action: action, - ).dispatch(context); + MagnifierDoubleTapCallback? onDoubleTap; + MagnifierGestureScaleStartCallback? onScaleStart; + MagnifierGestureScaleUpdateCallback? onScaleUpdate; + MagnifierGestureScaleEndCallback? onScaleEnd; + + if (useTapGesture) { + void _applyAction(EntryAction action, {IconData? Function()? icon}) { + _actionFeedbackChildNotifier.value = DecoratedIcon( + icon?.call() ?? action.getIconData(), + size: 48, + color: Colors.white, + shadows: const [ + Shadow( + color: Colors.black, + blurRadius: 4, + ) + ], + ); + VideoActionNotification( + controller: videoController, + action: action, + ).dispatch(context); + } + + onDoubleTap = (alignment) { + final x = alignment.x; + if (seekGesture) { + if (x < sideRatio) { + _applyAction(EntryAction.videoReplay10); + return true; + } else if (x > 1 - sideRatio) { + _applyAction(EntryAction.videoSkip10); + return true; + } + } + if (playGesture) { + _applyAction( + EntryAction.videoTogglePlay, + icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play, + ); + return true; + } + return false; + }; } - MagnifierDoubleTapCallback? _onDoubleTap = useActionGesture - ? (alignment) { - final x = alignment.x; - if (seekGesture) { - if (x < sideRatio) { - _applyAction(EntryAction.videoReplay10); - return true; - } else if (x > 1 - sideRatio) { - _applyAction(EntryAction.videoSkip10); - return true; - } - } - if (playGesture) { - _applyAction( - EntryAction.videoTogglePlay, - icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play, - ); - return true; - } - return false; - } - : null; + if (useVerticalDragGesture) { + SwipeAction? swipeAction; + var move = Offset.zero; + var dropped = false; + double? startValue; + final valueNotifier = ValueNotifier(null); + onScaleStart = (details, doubleTap, boundaries) { + dropped = details.pointerCount > 1 || doubleTap; + if (dropped) return; + + startValue = null; + valueNotifier.value = null; + final alignmentX = details.focalPoint.dx / boundaries.viewportSize.width; + final action = alignmentX > .5 ? SwipeAction.volume : SwipeAction.brightness; + action.get().then((v) => startValue = v); + swipeAction = action; + move = Offset.zero; + _actionFeedbackOverlayEntry = OverlayEntry( + builder: (context) => SwipeActionFeedback( + action: action, + valueNotifier: valueNotifier, + ), + ); + Overlay.of(context)!.insert(_actionFeedbackOverlayEntry!); + }; + onScaleUpdate = (details) { + move += details.focalPointDelta; + dropped |= details.pointerCount > 1; + if (valueNotifier.value == null) { + dropped |= MagnifierGestureRecognizer.isXPan(move); + } + if (dropped) return false; + + final _startValue = startValue; + if (_startValue != null) { + final double value = (_startValue - move.dy / SwipeActionFeedback.height).clamp(0, 1); + valueNotifier.value = value; + swipeAction?.set(value); + } + return true; + }; + onScaleEnd = (details) { + if (_actionFeedbackOverlayEntry != null) { + _actionFeedbackOverlayEntry!.remove(); + _actionFeedbackOverlayEntry = null; + } + }; + } + + Widget videoChild = Stack( + children: [ + _buildMagnifier( + displaySize: videoDisplaySize, + onScaleStart: onScaleStart, + onScaleUpdate: onScaleUpdate, + onScaleEnd: onScaleEnd, + onDoubleTap: onDoubleTap, + child: VideoView( + entry: entry, + controller: videoController, + ), + ), + VideoSubtitles( + controller: videoController, + viewStateNotifier: _viewStateNotifier, + ), + if (useTapGesture) + ValueListenableBuilder( + valueListenable: _actionFeedbackChildNotifier, + builder: (context, feedbackChild, child) => ActionFeedback( + child: feedbackChild, + ), + ), + ], + ); + if (useVerticalDragGesture) { + videoChild = MagnifierGestureDetectorScope.of(context)!.copyWith( + acceptPointerEvent: MagnifierGestureRecognizer.isYPan, + child: videoChild, + ); + } return Stack( fit: StackFit.expand, children: [ - Stack( - children: [ - _buildMagnifier( - displaySize: videoDisplaySize, - onDoubleTap: _onDoubleTap, - child: VideoView( - entry: entry, - controller: videoController, - ), - ), - VideoSubtitles( - controller: videoController, - viewStateNotifier: _viewStateNotifier, - ), - if (settings.videoShowRawTimedText) - VideoSubtitles( - controller: videoController, - viewStateNotifier: _viewStateNotifier, - debugMode: true, - ), - if (useActionGesture) - ValueListenableBuilder( - valueListenable: _actionFeedbackChildNotifier, - builder: (context, feedbackChild, child) => ActionFeedback( - child: feedbackChild, - ), - ), - ], - ), - _buildVideoCover( + videoChild, + VideoCover( + mainEntry: mainEntry, + pageEntry: entry, + magnifierController: _magnifierController, videoController: videoController, videoDisplaySize: videoDisplaySize, - onDoubleTap: _onDoubleTap, - ), - ], - ); - }, - ); - }, - ); - } - - StreamBuilder _buildVideoCover({ - required AvesVideoController videoController, - required Size videoDisplaySize, - required MagnifierDoubleTapCallback? onDoubleTap, - }) { - // fade out image to ease transition with the player - return StreamBuilder( - stream: videoController.statusStream, - builder: (context, snapshot) { - final showCover = !videoController.isReady; - return IgnorePointer( - ignoring: !showCover, - child: AnimatedOpacity( - opacity: showCover ? 1 : 0, - curve: Curves.easeInCirc, - duration: Durations.viewerVideoPlayerTransition, - onEnd: () { - // while cover is fading out, the same controller is used for both the cover and the video, - // and both fire scale boundaries events, so we make sure that in the end - // the scale boundaries from the video are used after the cover is gone - final boundaries = _magnifierController.scaleBoundaries; - if (boundaries != null) { - _magnifierController.setScaleBoundaries( - boundaries.copyWith( - childSize: videoDisplaySize, - ), - ); - } - }, - child: ValueListenableBuilder( - valueListenable: _videoCoverInfoNotifier, - builder: (context, videoCoverInfo, child) { - if (videoCoverInfo != null) { - // full cover image may have a different size and different aspect ratio - final coverSize = Size( - videoCoverInfo.image.width.toDouble(), - videoCoverInfo.image.height.toDouble(), - ); - // when the cover is the same size as the video itself - // (which is often the case when the cover is not embedded but just a frame), - // we can reuse the same magnifier and preserve its state when switching from cover to video - final coverController = showCover || coverSize == videoDisplaySize ? _magnifierController : dismissedCoverMagnifierController; - return _buildMagnifier( + onTap: _onTap, + magnifierBuilder: (coverController, coverSize, videoCoverUriImage) => _buildMagnifier( controller: coverController, displaySize: coverSize, onDoubleTap: onDoubleTap, child: Image( image: videoCoverUriImage, ), - ); - } - - // default to cached thumbnail, if any - final extent = entry.cachedThumbnails.firstOrNull?.key.extent; - if (extent != null && extent > 0) { - return GestureDetector( - onTap: _onTap, - child: ThumbnailImage( - entry: entry, - extent: extent, - fit: BoxFit.contain, - showLoadingBackground: false, - ), - ); - } - - return const SizedBox(); - }, - ), - ), + ), + ), + ], + ); + }, ); }, ); @@ -396,6 +375,9 @@ class _EntryPageViewState extends State with SingleTickerProvider ScaleLevel maxScale = rasterMaxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, + MagnifierGestureScaleStartCallback? onScaleStart, + MagnifierGestureScaleUpdateCallback? onScaleUpdate, + MagnifierGestureScaleEndCallback? onScaleEnd, MagnifierDoubleTapCallback? onDoubleTap, required Widget child, }) { @@ -413,6 +395,9 @@ class _EntryPageViewState extends State with SingleTickerProvider initialScale: viewerController.initialScale, scaleStateCycle: scaleStateCycle, applyScale: applyScale, + onScaleStart: onScaleStart, + onScaleUpdate: onScaleUpdate, + onScaleEnd: onScaleEnd, onTap: (c, s, a, p) => _onTap(alignment: a), onDoubleTap: onDoubleTap, child: child, @@ -487,5 +472,3 @@ class _EntryPageViewState extends State with SingleTickerProvider } } } - -typedef MagnifierTapCallback = void Function(Offset childPosition); diff --git a/lib/widgets/viewer/visual/video/cover.dart b/lib/widgets/viewer/visual/video/cover.dart new file mode 100644 index 000000000..e9a3a81e1 --- /dev/null +++ b/lib/widgets/viewer/visual/video/cover.dart @@ -0,0 +1,160 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class VideoCover extends StatefulWidget { + final AvesEntry mainEntry, pageEntry; + final AvesMagnifierController magnifierController; + final AvesVideoController videoController; + final Size videoDisplaySize; + final void Function({Alignment? alignment}) onTap; + final Widget Function( + AvesMagnifierController coverController, + Size coverSize, + ImageProvider videoCoverUriImage, + ) magnifierBuilder; + + const VideoCover({ + super.key, + required this.mainEntry, + required this.pageEntry, + required this.magnifierController, + required this.videoController, + required this.videoDisplaySize, + required this.onTap, + required this.magnifierBuilder, + }); + + @override + State createState() => _VideoCoverState(); +} + +class _VideoCoverState extends State { + ImageStream? _videoCoverStream; + late ImageStreamListener _videoCoverStreamListener; + final ValueNotifier _videoCoverInfoNotifier = ValueNotifier(null); + + AvesMagnifierController? _dismissedCoverMagnifierController; + + AvesMagnifierController get dismissedCoverMagnifierController { + _dismissedCoverMagnifierController ??= AvesMagnifierController(); + return _dismissedCoverMagnifierController!; + } + + AvesEntry get mainEntry => widget.mainEntry; + + AvesEntry get entry => widget.pageEntry; + + AvesMagnifierController get magnifierController => widget.magnifierController; + + AvesVideoController get videoController => widget.videoController; + + Size get videoDisplaySize => widget.videoDisplaySize; + + // use the high res photo as cover for the video part of a motion photo + ImageProvider get videoCoverUriImage => mainEntry.isMotionPhoto ? mainEntry.uriImage : entry.uriImage; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant VideoCover oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.pageEntry != widget.pageEntry) { + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(VideoCover widget) { + _videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image); + _videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty); + _videoCoverStream!.addListener(_videoCoverStreamListener); + } + + void _unregisterWidget(VideoCover oldWidget) { + _videoCoverStream?.removeListener(_videoCoverStreamListener); + _videoCoverStream = null; + _videoCoverInfoNotifier.value = null; + } + + @override + Widget build(BuildContext context) { + // fade out image to ease transition with the player + return StreamBuilder( + stream: videoController.statusStream, + builder: (context, snapshot) { + final showCover = !videoController.isReady; + return IgnorePointer( + ignoring: !showCover, + child: AnimatedOpacity( + opacity: showCover ? 1 : 0, + curve: Curves.easeInCirc, + duration: Durations.viewerVideoPlayerTransition, + onEnd: () { + // while cover is fading out, the same controller is used for both the cover and the video, + // and both fire scale boundaries events, so we make sure that in the end + // the scale boundaries from the video are used after the cover is gone + final boundaries = magnifierController.scaleBoundaries; + if (boundaries != null) { + magnifierController.setScaleBoundaries( + boundaries.copyWith( + childSize: videoDisplaySize, + ), + ); + } + }, + child: ValueListenableBuilder( + valueListenable: _videoCoverInfoNotifier, + builder: (context, videoCoverInfo, child) { + if (videoCoverInfo != null) { + // full cover image may have a different size and different aspect ratio + final coverSize = Size( + videoCoverInfo.image.width.toDouble(), + videoCoverInfo.image.height.toDouble(), + ); + // when the cover is the same size as the video itself + // (which is often the case when the cover is not embedded but just a frame), + // we can reuse the same magnifier and preserve its state when switching from cover to video + final coverController = showCover || coverSize == videoDisplaySize ? magnifierController : dismissedCoverMagnifierController; + return widget.magnifierBuilder(coverController, coverSize, videoCoverUriImage); + } + + // default to cached thumbnail, if any + final extent = entry.cachedThumbnails.firstOrNull?.key.extent; + if (extent != null && extent > 0) { + return GestureDetector( + onTap: widget.onTap, + child: ThumbnailImage( + entry: entry, + extent: extent, + fit: BoxFit.contain, + showLoadingBackground: false, + ), + ); + } + + return const SizedBox(); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/viewer/visual/subtitle/ass_parser.dart b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart similarity index 98% rename from lib/widgets/viewer/visual/subtitle/ass_parser.dart rename to lib/widgets/viewer/visual/video/subtitle/ass_parser.dart index b15414474..3e9f24cba 100644 --- a/lib/widgets/viewer/visual/subtitle/ass_parser.dart +++ b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart @@ -1,6 +1,6 @@ -import 'package:aves/widgets/viewer/visual/subtitle/line.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/span.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/style.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/line.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/visual/subtitle/line.dart b/lib/widgets/viewer/visual/video/subtitle/line.dart similarity index 93% rename from lib/widgets/viewer/visual/subtitle/line.dart rename to lib/widgets/viewer/visual/video/subtitle/line.dart index dd44cb49c..f7a213b80 100644 --- a/lib/widgets/viewer/visual/subtitle/line.dart +++ b/lib/widgets/viewer/visual/video/subtitle/line.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/viewer/visual/subtitle/span.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/widgets/viewer/visual/subtitle/span.dart b/lib/widgets/viewer/visual/video/subtitle/span.dart similarity index 92% rename from lib/widgets/viewer/visual/subtitle/span.dart rename to lib/widgets/viewer/visual/video/subtitle/span.dart index 8cee296a0..0f5870582 100644 --- a/lib/widgets/viewer/visual/subtitle/span.dart +++ b/lib/widgets/viewer/visual/video/subtitle/span.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/viewer/visual/subtitle/style.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/widgets/viewer/visual/subtitle/style.dart b/lib/widgets/viewer/visual/video/subtitle/style.dart similarity index 100% rename from lib/widgets/viewer/visual/subtitle/style.dart rename to lib/widgets/viewer/visual/video/subtitle/style.dart diff --git a/lib/widgets/viewer/visual/subtitle/subtitle.dart b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart similarity index 98% rename from lib/widgets/viewer/visual/subtitle/subtitle.dart rename to lib/widgets/viewer/visual/video/subtitle/subtitle.dart index dbb32113a..c83f9db6f 100644 --- a/lib/widgets/viewer/visual/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart @@ -4,9 +4,9 @@ import 'package:aves/widgets/common/basic/text/background_painter.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/ass_parser.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/span.dart'; -import 'package:aves/widgets/viewer/visual/subtitle/style.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/ass_parser.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart'; +import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; diff --git a/lib/widgets/viewer/visual/video/swipe_action.dart b/lib/widgets/viewer/visual/video/swipe_action.dart new file mode 100644 index 000000000..a921cca38 --- /dev/null +++ b/lib/widgets/viewer/visual/video/swipe_action.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:aves/theme/icons.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:screen_brightness/screen_brightness.dart'; +import 'package:volume_controller/volume_controller.dart'; + +enum SwipeAction { brightness, volume } + +extension ExtraSwipeAction on SwipeAction { + Future get() { + switch (this) { + case SwipeAction.brightness: + return ScreenBrightness().current; + case SwipeAction.volume: + return VolumeController().getVolume(); + } + } + + Future set(double value) async { + switch (this) { + case SwipeAction.brightness: + await ScreenBrightness().setScreenBrightness(value); + break; + case SwipeAction.volume: + VolumeController().setVolume(value, showSystemUI: false); + break; + } + } +} + +class SwipeActionFeedback extends StatelessWidget { + final SwipeAction action; + final ValueNotifier valueNotifier; + + const SwipeActionFeedback({ + super.key, + required this.action, + required this.valueNotifier, + }); + + static const double width = 32; + static const double height = 160; + static const Radius radius = Radius.circular(width / 2); + static const double borderWidth = 2; + static const Color borderColor = Colors.white; + static final Color fillColor = Colors.white.withOpacity(.8); + static final Color backgroundColor = Colors.black.withOpacity(.2); + static final Color innerBorderColor = Colors.black.withOpacity(.5); + static const Color iconColor = Colors.white; + static const Color shadowColor = Colors.black; + + @override + Widget build(BuildContext context) { + return Center( + child: ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, child) { + if (value == null) return const SizedBox(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildIcon(_getMaxIcon()), + Container( + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all( + width: borderWidth * 2, + color: innerBorderColor, + ), + borderRadius: const BorderRadius.all(radius), + ), + foregroundDecoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: borderWidth, + ), + borderRadius: const BorderRadius.all(radius), + ), + width: width, + height: height, + child: ClipRRect( + borderRadius: const BorderRadius.all(radius), + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + color: fillColor, + width: width, + height: height * value, + ), + ), + ), + ), + _buildIcon(_getMinIcon()), + ], + ); + }, + ), + ); + } + + Widget _buildIcon(IconData icon) { + return Padding( + padding: const EdgeInsets.all(16), + child: DecoratedIcon( + icon, + size: width, + color: iconColor, + shadows: const [ + Shadow( + color: shadowColor, + blurRadius: 4, + ) + ], + ), + ); + } + + IconData _getMinIcon() { + switch (action) { + case SwipeAction.brightness: + return AIcons.brightnessMin; + case SwipeAction.volume: + return AIcons.volumeMin; + } + } + + IconData _getMaxIcon() { + switch (action) { + case SwipeAction.brightness: + return AIcons.brightnessMax; + case SwipeAction.volume: + return AIcons.volumeMax; + } + } +} diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video/video_view.dart similarity index 100% rename from lib/widgets/viewer/visual/video.dart rename to lib/widgets/viewer/visual/video/video_view.dart diff --git a/plugins/aves_magnifier/lib/aves_magnifier.dart b/plugins/aves_magnifier/lib/aves_magnifier.dart index bda2e655b..dae50972d 100644 --- a/plugins/aves_magnifier/lib/aves_magnifier.dart +++ b/plugins/aves_magnifier/lib/aves_magnifier.dart @@ -2,6 +2,7 @@ library aves_magnifier; export 'src/controller/controller.dart'; export 'src/controller/state.dart'; +export 'src/core/scale_gesture_recognizer.dart'; export 'src/magnifier.dart'; export 'src/pan/gesture_detector_scope.dart'; export 'src/pan/scroll_physics.dart'; diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index eb24275ba..0ce553290 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -18,6 +18,9 @@ class MagnifierCore extends StatefulWidget { final ScaleStateCycle scaleStateCycle; final bool applyScale; final double panInertia; + final MagnifierGestureScaleStartCallback? onScaleStart; + final MagnifierGestureScaleUpdateCallback? onScaleUpdate; + final MagnifierGestureScaleEndCallback? onScaleEnd; final MagnifierTapCallback? onTap; final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; @@ -28,6 +31,9 @@ class MagnifierCore extends StatefulWidget { required this.scaleStateCycle, required this.applyScale, this.panInertia = .2, + this.onScaleStart, + this.onScaleUpdate, + this.onScaleEnd, this.onTap, this.onDoubleTap, required this.child, @@ -40,7 +46,7 @@ class MagnifierCore extends StatefulWidget { class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector { Offset? _startFocalPoint, _lastViewportFocalPosition; double? _startScale, _quickScaleLastY, _quickScaleLastDistance; - late bool _doubleTap, _quickScaleMoved; + late bool _dropped, _doubleTap, _quickScaleMoved; DateTime _lastScaleGestureDate = DateTime.now(); late AnimationController _scaleAnimationController; @@ -99,9 +105,15 @@ class _MagnifierCoreState extends State with TickerProviderStateM } void onScaleStart(ScaleStartDetails details, bool doubleTap) { + final boundaries = scaleBoundaries; + if (boundaries == null) return; + + widget.onScaleStart?.call(details, doubleTap, boundaries); + _startScale = scale; _startFocalPoint = details.localFocalPoint; _lastViewportFocalPosition = _startFocalPoint; + _dropped = false; _doubleTap = doubleTap; _quickScaleLastDistance = null; _quickScaleLastY = _startFocalPoint!.dy; @@ -115,6 +127,9 @@ class _MagnifierCoreState extends State with TickerProviderStateM final boundaries = scaleBoundaries; if (boundaries == null) return; + _dropped |= widget.onScaleUpdate?.call(details) ?? false; + if (_dropped) return; + double newScale; if (_doubleTap) { // quick scale, aka one finger zoom @@ -151,6 +166,8 @@ class _MagnifierCoreState extends State with TickerProviderStateM final boundaries = scaleBoundaries; if (boundaries == null) return; + widget.onScaleEnd?.call(details); + final _position = controller.position; final _scale = controller.scale!; final maxScale = boundaries.maxScale; @@ -228,7 +245,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM if (onDoubleTap != null) { final viewportSize = boundaries.viewportSize; final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); - if (onDoubleTap.call(alignment) == true) return; + if (onDoubleTap(alignment) == true) return; } final childTapPosition = boundaries.viewportToChildPosition(controller, viewportTapPosition); @@ -307,12 +324,12 @@ class _MagnifierCoreState extends State with TickerProviderStateM ); return MagnifierGestureDetector( - onDoubleTap: onDoubleTap, + hitDetector: this, onScaleStart: onScaleStart, onScaleUpdate: onScaleUpdate, onScaleEnd: onScaleEnd, - hitDetector: this, onTapUp: widget.onTap == null ? null : onTap, + onDoubleTap: onDoubleTap, child: child, ); }, diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart index 7d27b623b..e9ae6017b 100644 --- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart +++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart @@ -60,8 +60,7 @@ class _MagnifierGestureDetectorState extends State { () => MagnifierGestureRecognizer( debugOwner: this, hitDetector: widget.hitDetector, - validateAxis: scope.axis, - touchSlopFactor: scope.touchSlopFactor, + scope: scope, doubleTapDetails: doubleTapDetails, ), (instance) { diff --git a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart index 0c35ca666..eb95393b9 100644 --- a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart +++ b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart @@ -1,20 +1,19 @@ import 'dart:math'; +import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:aves_magnifier/src/pan/corner_hit_detector.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; class MagnifierGestureRecognizer extends ScaleGestureRecognizer { final CornerHitDetector hitDetector; - final List validateAxis; - final double touchSlopFactor; + final MagnifierGestureDetectorScope scope; final ValueNotifier doubleTapDetails; MagnifierGestureRecognizer({ super.debugOwner, required this.hitDetector, - required this.validateAxis, - this.touchSlopFactor = 2, + required this.scope, required this.doubleTapDetails, }); @@ -46,7 +45,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { @override void handleEvent(PointerEvent event) { - if (validateAxis.isNotEmpty) { + if (scope.axis.isNotEmpty) { var didChangeConfiguration = false; if (event is PointerMoveEvent) { if (!event.synthesized) { @@ -104,26 +103,27 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { return; } + final validateAxis = scope.axis; final move = _initialFocalPoint! - _currentFocalPoint!; - var shouldMove = false; - if (validateAxis.length == 2) { - // the image is the descendant of gesture detector(s) handling drag in both directions - final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move); - final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move); - if (shouldMoveX == shouldMoveY) { - // consistently can/cannot pan the image in both direction the same way - shouldMove = shouldMoveX; + bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false; + + if (!shouldMove) { + if (validateAxis.length == 2) { + // the image is the descendant of gesture detector(s) handling drag in both directions + final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move); + final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move); + if (shouldMoveX == shouldMoveY) { + // consistently can/cannot pan the image in both direction the same way + shouldMove = shouldMoveX; + } else { + // can pan the image in one direction, but should yield to an ascendant gesture detector in the other one + // the gesture direction angle is in ]-pi, pi], cf `Offset` doc for details + shouldMove = (isXPan(move) && shouldMoveX) || (isYPan(move) && shouldMoveY); + } } else { - // can pan the image in one direction, but should yield to an ascendant gesture detector in the other one - final d = move.direction; - // the gesture direction angle is in ]-pi, pi], cf `Offset` doc for details - final xPan = (-pi / 4 < d && d < pi / 4) || (3 / 4 * pi < d && d <= pi) || (-pi < d && d < -3 / 4 * pi); - final yPan = (pi / 4 < d && d < 3 / 4 * pi) || (-3 / 4 * pi < d && d < -pi / 4); - shouldMove = (xPan && shouldMoveX) || (yPan && shouldMoveY); + // the image is the descendant of a gesture detector handling drag in one direction + shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move); } - } else { - // the image is the descendant of a gesture detector handling drag in one direction - shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move); } final doubleTap = doubleTapDetails.value != null; @@ -137,9 +137,19 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { // and the magnifier recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView` // setting `touchSlopFactor` to 2 restores default `ScaleGestureRecognizer` behaviour as `kPanSlop = kTouchSlop * 2.0` // setting `touchSlopFactor` in [0, 1] will allow this recognizer to accept the gesture before the one from `PageView` - if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computeHitSlop(pointerDeviceKind, gestureSettings) * touchSlopFactor) { + if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computeHitSlop(pointerDeviceKind, gestureSettings) * scope.touchSlopFactor) { acceptGesture(event.pointer); } } } + + static bool isXPan(Offset move) { + final d = move.direction; + return (-pi / 4 < d && d < pi / 4) || (3 / 4 * pi < d && d <= pi) || (-pi < d && d < -3 / 4 * pi); + } + + static bool isYPan(Offset move) { + final d = move.direction; + return (pi / 4 < d && d < 3 / 4 * pi) || (-3 / 4 * pi < d && d < -pi / 4); + } } diff --git a/plugins/aves_magnifier/lib/src/magnifier.dart b/plugins/aves_magnifier/lib/src/magnifier.dart index f93a07cad..88c3f7586 100644 --- a/plugins/aves_magnifier/lib/src/magnifier.dart +++ b/plugins/aves_magnifier/lib/src/magnifier.dart @@ -29,6 +29,9 @@ class AvesMagnifier extends StatelessWidget { this.initialScale = const ScaleLevel(ref: ScaleReference.contained), this.scaleStateCycle = defaultScaleStateCycle, this.applyScale = true, + this.onScaleStart, + this.onScaleUpdate, + this.onScaleEnd, this.onTap, this.onDoubleTap, required this.child, @@ -52,6 +55,9 @@ class AvesMagnifier extends StatelessWidget { final ScaleStateCycle scaleStateCycle; final bool applyScale; + final MagnifierGestureScaleStartCallback? onScaleStart; + final MagnifierGestureScaleUpdateCallback? onScaleUpdate; + final MagnifierGestureScaleEndCallback? onScaleEnd; final MagnifierTapCallback? onTap; final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; @@ -73,6 +79,9 @@ class AvesMagnifier extends StatelessWidget { controller: controller, scaleStateCycle: scaleStateCycle, applyScale: applyScale, + onScaleStart: onScaleStart, + onScaleUpdate: onScaleUpdate, + onScaleEnd: onScaleEnd, onTap: onTap, onDoubleTap: onDoubleTap, child: child, @@ -88,7 +97,7 @@ typedef MagnifierTapCallback = Function( Alignment alignment, Offset childTapPosition, ); - -typedef MagnifierDoubleTapCallback = bool Function( - Alignment alignment, -); +typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment); +typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries); +typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details); +typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details); diff --git a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart index e7a597baf..a9b52c9fe 100644 --- a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart +++ b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart @@ -7,18 +7,6 @@ import 'package:flutter/widgets.dart'; /// Useful when placing Magnifier inside a gesture sensitive context, /// such as [PageView], [Dismissible], [BottomSheet]. class MagnifierGestureDetectorScope extends InheritedWidget { - const MagnifierGestureDetectorScope({ - super.key, - required this.axis, - this.touchSlopFactor = .8, - required Widget child, - }) : super(child: child); - - static MagnifierGestureDetectorScope? of(BuildContext context) { - final scope = context.dependOnInheritedWidgetOfExactType(); - return scope; - } - final List axis; // in [0, 1[ @@ -26,9 +14,36 @@ class MagnifierGestureDetectorScope extends InheritedWidget { // <1: less reactive but gives the most leeway to other recognizers // 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree final double touchSlopFactor; + final bool? Function(Offset move)? acceptPointerEvent; + + const MagnifierGestureDetectorScope({ + super.key, + required this.axis, + this.touchSlopFactor = .8, + this.acceptPointerEvent, + required Widget child, + }) : super(child: child); + + static MagnifierGestureDetectorScope? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + MagnifierGestureDetectorScope copyWith({ + List? axis, + double? touchSlopFactor, + bool? Function(Offset move)? acceptPointerEvent, + required Widget child, + }) { + return MagnifierGestureDetectorScope( + axis: axis ?? this.axis, + touchSlopFactor: touchSlopFactor ?? this.touchSlopFactor, + acceptPointerEvent: acceptPointerEvent ?? this.acceptPointerEvent, + child: child, + ); + } @override bool updateShouldNotify(MagnifierGestureDetectorScope oldWidget) { - return axis != oldWidget.axis && touchSlopFactor != oldWidget.touchSlopFactor; + return axis != oldWidget.axis || touchSlopFactor != oldWidget.touchSlopFactor || acceptPointerEvent != oldWidget.acceptPointerEvent; } } diff --git a/pubspec.lock b/pubspec.lock index af51869ef..1c7170c2e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1196,6 +1196,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "9.0.0" + volume_controller: + dependency: "direct main" + description: + name: volume_controller + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0e42057ba..1b166e0ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt -version: 1.7.9+89 +version: 1.7.10+90 publish_to: none environment: @@ -95,6 +95,7 @@ dependencies: transparent_image: tuple: url_launcher: + volume_controller: xml: dev_dependencies: diff --git a/untranslated.json b/untranslated.json index 4301f7282..c87e286de 100644 --- a/untranslated.json +++ b/untranslated.json @@ -475,6 +475,7 @@ "settingsVideoButtonsTile", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsPrivacySectionTitle", "settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccessSubtitle", @@ -576,6 +577,7 @@ "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsDisplayUseTvInterface" ], @@ -586,16 +588,14 @@ "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ], "el": [ - "tooManyItemsErrorDialogMessage" - ], - - "es": [ - "tooManyItemsErrorDialogMessage" + "tooManyItemsErrorDialogMessage", + "settingsVideoGestureVerticalDragBrightnessVolume" ], "fa": [ @@ -933,6 +933,7 @@ "settingsVideoButtonsTile", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsPrivacySectionTitle", "settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccessSubtitle", @@ -1404,6 +1405,7 @@ "settingsVideoButtonsTile", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsPrivacySectionTitle", "settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccessSubtitle", @@ -1512,6 +1514,613 @@ "filePickerUseThisFolder" ], + "he": [ + "itemCount", + "columnCount", + "timeSeconds", + "timeMinutes", + "timeDays", + "focalLength", + "applyButtonLabel", + "deleteButtonLabel", + "nextButtonLabel", + "showButtonLabel", + "hideButtonLabel", + "continueButtonLabel", + "cancelTooltip", + "changeTooltip", + "clearTooltip", + "previousTooltip", + "nextTooltip", + "showTooltip", + "hideTooltip", + "actionRemove", + "resetTooltip", + "saveTooltip", + "pickTooltip", + "doubleBackExitMessage", + "doNotAskAgain", + "sourceStateLoading", + "sourceStateCataloguing", + "sourceStateLocatingCountries", + "sourceStateLocatingPlaces", + "chipActionDelete", + "chipActionGoToAlbumPage", + "chipActionGoToCountryPage", + "chipActionGoToTagPage", + "chipActionFilterOut", + "chipActionFilterIn", + "chipActionHide", + "chipActionPin", + "chipActionUnpin", + "chipActionRename", + "chipActionSetCover", + "chipActionCreateAlbum", + "entryActionCopyToClipboard", + "entryActionDelete", + "entryActionConvert", + "entryActionExport", + "entryActionInfo", + "entryActionRename", + "entryActionRestore", + "entryActionRotateCCW", + "entryActionRotateCW", + "entryActionFlip", + "entryActionPrint", + "entryActionShare", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", + "entryActionViewSource", + "entryActionShowGeoTiffOnMap", + "entryActionConvertMotionPhotoToStillImage", + "entryActionViewMotionPhotoVideo", + "entryActionEdit", + "entryActionOpen", + "entryActionSetAs", + "entryActionOpenMap", + "entryActionRotateScreen", + "entryActionAddFavourite", + "entryActionRemoveFavourite", + "videoActionCaptureFrame", + "videoActionMute", + "videoActionUnmute", + "videoActionPause", + "videoActionPlay", + "videoActionReplay10", + "videoActionSkip10", + "videoActionSelectStreams", + "videoActionSetSpeed", + "videoActionSettings", + "slideshowActionResume", + "slideshowActionShowInCollection", + "entryInfoActionEditDate", + "entryInfoActionEditLocation", + "entryInfoActionEditTitleDescription", + "entryInfoActionEditRating", + "entryInfoActionEditTags", + "entryInfoActionRemoveMetadata", + "entryInfoActionExportMetadata", + "entryInfoActionRemoveLocation", + "filterAspectRatioLandscapeLabel", + "filterAspectRatioPortraitLabel", + "filterBinLabel", + "filterFavouriteLabel", + "filterNoDateLabel", + "filterNoAddressLabel", + "filterLocatedLabel", + "filterNoLocationLabel", + "filterNoRatingLabel", + "filterTaggedLabel", + "filterNoTagLabel", + "filterNoTitleLabel", + "filterOnThisDayLabel", + "filterRecentlyAddedLabel", + "filterRatingRejectedLabel", + "filterTypeAnimatedLabel", + "filterTypeMotionPhotoLabel", + "filterTypePanoramaLabel", + "filterTypeRawLabel", + "filterTypeSphericalVideoLabel", + "filterTypeGeotiffLabel", + "filterMimeImageLabel", + "filterMimeVideoLabel", + "coordinateFormatDms", + "coordinateFormatDecimal", + "coordinateDms", + "coordinateDmsNorth", + "coordinateDmsSouth", + "coordinateDmsEast", + "coordinateDmsWest", + "unitSystemMetric", + "unitSystemImperial", + "videoLoopModeNever", + "videoLoopModeShortOnly", + "videoLoopModeAlways", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", + "videoControlsNone", + "mapStyleGoogleNormal", + "mapStyleGoogleHybrid", + "mapStyleGoogleTerrain", + "mapStyleHuaweiNormal", + "mapStyleHuaweiTerrain", + "mapStyleOsmHot", + "mapStyleStamenToner", + "mapStyleStamenWatercolor", + "nameConflictStrategyRename", + "nameConflictStrategyReplace", + "nameConflictStrategySkip", + "keepScreenOnNever", + "keepScreenOnVideoPlayback", + "keepScreenOnViewerOnly", + "keepScreenOnAlways", + "accessibilityAnimationsRemove", + "accessibilityAnimationsKeep", + "displayRefreshRatePreferHighest", + "displayRefreshRatePreferLowest", + "subtitlePositionTop", + "subtitlePositionBottom", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "viewerTransitionSlide", + "viewerTransitionParallax", + "viewerTransitionFade", + "viewerTransitionZoomIn", + "viewerTransitionNone", + "wallpaperTargetHome", + "wallpaperTargetLock", + "wallpaperTargetHomeLock", + "widgetDisplayedItemRandom", + "widgetDisplayedItemMostRecent", + "widgetOpenPageHome", + "widgetOpenPageCollection", + "widgetOpenPageViewer", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", + "albumTierRegular", + "storageVolumeDescriptionFallbackPrimary", + "storageVolumeDescriptionFallbackNonPrimary", + "rootDirectoryDescription", + "otherDirectoryDescription", + "storageAccessDialogMessage", + "restrictedAccessDialogMessage", + "notEnoughSpaceDialogMessage", + "missingSystemFilePickerDialogMessage", + "unsupportedTypeDialogMessage", + "nameConflictDialogSingleSourceMessage", + "nameConflictDialogMultipleSourceMessage", + "addShortcutDialogLabel", + "addShortcutButtonLabel", + "noMatchingAppDialogMessage", + "binEntriesConfirmationDialogMessage", + "deleteEntriesConfirmationDialogMessage", + "moveUndatedConfirmationDialogMessage", + "moveUndatedConfirmationDialogSetDate", + "videoResumeDialogMessage", + "videoStartOverButtonLabel", + "videoResumeButtonLabel", + "setCoverDialogLatest", + "setCoverDialogAuto", + "setCoverDialogCustom", + "hideFilterConfirmationDialogMessage", + "newAlbumDialogTitle", + "newAlbumDialogNameLabel", + "newAlbumDialogNameLabelAlreadyExistsHelper", + "newAlbumDialogStorageLabel", + "renameAlbumDialogLabel", + "renameAlbumDialogLabelAlreadyExistsHelper", + "renameEntrySetPageTitle", + "renameEntrySetPagePatternFieldLabel", + "renameEntrySetPageInsertTooltip", + "renameEntrySetPagePreviewSectionTitle", + "renameProcessorCounter", + "renameProcessorName", + "deleteSingleAlbumConfirmationDialogMessage", + "deleteMultiAlbumConfirmationDialogMessage", + "exportEntryDialogFormat", + "exportEntryDialogWidth", + "exportEntryDialogHeight", + "renameEntryDialogLabel", + "editEntryDialogCopyFromItem", + "editEntryDialogTargetFieldsHeader", + "editEntryDateDialogTitle", + "editEntryDateDialogSetCustom", + "editEntryDateDialogCopyField", + "editEntryDateDialogExtractFromTitle", + "editEntryDateDialogShift", + "editEntryDateDialogSourceFileModifiedDate", + "durationDialogHours", + "durationDialogMinutes", + "durationDialogSeconds", + "editEntryLocationDialogTitle", + "editEntryLocationDialogSetCustom", + "editEntryLocationDialogChooseOnMap", + "editEntryLocationDialogLatitude", + "editEntryLocationDialogLongitude", + "locationPickerUseThisLocationButton", + "editEntryRatingDialogTitle", + "removeEntryMetadataDialogTitle", + "removeEntryMetadataDialogMore", + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage", + "videoSpeedDialogLabel", + "videoStreamSelectionDialogVideo", + "videoStreamSelectionDialogAudio", + "videoStreamSelectionDialogText", + "videoStreamSelectionDialogOff", + "videoStreamSelectionDialogTrack", + "videoStreamSelectionDialogNoSelection", + "genericSuccessFeedback", + "genericFailureFeedback", + "genericDangerWarningDialogMessage", + "tooManyItemsErrorDialogMessage", + "menuActionConfigureView", + "menuActionSelect", + "menuActionSelectAll", + "menuActionSelectNone", + "menuActionMap", + "menuActionSlideshow", + "menuActionStats", + "viewDialogSortSectionTitle", + "viewDialogGroupSectionTitle", + "viewDialogLayoutSectionTitle", + "viewDialogReverseSortOrder", + "tileLayoutMosaic", + "tileLayoutGrid", + "tileLayoutList", + "coverDialogTabCover", + "coverDialogTabApp", + "coverDialogTabColor", + "appPickDialogTitle", + "appPickDialogNone", + "aboutPageTitle", + "aboutLinkLicense", + "aboutLinkPolicy", + "aboutBugSectionTitle", + "aboutBugSaveLogInstruction", + "aboutBugCopyInfoInstruction", + "aboutBugCopyInfoButton", + "aboutBugReportInstruction", + "aboutBugReportButton", + "aboutCreditsSectionTitle", + "aboutCreditsWorldAtlas1", + "aboutCreditsWorldAtlas2", + "aboutTranslatorsSectionTitle", + "aboutLicensesSectionTitle", + "aboutLicensesBanner", + "aboutLicensesAndroidLibrariesSectionTitle", + "aboutLicensesFlutterPluginsSectionTitle", + "aboutLicensesFlutterPackagesSectionTitle", + "aboutLicensesDartPackagesSectionTitle", + "aboutLicensesShowAllButtonLabel", + "policyPageTitle", + "collectionPageTitle", + "collectionPickPageTitle", + "collectionSelectPageTitle", + "collectionActionShowTitleSearch", + "collectionActionHideTitleSearch", + "collectionActionAddShortcut", + "collectionActionEmptyBin", + "collectionActionCopy", + "collectionActionMove", + "collectionActionRescan", + "collectionActionEdit", + "collectionSearchTitlesHintText", + "collectionGroupAlbum", + "collectionGroupMonth", + "collectionGroupDay", + "collectionGroupNone", + "sectionUnknown", + "dateToday", + "dateYesterday", + "dateThisMonth", + "collectionDeleteFailureFeedback", + "collectionCopyFailureFeedback", + "collectionMoveFailureFeedback", + "collectionRenameFailureFeedback", + "collectionEditFailureFeedback", + "collectionExportFailureFeedback", + "collectionCopySuccessFeedback", + "collectionMoveSuccessFeedback", + "collectionRenameSuccessFeedback", + "collectionEditSuccessFeedback", + "collectionEmptyFavourites", + "collectionEmptyVideos", + "collectionEmptyImages", + "collectionEmptyGrantAccessButtonLabel", + "collectionSelectSectionTooltip", + "collectionDeselectSectionTooltip", + "drawerAboutButton", + "drawerSettingsButton", + "drawerCollectionAll", + "drawerCollectionFavourites", + "drawerCollectionImages", + "drawerCollectionVideos", + "drawerCollectionAnimated", + "drawerCollectionMotionPhotos", + "drawerCollectionPanoramas", + "drawerCollectionRaws", + "drawerCollectionSphericalVideos", + "drawerAlbumPage", + "drawerCountryPage", + "drawerTagPage", + "sortByDate", + "sortByName", + "sortByItemCount", + "sortBySize", + "sortByAlbumFileName", + "sortByRating", + "sortOrderNewestFirst", + "sortOrderOldestFirst", + "sortOrderAtoZ", + "sortOrderZtoA", + "sortOrderHighestFirst", + "sortOrderLowestFirst", + "sortOrderLargestFirst", + "sortOrderSmallestFirst", + "albumGroupTier", + "albumGroupType", + "albumGroupVolume", + "albumGroupNone", + "albumMimeTypeMixed", + "albumPickPageTitleCopy", + "albumPickPageTitleExport", + "albumPickPageTitleMove", + "albumPickPageTitlePick", + "albumCamera", + "albumDownload", + "albumScreenshots", + "albumScreenRecordings", + "albumVideoCaptures", + "albumPageTitle", + "albumEmpty", + "createAlbumTooltip", + "createAlbumButtonLabel", + "newFilterBanner", + "countryPageTitle", + "countryEmpty", + "tagPageTitle", + "tagEmpty", + "binPageTitle", + "searchCollectionFieldHint", + "searchRecentSectionTitle", + "searchDateSectionTitle", + "searchAlbumsSectionTitle", + "searchCountriesSectionTitle", + "searchPlacesSectionTitle", + "searchTagsSectionTitle", + "searchRatingSectionTitle", + "searchMetadataSectionTitle", + "settingsPageTitle", + "settingsSystemDefault", + "settingsDefault", + "settingsDisabled", + "settingsModificationWarningDialogMessage", + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsActionExport", + "settingsActionExportDialogTitle", + "settingsActionImport", + "settingsActionImportDialogTitle", + "appExportCovers", + "appExportFavourites", + "appExportSettings", + "settingsNavigationSectionTitle", + "settingsHomeTile", + "settingsHomeDialogTitle", + "settingsShowBottomNavigationBar", + "settingsKeepScreenOnTile", + "settingsKeepScreenOnDialogTitle", + "settingsDoubleBackExit", + "settingsConfirmationTile", + "settingsConfirmationDialogTitle", + "settingsConfirmationBeforeDeleteItems", + "settingsConfirmationBeforeMoveToBinItems", + "settingsConfirmationBeforeMoveUndatedItems", + "settingsConfirmationAfterMoveToBinItems", + "settingsNavigationDrawerTile", + "settingsNavigationDrawerEditorPageTitle", + "settingsNavigationDrawerBanner", + "settingsNavigationDrawerTabTypes", + "settingsNavigationDrawerTabAlbums", + "settingsNavigationDrawerTabPages", + "settingsNavigationDrawerAddAlbum", + "settingsThumbnailSectionTitle", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayPageTitle", + "settingsThumbnailShowFavouriteIcon", + "settingsThumbnailShowTagIcon", + "settingsThumbnailShowLocationIcon", + "settingsThumbnailShowMotionPhotoIcon", + "settingsThumbnailShowRating", + "settingsThumbnailShowRawIcon", + "settingsThumbnailShowVideoDuration", + "settingsCollectionQuickActionsTile", + "settingsCollectionQuickActionEditorPageTitle", + "settingsCollectionQuickActionTabBrowsing", + "settingsCollectionQuickActionTabSelecting", + "settingsCollectionBrowsingQuickActionEditorBanner", + "settingsCollectionSelectionQuickActionEditorBanner", + "settingsViewerSectionTitle", + "settingsViewerGestureSideTapNext", + "settingsViewerUseCutout", + "settingsViewerMaximumBrightness", + "settingsMotionPhotoAutoPlay", + "settingsImageBackground", + "settingsViewerQuickActionsTile", + "settingsViewerQuickActionEditorPageTitle", + "settingsViewerQuickActionEditorBanner", + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle", + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle", + "settingsViewerQuickActionEmpty", + "settingsViewerOverlayTile", + "settingsViewerOverlayPageTitle", + "settingsViewerShowOverlayOnOpening", + "settingsViewerShowMinimap", + "settingsViewerShowInformation", + "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", + "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", + "settingsViewerShowOverlayThumbnails", + "settingsViewerEnableOverlayBlurEffect", + "settingsViewerSlideshowTile", + "settingsViewerSlideshowPageTitle", + "settingsSlideshowRepeat", + "settingsSlideshowShuffle", + "settingsSlideshowFillScreen", + "settingsSlideshowAnimatedZoomEffect", + "settingsSlideshowTransitionTile", + "settingsSlideshowIntervalTile", + "settingsSlideshowVideoPlaybackTile", + "settingsSlideshowVideoPlaybackDialogTitle", + "settingsVideoPageTitle", + "settingsVideoSectionTitle", + "settingsVideoShowVideos", + "settingsVideoEnableHardwareAcceleration", + "settingsVideoAutoPlay", + "settingsVideoLoopModeTile", + "settingsVideoLoopModeDialogTitle", + "settingsSubtitleThemeTile", + "settingsSubtitleThemePageTitle", + "settingsSubtitleThemeSample", + "settingsSubtitleThemeTextAlignmentTile", + "settingsSubtitleThemeTextAlignmentDialogTitle", + "settingsSubtitleThemeTextPositionTile", + "settingsSubtitleThemeTextPositionDialogTitle", + "settingsSubtitleThemeTextSize", + "settingsSubtitleThemeShowOutline", + "settingsSubtitleThemeTextColor", + "settingsSubtitleThemeTextOpacity", + "settingsSubtitleThemeBackgroundColor", + "settingsSubtitleThemeBackgroundOpacity", + "settingsSubtitleThemeTextAlignmentLeft", + "settingsSubtitleThemeTextAlignmentCenter", + "settingsSubtitleThemeTextAlignmentRight", + "settingsVideoControlsTile", + "settingsVideoControlsPageTitle", + "settingsVideoButtonsTile", + "settingsVideoGestureDoubleTapTogglePlay", + "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsPrivacySectionTitle", + "settingsAllowInstalledAppAccess", + "settingsAllowInstalledAppAccessSubtitle", + "settingsAllowErrorReporting", + "settingsSaveSearchHistory", + "settingsEnableBin", + "settingsEnableBinSubtitle", + "settingsAllowMediaManagement", + "settingsHiddenItemsTile", + "settingsHiddenItemsPageTitle", + "settingsHiddenItemsTabFilters", + "settingsHiddenFiltersBanner", + "settingsHiddenFiltersEmpty", + "settingsHiddenItemsTabPaths", + "settingsHiddenPathsBanner", + "addPathTooltip", + "settingsStorageAccessTile", + "settingsStorageAccessPageTitle", + "settingsStorageAccessBanner", + "settingsStorageAccessEmpty", + "settingsStorageAccessRevokeTooltip", + "settingsAccessibilitySectionTitle", + "settingsRemoveAnimationsTile", + "settingsRemoveAnimationsDialogTitle", + "settingsTimeToTakeActionTile", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplaySectionTitle", + "settingsThemeBrightnessTile", + "settingsThemeBrightnessDialogTitle", + "settingsThemeColorHighlights", + "settingsThemeEnableDynamicColor", + "settingsDisplayRefreshRateModeTile", + "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", + "settingsLanguageSectionTitle", + "settingsLanguageTile", + "settingsLanguagePageTitle", + "settingsCoordinateFormatTile", + "settingsCoordinateFormatDialogTitle", + "settingsUnitSystemTile", + "settingsUnitSystemDialogTitle", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline", + "settingsWidgetOpenPage", + "settingsWidgetDisplayedItem", + "settingsCollectionTile", + "statsPageTitle", + "statsWithGps", + "statsTopCountriesSectionTitle", + "statsTopPlacesSectionTitle", + "statsTopTagsSectionTitle", + "statsTopAlbumsSectionTitle", + "viewerOpenPanoramaButtonLabel", + "viewerSetWallpaperButtonLabel", + "viewerErrorUnknown", + "viewerErrorDoesNotExist", + "viewerInfoPageTitle", + "viewerInfoBackToViewerTooltip", + "viewerInfoUnknown", + "viewerInfoLabelDescription", + "viewerInfoLabelTitle", + "viewerInfoLabelDate", + "viewerInfoLabelResolution", + "viewerInfoLabelSize", + "viewerInfoLabelUri", + "viewerInfoLabelPath", + "viewerInfoLabelDuration", + "viewerInfoLabelOwner", + "viewerInfoLabelCoordinates", + "viewerInfoLabelAddress", + "mapStyleDialogTitle", + "mapStyleTooltip", + "mapZoomInTooltip", + "mapZoomOutTooltip", + "mapPointNorthUpTooltip", + "mapAttributionOsmHot", + "mapAttributionStamen", + "openMapPageTooltip", + "mapEmptyRegion", + "viewerInfoOpenEmbeddedFailureFeedback", + "viewerInfoOpenLinkText", + "viewerInfoViewXmlLinkText", + "viewerInfoSearchFieldLabel", + "viewerInfoSearchEmpty", + "viewerInfoSearchSuggestionDate", + "viewerInfoSearchSuggestionDescription", + "viewerInfoSearchSuggestionDimensions", + "viewerInfoSearchSuggestionResolution", + "viewerInfoSearchSuggestionRights", + "wallpaperUseScrollEffect", + "tagEditorPageTitle", + "tagEditorPageNewTagFieldLabel", + "tagEditorPageAddTagTooltip", + "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderPlace", + "panoramaEnableSensorControl", + "panoramaDisableSensorControl", + "sourceViewerPageTitle", + "filePickerShowHiddenFiles", + "filePickerDoNotShowHiddenFiles", + "filePickerOpenFrom", + "filePickerNoItems", + "filePickerUseThisFolder" + ], + + "it": [ + "settingsVideoGestureVerticalDragBrightnessVolume" + ], + "ja": [ "columnCount", "chipActionFilterIn", @@ -1526,6 +2135,7 @@ "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" @@ -1539,23 +2149,13 @@ "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ], "nb": [ - "columnCount", - "entryActionShareImageOnly", - "entryActionShareVideoOnly", - "entryInfoActionRemoveLocation", - "filterLocatedLabel", - "filterTaggedLabel", - "keepScreenOnVideoPlayback", - "tooManyItemsErrorDialogMessage", - "settingsModificationWarningDialogMessage", - "settingsViewerShowDescription", - "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplayUseTvInterface" + "settingsVideoGestureVerticalDragBrightnessVolume" ], "nl": [ @@ -1580,6 +2180,7 @@ "settingsViewerShowDescription", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" @@ -1828,6 +2429,7 @@ "settingsVideoButtonsTile", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsPrivacySectionTitle", "settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccessSubtitle", @@ -1872,17 +2474,14 @@ "wallpaperUseScrollEffect" ], - "pl": [ - "tooManyItemsErrorDialogMessage" - ], - "pt": [ "columnCount", - "tooManyItemsErrorDialogMessage" + "tooManyItemsErrorDialogMessage", + "settingsVideoGestureVerticalDragBrightnessVolume" ], "ro": [ - "tooManyItemsErrorDialogMessage" + "settingsVideoGestureVerticalDragBrightnessVolume" ], "ru": [ @@ -1890,6 +2489,7 @@ "filterTaggedLabel", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsDisplayUseTvInterface" ], @@ -1901,29 +2501,6 @@ "timeDays", "focalLength", "applyButtonLabel", - "entryActionShareImageOnly", - "entryActionShareVideoOnly", - "entryActionShowGeoTiffOnMap", - "videoActionCaptureFrame", - "entryInfoActionRemoveLocation", - "filterAspectRatioLandscapeLabel", - "filterAspectRatioPortraitLabel", - "filterNoAddressLabel", - "filterLocatedLabel", - "filterTaggedLabel", - "coordinateDms", - "keepScreenOnVideoPlayback", - "keepScreenOnViewerOnly", - "accessibilityAnimationsRemove", - "accessibilityAnimationsKeep", - "widgetOpenPageViewer", - "missingSystemFilePickerDialogMessage", - "unsupportedTypeDialogMessage", - "addShortcutDialogLabel", - "binEntriesConfirmationDialogMessage", - "deleteEntriesConfirmationDialogMessage", - "deleteSingleAlbumConfirmationDialogMessage", - "deleteMultiAlbumConfirmationDialogMessage", "editEntryDateDialogExtractFromTitle", "editEntryDateDialogShift", "removeEntryMetadataDialogTitle", @@ -2133,6 +2710,7 @@ "settingsVideoButtonsTile", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsPrivacySectionTitle", "settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccessSubtitle", @@ -2241,16 +2819,13 @@ "filePickerUseThisFolder" ], - "uk": [ - "tooManyItemsErrorDialogMessage" - ], - "zh": [ "filterLocatedLabel", "filterTaggedLabel", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ], @@ -2262,6 +2837,7 @@ "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", "settingsViewerShowDescription", + "settingsVideoGestureVerticalDragBrightnessVolume", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ] diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index ec10c2286..7683711d3 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,4 +1,4 @@ -In v1.7.9: +In v1.7.10: - Android TV support (cont'd) - interact with videos via media session controls - enjoy the app in Czech & Polish