Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2023-01-18 16:23:49 +01:00
commit ef2953dd2a
94 changed files with 2075 additions and 660 deletions

View file

@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.7.10"></a>[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
## <a id="v1.7.9"></a>[v1.7.9] - 2023-01-15
### Added

View file

@ -213,6 +213,15 @@ This change eventually prevents building the app with Flutter v3.3.3.
</intent-filter>
</receiver>
<service
android:name=".MediaPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<service
android:name=".AnalysisService"
android:description="@string/analysis_service_description"

View file

@ -0,0 +1,18 @@
package deckers.thibault.aves
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat
import androidx.media.MediaBrowserServiceCompat
// dummy service to handle media button events
// when there is no active media sessions
class MediaPlaybackService : MediaBrowserServiceCompat() {
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
return null
}
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val children = mutableListOf<MediaBrowserCompat.MediaItem>()
result.sendResult(children)
}
}

View file

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

View file

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

View file

@ -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<XMPPropName>): 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<XMPPropName>): Int {
return countArrayItems(props.first().nsUri, props.joinToString("/"))
}
fun XMPMeta.getPropArrayItemValues(prop: XMPPropName): List<String> {
val schema = prop.nsUri
val propName = prop.toString()

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">אייבז</string>
<string name="app_widget_label">מסגרת תמונה</string>
<string name="wallpaper">טפט</string>
<string name="search_shortcut_short_label">חיפוש</string>
<string name="videos_shortcut_short_label">סרטים</string>
<string name="analysis_channel_name">סריקת מדיה</string>
<string name="analysis_service_description">סרוק תמונות וסרטים</string>
<string name="analysis_notification_default_title">סורק מדיה</string>
<string name="analysis_notification_action_stop">הפסק</string>
</resources>

View file

@ -2,10 +2,10 @@
<resources>
<string name="app_widget_label">Ramka Zdjęcia</string>
<string name="search_shortcut_short_label">Szukaj</string>
<string name="videos_shortcut_short_label">Filmy</string>
<string name="analysis_channel_name">Skan mediów</string>
<string name="analysis_service_description">Skan obrazów &amp; filmów</string>
<string name="analysis_notification_default_title">Skanowanie mediów</string>
<string name="videos_shortcut_short_label">Wideo</string>
<string name="analysis_channel_name">Przeskanuj multimedia</string>
<string name="analysis_service_description">Przeskanuj obrazy oraz wideo</string>
<string name="analysis_notification_default_title">Skanowanie multimediów</string>
<string name="analysis_notification_action_stop">Zatrzymaj</string>
<string name="app_name">Aves</string>
<string name="wallpaper">Tapeta</string>

View file

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

View file

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

View file

@ -0,0 +1,5 @@
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.

View file

@ -0,0 +1 @@
Gallery and metadata explorer

View file

@ -1,4 +1,4 @@
<i>Aves</i> obsługuje wszelkiego rodzaju obrazy i filmy, w tym typowe pliki JPEG i MP4 ale także bardziej egzotyczne formaty takie jak <b>wielostronnicowe pliki TIFF, SVG, stare pliki AVI i wiele więcej</b>! Skanuje twoją kolekcję multimediów aby zidentyfikować <b>ruchome zdjęcia</b>, <b>panoramy</b> (inaczej zdjęcia sferyczne), <b>filmy 360°</b>, a także pliki <b>GeoTIFF</b>.
<i>Aves</i> obsługuje wszelkiego rodzaju obrazy i filmy, w tym typowe pliki JPEG i MP4, ale także bardziej egzotyczne formaty, takie jak <b>wielostronicowe pliki TIFF, SVG, stare pliki AVI i wiele innych</b>! Skanuje twoją kolekcję multimediów, aby zidentyfikować <b>ruchome zdjęcia</b>, <b>zdjęcia panoramiczne</b> (inaczej zdjęcia sferyczne), <b>wideo 360°</b>, a także pliki <b>GeoTIFF</b>.
<b>Nawigacja i wyszukiwanie</b> jest ważną częścią <i>Aves</i>. Celem jest aby użytkownicy mogli łatwo przechodzić od albumów do zdjęć, tagów, map itd.

View file

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

View file

@ -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": {}
}

View file

@ -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": {}
}

10
lib/l10n/app_he.arb Normal file
View file

@ -0,0 +1,10 @@
{
"appName": "אייבז",
"@appName": {},
"welcomeMessage": "ברוך הבא לאייבז",
"@welcomeMessage": {},
"welcomeOptional": "אופציונלי",
"@welcomeOptional": {},
"welcomeTermsToggle": "אני מסכימ/ה לתנאים",
"@welcomeTermsToggle": {}
}

View file

@ -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": {}
}

View file

@ -1208,5 +1208,7 @@
"filterLocatedLabel": "위치 있음",
"@filterLocatedLabel": {},
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
"@tooManyItemsErrorDialogMessage": {}
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
}

View file

@ -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": {}
}

View file

@ -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ń metada",
"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": {}
}

View file

@ -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": {}
}

View file

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

View file

@ -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": {}
}

View file

@ -1364,5 +1364,9 @@
"filterLocatedLabel": "Розташований",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Позначений тегом",
"@filterTaggedLabel": {}
"@filterTaggedLabel": {},
"tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
}

View file

@ -463,7 +463,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
modified |= XMP.removeElements(
descriptions,
XMP.containerDirectory,
Namespaces.container,
Namespaces.gContainer,
);
modified |= [

View file

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

View file

@ -41,7 +41,6 @@ class Settings extends ChangeNotifier {
static const Set<String> _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:

View file

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

View file

@ -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<Dependency> _googleMobileServices = [

View file

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

View file

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

View file

@ -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<AppReference> createState() => _AppReferenceState();
@ -24,6 +19,13 @@ class AppReference extends StatefulWidget {
class _AppReferenceState extends State<AppReference> {
late Future<PackageInfo> _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<AppReference> {
}
Widget _buildAvesLine() {
const style = TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
letterSpacing: 1.0,
fontFeatures: [FontFeature.enable('smcp')],
);
return FutureBuilder<PackageInfo>(
future: _packageInfoLoader,
builder: (context, snapshot) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showLogo) ...[
AvesLogo(
size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3,
size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3,
),
const SizedBox(width: 8),
],
Text(
'${context.l10n.appName} ${snapshot.data?.version}',
style: style,
style: _appTitleStyle,
),
],
);

View file

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

View file

@ -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,6 +51,7 @@ class _LicensesState extends State<Licenses> {
[
_buildHeader(),
const SizedBox(height: 16),
if (!settings.useTvLayout) ...[
AvesExpansionTile(
title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle,
highlightColor: colors.fromBrandColor(BrandColors.android),
@ -74,6 +76,7 @@ class _LicensesState extends State<Licenses> {
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<Licenses> {
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),
],

View file

@ -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<PolicyPage> {
late Future<String> _termsLoader;
final ScrollController _scrollController = ScrollController();
static const termsPath = 'assets/terms.md';
static const termsDirection = TextDirection.ltr;
@ -28,9 +30,19 @@ class _PolicyPageState extends State<PolicyPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout,
title: Text(context.l10n.policyPageTitle),
),
body: SafeArea(
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<String>(
future: _termsLoader,
@ -40,6 +52,7 @@ class _PolicyPageState extends State<PolicyPage> {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: MarkdownContainer(
scrollController: _scrollController,
data: terms,
textDirection: termsDirection,
),
@ -48,6 +61,41 @@ class _PolicyPageState extends State<PolicyPage> {
),
),
),
),
);
}
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,
}

View file

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

View file

@ -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<String> 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,
));
}

View file

@ -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<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList();
static final ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero);
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator');
@ -540,6 +540,7 @@ class _AvesAppState extends State<AvesApp> 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})',
});

View file

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

View file

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

View file

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

View file

@ -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<Settings, bool>((s) => s.useTvLayout);
return useTvLayout
? Focus(
focusNode: focusNode,
child: const SizedBox(),
)
: const SizedBox();
}
}

View file

@ -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;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
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: [
Padding(
header,
],
),
),
),
);
} else {
header = Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
title,
style: Constants.knownTitleTextStyle,
),
header,
const Spacer(),
IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: () => expandedNotifier.value = isExpanded ? null : title,
onPressed: toggle,
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
header,
ExpandableFilterRow(
filters: filters,
isExpanded: isExpanded,

View file

@ -34,18 +34,10 @@ class SectionHeader<T> 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(
child = InkWell(
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,
),
);
}
return Container(

View file

@ -8,7 +8,7 @@ class CaptionedButton extends StatefulWidget {
final Animation<double> 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<CaptionedButton> {
final FocusNode _focusNode = FocusNode();
final ValueNotifier<bool> _focusedNotifier = ValueNotifier(false);
bool _didAutofocus = false;
@override
void initState() {
@ -65,12 +67,21 @@ class _CaptionedButtonState extends State<CaptionedButton> {
_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<CaptionedButton> {
);
}
void _handleAutofocus() {
if (!_didAutofocus && widget.autofocus) {
FocusScope.of(context).autofocus(_focusNode);
_didAutofocus = true;
}
}
void _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus;
void _updateTraversal() {

View file

@ -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<Widget>? 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();
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<double> get transitionAnimation => proxyAnimation;
FocusNode? focusNode;
FocusNode? searchFieldFocusNode;
FocusNode? get suggestionsFocusNode => null;
ScrollController? get suggestionsScrollController => null;
final TextEditingController queryTextController = TextEditingController();

View file

@ -29,7 +29,7 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> {
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<SearchPage> {
super.initState();
_registerWidget(widget);
widget.animation.addStatusListener(_onAnimationStatusChanged);
_focusNode.addListener(_onFocusChanged);
_searchFieldFocusNode.addListener(_onFocusChanged);
}
@override
@ -53,21 +53,22 @@ class _SearchPageState extends State<SearchPage> {
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<SearchPage> {
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<SearchPage> {
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,

View file

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

View file

@ -135,16 +135,14 @@ class SelectionRadioListTile<T> 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,

View file

@ -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<FilterGridItem<AlbumFilter>> 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<Widget> _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<Widget> _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<ChipSetAction>(
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),
];

View file

@ -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<AppPickPage> {
@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<AppPickPage> {
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<String>(
valueListenable: _queryNotifier,
builder: (context, query, child) {

View file

@ -70,6 +70,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> 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<T extends CollectionFilter> 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

View file

@ -115,14 +115,22 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
);
if (useTvLayout) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
return Scaffold(
body: Row(
body: canNavigate
? Row(
children: [
TvRail(
controller: context.read<TvRailController>(),
),
Expanded(child: body),
],
)
: DirectionalSafeArea(
top: false,
end: false,
bottom: false,
child: body,
),
resizeToAvoidBottomInset: false,
extendBody: true,

View file

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

View file

@ -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<String?> _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: [

View file

@ -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<DrawerAlbumTab> {
final source = context.read<CollectionSource>();
return Column(
children: [
if (!settings.useTvLayout) ...[
const DrawerEditorBanner(),
const Divider(height: 0),
],
Flexible(
child: ReorderableListView.builder(
itemBuilder: (context, index) {

View file

@ -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<T> extends State<DrawerFixedListTab<T>> {
Widget build(BuildContext context) {
return Column(
children: [
if (!settings.useTvLayout) ...[
const DrawerEditorBanner(),
const Divider(height: 0),
],
Flexible(
child: ReorderableListView.builder(
itemBuilder: (context, index) {

View file

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

View file

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

View file

@ -111,22 +111,15 @@ class _MimeDonutState extends State<MimeDonut> 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(
.map((d) => InkWell(
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: [
@ -150,7 +143,6 @@ class _MimeDonutState extends State<MimeDonut> with AutomaticKeepAliveClientMixi
const SizedBox(width: 4),
],
),
),
))
.toList(),
),

View file

@ -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<StatsPage> {
@override
Widget build(BuildContext context) {
final useTvLayout = settings.useTvLayout;
return ValueListenableBuilder<bool>(
valueListenable: _isPageAnimatingNotifier,
builder: (context, animating, child) {
@ -196,6 +198,7 @@ class _StatsPageState extends State<StatsPage> {
),
),
children: [
const TvEdgeFocus(),
mimeDonuts,
Histogram(
entries: entries,
@ -218,7 +221,7 @@ class _StatsPageState extends State<StatsPage> {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout,
automaticallyImplyLeading: !useTvLayout,
title: Text(l10n.statsPageTitle),
),
body: GestureAreaProtectorStack(
@ -274,25 +277,17 @@ class _StatsPageState extends State<StatsPage> {
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(
child: InkWell(
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,
),
),
),
);
} else {
header = Padding(

View file

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

View file

@ -138,12 +138,14 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
child: child!,
);
},
child: FocusScope(
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
isScrollingNotifier: _isVerticallyScrollingNotifier,
),
),
),
);
pages.add(infoPage);
@ -286,10 +288,10 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
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);
});
}

View file

@ -680,9 +680,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
}
Future<void> _onLeave() async {
if (settings.viewerMaxBrightness) {
await ScreenBrightness().resetScreenBrightness();
}
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
await windowService.keepScreenOn(false);
}

View file

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

View file

@ -22,56 +22,64 @@ import 'package:tuple/tuple.dart';
@immutable
class XmpNamespace extends Equatable {
final Map<String, String> schemaRegistryPrefixes;
final String nsUri, nsPrefix;
final Map<String, String> rawProps;
@override
List<Object?> 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<String, String> rawProps) {
factory XmpNamespace.create(Map<String, String> schemaRegistryPrefixes, String nsPrefix, Map<String, String> 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<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) => {};
static String prefixForUri(Map<String, String> schemaRegistryPrefixes, String nsUri) => schemaRegistryPrefixes.entries.firstWhereOrNull((kv) => kv.value == nsUri)?.key ?? '';
}
class XmpProp implements Comparable<XmpProp> {

View file

@ -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<String, String> rawProps) : super(Namespaces.crs, nsPrefix, rawProps);
XmpCrsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.crs);
@override
late final List<XmpCardData> cards = [

View file

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

View file

@ -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<String, String> rawProps) : super(nsUri, nsPrefix, rawProps);
XmpGoogleNamespace({
required super.nsUri,
required super.schemaRegistryPrefixes,
required super.rawProps,
});
List<Tuple2<String, String>> get dataProps;
@ -53,14 +57,34 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
}
class XmpGAudioNamespace extends XmpGoogleNamespace {
const XmpGAudioNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gAudio, nsPrefix, rawProps);
XmpGAudioNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gAudio);
@override
List<Tuple2<String, String>> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')];
List<Tuple2<String, String>> get dataProps => [
Tuple2('${nsPrefix}Data', '${nsPrefix}Mime'),
];
}
class XmpGCameraNamespace extends XmpGoogleNamespace {
XmpGCameraNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gCamera);
@override
List<Tuple2<String, String>> 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<XmpCardData> cards = [
XmpCardData(RegExp(nsPrefix + r'Directory\[(\d+)\]/' + nsPrefix + r'Item/(.*)'), title: 'Directory Item'),
];
}
class XmpGDepthNamespace extends XmpGoogleNamespace {
const XmpGDepthNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gDepth, nsPrefix, rawProps);
XmpGDepthNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gDepth);
@override
List<Tuple2<String, String>> get dataProps => [
@ -70,8 +94,16 @@ class XmpGDepthNamespace extends XmpGoogleNamespace {
}
class XmpGDeviceNamespace extends XmpNamespace {
XmpGDeviceNamespace(String nsPrefix, Map<String, String> 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<String, String> rawProps) : super(Namespaces.gImage, nsPrefix, rawProps);
XmpGImageNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gImage);
@override
List<Tuple2<String, String>> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')];
}
class XmpContainer extends XmpNamespace {
XmpContainer(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.container, nsPrefix, rawProps);
@override
late final List<XmpCardData> cards = [
XmpCardData(RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)'), title: 'Directory Item'),
List<Tuple2<String, String>> get dataProps => [
Tuple2('${nsPrefix}Data', '${nsPrefix}Mime'),
];
}

View file

@ -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<String, String> rawProps) : super(Namespaces.creatorAtom, nsPrefix, rawProps);
XmpCreatorAtom({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.creatorAtom);
@override
late final List<XmpCardData> cards = [
@ -11,7 +11,7 @@ class XmpCreatorAtom extends XmpNamespace {
}
class XmpDarktableNamespace extends XmpNamespace {
XmpDarktableNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.darktable, nsPrefix, rawProps);
XmpDarktableNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.darktable);
@override
late final List<XmpCardData> cards = [
@ -20,7 +20,7 @@ class XmpDarktableNamespace extends XmpNamespace {
}
class XmpDwcNamespace extends XmpNamespace {
XmpDwcNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.dwc, nsPrefix, rawProps);
XmpDwcNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.dwc);
@override
late final List<XmpCardData> cards = [
@ -37,7 +37,7 @@ class XmpDwcNamespace extends XmpNamespace {
}
class XmpIptcCoreNamespace extends XmpNamespace {
XmpIptcCoreNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps);
XmpIptcCoreNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.iptc4xmpCore);
@override
late final List<XmpCardData> cards = [
@ -46,7 +46,7 @@ class XmpIptcCoreNamespace extends XmpNamespace {
}
class XmpIptc4xmpExtNamespace extends XmpNamespace {
XmpIptc4xmpExtNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps);
XmpIptc4xmpExtNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.iptc4xmpExt);
@override
late final List<XmpCardData> cards = [
@ -55,7 +55,7 @@ class XmpIptc4xmpExtNamespace extends XmpNamespace {
}
class XmpMPNamespace extends XmpNamespace {
XmpMPNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.mp, nsPrefix, rawProps);
XmpMPNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.mp);
@override
late final List<XmpCardData> 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<String, String> rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps);
XmpMgwRegionsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.mwgrs);
@override
late final List<XmpCardData> cards = [
@ -75,7 +75,7 @@ class XmpMgwRegionsNamespace extends XmpNamespace {
}
class XmpPlusNamespace extends XmpNamespace {
XmpPlusNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.plus, nsPrefix, rawProps);
XmpPlusNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.plus);
@override
late final List<XmpCardData> cards = [
@ -86,7 +86,7 @@ class XmpPlusNamespace extends XmpNamespace {
}
class XmpMMNamespace extends XmpNamespace {
XmpMMNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps);
XmpMMNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.xmpMM);
@override
late final List<XmpCardData> cards = [

View file

@ -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<String, String> rawProps) : super(Namespaces.photoshop, nsPrefix, rawProps);
XmpPhotoshopNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.photoshop);
@override
late final List<XmpCardData> cards = [

View file

@ -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<String, String> rawProps) : super(Namespaces.tiff, nsPrefix, rawProps);
XmpTiffNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.tiff);
@override
String formatValue(XmpProp prop) {

View file

@ -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<String, String> rawProps) : super(Namespaces.xmp, nsPrefix, rawProps);
XmpBasicNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.xmp);
@override
late final List<XmpCardData> cards = [

View file

@ -56,9 +56,8 @@ class _XmpDirTileState extends State<XmpDirTile> {
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(

View file

@ -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<EntryPageView> with SingleTickerProvider
late ValueNotifier<ViewState> _viewStateNotifier;
late AvesMagnifierController _magnifierController;
final List<StreamSubscription> _subscriptions = [];
ImageStream? _videoCoverStream;
late ImageStreamListener _videoCoverStreamListener;
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
final ValueNotifier<Widget?> _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<EntryPageView> 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<EntryPageView> 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<EntryPageView> 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,13 +201,24 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar);
return Selector<Settings, Tuple2<bool, bool>>(
selector: (context, s) => Tuple2(s.videoGestureDoubleTapTogglePlay, s.videoGestureSideDoubleTapSeek),
return Selector<Settings, Tuple3<bool, bool, bool>>(
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;
MagnifierDoubleTapCallback? onDoubleTap;
MagnifierGestureScaleStartCallback? onScaleStart;
MagnifierGestureScaleUpdateCallback? onScaleUpdate;
MagnifierGestureScaleEndCallback? onScaleEnd;
if (useTapGesture) {
void _applyAction(EntryAction action, {IconData? Function()? icon}) {
_actionFeedbackChildNotifier.value = DecoratedIcon(
icon?.call() ?? action.getIconData(),
@ -247,8 +237,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
).dispatch(context);
}
MagnifierDoubleTapCallback? _onDoubleTap = useActionGesture
? (alignment) {
onDoubleTap = (alignment) {
final x = alignment.x;
if (seekGesture) {
if (x < sideRatio) {
@ -267,17 +256,67 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
return true;
}
return false;
};
}
: null;
return Stack(
fit: StackFit.expand,
children: [
Stack(
if (useVerticalDragGesture) {
SwipeAction? swipeAction;
var move = Offset.zero;
var dropped = false;
double? startValue;
final valueNotifier = ValueNotifier<double?>(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,
onDoubleTap: _onDoubleTap,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onDoubleTap: onDoubleTap,
child: VideoView(
entry: entry,
controller: videoController,
@ -287,13 +326,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
controller: videoController,
viewStateNotifier: _viewStateNotifier,
),
if (settings.videoShowRawTimedText)
VideoSubtitles(
controller: videoController,
viewStateNotifier: _viewStateNotifier,
debugMode: true,
),
if (useActionGesture)
if (useTapGesture)
ValueListenableBuilder<Widget?>(
valueListenable: _actionFeedbackChildNotifier,
builder: (context, feedbackChild, child) => ActionFeedback(
@ -301,90 +334,36 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
),
),
],
),
_buildVideoCover(
);
if (useVerticalDragGesture) {
videoChild = MagnifierGestureDetectorScope.of(context)!.copyWith(
acceptPointerEvent: MagnifierGestureRecognizer.isYPan,
child: videoChild,
);
}
return Stack(
fit: StackFit.expand,
children: [
videoChild,
VideoCover(
mainEntry: mainEntry,
pageEntry: entry,
magnifierController: _magnifierController,
videoController: videoController,
videoDisplaySize: videoDisplaySize,
onDoubleTap: _onDoubleTap,
),
],
);
},
);
},
);
}
StreamBuilder<VideoStatus> _buildVideoCover({
required AvesVideoController videoController,
required Size videoDisplaySize,
required MagnifierDoubleTapCallback? onDoubleTap,
}) {
// fade out image to ease transition with the player
return StreamBuilder<VideoStatus>(
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<ImageInfo?>(
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<EntryPageView> 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<EntryPageView> 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<EntryPageView> with SingleTickerProvider
}
}
}
typedef MagnifierTapCallback = void Function(Offset childPosition);

View file

@ -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<VideoCover> createState() => _VideoCoverState();
}
class _VideoCoverState extends State<VideoCover> {
ImageStream? _videoCoverStream;
late ImageStreamListener _videoCoverStreamListener;
final ValueNotifier<ImageInfo?> _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<VideoStatus>(
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<ImageInfo?>(
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();
},
),
),
);
},
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<double> get() {
switch (this) {
case SwipeAction.brightness:
return ScreenBrightness().current;
case SwipeAction.volume:
return VolumeController().getVolume();
}
}
Future<void> 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<double?> 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<double?>(
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;
}
}
}

View file

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

View file

@ -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<MagnifierCore> 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<MagnifierCore> 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<MagnifierCore> 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<MagnifierCore> 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<MagnifierCore> 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<MagnifierCore> 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,
);
},

View file

@ -60,8 +60,7 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
() => MagnifierGestureRecognizer(
debugOwner: this,
hitDetector: widget.hitDetector,
validateAxis: scope.axis,
touchSlopFactor: scope.touchSlopFactor,
scope: scope,
doubleTapDetails: doubleTapDetails,
),
(instance) {

View file

@ -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<Axis> validateAxis;
final double touchSlopFactor;
final MagnifierGestureDetectorScope scope;
final ValueNotifier<TapDownDetails?> 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,8 +103,11 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
return;
}
final validateAxis = scope.axis;
final move = _initialFocalPoint! - _currentFocalPoint!;
var shouldMove = false;
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);
@ -115,16 +117,14 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
shouldMove = shouldMoveX;
} 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);
shouldMove = (isXPan(move) && shouldMoveX) || (isYPan(move) && shouldMoveY);
}
} 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;
if (shouldMove || doubleTap) {
@ -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);
}
}

View file

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

View file

@ -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<MagnifierGestureDetectorScope>();
return scope;
}
final List<Axis> 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>();
}
MagnifierGestureDetectorScope copyWith({
List<Axis>? 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;
}
}

View file

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

View file

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

View file

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

View file

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