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