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="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 ## <a id="v1.7.9"></a>[v1.7.9] - 2023-01-15
### Added ### Added

View file

@ -213,6 +213,15 @@ This change eventually prevents building the app with Flutter v3.3.3.
</intent-filter> </intent-filter>
</receiver> </receiver>
<service
android:name=".MediaPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<service <service
android:name=".AnalysisService" android:name=".AnalysisService"
android:description="@string/analysis_service_description" 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.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.* 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.XMP.getSafeStructField
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
@ -104,7 +104,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
try { try {
container = xmpDirs.firstNotNullOfOrNull { container = xmpDirs.firstNotNullOfOrNull {
val xmpMeta = it.xmpMeta 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) } GoogleDeviceContainer().apply { findItems(xmpMeta) }
} else { } else {
null null

View file

@ -3,7 +3,7 @@ package deckers.thibault.aves.metadata
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.adobe.internal.xmp.XMPMeta 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.metadata.XMP.getSafeStructField
import deckers.thibault.aves.utils.indexOfBytes import deckers.thibault.aves.utils.indexOfBytes
import java.io.DataInputStream import java.io.DataInputStream
@ -15,11 +15,12 @@ class GoogleDeviceContainer {
private val offsets: MutableList<Int> = ArrayList() private val offsets: MutableList<Int> = ArrayList()
fun findItems(xmpMeta: XMPMeta) { 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) { 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 mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(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 length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(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 dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
if (mimeType != null && length != null && dataUri != null) { if (mimeType != null && length != null && dataUri != null) {
items.add( items.add(
GoogleDeviceContainerItem( 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 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 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_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 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 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/" 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 // cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
private val knownDataProps = listOf( private val knownDataProps = listOf(
XMPPropName(GAUDIO_NS_URI, "Data"), XMPPropName(GAUDIO_NS_URI, "Data"),
XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"),
XMPPropName(GIMAGE_NS_URI, "Data"), XMPPropName(GIMAGE_NS_URI, "Data"),
XMPPropName(GDEPTH_NS_URI, "Data"), XMPPropName(GDEPTH_NS_URI, "Data"),
XMPPropName(GDEPTH_NS_URI, "Confidence"), XMPPropName(GDEPTH_NS_URI, "Confidence"),
@ -79,7 +81,8 @@ object XMP {
// google portrait // 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_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_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime") 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()) 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 { fun XMPMeta.countPropArrayItems(prop: XMPPropName): Int {
return countArrayItems(prop.nsUri, prop.toString()) 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> { fun XMPMeta.getPropArrayItemValues(prop: XMPPropName): List<String> {
val schema = prop.nsUri val schema = prop.nsUri
val propName = prop.toString() 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> <resources>
<string name="app_widget_label">Ramka Zdjęcia</string> <string name="app_widget_label">Ramka Zdjęcia</string>
<string name="search_shortcut_short_label">Szukaj</string> <string name="search_shortcut_short_label">Szukaj</string>
<string name="videos_shortcut_short_label">Filmy</string> <string name="videos_shortcut_short_label">Wideo</string>
<string name="analysis_channel_name">Skan mediów</string> <string name="analysis_channel_name">Przeskanuj multimedia</string>
<string name="analysis_service_description">Skan obrazów &amp; filmów</string> <string name="analysis_service_description">Przeskanuj obrazy oraz wideo</string>
<string name="analysis_notification_default_title">Skanowanie mediów</string> <string name="analysis_notification_default_title">Skanowanie multimediów</string>
<string name="analysis_notification_action_stop">Zatrzymaj</string> <string name="analysis_notification_action_stop">Zatrzymaj</string>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="wallpaper">Tapeta</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. <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", "settingsVideoButtonsTile": "Buttons",
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause", "settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward", "settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
"settingsVideoGestureVerticalDragBrightnessVolume": "Swipe up or down to adjust brightness/volume",
"settingsPrivacySectionTitle": "Privacy", "settingsPrivacySectionTitle": "Privacy",
"settingsAllowInstalledAppAccess": "Allow access to app inventory", "settingsAllowInstalledAppAccess": "Allow access to app inventory",

View file

@ -1206,5 +1206,9 @@
"filterLocatedLabel": "Localizado", "filterLocatedLabel": "Localizado",
"@filterLocatedLabel": {}, "@filterLocatedLabel": {},
"filterTaggedLabel": "Etiquetado", "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": "Localisé",
"@filterLocatedLabel": {}, "@filterLocatedLabel": {},
"tooManyItemsErrorDialogMessage": "Réessayez avec moins déléments.", "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": "Dilabel",
"@filterTaggedLabel": {}, "@filterTaggedLabel": {},
"tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.", "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": "위치 있음",
"@filterLocatedLabel": {}, "@filterLocatedLabel": {},
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.", "tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
"@tooManyItemsErrorDialogMessage": {} "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
} }

View file

@ -1265,7 +1265,7 @@
"count": {} "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": { "@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1338,5 +1338,33 @@
"albumTierSpecial": "Ofte åpnet", "albumTierSpecial": "Ofte åpnet",
"@albumTierSpecial": {}, "@albumTierSpecial": {},
"editEntryLocationDialogTitle": "Plassering", "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": {}, "@resetTooltip": {},
"pickTooltip": "Wybierz", "pickTooltip": "Wybierz",
"@pickTooltip": {}, "@pickTooltip": {},
"doubleBackExitMessage": "Tapnij ponownie „wstecz” aby wyjść.", "doubleBackExitMessage": "Dotknij ponownie „wstecz”, aby wyjść.",
"@doubleBackExitMessage": {}, "@doubleBackExitMessage": {},
"saveTooltip": "Zapisz", "saveTooltip": "Zapisz",
"@saveTooltip": {}, "@saveTooltip": {},
@ -29,7 +29,7 @@
"@appName": {}, "@appName": {},
"welcomeMessage": "Witaj w Aves", "welcomeMessage": "Witaj w Aves",
"@welcomeMessage": {}, "@welcomeMessage": {},
"welcomeOptional": "Opcjonalny", "welcomeOptional": "Opcjonalnie",
"@welcomeOptional": {}, "@welcomeOptional": {},
"welcomeTermsToggle": "Akceptuję warunki i zasady", "welcomeTermsToggle": "Akceptuję warunki i zasady",
"@welcomeTermsToggle": {}, "@welcomeTermsToggle": {},
@ -99,9 +99,9 @@
"@entryInfoActionEditTitleDescription": {}, "@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Edytuj ocenę", "entryInfoActionEditRating": "Edytuj ocenę",
"@entryInfoActionEditRating": {}, "@entryInfoActionEditRating": {},
"entryInfoActionEditTags": "Edytuj tagi", "entryInfoActionEditTags": "Edytuj znaczniki",
"@entryInfoActionEditTags": {}, "@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Usuń metada", "entryInfoActionRemoveMetadata": "Usuń metadane",
"@entryInfoActionRemoveMetadata": {}, "@entryInfoActionRemoveMetadata": {},
"filterBinLabel": "Kosz", "filterBinLabel": "Kosz",
"@filterBinLabel": {}, "@filterBinLabel": {},
@ -111,17 +111,17 @@
"@filterNoDateLabel": {}, "@filterNoDateLabel": {},
"filterNoRatingLabel": "Nieoceniony", "filterNoRatingLabel": "Nieoceniony",
"@filterNoRatingLabel": {}, "@filterNoRatingLabel": {},
"filterNoTagLabel": "Nieoznakowany", "filterNoTagLabel": "Nieoznaczone",
"@filterNoTagLabel": {}, "@filterNoTagLabel": {},
"filterNoTitleLabel": "Bez tytułu", "filterNoTitleLabel": "Bez tytułu",
"@filterNoTitleLabel": {}, "@filterNoTitleLabel": {},
"filterOnThisDayLabel": "Tego dnia", "filterOnThisDayLabel": "Tego dnia",
"@filterOnThisDayLabel": {}, "@filterOnThisDayLabel": {},
"filterRecentlyAddedLabel": "Ostatnio dodany", "filterRecentlyAddedLabel": "Ostatnio dodane",
"@filterRecentlyAddedLabel": {}, "@filterRecentlyAddedLabel": {},
"filterTypeMotionPhotoLabel": "Ruchome Zdjęcie", "filterTypeMotionPhotoLabel": "Ruchome Zdjęcie",
"@filterTypeMotionPhotoLabel": {}, "@filterTypeMotionPhotoLabel": {},
"filterTypePanoramaLabel": "Zdjęcie sferyczne", "filterTypePanoramaLabel": "Zdjęcie panoramiczne",
"@filterTypePanoramaLabel": {}, "@filterTypePanoramaLabel": {},
"entryActionFlip": "Obróć w poziomie", "entryActionFlip": "Obróć w poziomie",
"@entryActionFlip": {}, "@entryActionFlip": {},
@ -171,7 +171,7 @@
"@entryActionSetAs": {}, "@entryActionSetAs": {},
"entryActionAddFavourite": "Dodaj do ulubionych", "entryActionAddFavourite": "Dodaj do ulubionych",
"@entryActionAddFavourite": {}, "@entryActionAddFavourite": {},
"filterNoLocationLabel": "Nieumiejscowiony", "filterNoLocationLabel": "Nieumiejscowione",
"@filterNoLocationLabel": {}, "@filterNoLocationLabel": {},
"filterRatingRejectedLabel": "Odrzucony", "filterRatingRejectedLabel": "Odrzucony",
"@filterRatingRejectedLabel": {}, "@filterRatingRejectedLabel": {},
@ -193,9 +193,9 @@
"@nameConflictStrategySkip": {}, "@nameConflictStrategySkip": {},
"videoLoopModeAlways": "Zawsze", "videoLoopModeAlways": "Zawsze",
"@videoLoopModeAlways": {}, "@videoLoopModeAlways": {},
"filterLocatedLabel": "Usytuowany", "filterLocatedLabel": "Umiejscowione",
"@filterLocatedLabel": {}, "@filterLocatedLabel": {},
"filterTaggedLabel": "Oznaczony", "filterTaggedLabel": "Oznaczone",
"@filterTaggedLabel": {}, "@filterTaggedLabel": {},
"nameConflictStrategyReplace": "Zastąp", "nameConflictStrategyReplace": "Zastąp",
"@nameConflictStrategyReplace": {}, "@nameConflictStrategyReplace": {},
@ -211,9 +211,9 @@
"@filterAspectRatioPortraitLabel": {}, "@filterAspectRatioPortraitLabel": {},
"filterNoAddressLabel": "Brak adresu", "filterNoAddressLabel": "Brak adresu",
"@filterNoAddressLabel": {}, "@filterNoAddressLabel": {},
"videoControlsPlaySeek": "Odtwórz i szukaj do przodu/do tyłu", "videoControlsPlaySeek": "Odtwórz oraz przeszukuj",
"@videoControlsPlaySeek": {}, "@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Otwórz w innym odtwarzaczu", "videoControlsPlayOutside": "Odtwórz innym odtwarzaczem",
"@videoControlsPlayOutside": {}, "@videoControlsPlayOutside": {},
"mapStyleGoogleNormal": "Mapy Google", "mapStyleGoogleNormal": "Mapy Google",
"@mapStyleGoogleNormal": {}, "@mapStyleGoogleNormal": {},
@ -229,7 +229,7 @@
"@nameConflictStrategyRename": {}, "@nameConflictStrategyRename": {},
"mapStyleOsmHot": "Humanitarny OSM", "mapStyleOsmHot": "Humanitarny OSM",
"@mapStyleOsmHot": {}, "@mapStyleOsmHot": {},
"keepScreenOnVideoPlayback": "Podczas odtwarzania wideo", "keepScreenOnVideoPlayback": "Przy odtwarzaniu wideo",
"@keepScreenOnVideoPlayback": {}, "@keepScreenOnVideoPlayback": {},
"displayRefreshRatePreferLowest": "Najniższa", "displayRefreshRatePreferLowest": "Najniższa",
"@displayRefreshRatePreferLowest": {}, "@displayRefreshRatePreferLowest": {},
@ -327,11 +327,11 @@
"@filterTypeGeotiffLabel": {}, "@filterTypeGeotiffLabel": {},
"filterMimeImageLabel": "Obraz", "filterMimeImageLabel": "Obraz",
"@filterMimeImageLabel": {}, "@filterMimeImageLabel": {},
"unitSystemImperial": "Imperialny", "unitSystemImperial": "Imperialne",
"@unitSystemImperial": {}, "@unitSystemImperial": {},
"videoLoopModeNever": "Nigdy", "videoLoopModeNever": "Nigdy",
"@videoLoopModeNever": {}, "@videoLoopModeNever": {},
"videoControlsNone": "Nic", "videoControlsNone": "Brak",
"@videoControlsNone": {}, "@videoControlsNone": {},
"accessibilityAnimationsRemove": "Zapobiegaj efektom ekranu", "accessibilityAnimationsRemove": "Zapobiegaj efektom ekranu",
"@accessibilityAnimationsRemove": {}, "@accessibilityAnimationsRemove": {},
@ -341,7 +341,7 @@
"@displayRefreshRatePreferHighest": {}, "@displayRefreshRatePreferHighest": {},
"keepScreenOnNever": "Nigdy", "keepScreenOnNever": "Nigdy",
"@keepScreenOnNever": {}, "@keepScreenOnNever": {},
"keepScreenOnViewerOnly": "Tylko na stronie przeglądarki", "keepScreenOnViewerOnly": "Na stronie przeglądarki",
"@keepScreenOnViewerOnly": {}, "@keepScreenOnViewerOnly": {},
"videoPlaybackWithSound": "Odtwarzaj z dźwiękiem", "videoPlaybackWithSound": "Odtwarzaj z dźwiękiem",
"@videoPlaybackWithSound": {}, "@videoPlaybackWithSound": {},
@ -359,7 +359,7 @@
"@coordinateDmsEast": {}, "@coordinateDmsEast": {},
"coordinateDmsWest": "Z", "coordinateDmsWest": "Z",
"@coordinateDmsWest": {}, "@coordinateDmsWest": {},
"unitSystemMetric": "Metryczny", "unitSystemMetric": "Metryczne",
"@unitSystemMetric": {}, "@unitSystemMetric": {},
"videoControlsPlay": "Odtwórz", "videoControlsPlay": "Odtwórz",
"@videoControlsPlay": {}, "@videoControlsPlay": {},
@ -375,7 +375,7 @@
"@widgetOpenPageViewer": {}, "@widgetOpenPageViewer": {},
"albumTierNew": "Nowy", "albumTierNew": "Nowy",
"@albumTierNew": {}, "@albumTierNew": {},
"albumTierSpecial": "Wspólny", "albumTierSpecial": "Wspólne",
"@albumTierSpecial": {}, "@albumTierSpecial": {},
"albumTierApps": "Aplikacje", "albumTierApps": "Aplikacje",
"@albumTierApps": {}, "@albumTierApps": {},
@ -617,7 +617,7 @@
}, },
"collectionEmptyFavourites": "Brak ulubionych", "collectionEmptyFavourites": "Brak ulubionych",
"@collectionEmptyFavourites": {}, "@collectionEmptyFavourites": {},
"collectionEmptyVideos": "Brak filmów", "collectionEmptyVideos": "Brak wideo",
"@collectionEmptyVideos": {}, "@collectionEmptyVideos": {},
"sortByDate": "Według daty", "sortByDate": "Według daty",
"@sortByDate": {}, "@sortByDate": {},
@ -775,7 +775,7 @@
"@albumPickPageTitleCopy": {}, "@albumPickPageTitleCopy": {},
"albumPickPageTitleExport": "Wyeksportuj do albumu", "albumPickPageTitleExport": "Wyeksportuj do albumu",
"@albumPickPageTitleExport": {}, "@albumPickPageTitleExport": {},
"tagEmpty": "Bez znaczników", "tagEmpty": "Brak znaczników",
"@tagEmpty": {}, "@tagEmpty": {},
"searchCountriesSectionTitle": "Kraje", "searchCountriesSectionTitle": "Kraje",
"@searchCountriesSectionTitle": {}, "@searchCountriesSectionTitle": {},
@ -1003,7 +1003,7 @@
"@settingsPrivacySectionTitle": {}, "@settingsPrivacySectionTitle": {},
"settingsAllowInstalledAppAccess": "Zezwól na dostęp do spisu aplikacji", "settingsAllowInstalledAppAccess": "Zezwól na dostęp do spisu aplikacji",
"@settingsAllowInstalledAppAccess": {}, "@settingsAllowInstalledAppAccess": {},
"settingsAllowErrorReporting": "Pozwól na anonimowe zgłaszanie błędów", "settingsAllowErrorReporting": "Zezwól na anonimowe zgłaszanie błędów",
"@settingsAllowErrorReporting": {}, "@settingsAllowErrorReporting": {},
"settingsSaveSearchHistory": "Zapisz historię wyszukiwania", "settingsSaveSearchHistory": "Zapisz historię wyszukiwania",
"@settingsSaveSearchHistory": {}, "@settingsSaveSearchHistory": {},
@ -1045,7 +1045,7 @@
"@filePickerUseThisFolder": {}, "@filePickerUseThisFolder": {},
"mapEmptyRegion": "Brak obrazów w tym regionie", "mapEmptyRegion": "Brak obrazów w tym regionie",
"@mapEmptyRegion": {}, "@mapEmptyRegion": {},
"settingsKeepScreenOnTile": "Pozostaw ekran załączony", "settingsKeepScreenOnTile": "Pozostaw ekran włączony",
"@settingsKeepScreenOnTile": {}, "@settingsKeepScreenOnTile": {},
"filePickerOpenFrom": "Otwórz z", "filePickerOpenFrom": "Otwórz z",
"@filePickerOpenFrom": {}, "@filePickerOpenFrom": {},
@ -1057,7 +1057,7 @@
"@filePickerDoNotShowHiddenFiles": {}, "@filePickerDoNotShowHiddenFiles": {},
"settingsActionImportDialogTitle": "Zaimportuj", "settingsActionImportDialogTitle": "Zaimportuj",
"@settingsActionImportDialogTitle": {}, "@settingsActionImportDialogTitle": {},
"settingsKeepScreenOnDialogTitle": "Pozostaw ekran załączony", "settingsKeepScreenOnDialogTitle": "Pozostaw ekran włączony",
"@settingsKeepScreenOnDialogTitle": {}, "@settingsKeepScreenOnDialogTitle": {},
"settingsNavigationDrawerTile": "Menu nawigacyjne", "settingsNavigationDrawerTile": "Menu nawigacyjne",
"@settingsNavigationDrawerTile": {}, "@settingsNavigationDrawerTile": {},
@ -1083,7 +1083,7 @@
"@settingsVideoEnableHardwareAcceleration": {}, "@settingsVideoEnableHardwareAcceleration": {},
"settingsVideoAutoPlay": "Odtwarzaj automatycznie", "settingsVideoAutoPlay": "Odtwarzaj automatycznie",
"@settingsVideoAutoPlay": {}, "@settingsVideoAutoPlay": {},
"settingsSubtitleThemeSample": "To jest próbka.", "settingsSubtitleThemeSample": "Przykładowy napis.",
"@settingsSubtitleThemeSample": {}, "@settingsSubtitleThemeSample": {},
"settingsSubtitleThemeTextAlignmentDialogTitle": "Dopasowanie tekstu", "settingsSubtitleThemeTextAlignmentDialogTitle": "Dopasowanie tekstu",
"@settingsSubtitleThemeTextAlignmentDialogTitle": {}, "@settingsSubtitleThemeTextAlignmentDialogTitle": {},
@ -1131,7 +1131,7 @@
"@mapStyleTooltip": {}, "@mapStyleTooltip": {},
"mapStyleDialogTitle": "Styl mapy", "mapStyleDialogTitle": "Styl mapy",
"@mapStyleDialogTitle": {}, "@mapStyleDialogTitle": {},
"wallpaperUseScrollEffect": "Użyj efektu przewijania na ekranie głównym", "wallpaperUseScrollEffect": "Używaj efektu przewijania na ekranie głównym",
"@wallpaperUseScrollEffect": {}, "@wallpaperUseScrollEffect": {},
"tagEditorSectionRecent": "Ostatnie", "tagEditorSectionRecent": "Ostatnie",
"@tagEditorSectionRecent": {}, "@tagEditorSectionRecent": {},
@ -1231,7 +1231,7 @@
"@settingsUnitSystemTile": {}, "@settingsUnitSystemTile": {},
"addPathTooltip": "Dodaj ścieżkę", "addPathTooltip": "Dodaj ścieżkę",
"@addPathTooltip": {}, "@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": {}, "@settingsHiddenPathsBanner": {},
"viewerInfoLabelOwner": "Właściciel", "viewerInfoLabelOwner": "Właściciel",
"@viewerInfoLabelOwner": {}, "@viewerInfoLabelOwner": {},
@ -1307,7 +1307,7 @@
"@settingsVideoButtonsTile": {}, "@settingsVideoButtonsTile": {},
"settingsAllowInstalledAppAccessSubtitle": "Używane do poprawy wyświetlania albumu", "settingsAllowInstalledAppAccessSubtitle": "Używane do poprawy wyświetlania albumu",
"@settingsAllowInstalledAppAccessSubtitle": {}, "@settingsAllowInstalledAppAccessSubtitle": {},
"settingsEnableBin": "Użyj kosza", "settingsEnableBin": "Używaj kosza",
"@settingsEnableBin": {}, "@settingsEnableBin": {},
"settingsWidgetShowOutline": "Zarys", "settingsWidgetShowOutline": "Zarys",
"@settingsWidgetShowOutline": {}, "@settingsWidgetShowOutline": {},
@ -1364,5 +1364,9 @@
"settingsSubtitleThemeTile": "Napisy", "settingsSubtitleThemeTile": "Napisy",
"@settingsSubtitleThemeTile": {}, "@settingsSubtitleThemeTile": {},
"openMapPageTooltip": "Wyświetl na mapie", "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": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Nume", "renameProcessorName": "Nume",
"@renameProcessorName": {}, "@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": { "@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "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": { "@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1364,5 +1364,7 @@
"settingsModificationWarningDialogMessage": "Alte setări vor fi modificate.", "settingsModificationWarningDialogMessage": "Alte setări vor fi modificate.",
"@settingsModificationWarningDialogMessage": {}, "@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Interfață Android TV", "settingsDisplayUseTvInterface": "Interfață Android TV",
"@settingsDisplayUseTvInterface": {} "@settingsDisplayUseTvInterface": {},
"tooManyItemsErrorDialogMessage": "Încearcă din nou cu mai puține elemente.",
"@tooManyItemsErrorDialogMessage": {}
} }

View file

@ -569,5 +569,87 @@
"viewDialogLayoutSectionTitle": "เค้าโครง", "viewDialogLayoutSectionTitle": "เค้าโครง",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
"aboutLinkPolicy": "นโยบายความเป็นส่วนตัว", "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": "Etiketli",
"@filterTaggedLabel": {}, "@filterTaggedLabel": {},
"tooManyItemsErrorDialogMessage": "Daha az ögeyle tekrar deneyin.", "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": "Розташований",
"@filterLocatedLabel": {}, "@filterLocatedLabel": {},
"filterTaggedLabel": "Позначений тегом", "filterTaggedLabel": "Позначений тегом",
"@filterTaggedLabel": {} "@filterTaggedLabel": {},
"tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
} }

View file

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

View file

@ -97,6 +97,7 @@ class SettingsDefaults {
static const videoControls = VideoControls.play; static const videoControls = VideoControls.play;
static const videoGestureDoubleTapTogglePlay = false; static const videoGestureDoubleTapTogglePlay = false;
static const videoGestureSideDoubleTapSeek = true; static const videoGestureSideDoubleTapSeek = true;
static const videoGestureVerticalDragBrightnessVolume = false;
// subtitles // subtitles
static const subtitleFontSize = 20.0; static const subtitleFontSize = 20.0;

View file

@ -41,7 +41,6 @@ class Settings extends ChangeNotifier {
static const Set<String> _internalKeys = { static const Set<String> _internalKeys = {
hasAcceptedTermsKey, hasAcceptedTermsKey,
catalogTimeZoneKey, catalogTimeZoneKey,
videoShowRawTimedTextKey,
searchHistoryKey, searchHistoryKey,
platformAccelerometerRotationKey, platformAccelerometerRotationKey,
platformTransitionAnimationScaleKey, platformTransitionAnimationScaleKey,
@ -131,10 +130,10 @@ class Settings extends ChangeNotifier {
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec'; static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
static const videoAutoPlayModeKey = 'video_auto_play_mode'; static const videoAutoPlayModeKey = 'video_auto_play_mode';
static const videoLoopModeKey = 'video_loop'; static const videoLoopModeKey = 'video_loop';
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
static const videoControlsKey = 'video_controls'; static const videoControlsKey = 'video_controls';
static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play'; static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play';
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip'; static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
static const videoGestureVerticalDragBrightnessVolumeKey = 'video_gesture_vertical_drag_brightness_volume';
// subtitles // subtitles
static const subtitleFontSizeKey = 'subtitle_font_size'; static const subtitleFontSizeKey = 'subtitle_font_size';
@ -637,10 +636,6 @@ class Settings extends ChangeNotifier {
set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString()); 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); VideoControls get videoControls => getEnumOrDefault(videoControlsKey, SettingsDefaults.videoControls, VideoControls.values);
set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString()); set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString());
@ -653,6 +648,10 @@ class Settings extends ChangeNotifier {
set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue); set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue);
bool get videoGestureVerticalDragBrightnessVolume => getBool(videoGestureVerticalDragBrightnessVolumeKey) ?? SettingsDefaults.videoGestureVerticalDragBrightnessVolume;
set videoGestureVerticalDragBrightnessVolume(bool newValue) => _set(videoGestureVerticalDragBrightnessVolumeKey, newValue);
// subtitles // subtitles
double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize; double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
@ -1039,6 +1038,7 @@ class Settings extends ChangeNotifier {
case enableVideoHardwareAccelerationKey: case enableVideoHardwareAccelerationKey:
case videoGestureDoubleTapTogglePlayKey: case videoGestureDoubleTapTogglePlayKey:
case videoGestureSideDoubleTapSeekKey: case videoGestureSideDoubleTapSeekKey:
case videoGestureVerticalDragBrightnessVolumeKey:
case subtitleShowOutlineKey: case subtitleShowOutlineKey:
case tagEditorCurrentFilterSectionExpandedKey: case tagEditorCurrentFilterSectionExpandedKey:
case saveSearchHistoryKey: case saveSearchHistoryKey:

View file

@ -14,6 +14,8 @@ class AIcons {
static const IconData aspectRatio = Icons.aspect_ratio_outlined; static const IconData aspectRatio = Icons.aspect_ratio_outlined;
static const IconData bin = Icons.delete_outlined; static const IconData bin = Icons.delete_outlined;
static const IconData broken = Icons.broken_image_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 checked = Icons.done_outlined;
static const IconData count = MdiIcons.counter; static const IconData count = MdiIcons.counter;
static const IconData counter = Icons.plus_one_outlined; 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 text = Icons.format_quote_outlined;
static const IconData tag = Icons.local_offer_outlined; static const IconData tag = Icons.local_offer_outlined;
static const IconData tagUntagged = MdiIcons.tagOffOutline; static const IconData tagUntagged = MdiIcons.tagOffOutline;
static const IconData volumeMin = Icons.volume_mute_outlined;
static const IconData volumeMax = Icons.volume_up_outlined;
// view // view
static const IconData group = Icons.group_work_outlined; 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', 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', 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 = [ 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 avm = 'http://www.communicatingastronomy.org/avm/1.0/';
static const camera = 'http://pix4d.com/camera/1.0/'; static const camera = 'http://pix4d.com/camera/1.0/';
static const cc = 'http://creativecommons.org/ns#'; 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 creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/';
static const crd = 'http://ns.adobe.com/camera-raw-defaults/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/'; 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 exifEx = 'http://cipa.jp/exif/1.0/';
static const gAudio = 'http://ns.google.com/photos/1.0/audio/'; static const gAudio = 'http://ns.google.com/photos/1.0/audio/';
static const gCamera = 'http://ns.google.com/photos/1.0/camera/'; 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 gCreations = 'http://ns.google.com/photos/1.0/creations/';
static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/'; 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 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 gFocus = 'http://ns.google.com/photos/1.0/focus/';
static const gImage = 'http://ns.google.com/photos/1.0/image/'; static const gImage = 'http://ns.google.com/photos/1.0/image/';
static const gPano = 'http://ns.google.com/photos/1.0/panorama/'; static const gPano = 'http://ns.google.com/photos/1.0/panorama/';
@ -83,7 +86,6 @@ class Namespaces {
avm: 'Astronomy Visualization', avm: 'Astronomy Visualization',
camera: 'Pix4D Camera', camera: 'Pix4D Camera',
cc: 'Creative Commons', cc: 'Creative Commons',
container: 'Container',
crd: 'Camera Raw Defaults', crd: 'Camera Raw Defaults',
creatorAtom: 'After Effects', creatorAtom: 'After Effects',
crs: 'Camera Raw Settings', crs: 'Camera Raw Settings',
@ -97,6 +99,7 @@ class Namespaces {
exifEx: 'Exif Ex', exifEx: 'Exif Ex',
gAudio: 'Google Audio', gAudio: 'Google Audio',
gCamera: 'Google Camera', gCamera: 'Google Camera',
gContainer: 'Google Container',
gCreations: 'Google Creations', gCreations: 'Google Creations',
gDepth: 'Google Depth', gDepth: 'Google Depth',
gDevice: 'Google Device', gDevice: 'Google Device',
@ -138,7 +141,7 @@ class Namespaces {
}; };
static final defaultPrefixes = { static final defaultPrefixes = {
container: 'Container', gContainer: 'Container',
dc: 'dc', dc: 'dc',
gCamera: 'GCamera', gCamera: 'GCamera',
microsoftPhoto: 'MicrosoftPhoto', 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/licenses.dart';
import 'package:aves/widgets/about/translators.dart'; import 'package:aves/widgets/about/translators.dart';
import 'package:aves/widgets/common/basic/insets.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/scope.dart';
import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -28,7 +29,8 @@ class AboutPage extends StatelessWidget {
sliver: SliverList( sliver: SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
AppReference(showLogo: !useTvLayout), const TvEdgeFocus(),
const AppReference(),
if (!settings.useTvLayout) ...[ if (!settings.useTvLayout) ...[
const Divider(), const Divider(),
const BugReport(), const BugReport(),

View file

@ -10,12 +10,7 @@ import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
class AppReference extends StatefulWidget { class AppReference extends StatefulWidget {
final bool showLogo; const AppReference({super.key});
const AppReference({
super.key,
required this.showLogo,
});
@override @override
State<AppReference> createState() => _AppReferenceState(); State<AppReference> createState() => _AppReferenceState();
@ -24,6 +19,13 @@ class AppReference extends StatefulWidget {
class _AppReferenceState extends State<AppReference> { class _AppReferenceState extends State<AppReference> {
late Future<PackageInfo> _packageInfoLoader; late Future<PackageInfo> _packageInfoLoader;
static const _appTitleStyle = TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
letterSpacing: 1.0,
fontFeatures: [FontFeature.enable('smcp')],
);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -44,28 +46,19 @@ class _AppReferenceState extends State<AppReference> {
} }
Widget _buildAvesLine() { Widget _buildAvesLine() {
const style = TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
letterSpacing: 1.0,
fontFeatures: [FontFeature.enable('smcp')],
);
return FutureBuilder<PackageInfo>( return FutureBuilder<PackageInfo>(
future: _packageInfoLoader, future: _packageInfoLoader,
builder: (context, snapshot) { builder: (context, snapshot) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.showLogo) ...[ AvesLogo(
AvesLogo( size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3,
size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, ),
), const SizedBox(width: 8),
const SizedBox(width: 8),
],
Text( Text(
'${context.l10n.appName} ${snapshot.data?.version}', '${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/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -14,13 +14,7 @@ class AboutCredits extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( AboutSectionTitle(text: l10n.aboutCreditsSectionTitle),
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.aboutCreditsSectionTitle, style: Constants.knownTitleTextStyle),
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text.rich( Text.rich(
TextSpan( TextSpan(

View file

@ -1,8 +1,9 @@
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/ref/brand_colors.dart'; import 'package:aves/ref/brand_colors.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/utils/dependencies.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/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
@ -50,30 +51,32 @@ class _LicensesState extends State<Licenses> {
[ [
_buildHeader(), _buildHeader(),
const SizedBox(height: 16), const SizedBox(height: 16),
AvesExpansionTile( if (!settings.useTvLayout) ...[
title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, AvesExpansionTile(
highlightColor: colors.fromBrandColor(BrandColors.android), title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle,
expandedNotifier: _expandedNotifier, highlightColor: colors.fromBrandColor(BrandColors.android),
children: _platform.map((package) => LicenseRow(package: package)).toList(), expandedNotifier: _expandedNotifier,
), children: _platform.map((package) => LicenseRow(package: package)).toList(),
AvesExpansionTile( ),
title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, AvesExpansionTile(
highlightColor: colors.fromBrandColor(BrandColors.flutter), title: context.l10n.aboutLicensesFlutterPluginsSectionTitle,
expandedNotifier: _expandedNotifier, highlightColor: colors.fromBrandColor(BrandColors.flutter),
children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), expandedNotifier: _expandedNotifier,
), children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(),
AvesExpansionTile( ),
title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, AvesExpansionTile(
highlightColor: colors.fromBrandColor(BrandColors.flutter), title: context.l10n.aboutLicensesFlutterPackagesSectionTitle,
expandedNotifier: _expandedNotifier, highlightColor: colors.fromBrandColor(BrandColors.flutter),
children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), expandedNotifier: _expandedNotifier,
), children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(),
AvesExpansionTile( ),
title: context.l10n.aboutLicensesDartPackagesSectionTitle, AvesExpansionTile(
highlightColor: colors.fromBrandColor(BrandColors.flutter), title: context.l10n.aboutLicensesDartPackagesSectionTitle,
expandedNotifier: _expandedNotifier, highlightColor: colors.fromBrandColor(BrandColors.flutter),
children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), expandedNotifier: _expandedNotifier,
), children: _dartPackages.map((package) => LicenseRow(package: package)).toList(),
),
],
Center( Center(
child: AvesOutlinedButton( child: AvesOutlinedButton(
label: context.l10n.aboutLicensesShowAllButtonLabel, label: context.l10n.aboutLicensesShowAllButtonLabel,
@ -104,13 +107,7 @@ class _LicensesState extends State<Licenses> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( AboutSectionTitle(text: context.l10n.aboutLicensesSectionTitle),
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(context.l10n.aboutLicensesSectionTitle, style: Constants.knownTitleTextStyle),
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(context.l10n.aboutLicensesBanner), 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/basic/markdown_container.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -14,6 +15,7 @@ class PolicyPage extends StatefulWidget {
class _PolicyPageState extends State<PolicyPage> { class _PolicyPageState extends State<PolicyPage> {
late Future<String> _termsLoader; late Future<String> _termsLoader;
final ScrollController _scrollController = ScrollController();
static const termsPath = 'assets/terms.md'; static const termsPath = 'assets/terms.md';
static const termsDirection = TextDirection.ltr; static const termsDirection = TextDirection.ltr;
@ -28,26 +30,72 @@ class _PolicyPageState extends State<PolicyPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout,
title: Text(context.l10n.policyPageTitle), title: Text(context.l10n.policyPageTitle),
), ),
body: SafeArea( body: SafeArea(
child: Center( child: FocusableActionDetector(
child: FutureBuilder<String>( autofocus: true,
future: _termsLoader, shortcuts: const {
builder: (context, snapshot) { SingleActivator(LogicalKeyboardKey.arrowUp): _ScrollIntent.up(),
if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); SingleActivator(LogicalKeyboardKey.arrowDown): _ScrollIntent.down(),
final terms = snapshot.data!; },
return Padding( actions: {
padding: const EdgeInsets.symmetric(vertical: 8), _ScrollIntent: CallbackAction<_ScrollIntent>(onInvoke: _onScrollIntent),
child: MarkdownContainer( },
data: terms, child: Center(
textDirection: termsDirection, child: FutureBuilder<String>(
), future: _termsLoader,
); builder: (context, snapshot) {
}, if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox();
final terms = snapshot.data!;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: MarkdownContainer(
scrollController: _scrollController,
data: terms,
textDirection: termsDirection,
),
);
},
),
), ),
), ),
), ),
); );
} }
void _onScrollIntent(_ScrollIntent intent) {
late int factor;
switch (intent.type) {
case _ScrollDirection.up:
factor = -1;
break;
case _ScrollDirection.down:
factor = 1;
break;
}
_scrollController.animateTo(
_scrollController.offset + factor * 150,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
);
}
}
class _ScrollIntent extends Intent {
const _ScrollIntent({
required this.type,
});
const _ScrollIntent.up() : type = _ScrollDirection.up;
const _ScrollIntent.down() : type = _ScrollDirection.down;
final _ScrollDirection type;
}
enum _ScrollDirection {
up,
down,
} }

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 'dart:math';
import 'package:aves/utils/constants.dart'; 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/basic/text/change_highlight.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -50,27 +51,21 @@ class AboutTranslators extends StatelessWidget {
// Contributor('slasb37', 'p84haghi@gmail.com'), // Persian // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian
// Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk
// Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai // Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
}; };
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( AboutSectionTitle(text: context.l10n.aboutTranslatorsSectionTitle),
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.aboutTranslatorsSectionTitle, style: Constants.knownTitleTextStyle),
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
_RandomTextSpanHighlighter( _RandomTextSpanHighlighter(
spans: translators.map((v) => v.name).toList(), spans: translators.map((v) => v.name).toList(),
highlightColor: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).colorScheme.onPrimary,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
@ -81,11 +76,11 @@ class AboutTranslators extends StatelessWidget {
class _RandomTextSpanHighlighter extends StatefulWidget { class _RandomTextSpanHighlighter extends StatefulWidget {
final List<String> spans; final List<String> spans;
final Color highlightColor; final Color color;
const _RandomTextSpanHighlighter({ const _RandomTextSpanHighlighter({
required this.spans, required this.spans,
required this.highlightColor, required this.color,
}); });
@override @override
@ -102,18 +97,21 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter>
void initState() { void initState() {
super.initState(); super.initState();
final color = widget.color;
_baseStyle = TextStyle( _baseStyle = TextStyle(
color: color.withOpacity(.7),
shadows: [ shadows: [
Shadow( Shadow(
color: widget.highlightColor.withOpacity(0), color: color.withOpacity(0),
blurRadius: 0, blurRadius: 0,
) )
], ],
); );
final highlightStyle = TextStyle( final highlightStyle = TextStyle(
color: color.withOpacity(1),
shadows: [ shadows: [
Shadow( Shadow(
color: widget.highlightColor, color: color.withOpacity(1),
blurRadius: 3, blurRadius: 3,
) )
], ],
@ -132,7 +130,7 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter>
..repeat(reverse: true); ..repeat(reverse: true);
_animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(CurvedAnimation( _animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.linear, curve: Curves.easeInOutCubic,
)); ));
} }

View file

@ -54,7 +54,7 @@ class AvesApp extends StatefulWidget {
final AppFlavor flavor; final AppFlavor flavor;
// temporary exclude locales not ready yet for prime time // 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 List<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList();
static final ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); static final ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero);
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator');
@ -540,6 +540,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
? 'profile' ? 'profile'
: 'debug', : 'debug',
'has_mobile_services': mobileServices.isServiceAvailable, 'has_mobile_services': mobileServices.isServiceAvailable,
'is_television': device.isTelevision,
'locales': WidgetsBinding.instance.window.locales.join(', '), 'locales': WidgetsBinding.instance.window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
}); });

View file

@ -58,6 +58,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
}) { }) {
final canWrite = !settings.isReadOnly; final canWrite = !settings.isReadOnly;
final isMain = appMode == AppMode.main; final isMain = appMode == AppMode.main;
final useTvLayout = settings.useTvLayout;
switch (action) { switch (action) {
// general // general
case EntrySetAction.configureView: case EntrySetAction.configureView:
@ -70,9 +71,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
return isSelecting && selectedItemCount == itemCount; return isSelecting && selectedItemCount == itemCount;
// browsing // browsing
case EntrySetAction.searchCollection: case EntrySetAction.searchCollection:
return !settings.useTvLayout && appMode.canNavigate && !isSelecting; return !useTvLayout && appMode.canNavigate && !isSelecting;
case EntrySetAction.toggleTitleSearch: case EntrySetAction.toggleTitleSearch:
return !isSelecting; return !useTvLayout && !isSelecting;
case EntrySetAction.addShortcut: case EntrySetAction.addShortcut:
return isMain && !isSelecting && device.canPinShortcut && !isTrash; return isMain && !isSelecting && device.canPinShortcut && !isTrash;
case EntrySetAction.emptyBin: case EntrySetAction.emptyBin:
@ -83,7 +84,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.stats: case EntrySetAction.stats:
return isMain; return isMain;
case EntrySetAction.rescan: case EntrySetAction.rescan:
return !settings.useTvLayout && isMain && !isTrash; return !useTvLayout && isMain && !isTrash;
// selecting // selecting
case EntrySetAction.share: case EntrySetAction.share:
case EntrySetAction.toggleFavourite: 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/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_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:aves/widgets/viewer/notifications.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -6,11 +6,13 @@ import 'package:flutter_markdown/flutter_markdown.dart';
class MarkdownContainer extends StatelessWidget { class MarkdownContainer extends StatelessWidget {
final String data; final String data;
final TextDirection? textDirection; final TextDirection? textDirection;
final ScrollController? scrollController;
const MarkdownContainer({ const MarkdownContainer({
super.key, super.key,
required this.data, required this.data,
this.textDirection, this.textDirection,
this.scrollController,
}); });
static const double maxWidth = 460; static const double maxWidth = 460;
@ -44,6 +46,7 @@ class MarkdownContainer extends StatelessWidget {
data: data, data: data,
selectable: true, selectable: true,
onTapLink: (text, href, title) => AvesApp.launchUrl(href), onTapLink: (text, href, title) => AvesApp.launchUrl(href),
controller: scrollController,
shrinkWrap: true, 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/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
@ -31,26 +32,49 @@ class TitledExpandableFilterRow extends StatelessWidget {
final isExpanded = expandedNotifier.value == title; final isExpanded = expandedNotifier.value == title;
Widget header = Text(
title,
style: Constants.knownTitleTextStyle,
);
void toggle() => expandedNotifier.value = isExpanded ? null : title;
if (settings.useTvLayout) {
header = Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: InkWell(
onTap: toggle,
borderRadius: const BorderRadius.all(Radius.circular(123)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
header,
],
),
),
),
);
} else {
header = Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
header,
const Spacer(),
IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: toggle,
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
),
],
),
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( header,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
title,
style: Constants.knownTitleTextStyle,
),
const Spacer(),
IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: () => expandedNotifier.value = isExpanded ? null : title,
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
),
],
),
),
ExpandableFilterRow( ExpandableFilterRow(
filters: filters, filters: filters,
isExpanded: isExpanded, isExpanded: isExpanded,

View file

@ -34,18 +34,10 @@ class SectionHeader<T> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = _buildContent(context); Widget child = _buildContent(context);
if (settings.useTvLayout) { if (settings.useTvLayout) {
final primaryColor = Theme.of(context).colorScheme.primary; child = InkWell(
child = Material( onTap: _onTap(context),
type: MaterialType.transparency, borderRadius: const BorderRadius.all(Radius.circular(123)),
child: InkResponse( child: child,
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( return Container(

View file

@ -8,7 +8,7 @@ class CaptionedButton extends StatefulWidget {
final Animation<double> scale; final Animation<double> scale;
final Widget captionText; final Widget captionText;
final CaptionedIconButtonBuilder iconButtonBuilder; final CaptionedIconButtonBuilder iconButtonBuilder;
final bool showCaption; final bool autofocus, showCaption;
final VoidCallback? onPressed; final VoidCallback? onPressed;
static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8);
@ -21,6 +21,7 @@ class CaptionedButton extends StatefulWidget {
CaptionedIconButtonBuilder? iconButtonBuilder, CaptionedIconButtonBuilder? iconButtonBuilder,
String? caption, String? caption,
Widget? captionText, Widget? captionText,
this.autofocus = false,
this.showCaption = true, this.showCaption = true,
required this.onPressed, required this.onPressed,
}) : assert(icon != null || iconButtonBuilder != null), }) : assert(icon != null || iconButtonBuilder != null),
@ -57,6 +58,7 @@ class CaptionedButton extends StatefulWidget {
class _CaptionedButtonState extends State<CaptionedButton> { class _CaptionedButtonState extends State<CaptionedButton> {
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
final ValueNotifier<bool> _focusedNotifier = ValueNotifier(false); final ValueNotifier<bool> _focusedNotifier = ValueNotifier(false);
bool _didAutofocus = false;
@override @override
void initState() { void initState() {
@ -65,12 +67,21 @@ class _CaptionedButtonState extends State<CaptionedButton> {
_focusNode.addListener(_onFocusChanged); _focusNode.addListener(_onFocusChanged);
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_handleAutofocus();
}
@override @override
void didUpdateWidget(covariant CaptionedButton oldWidget) { void didUpdateWidget(covariant CaptionedButton oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.onPressed != widget.onPressed) { if (oldWidget.onPressed != widget.onPressed) {
_updateTraversal(); _updateTraversal();
} }
if (oldWidget.autofocus != widget.autofocus) {
_handleAutofocus();
}
} }
@override @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 _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus;
void _updateTraversal() { void _updateTraversal() {

View file

@ -18,6 +18,9 @@ abstract class AvesSearchDelegate extends SearchDelegate {
query = initialQuery ?? ''; query = initialQuery ?? '';
} }
@mustCallSuper
void dispose() {}
@override @override
Widget? buildLeading(BuildContext context) { Widget? buildLeading(BuildContext context) {
if (settings.useTvLayout) { if (settings.useTvLayout) {
@ -44,7 +47,7 @@ abstract class AvesSearchDelegate extends SearchDelegate {
@override @override
List<Widget>? buildActions(BuildContext context) { List<Widget>? buildActions(BuildContext context) {
return [ return [
if (query.isNotEmpty) if (!settings.useTvLayout && query.isNotEmpty)
IconButton( IconButton(
icon: const Icon(AIcons.clear), icon: const Icon(AIcons.clear),
onPressed: () { onPressed: () {
@ -63,28 +66,40 @@ abstract class AvesSearchDelegate extends SearchDelegate {
void clean() { void clean() {
currentBody = null; currentBody = null;
focusNode?.unfocus(); searchFieldFocusNode?.unfocus();
} }
// adapted from Flutter `SearchDelegate` in `/material/search.dart` // adapted from Flutter `SearchDelegate` in `/material/search.dart`
@override @override
void showResults(BuildContext context) { void showResults(BuildContext context) {
focusNode?.unfocus(); if (settings.useTvLayout) {
currentBody = SearchBody.results; suggestionsScrollController?.jumpTo(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
suggestionsFocusNode?.requestFocus();
FocusScope.of(context).nextFocus();
});
} else {
searchFieldFocusNode?.unfocus();
currentBody = SearchBody.results;
}
} }
@override @override
void showSuggestions(BuildContext context) { void showSuggestions(BuildContext context) {
assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); assert(searchFieldFocusNode != null, '_focusNode must be set by route before showSuggestions is called.');
focusNode!.requestFocus(); searchFieldFocusNode!.requestFocus();
currentBody = SearchBody.suggestions; currentBody = SearchBody.suggestions;
} }
@override @override
Animation<double> get transitionAnimation => proxyAnimation; Animation<double> get transitionAnimation => proxyAnimation;
FocusNode? focusNode; FocusNode? searchFieldFocusNode;
FocusNode? get suggestionsFocusNode => null;
ScrollController? get suggestionsScrollController => null;
final TextEditingController queryTextController = TextEditingController(); final TextEditingController queryTextController = TextEditingController();

View file

@ -29,7 +29,7 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> { class _SearchPageState extends State<SearchPage> {
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
final FocusNode _focusNode = FocusNode(); final FocusNode _searchFieldFocusNode = FocusNode();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override @override
@ -37,7 +37,7 @@ class _SearchPageState extends State<SearchPage> {
super.initState(); super.initState();
_registerWidget(widget); _registerWidget(widget);
widget.animation.addStatusListener(_onAnimationStatusChanged); widget.animation.addStatusListener(_onAnimationStatusChanged);
_focusNode.addListener(_onFocusChanged); _searchFieldFocusNode.addListener(_onFocusChanged);
} }
@override @override
@ -53,21 +53,22 @@ class _SearchPageState extends State<SearchPage> {
void dispose() { void dispose() {
_unregisterWidget(widget); _unregisterWidget(widget);
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
_focusNode.dispose(); _searchFieldFocusNode.dispose();
_doubleBackPopHandler.dispose(); _doubleBackPopHandler.dispose();
widget.delegate.dispose();
super.dispose(); super.dispose();
} }
void _registerWidget(SearchPage widget) { void _registerWidget(SearchPage widget) {
widget.delegate.queryTextController.addListener(_onQueryChanged); widget.delegate.queryTextController.addListener(_onQueryChanged);
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
widget.delegate.focusNode = _focusNode; widget.delegate.searchFieldFocusNode = _searchFieldFocusNode;
} }
void _unregisterWidget(SearchPage widget) { void _unregisterWidget(SearchPage widget) {
widget.delegate.queryTextController.removeListener(_onQueryChanged); widget.delegate.queryTextController.removeListener(_onQueryChanged);
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate.focusNode = null; widget.delegate.searchFieldFocusNode = null;
} }
void _onAnimationStatusChanged(AnimationStatus status) { void _onAnimationStatusChanged(AnimationStatus status) {
@ -77,12 +78,12 @@ class _SearchPageState extends State<SearchPage> {
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) {
if (!mounted) return; if (!mounted) return;
_focusNode.requestFocus(); _searchFieldFocusNode.requestFocus();
}); });
} }
void _onFocusChanged() { void _onFocusChanged() {
if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) { if (_searchFieldFocusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
widget.delegate.showSuggestions(context); widget.delegate.showSuggestions(context);
} }
} }
@ -136,7 +137,7 @@ class _SearchPageState extends State<SearchPage> {
style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]),
child: TextField( child: TextField(
controller: widget.delegate.queryTextController, controller: widget.delegate.queryTextController,
focusNode: _focusNode, focusNode: _searchFieldFocusNode,
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
hintText: widget.delegate.searchFieldLabel, hintText: widget.delegate.searchFieldLabel,

View file

@ -43,11 +43,6 @@ class DebugSettingsSection extends StatelessWidget {
onChanged: (v) => settings.canUseAnalysisService = v, onChanged: (v) => settings.canUseAnalysisService = v,
title: const Text('canUseAnalysisService'), title: const Text('canUseAnalysisService'),
), ),
SwitchListTile(
value: settings.videoShowRawTimedText,
onChanged: (v) => settings.videoShowRawTimedText = v,
title: const Text('videoShowRawTimedText'),
),
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup( child: InfoRowGroup(

View file

@ -135,16 +135,14 @@ class SelectionRadioListTile<T> extends StatelessWidget {
reselectable: true, reselectable: true,
title: Text( title: Text(
title, title,
softWrap: false, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.fade, maxLines: 2,
maxLines: 1,
), ),
subtitle: subtitle != null subtitle: subtitle != null
? Text( ? Text(
subtitle, subtitle,
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
maxLines: 1,
) )
: null, : null,
dense: dense, 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/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.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/identity/empty.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.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, Selection<FilterGridItem<AlbumFilter>> selection,
AlbumChipSetActionDelegate actionDelegate, 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 [ return [
if (widget.moveType != null) if (widget.moveType != null)
IconButton( IconButton(
@ -149,7 +197,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
child: PopupMenuButton<ChipSetAction>( child: PopupMenuButton<ChipSetAction>(
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
FilterGridAppBar.toMenuItem(context, ChipSetAction.configureView, enabled: true), ...ChipSetActions.general.where(isVisible).map((action) => FilterGridAppBar.toMenuItem(context, action, enabled: true)),
const PopupMenuDivider(), const PopupMenuDivider(),
FilterGridAppBar.toMenuItem(context, ChipSetAction.toggleTitleSearch, enabled: true), 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/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/query_bar.dart';
@ -37,8 +38,10 @@ class _AppPickPageState extends State<AppPickPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final useTvLayout = settings.useTvLayout;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: !useTvLayout,
title: Text(context.l10n.appPickDialogTitle), title: Text(context.l10n.appPickDialogTitle),
), ),
body: SafeArea( 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))); final packages = allPackages.where((package) => package.categoryLauncher).toList()..sort((a, b) => compareAsciiUpperCase(_displayName(a), _displayName(b)));
return Column( return Column(
children: [ children: [
QueryBar(queryNotifier: _queryNotifier), if (!useTvLayout) QueryBar(queryNotifier: _queryNotifier),
ValueListenableBuilder<String>( ValueListenableBuilder<String>(
valueListenable: _queryNotifier, valueListenable: _queryNotifier,
builder: (context, query, child) { builder: (context, query, child) {

View file

@ -70,6 +70,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
final selectedItemCount = selectedFilters.length; final selectedItemCount = selectedFilters.length;
final hasSelection = selectedFilters.isNotEmpty; final hasSelection = selectedFilters.isNotEmpty;
final isMain = appMode == AppMode.main; final isMain = appMode == AppMode.main;
final useTvLayout = settings.useTvLayout;
switch (action) { switch (action) {
// general // general
case ChipSetAction.configureView: case ChipSetAction.configureView:
@ -82,9 +83,9 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
return isSelecting && selectedItemCount == itemCount; return isSelecting && selectedItemCount == itemCount;
// browsing // browsing
case ChipSetAction.search: case ChipSetAction.search:
return !settings.useTvLayout && appMode.canNavigate && !isSelecting; return !useTvLayout && appMode.canNavigate && !isSelecting;
case ChipSetAction.toggleTitleSearch: case ChipSetAction.toggleTitleSearch:
return !isSelecting; return !useTvLayout && !isSelecting;
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
return false; return false;
// browsing or selecting // browsing or selecting

View file

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

View file

@ -277,11 +277,12 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
MapAction.zoomIn, MapAction.zoomIn,
MapAction.zoomOut, MapAction.zoomOut,
] ]
.map((action) => Padding( .mapIndexed((i, action) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: CaptionedButton( child: CaptionedButton(
icon: action.getIcon(), icon: action.getIcon(),
caption: action.getText(context), caption: action.getText(context),
autofocus: i == 0,
onPressed: () => MapActionDelegate(_mapController).onActionSelected(context, action), 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/model/source/tag.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/widgets/collection/collection_page.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/expandable_filter_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
@ -33,6 +34,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
final CollectionSource source; final CollectionSource source;
final CollectionLens? parentCollection; final CollectionLens? parentCollection;
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null); 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 const int searchHistoryCount = 10;
static final typeFilters = [ static final typeFilters = [
@ -64,6 +73,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
query = initialQuery ?? ''; query = initialQuery ?? '';
} }
@override
void dispose() {
_expandedSectionNotifier.dispose();
_suggestionsTopFocusNode.dispose();
_suggestionsScrollController.dispose();
super.dispose();
}
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
final upQuery = query.trim().toUpperCase(); final upQuery = query.trim().toUpperCase();
@ -91,8 +108,12 @@ class CollectionSearchDelegate extends AvesSearchDelegate {
final history = settings.searchHistory.where(notHidden).toList(); final history = settings.searchHistory.where(notHidden).toList();
return ListView( return ListView(
controller: _suggestionsScrollController,
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
children: [ children: [
TvEdgeFocus(
focusNode: _suggestionsTopFocusNode,
),
_buildFilterRow( _buildFilterRow(
context: context, context: context,
filters: [ filters: [

View file

@ -1,9 +1,10 @@
import 'package:aves/model/filters/album.dart'; 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/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/outlined_button.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/navigation/drawer/tile.dart';
import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -29,8 +30,10 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
return Column( return Column(
children: [ children: [
const DrawerEditorBanner(), if (!settings.useTvLayout) ...[
const Divider(height: 0), const DrawerEditorBanner(),
const Divider(height: 0),
],
Flexible( Flexible(
child: ReorderableListView.builder( child: ReorderableListView.builder(
itemBuilder: (context, index) { 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/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/navigation/drawer_editor_banner.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) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
const DrawerEditorBanner(), if (!settings.useTvLayout) ...[
const Divider(height: 0), const DrawerEditorBanner(),
const Divider(height: 0),
],
Flexible( Flexible(
child: ReorderableListView.builder( child: ReorderableListView.builder(
itemBuilder: (context, index) { itemBuilder: (context, index) {

View file

@ -36,6 +36,11 @@ class VideoControlsPage extends StatelessWidget {
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v, onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
title: context.l10n.settingsVideoGestureSideDoubleTapSeek, 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/basic/text/outlined.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -111,45 +111,37 @@ class _MimeDonutState extends State<MimeDonut> with AutomaticKeepAliveClientMixi
], ],
), ),
); );
final primaryColor = Theme.of(context).colorScheme.primary;
final legend = SizedBox( final legend = SizedBox(
width: dim, width: dim,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: seriesData children: seriesData
.map((d) => Material( .map((d) => InkWell(
type: MaterialType.transparency, onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)),
child: InkResponse( borderRadius: const BorderRadius.all(Radius.circular(123)),
onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), child: Row(
containedInkWell: true, mainAxisSize: MainAxisSize.min,
highlightShape: BoxShape.rectangle, children: [
borderRadius: const BorderRadius.all(Radius.circular(123)), Icon(AIcons.disc, color: d.color),
hoverColor: primaryColor.withOpacity(0.04), const SizedBox(width: 8),
splashColor: primaryColor.withOpacity(0.12), Flexible(
child: Row( child: Text(
mainAxisSize: MainAxisSize.min, d.displayText,
children: [ overflow: TextOverflow.fade,
Icon(AIcons.disc, color: d.color), softWrap: false,
const SizedBox(width: 8), maxLines: 1,
Flexible(
child: Text(
d.displayText,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
), ),
const SizedBox(width: 8), ),
Text( const SizedBox(width: 8),
numberFormat.format(d.entryCount), Text(
style: TextStyle( numberFormat.format(d.entryCount),
color: Theme.of(context).textTheme.bodySmall!.color, style: TextStyle(
), color: Theme.of(context).textTheme.bodySmall!.color,
), ),
const SizedBox(width: 4), ),
], const SizedBox(width: 4),
), ],
), ),
)) ))
.toList(), .toList(),

View file

@ -15,6 +15,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/insets.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/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
@ -96,6 +97,7 @@ class _StatsPageState extends State<StatsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final useTvLayout = settings.useTvLayout;
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
valueListenable: _isPageAnimatingNotifier, valueListenable: _isPageAnimatingNotifier,
builder: (context, animating, child) { builder: (context, animating, child) {
@ -196,6 +198,7 @@ class _StatsPageState extends State<StatsPage> {
), ),
), ),
children: [ children: [
const TvEdgeFocus(),
mimeDonuts, mimeDonuts,
Histogram( Histogram(
entries: entries, entries: entries,
@ -218,7 +221,7 @@ class _StatsPageState extends State<StatsPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout, automaticallyImplyLeading: !useTvLayout,
title: Text(l10n.statsPageTitle), title: Text(l10n.statsPageTitle),
), ),
body: GestureAreaProtectorStack( body: GestureAreaProtectorStack(
@ -274,23 +277,15 @@ class _StatsPageState extends State<StatsPage> {
style: Constants.knownTitleTextStyle, style: Constants.knownTitleTextStyle,
); );
if (settings.useTvLayout) { if (settings.useTvLayout) {
final primaryColor = Theme.of(context).colorScheme.primary;
header = Container( header = Container(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: Material( child: InkWell(
type: MaterialType.transparency, onTap: onHeaderPressed,
child: InkResponse( borderRadius: const BorderRadius.all(Radius.circular(123)),
onTap: onHeaderPressed, child: Padding(
containedInkWell: true, padding: const EdgeInsets.all(16),
highlightShape: BoxShape.rectangle, child: header,
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,
),
), ),
), ),
); );

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/aves_dialog.dart';
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_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/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/entry_info_action_delegate.dart';
import 'package:aves/widgets/viewer/action/printer.dart'; import 'package:aves/widgets/viewer/action/printer.dart';
import 'package:aves/widgets/viewer/action/single_entry_editor.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart';

View file

@ -138,10 +138,12 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
child: child!, child: child!,
); );
}, },
child: InfoPage( child: FocusScope(
collection: collection, child: InfoPage(
entryNotifier: widget.entryNotifier, collection: collection,
isScrollingNotifier: _isVerticallyScrollingNotifier, entryNotifier: widget.entryNotifier,
isScrollingNotifier: _isVerticallyScrollingNotifier,
),
), ),
), ),
); );
@ -286,10 +288,10 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
final opacity = min(1.0, page); final opacity = min(1.0, page);
_backgroundOpacityNotifier.value = opacity * opacity; _backgroundOpacityNotifier.value = opacity * opacity;
if (page <= 1 && settings.viewerMaxBrightness) { if (settings.viewerMaxBrightness) {
_systemBrightness?.then((system) { _systemBrightness?.then((system) {
final transition = max(system, lerpDouble(system, maximumBrightness, page / 2)!); final value = lerpDouble(maximumBrightness, system, ((1 - page).abs() * 2).clamp(0, 1))!;
ScreenBrightness().setScreenBrightness(transition); ScreenBrightness().setScreenBrightness(value);
}); });
} }

View file

@ -680,9 +680,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
Future<void> _onLeave() async { Future<void> _onLeave() async {
if (settings.viewerMaxBrightness) { await ScreenBrightness().resetScreenBrightness();
await ScreenBrightness().resetScreenBrightness();
}
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
await windowService.keepScreenOn(false); 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/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/insets.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/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart'; import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart';
@ -281,6 +282,9 @@ class _InfoPageContentState extends State<_InfoPageContent> {
child: CustomScrollView( child: CustomScrollView(
controller: widget.scrollController, controller: widget.scrollController,
slivers: [ slivers: [
const SliverToBoxAdapter(
child: TvEdgeFocus(),
),
InfoAppBar( InfoAppBar(
entry: entry, entry: entry,
collection: collection, collection: collection,

View file

@ -22,56 +22,64 @@ import 'package:tuple/tuple.dart';
@immutable @immutable
class XmpNamespace extends Equatable { class XmpNamespace extends Equatable {
final Map<String, String> schemaRegistryPrefixes;
final String nsUri, nsPrefix; final String nsUri, nsPrefix;
final Map<String, String> rawProps; final Map<String, String> rawProps;
@override @override
List<Object?> get props => [nsUri, nsPrefix]; 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) { switch (nsUri) {
case Namespaces.container:
return XmpContainer(nsPrefix, rawProps);
case Namespaces.creatorAtom: case Namespaces.creatorAtom:
return XmpCreatorAtom(nsPrefix, rawProps); return XmpCreatorAtom(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.crs: case Namespaces.crs:
return XmpCrsNamespace(nsPrefix, rawProps); return XmpCrsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.darktable: case Namespaces.darktable:
return XmpDarktableNamespace(nsPrefix, rawProps); return XmpDarktableNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.dwc: case Namespaces.dwc:
return XmpDwcNamespace(nsPrefix, rawProps); return XmpDwcNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.exif: case Namespaces.exif:
return XmpExifNamespace(nsPrefix, rawProps); return XmpExifNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.gAudio: 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: case Namespaces.gDepth:
return XmpGDepthNamespace(nsPrefix, rawProps); return XmpGDepthNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.gDevice: case Namespaces.gDevice:
return XmpGDeviceNamespace(nsPrefix, rawProps); return XmpGDeviceNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.gImage: case Namespaces.gImage:
return XmpGImageNamespace(nsPrefix, rawProps); return XmpGImageNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.iptc4xmpCore: case Namespaces.iptc4xmpCore:
return XmpIptcCoreNamespace(nsPrefix, rawProps); return XmpIptcCoreNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.iptc4xmpExt: case Namespaces.iptc4xmpExt:
return XmpIptc4xmpExtNamespace(nsPrefix, rawProps); return XmpIptc4xmpExtNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.mwgrs: case Namespaces.mwgrs:
return XmpMgwRegionsNamespace(nsPrefix, rawProps); return XmpMgwRegionsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.mp: case Namespaces.mp:
return XmpMPNamespace(nsPrefix, rawProps); return XmpMPNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.photoshop: case Namespaces.photoshop:
return XmpPhotoshopNamespace(nsPrefix, rawProps); return XmpPhotoshopNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.plus: case Namespaces.plus:
return XmpPlusNamespace(nsPrefix, rawProps); return XmpPlusNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.tiff: case Namespaces.tiff:
return XmpTiffNamespace(nsPrefix, rawProps); return XmpTiffNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.xmp: case Namespaces.xmp:
return XmpBasicNamespace(nsPrefix, rawProps); return XmpBasicNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
case Namespaces.xmpMM: case Namespaces.xmpMM:
return XmpMMNamespace(nsPrefix, rawProps); return XmpMMNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps);
default: 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; String formatValue(XmpProp prop) => prop.value;
Map<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) => {}; 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> { 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'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
class XmpCrsNamespace extends XmpNamespace { 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 @override
late final List<XmpCardData> cards = [ 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 // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md
class XmpExifNamespace extends XmpNamespace { 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 @override
String formatValue(XmpProp prop) { String formatValue(XmpProp prop) {
final v = prop.value; final v = prop.value;
switch (prop.path) { final field = prop.path.replaceAll(nsPrefix, '');
case 'exif:ColorSpace': switch (field) {
case 'ColorSpace':
return Exif.getColorSpaceDescription(v); return Exif.getColorSpaceDescription(v);
case 'exif:Contrast': case 'Contrast':
return Exif.getContrastDescription(v); return Exif.getContrastDescription(v);
case 'exif:CustomRendered': case 'CustomRendered':
return Exif.getCustomRenderedDescription(v); return Exif.getCustomRenderedDescription(v);
case 'exif:ExifVersion': case 'ExifVersion':
case 'exif:FlashpixVersion': case 'FlashpixVersion':
return Exif.getExifVersionDescription(v); return Exif.getExifVersionDescription(v);
case 'exif:ExposureMode': case 'ExposureMode':
return Exif.getExposureModeDescription(v); return Exif.getExposureModeDescription(v);
case 'exif:ExposureProgram': case 'ExposureProgram':
return Exif.getExposureProgramDescription(v); return Exif.getExposureProgramDescription(v);
case 'exif:FileSource': case 'FileSource':
return Exif.getFileSourceDescription(v); return Exif.getFileSourceDescription(v);
case 'exif:Flash/exif:Mode': case 'Flash/Mode':
return Exif.getFlashModeDescription(v); return Exif.getFlashModeDescription(v);
case 'exif:Flash/exif:Return': case 'Flash/Return':
return Exif.getFlashReturnDescription(v); return Exif.getFlashReturnDescription(v);
case 'exif:FocalPlaneResolutionUnit': case 'FocalPlaneResolutionUnit':
return Exif.getResolutionUnitDescription(v); return Exif.getResolutionUnitDescription(v);
case 'exif:GainControl': case 'GainControl':
return Exif.getGainControlDescription(v); return Exif.getGainControlDescription(v);
case 'exif:LightSource': case 'LightSource':
return Exif.getLightSourceDescription(v); return Exif.getLightSourceDescription(v);
case 'exif:MeteringMode': case 'MeteringMode':
return Exif.getMeteringModeDescription(v); return Exif.getMeteringModeDescription(v);
case 'exif:Saturation': case 'Saturation':
return Exif.getSaturationDescription(v); return Exif.getSaturationDescription(v);
case 'exif:SceneCaptureType': case 'SceneCaptureType':
return Exif.getSceneCaptureTypeDescription(v); return Exif.getSceneCaptureTypeDescription(v);
case 'exif:SceneType': case 'SceneType':
return Exif.getSceneTypeDescription(v); return Exif.getSceneTypeDescription(v);
case 'exif:SensingMethod': case 'SensingMethod':
return Exif.getSensingMethodDescription(v); return Exif.getSensingMethodDescription(v);
case 'exif:Sharpness': case 'Sharpness':
return Exif.getSharpnessDescription(v); return Exif.getSharpnessDescription(v);
case 'exif:SubjectDistanceRange': case 'SubjectDistanceRange':
return Exif.getSubjectDistanceRangeDescription(v); return Exif.getSubjectDistanceRangeDescription(v);
case 'exif:WhiteBalance': case 'WhiteBalance':
return Exif.getWhiteBalanceDescription(v); return Exif.getWhiteBalanceDescription(v);
case 'exif:GPSAltitudeRef': case 'GPSAltitudeRef':
return Exif.getGPSAltitudeRefDescription(v); return Exif.getGPSAltitudeRefDescription(v);
case 'exif:GPSDestBearingRef': case 'GPSDestBearingRef':
case 'exif:GPSImgDirectionRef': case 'GPSImgDirectionRef':
case 'exif:GPSTrackRef': case 'GPSTrackRef':
return Exif.getGPSDirectionRefDescription(v); return Exif.getGPSDirectionRefDescription(v);
case 'exif:GPSDestDistanceRef': case 'GPSDestDistanceRef':
return Exif.getGPSDestDistanceRefDescription(v); return Exif.getGPSDestDistanceRefDescription(v);
case 'exif:GPSDifferential': case 'GPSDifferential':
return Exif.getGPSDifferentialDescription(v); return Exif.getGPSDifferentialDescription(v);
case 'exif:GPSMeasureMode': case 'GPSMeasureMode':
return Exif.getGPSMeasureModeDescription(v); return Exif.getGPSMeasureModeDescription(v);
case 'exif:GPSSpeedRef': case 'GPSSpeedRef':
return Exif.getGPSSpeedRefDescription(v); return Exif.getGPSSpeedRefDescription(v);
case 'exif:GPSStatus': case 'GPSStatus':
return Exif.getGPSStatusDescription(v); return Exif.getGPSStatusDescription(v);
default: default:
return v; return v;

View file

@ -7,7 +7,11 @@ import 'package:collection/collection.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
abstract class XmpGoogleNamespace extends XmpNamespace { 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; List<Tuple2<String, String>> get dataProps;
@ -53,14 +57,34 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
} }
class XmpGAudioNamespace extends XmpGoogleNamespace { 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 @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 { 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 @override
List<Tuple2<String, String>> get dataProps => [ List<Tuple2<String, String>> get dataProps => [
@ -70,8 +94,16 @@ class XmpGDepthNamespace extends XmpGoogleNamespace {
} }
class XmpGDeviceNamespace extends XmpNamespace { class XmpGDeviceNamespace extends XmpNamespace {
XmpGDeviceNamespace(String nsPrefix, Map<String, String> rawProps) : super(Namespaces.gDevice, nsPrefix, rawProps) { late final String _cameraNsPrefix;
final mimePattern = RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/Item:Mime'); 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(); final originalProps = rawProps.entries.toList();
originalProps.forEach((kv) { originalProps.forEach((kv) {
final path = kv.key; final path = kv.key;
@ -81,7 +113,7 @@ class XmpGDeviceNamespace extends XmpNamespace {
if (indexString != null) { if (indexString != null) {
final index = int.tryParse(indexString); final index = int.tryParse(indexString);
if (index != null) { if (index != null) {
final dataPath = '${nsPrefix}Container/Container:Directory[$index]/Item:Data'; final dataPath = '${nsPrefix}Container/${_containerNsPrefix}Directory[$index]/${_itemNsPrefix}Data';
rawProps[dataPath] = '[skipped]'; rawProps[dataPath] = '[skipped]';
} }
} }
@ -94,16 +126,16 @@ class XmpGDeviceNamespace extends XmpNamespace {
XmpCardData( XmpCardData(
RegExp(nsPrefix + r'Cameras\[(\d+)\]/(.*)'), RegExp(nsPrefix + r'Cameras\[(\d+)\]/(.*)'),
cards: [ cards: [
XmpCardData(RegExp(r'Camera:DepthMap/(.*)')), XmpCardData(RegExp(_cameraNsPrefix + r'DepthMap/(.*)')),
XmpCardData(RegExp(r'Camera:Image/(.*)')), XmpCardData(RegExp(_cameraNsPrefix + r'Image/(.*)')),
XmpCardData(RegExp(r'Camera:ImagingModel/(.*)')), XmpCardData(RegExp(_cameraNsPrefix + r'ImagingModel/(.*)')),
], ],
), ),
XmpCardData( XmpCardData(
RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/(.*)'), RegExp(nsPrefix + r'Container/' + _containerNsPrefix + r'Directory\[(\d+)\]/(.*)'),
spanBuilders: (index, struct) { spanBuilders: (index, struct) {
if (struct.containsKey('Item:Data') && struct.containsKey('Item:DataURI')) { if (struct.containsKey('${_itemNsPrefix}Data') && struct.containsKey('${_itemNsPrefix}DataURI')) {
final dataUriProp = struct['Item:DataURI']; final dataUriProp = struct['${_itemNsPrefix}DataURI'];
if (dataUriProp != null) { if (dataUriProp != null) {
return { return {
'Data': InfoRowGroup.linkSpanBuilder( 'Data': InfoRowGroup.linkSpanBuilder(
@ -121,17 +153,10 @@ class XmpGDeviceNamespace extends XmpNamespace {
} }
class XmpGImageNamespace extends XmpGoogleNamespace { 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 @override
List<Tuple2<String, String>> get dataProps => [Tuple2('${nsPrefix}Data', '${nsPrefix}Mime')]; 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'),
];
} }

View file

@ -2,7 +2,7 @@ import 'package:aves/utils/xmp_utils.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
class XmpCreatorAtom extends XmpNamespace { 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -11,7 +11,7 @@ class XmpCreatorAtom extends XmpNamespace {
} }
class XmpDarktableNamespace 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -20,7 +20,7 @@ class XmpDarktableNamespace extends XmpNamespace {
} }
class XmpDwcNamespace 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -37,7 +37,7 @@ class XmpDwcNamespace extends XmpNamespace {
} }
class XmpIptcCoreNamespace 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -46,7 +46,7 @@ class XmpIptcCoreNamespace extends XmpNamespace {
} }
class XmpIptc4xmpExtNamespace 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -55,7 +55,7 @@ class XmpIptc4xmpExtNamespace extends XmpNamespace {
} }
class XmpMPNamespace 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 @override
late final List<XmpCardData> cards = [ 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) // cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15)
class XmpMgwRegionsNamespace extends XmpNamespace { 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -75,7 +75,7 @@ class XmpMgwRegionsNamespace extends XmpNamespace {
} }
class XmpPlusNamespace 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [
@ -86,7 +86,7 @@ class XmpPlusNamespace extends XmpNamespace {
} }
class XmpMMNamespace 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 @override
late final List<XmpCardData> cards = [ 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 // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
class XmpPhotoshopNamespace extends XmpNamespace { 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 @override
late final List<XmpCardData> cards = [ 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 // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md
class XmpTiffNamespace extends XmpNamespace { 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 @override
String formatValue(XmpProp prop) { 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'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
class XmpBasicNamespace extends XmpNamespace { 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 @override
late final List<XmpCardData> cards = [ late final List<XmpCardData> cards = [

View file

@ -56,9 +56,8 @@ class _XmpDirTileState extends State<XmpDirTile> {
return nsPrefix; return nsPrefix;
}).entries.map((kv) { }).entries.map((kv) {
final nsPrefix = kv.key; final nsPrefix = kv.key;
final nsUri = _schemaRegistryPrefixes[nsPrefix] ?? '';
final rawProps = Map.fromEntries(kv.value); final rawProps = Map.fromEntries(kv.value);
return XmpNamespace.create(nsUri, nsPrefix, rawProps); return XmpNamespace.create(_schemaRegistryPrefixes, nsPrefix, rawProps);
}).toList() }).toList()
..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle)); ..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle));
return AvesExpansionTile( return AvesExpansionTile(

View file

@ -3,30 +3,27 @@ import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/entry.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/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/media_session_service.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/theme/icons.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.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/controller.dart';
import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/video/conductor.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/conductor.dart';
import 'package:aves/widgets/viewer/visual/error.dart'; import 'package:aves/widgets/viewer/visual/error.dart';
import 'package:aves/widgets/viewer/visual/raster.dart'; import 'package:aves/widgets/viewer/visual/raster.dart';
import 'package:aves/widgets/viewer/visual/state.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/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:aves_magnifier/aves_magnifier.dart';
import 'package:collection/collection.dart';
import 'package:decorated_icon/decorated_icon.dart'; import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -55,17 +52,8 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
late ValueNotifier<ViewState> _viewStateNotifier; late ValueNotifier<ViewState> _viewStateNotifier;
late AvesMagnifierController _magnifierController; late AvesMagnifierController _magnifierController;
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
ImageStream? _videoCoverStream;
late ImageStreamListener _videoCoverStreamListener;
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null); final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
OverlayEntry? _actionFeedbackOverlayEntry;
AvesMagnifierController? _dismissedCoverMagnifierController;
AvesMagnifierController get dismissedCoverMagnifierController {
_dismissedCoverMagnifierController ??= AvesMagnifierController();
return _dismissedCoverMagnifierController!;
}
AvesEntry get mainEntry => widget.mainEntry; AvesEntry get mainEntry => widget.mainEntry;
@ -73,9 +61,6 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
ViewerController get viewerController => widget.viewerController; 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 rasterMaxScale = ScaleLevel(factor: 5);
static const vectorMaxScale = ScaleLevel(factor: 25); static const vectorMaxScale = ScaleLevel(factor: 25);
@ -110,9 +95,6 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
if (entry.isVideo) { if (entry.isVideo) {
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand)); _subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
_videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image);
_videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty);
_videoCoverStream!.addListener(_videoCoverStreamListener);
} }
viewerController.startAutopilotAnimation( viewerController.startAutopilotAnimation(
vsync: this, vsync: this,
@ -127,9 +109,6 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
void _unregisterWidget(EntryPageView oldWidget) { void _unregisterWidget(EntryPageView oldWidget) {
viewerController.stopAutopilotAnimation(vsync: this); viewerController.stopAutopilotAnimation(vsync: this);
_videoCoverStream?.removeListener(_videoCoverStreamListener);
_videoCoverStream = null;
_videoCoverInfoNotifier.value = null;
_magnifierController.dispose(); _magnifierController.dispose();
_subscriptions _subscriptions
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
@ -222,169 +201,169 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
builder: (context, sar, child) { builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar); final videoDisplaySize = entry.videoDisplaySize(sar);
return Selector<Settings, Tuple2<bool, bool>>( return Selector<Settings, Tuple3<bool, bool, bool>>(
selector: (context, s) => Tuple2(s.videoGestureDoubleTapTogglePlay, s.videoGestureSideDoubleTapSeek), selector: (context, s) => Tuple3(
s.videoGestureDoubleTapTogglePlay,
s.videoGestureSideDoubleTapSeek,
s.videoGestureVerticalDragBrightnessVolume,
),
builder: (context, s, child) { builder: (context, s, child) {
final playGesture = s.item1; final playGesture = s.item1;
final seekGesture = s.item2; final seekGesture = s.item2;
final useActionGesture = playGesture || seekGesture; final useVerticalDragGesture = s.item3;
final useTapGesture = playGesture || seekGesture;
void _applyAction(EntryAction action, {IconData? Function()? icon}) { MagnifierDoubleTapCallback? onDoubleTap;
_actionFeedbackChildNotifier.value = DecoratedIcon( MagnifierGestureScaleStartCallback? onScaleStart;
icon?.call() ?? action.getIconData(), MagnifierGestureScaleUpdateCallback? onScaleUpdate;
size: 48, MagnifierGestureScaleEndCallback? onScaleEnd;
color: Colors.white,
shadows: const [ if (useTapGesture) {
Shadow( void _applyAction(EntryAction action, {IconData? Function()? icon}) {
color: Colors.black, _actionFeedbackChildNotifier.value = DecoratedIcon(
blurRadius: 4, icon?.call() ?? action.getIconData(),
) size: 48,
], color: Colors.white,
); shadows: const [
VideoActionNotification( Shadow(
controller: videoController, color: Colors.black,
action: action, blurRadius: 4,
).dispatch(context); )
],
);
VideoActionNotification(
controller: videoController,
action: action,
).dispatch(context);
}
onDoubleTap = (alignment) {
final x = alignment.x;
if (seekGesture) {
if (x < sideRatio) {
_applyAction(EntryAction.videoReplay10);
return true;
} else if (x > 1 - sideRatio) {
_applyAction(EntryAction.videoSkip10);
return true;
}
}
if (playGesture) {
_applyAction(
EntryAction.videoTogglePlay,
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
);
return true;
}
return false;
};
} }
MagnifierDoubleTapCallback? _onDoubleTap = useActionGesture if (useVerticalDragGesture) {
? (alignment) { SwipeAction? swipeAction;
final x = alignment.x; var move = Offset.zero;
if (seekGesture) { var dropped = false;
if (x < sideRatio) { double? startValue;
_applyAction(EntryAction.videoReplay10); final valueNotifier = ValueNotifier<double?>(null);
return true;
} else if (x > 1 - sideRatio) {
_applyAction(EntryAction.videoSkip10);
return true;
}
}
if (playGesture) {
_applyAction(
EntryAction.videoTogglePlay,
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
);
return true;
}
return false;
}
: null;
onScaleStart = (details, doubleTap, boundaries) {
dropped = details.pointerCount > 1 || doubleTap;
if (dropped) return;
startValue = null;
valueNotifier.value = null;
final alignmentX = details.focalPoint.dx / boundaries.viewportSize.width;
final action = alignmentX > .5 ? SwipeAction.volume : SwipeAction.brightness;
action.get().then((v) => startValue = v);
swipeAction = action;
move = Offset.zero;
_actionFeedbackOverlayEntry = OverlayEntry(
builder: (context) => SwipeActionFeedback(
action: action,
valueNotifier: valueNotifier,
),
);
Overlay.of(context)!.insert(_actionFeedbackOverlayEntry!);
};
onScaleUpdate = (details) {
move += details.focalPointDelta;
dropped |= details.pointerCount > 1;
if (valueNotifier.value == null) {
dropped |= MagnifierGestureRecognizer.isXPan(move);
}
if (dropped) return false;
final _startValue = startValue;
if (_startValue != null) {
final double value = (_startValue - move.dy / SwipeActionFeedback.height).clamp(0, 1);
valueNotifier.value = value;
swipeAction?.set(value);
}
return true;
};
onScaleEnd = (details) {
if (_actionFeedbackOverlayEntry != null) {
_actionFeedbackOverlayEntry!.remove();
_actionFeedbackOverlayEntry = null;
}
};
}
Widget videoChild = Stack(
children: [
_buildMagnifier(
displaySize: videoDisplaySize,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onDoubleTap: onDoubleTap,
child: VideoView(
entry: entry,
controller: videoController,
),
),
VideoSubtitles(
controller: videoController,
viewStateNotifier: _viewStateNotifier,
),
if (useTapGesture)
ValueListenableBuilder<Widget?>(
valueListenable: _actionFeedbackChildNotifier,
builder: (context, feedbackChild, child) => ActionFeedback(
child: feedbackChild,
),
),
],
);
if (useVerticalDragGesture) {
videoChild = MagnifierGestureDetectorScope.of(context)!.copyWith(
acceptPointerEvent: MagnifierGestureRecognizer.isYPan,
child: videoChild,
);
}
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Stack( videoChild,
children: [ VideoCover(
_buildMagnifier( mainEntry: mainEntry,
displaySize: videoDisplaySize, pageEntry: entry,
onDoubleTap: _onDoubleTap, magnifierController: _magnifierController,
child: VideoView(
entry: entry,
controller: videoController,
),
),
VideoSubtitles(
controller: videoController,
viewStateNotifier: _viewStateNotifier,
),
if (settings.videoShowRawTimedText)
VideoSubtitles(
controller: videoController,
viewStateNotifier: _viewStateNotifier,
debugMode: true,
),
if (useActionGesture)
ValueListenableBuilder<Widget?>(
valueListenable: _actionFeedbackChildNotifier,
builder: (context, feedbackChild, child) => ActionFeedback(
child: feedbackChild,
),
),
],
),
_buildVideoCover(
videoController: videoController, videoController: videoController,
videoDisplaySize: videoDisplaySize, videoDisplaySize: videoDisplaySize,
onDoubleTap: _onDoubleTap, onTap: _onTap,
), magnifierBuilder: (coverController, coverSize, videoCoverUriImage) => _buildMagnifier(
],
);
},
);
},
);
}
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(
controller: coverController, controller: coverController,
displaySize: coverSize, displaySize: coverSize,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
child: Image( child: Image(
image: videoCoverUriImage, 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, ScaleLevel maxScale = rasterMaxScale,
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
bool applyScale = true, bool applyScale = true,
MagnifierGestureScaleStartCallback? onScaleStart,
MagnifierGestureScaleUpdateCallback? onScaleUpdate,
MagnifierGestureScaleEndCallback? onScaleEnd,
MagnifierDoubleTapCallback? onDoubleTap, MagnifierDoubleTapCallback? onDoubleTap,
required Widget child, required Widget child,
}) { }) {
@ -413,6 +395,9 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
initialScale: viewerController.initialScale, initialScale: viewerController.initialScale,
scaleStateCycle: scaleStateCycle, scaleStateCycle: scaleStateCycle,
applyScale: applyScale, applyScale: applyScale,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onTap: (c, s, a, p) => _onTap(alignment: a), onTap: (c, s, a, p) => _onTap(alignment: a),
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
child: child, 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/video/subtitle/line.dart';
import 'package:aves/widgets/viewer/visual/subtitle/span.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
import 'package:aves/widgets/viewer/visual/subtitle/style.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.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:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.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:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.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/common/basic/text/outlined.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/visual/state.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/video/subtitle/ass_parser.dart';
import 'package:aves/widgets/viewer/visual/subtitle/span.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
import 'package:aves/widgets/viewer/visual/subtitle/style.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.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/controller.dart';
export 'src/controller/state.dart'; export 'src/controller/state.dart';
export 'src/core/scale_gesture_recognizer.dart';
export 'src/magnifier.dart'; export 'src/magnifier.dart';
export 'src/pan/gesture_detector_scope.dart'; export 'src/pan/gesture_detector_scope.dart';
export 'src/pan/scroll_physics.dart'; export 'src/pan/scroll_physics.dart';

View file

@ -18,6 +18,9 @@ class MagnifierCore extends StatefulWidget {
final ScaleStateCycle scaleStateCycle; final ScaleStateCycle scaleStateCycle;
final bool applyScale; final bool applyScale;
final double panInertia; final double panInertia;
final MagnifierGestureScaleStartCallback? onScaleStart;
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
final MagnifierGestureScaleEndCallback? onScaleEnd;
final MagnifierTapCallback? onTap; final MagnifierTapCallback? onTap;
final MagnifierDoubleTapCallback? onDoubleTap; final MagnifierDoubleTapCallback? onDoubleTap;
final Widget child; final Widget child;
@ -28,6 +31,9 @@ class MagnifierCore extends StatefulWidget {
required this.scaleStateCycle, required this.scaleStateCycle,
required this.applyScale, required this.applyScale,
this.panInertia = .2, this.panInertia = .2,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.onTap, this.onTap,
this.onDoubleTap, this.onDoubleTap,
required this.child, required this.child,
@ -40,7 +46,7 @@ class MagnifierCore extends StatefulWidget {
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector { class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
Offset? _startFocalPoint, _lastViewportFocalPosition; Offset? _startFocalPoint, _lastViewportFocalPosition;
double? _startScale, _quickScaleLastY, _quickScaleLastDistance; double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
late bool _doubleTap, _quickScaleMoved; late bool _dropped, _doubleTap, _quickScaleMoved;
DateTime _lastScaleGestureDate = DateTime.now(); DateTime _lastScaleGestureDate = DateTime.now();
late AnimationController _scaleAnimationController; late AnimationController _scaleAnimationController;
@ -99,9 +105,15 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
} }
void onScaleStart(ScaleStartDetails details, bool doubleTap) { void onScaleStart(ScaleStartDetails details, bool doubleTap) {
final boundaries = scaleBoundaries;
if (boundaries == null) return;
widget.onScaleStart?.call(details, doubleTap, boundaries);
_startScale = scale; _startScale = scale;
_startFocalPoint = details.localFocalPoint; _startFocalPoint = details.localFocalPoint;
_lastViewportFocalPosition = _startFocalPoint; _lastViewportFocalPosition = _startFocalPoint;
_dropped = false;
_doubleTap = doubleTap; _doubleTap = doubleTap;
_quickScaleLastDistance = null; _quickScaleLastDistance = null;
_quickScaleLastY = _startFocalPoint!.dy; _quickScaleLastY = _startFocalPoint!.dy;
@ -115,6 +127,9 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
final boundaries = scaleBoundaries; final boundaries = scaleBoundaries;
if (boundaries == null) return; if (boundaries == null) return;
_dropped |= widget.onScaleUpdate?.call(details) ?? false;
if (_dropped) return;
double newScale; double newScale;
if (_doubleTap) { if (_doubleTap) {
// quick scale, aka one finger zoom // quick scale, aka one finger zoom
@ -151,6 +166,8 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
final boundaries = scaleBoundaries; final boundaries = scaleBoundaries;
if (boundaries == null) return; if (boundaries == null) return;
widget.onScaleEnd?.call(details);
final _position = controller.position; final _position = controller.position;
final _scale = controller.scale!; final _scale = controller.scale!;
final maxScale = boundaries.maxScale; final maxScale = boundaries.maxScale;
@ -228,7 +245,7 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
if (onDoubleTap != null) { if (onDoubleTap != null) {
final viewportSize = boundaries.viewportSize; final viewportSize = boundaries.viewportSize;
final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); 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); final childTapPosition = boundaries.viewportToChildPosition(controller, viewportTapPosition);
@ -307,12 +324,12 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
); );
return MagnifierGestureDetector( return MagnifierGestureDetector(
onDoubleTap: onDoubleTap, hitDetector: this,
onScaleStart: onScaleStart, onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate, onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd, onScaleEnd: onScaleEnd,
hitDetector: this,
onTapUp: widget.onTap == null ? null : onTap, onTapUp: widget.onTap == null ? null : onTap,
onDoubleTap: onDoubleTap,
child: child, child: child,
); );
}, },

View file

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

View file

@ -1,20 +1,19 @@
import 'dart:math'; import 'dart:math';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_magnifier/src/pan/corner_hit_detector.dart'; import 'package:aves_magnifier/src/pan/corner_hit_detector.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class MagnifierGestureRecognizer extends ScaleGestureRecognizer { class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
final CornerHitDetector hitDetector; final CornerHitDetector hitDetector;
final List<Axis> validateAxis; final MagnifierGestureDetectorScope scope;
final double touchSlopFactor;
final ValueNotifier<TapDownDetails?> doubleTapDetails; final ValueNotifier<TapDownDetails?> doubleTapDetails;
MagnifierGestureRecognizer({ MagnifierGestureRecognizer({
super.debugOwner, super.debugOwner,
required this.hitDetector, required this.hitDetector,
required this.validateAxis, required this.scope,
this.touchSlopFactor = 2,
required this.doubleTapDetails, required this.doubleTapDetails,
}); });
@ -46,7 +45,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
@override @override
void handleEvent(PointerEvent event) { void handleEvent(PointerEvent event) {
if (validateAxis.isNotEmpty) { if (scope.axis.isNotEmpty) {
var didChangeConfiguration = false; var didChangeConfiguration = false;
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
if (!event.synthesized) { if (!event.synthesized) {
@ -104,26 +103,27 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
return; return;
} }
final validateAxis = scope.axis;
final move = _initialFocalPoint! - _currentFocalPoint!; final move = _initialFocalPoint! - _currentFocalPoint!;
var shouldMove = false; bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false;
if (validateAxis.length == 2) {
// the image is the descendant of gesture detector(s) handling drag in both directions if (!shouldMove) {
final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move); if (validateAxis.length == 2) {
final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move); // the image is the descendant of gesture detector(s) handling drag in both directions
if (shouldMoveX == shouldMoveY) { final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
// consistently can/cannot pan the image in both direction the same way final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
shouldMove = shouldMoveX; if (shouldMoveX == shouldMoveY) {
// consistently can/cannot pan the image in both direction the same way
shouldMove = shouldMoveX;
} else {
// can pan the image in one direction, but should yield to an ascendant gesture detector in the other one
// the gesture direction angle is in ]-pi, pi], cf `Offset` doc for details
shouldMove = (isXPan(move) && shouldMoveX) || (isYPan(move) && shouldMoveY);
}
} else { } else {
// can pan the image in one direction, but should yield to an ascendant gesture detector in the other one // the image is the descendant of a gesture detector handling drag in one direction
final d = move.direction; shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move);
// 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);
} }
} 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; final doubleTap = doubleTapDetails.value != null;
@ -137,9 +137,19 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
// and the magnifier recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView` // 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` 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` // 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); 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.initialScale = const ScaleLevel(ref: ScaleReference.contained),
this.scaleStateCycle = defaultScaleStateCycle, this.scaleStateCycle = defaultScaleStateCycle,
this.applyScale = true, this.applyScale = true,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.onTap, this.onTap,
this.onDoubleTap, this.onDoubleTap,
required this.child, required this.child,
@ -52,6 +55,9 @@ class AvesMagnifier extends StatelessWidget {
final ScaleStateCycle scaleStateCycle; final ScaleStateCycle scaleStateCycle;
final bool applyScale; final bool applyScale;
final MagnifierGestureScaleStartCallback? onScaleStart;
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
final MagnifierGestureScaleEndCallback? onScaleEnd;
final MagnifierTapCallback? onTap; final MagnifierTapCallback? onTap;
final MagnifierDoubleTapCallback? onDoubleTap; final MagnifierDoubleTapCallback? onDoubleTap;
final Widget child; final Widget child;
@ -73,6 +79,9 @@ class AvesMagnifier extends StatelessWidget {
controller: controller, controller: controller,
scaleStateCycle: scaleStateCycle, scaleStateCycle: scaleStateCycle,
applyScale: applyScale, applyScale: applyScale,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onTap: onTap, onTap: onTap,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
child: child, child: child,
@ -88,7 +97,7 @@ typedef MagnifierTapCallback = Function(
Alignment alignment, Alignment alignment,
Offset childTapPosition, Offset childTapPosition,
); );
typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
typedef MagnifierDoubleTapCallback = bool Function( typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
Alignment alignment, 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, /// Useful when placing Magnifier inside a gesture sensitive context,
/// such as [PageView], [Dismissible], [BottomSheet]. /// such as [PageView], [Dismissible], [BottomSheet].
class MagnifierGestureDetectorScope extends InheritedWidget { 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; final List<Axis> axis;
// in [0, 1[ // in [0, 1[
@ -26,9 +14,36 @@ class MagnifierGestureDetectorScope extends InheritedWidget {
// <1: less reactive but gives the most leeway to other recognizers // <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 // 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
final double touchSlopFactor; 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 @override
bool updateShouldNotify(MagnifierGestureDetectorScope oldWidget) { 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" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.0.0" 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: watcher:
dependency: transitive dependency: transitive
description: description:

View file

@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US # - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt
version: 1.7.9+89 version: 1.7.10+90
publish_to: none publish_to: none
environment: environment:
@ -95,6 +95,7 @@ dependencies:
transparent_image: transparent_image:
tuple: tuple:
url_launcher: url_launcher:
volume_controller:
xml: xml:
dev_dependencies: dev_dependencies:

View file

@ -475,6 +475,7 @@
"settingsVideoButtonsTile", "settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek", "settingsVideoGestureSideDoubleTapSeek",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle", "settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle", "settingsAllowInstalledAppAccessSubtitle",
@ -576,6 +577,7 @@
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
@ -586,16 +588,14 @@
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
"el": [ "el": [
"tooManyItemsErrorDialogMessage" "tooManyItemsErrorDialogMessage",
], "settingsVideoGestureVerticalDragBrightnessVolume"
"es": [
"tooManyItemsErrorDialogMessage"
], ],
"fa": [ "fa": [
@ -933,6 +933,7 @@
"settingsVideoButtonsTile", "settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek", "settingsVideoGestureSideDoubleTapSeek",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle", "settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle", "settingsAllowInstalledAppAccessSubtitle",
@ -1404,6 +1405,7 @@
"settingsVideoButtonsTile", "settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek", "settingsVideoGestureSideDoubleTapSeek",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle", "settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle", "settingsAllowInstalledAppAccessSubtitle",
@ -1512,6 +1514,613 @@
"filePickerUseThisFolder" "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": [ "ja": [
"columnCount", "columnCount",
"chipActionFilterIn", "chipActionFilterIn",
@ -1526,6 +2135,7 @@
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface", "settingsDisplayUseTvInterface",
"settingsWidgetDisplayedItem" "settingsWidgetDisplayedItem"
@ -1539,23 +2149,13 @@
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
"nb": [ "nb": [
"columnCount", "settingsVideoGestureVerticalDragBrightnessVolume"
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"entryInfoActionRemoveLocation",
"filterLocatedLabel",
"filterTaggedLabel",
"keepScreenOnVideoPlayback",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface"
], ],
"nl": [ "nl": [
@ -1580,6 +2180,7 @@
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionTile",
"settingsSubtitleThemeTextPositionDialogTitle", "settingsSubtitleThemeTextPositionDialogTitle",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface", "settingsDisplayUseTvInterface",
"settingsWidgetDisplayedItem" "settingsWidgetDisplayedItem"
@ -1828,6 +2429,7 @@
"settingsVideoButtonsTile", "settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek", "settingsVideoGestureSideDoubleTapSeek",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle", "settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle", "settingsAllowInstalledAppAccessSubtitle",
@ -1872,17 +2474,14 @@
"wallpaperUseScrollEffect" "wallpaperUseScrollEffect"
], ],
"pl": [
"tooManyItemsErrorDialogMessage"
],
"pt": [ "pt": [
"columnCount", "columnCount",
"tooManyItemsErrorDialogMessage" "tooManyItemsErrorDialogMessage",
"settingsVideoGestureVerticalDragBrightnessVolume"
], ],
"ro": [ "ro": [
"tooManyItemsErrorDialogMessage" "settingsVideoGestureVerticalDragBrightnessVolume"
], ],
"ru": [ "ru": [
@ -1890,6 +2489,7 @@
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
@ -1901,29 +2501,6 @@
"timeDays", "timeDays",
"focalLength", "focalLength",
"applyButtonLabel", "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", "editEntryDateDialogExtractFromTitle",
"editEntryDateDialogShift", "editEntryDateDialogShift",
"removeEntryMetadataDialogTitle", "removeEntryMetadataDialogTitle",
@ -2133,6 +2710,7 @@
"settingsVideoButtonsTile", "settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek", "settingsVideoGestureSideDoubleTapSeek",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle", "settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess", "settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle", "settingsAllowInstalledAppAccessSubtitle",
@ -2241,16 +2819,13 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"uk": [
"tooManyItemsErrorDialogMessage"
],
"zh": [ "zh": [
"filterLocatedLabel", "filterLocatedLabel",
"filterTaggedLabel", "filterTaggedLabel",
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
], ],
@ -2262,6 +2837,7 @@
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface" "settingsDisplayUseTvInterface"
] ]

View file

@ -1,4 +1,4 @@
In v1.7.9: In v1.7.10:
- Android TV support (cont'd) - Android TV support (cont'd)
- interact with videos via media session controls - interact with videos via media session controls
- enjoy the app in Czech & Polish - enjoy the app in Czech & Polish