Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2025-04-06 23:13:41 +02:00
commit 4dd77483cd
58 changed files with 2775 additions and 951 deletions

View file

@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit
@ -52,7 +52,7 @@ jobs:
build-mode: manual
steps:
- name: Harden Runner
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit

View file

@ -18,7 +18,7 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit
@ -43,20 +43,15 @@ jobs:
# `KEY_JKS` should contain the result of:
# gpg -c --armor keystore.jks
# `KEY_JKS_PASSPHRASE` should contain the passphrase used for the command above
# The SkSL bundle must be produced with the same Flutter engine as the one used to build the artifact
# flutter build <subcommand> --bundle-sksl-path shaders.sksl.json
# do not bundle shaders for izzy/libre flavours, to avoid crashes in some environments:
# cf https://github.com/deckerst/aves/issues/388
# cf https://github.com/deckerst/aves/issues/398
run: |
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
rm release.keystore.asc
mkdir outputs
scripts/apply_flavor_play.sh
./flutterw build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders.sksl.json
./flutterw build appbundle -t lib/main_play.dart --flavor play
cp build/app/outputs/bundle/playRelease/*.aab outputs
./flutterw build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders.sksl.json
./flutterw build apk -t lib/main_play.dart --flavor play
cp build/app/outputs/apk/play/release/*.apk outputs
scripts/apply_flavor_izzy.sh
./flutterw build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi
@ -98,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit

View file

@ -31,7 +31,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit

View file

@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.12.9"></a>[v1.12.9] - 2025-04-06
### Added
- Kannada translation (thanks Chethan, Prasannakumar T Bhat)
### Changed
- enable Impeller rendering engine
### Fixed
- memory pressure during browsing
## <a id="v1.12.8"></a>[v1.12.8] - 2025-03-25
### Fixed

View file

@ -329,12 +329,8 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!--
Screenshot driver scenario is not supported by Impeller: "Compressed screenshots not supported for Impeller".
As of Flutter v3.29.2, switching pages with alpha transition yields artifacts when Impeller is enabled.
-->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
android:value="true" />
</application>
</manifest>

View file

@ -1,6 +1,8 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
@ -21,7 +23,8 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getEntry" -> ioScope.launch { safe(call, result, ::getEntry) }
"clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) }
"clearImageDiskCache" -> ioScope.launch { safe(call, result, ::clearImageDiskCache) }
"clearImageMemoryCache" -> ioScope.launch { safe(call, result, ::clearImageMemoryCache) }
else -> result.notImplemented()
}
}
@ -47,11 +50,18 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
})
}
private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
private fun clearImageDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Glide.get(context).clearDiskCache()
result.success(null)
}
private fun clearImageMemoryCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Handler(Looper.getMainLooper()).post {
Glide.get(context).clearMemory()
}
result.success(null)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/media_fetch_object"
}

View file

@ -29,10 +29,6 @@ import kotlin.math.roundToInt
class RegionFetcher internal constructor(
private val context: Context,
) {
private var lastDecoderRef: LastDecoderRef? = null
private val exportUris = HashMap<Pair<Uri, Int?>, Uri>()
// return decoded bytes in ARGB_8888, with trailer bytes:
// - width (int32)
// - height (int32)
@ -63,24 +59,12 @@ class RegionFetcher internal constructor(
return
}
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.requestKey != requestKey) {
currentDecoderRef = null
}
try {
if (currentDecoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
BitmapRegionDecoderCompat.newInstance(input)
}
if (newDecoder == null) {
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
return
}
currentDecoderRef = LastDecoderRef(requestKey, newDecoder)
val decoder = getOrCreateDecoder(uri, requestKey)
if (decoder == null) {
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
return
}
val decoder = currentDecoderRef.decoder
lastDecoderRef = currentDecoderRef
// with raw images, the known image size may not match the decoded image size
// so we scale the requested region accordingly
@ -159,6 +143,26 @@ class RegionFetcher internal constructor(
}
}
private fun getOrCreateDecoder(uri: Uri, requestKey: Pair<Uri, Int?>): BitmapRegionDecoder? {
var decoderRef = decoderPool.firstOrNull { it.requestKey == requestKey }
if (decoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
BitmapRegionDecoderCompat.newInstance(input)
}
if (newDecoder == null) {
return null
}
decoderRef = DecoderRef(requestKey, newDecoder)
} else {
decoderPool.remove(decoderRef)
}
decoderPool.add(0, decoderRef)
while (decoderPool.size > DECODER_POOL_SIZE) {
decoderPool.removeAt(decoderPool.size - 1)
}
return decoderRef.decoder
}
private fun createTemporaryJpegExport(uri: Uri, mimeType: String, pageId: Int?): Uri {
Log.d(LOG_TAG, "create JPEG export for uri=$uri mimeType=$mimeType pageId=$pageId")
val target = Glide.with(context)
@ -180,7 +184,7 @@ class RegionFetcher internal constructor(
}
}
private data class LastDecoderRef(
private data class DecoderRef(
val requestKey: Pair<Uri, Int?>,
val decoder: BitmapRegionDecoder,
)
@ -188,5 +192,8 @@ class RegionFetcher internal constructor(
companion object {
private val LOG_TAG = LogUtils.createTag<RegionFetcher>()
private val PREFERRED_CONFIG = Bitmap.Config.ARGB_8888
private const val DECODER_POOL_SIZE = 3
private val decoderPool = ArrayList<DecoderRef>()
private val exportUris = HashMap<Pair<Uri, Int?>, Uri>()
}
}

View file

@ -22,8 +22,6 @@ import kotlin.math.ceil
class SvgRegionFetcher internal constructor(
private val context: Context,
) {
private var lastSvgRef: LastSvgRef? = null
fun fetch(
uri: Uri,
sizeBytes: Long?,
@ -39,32 +37,12 @@ class SvgRegionFetcher internal constructor(
return
}
var currentSvgRef = lastSvgRef
if (currentSvgRef != null && currentSvgRef.uri != uri) {
currentSvgRef = null
}
try {
if (currentSvgRef == null) {
val newSvg = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
} catch (ex: SVGParseException) {
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
return
}
}
if (newSvg == null) {
result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
return
}
newSvg.normalizeSize()
currentSvgRef = LastSvgRef(uri, newSvg)
val svg = getOrCreateDecoder(uri)
if (svg == null) {
result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
return
}
val svg = currentSvgRef.svg
lastSvgRef = currentSvgRef
// we scale the requested region accordingly to the viewbox size
val viewBox = svg.documentViewBox
@ -110,17 +88,42 @@ class SvgRegionFetcher internal constructor(
bitmap = Bitmap.createBitmap(bitmap, bleedX, bleedY, targetBitmapWidth, targetBitmapHeight)
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = true)
result.success(bytes)
} catch (e: SVGParseException) {
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
} catch (e: Exception) {
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
}
}
private data class LastSvgRef(
private fun getOrCreateDecoder(uri: Uri): SVG? {
var decoderRef = decoderPool.firstOrNull { it.uri == uri }
if (decoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
}
if (newDecoder == null) {
return null
}
newDecoder.normalizeSize()
decoderRef = DecoderRef(uri, newDecoder)
} else {
decoderPool.remove(decoderRef)
}
decoderPool.add(0, decoderRef)
while (decoderPool.size > DECODER_POOL_SIZE) {
decoderPool.removeAt(decoderPool.size - 1)
}
return decoderRef.decoder
}
private data class DecoderRef(
val uri: Uri,
val svg: SVG,
val decoder: SVG,
)
companion object {
private val PREFERRED_CONFIG = Bitmap.Config.ARGB_8888
private const val DECODER_POOL_SIZE = 3
private val decoderPool = ArrayList<DecoderRef>()
}
}

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves.decoder
import android.content.Context
import android.net.Uri
import android.text.format.Formatter
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
@ -10,9 +11,17 @@ import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter
import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool
import com.bumptech.glide.load.engine.cache.DiskCache
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.engine.cache.LruResourceCache
import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
@ -23,6 +32,30 @@ class AvesAppGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// hide noisy warning (e.g. for images that can't be decoded)
builder.setLogLevel(Log.ERROR)
// sizing
val memorySizeCalculator = MemorySizeCalculator.Builder(context).build()
builder.setMemorySizeCalculator(memorySizeCalculator)
val size: Int = memorySizeCalculator.bitmapPoolSize
if (size > 0) {
builder.setBitmapPool(LruBitmapPool(size.toLong()))
} else {
builder.setBitmapPool(BitmapPoolAdapter())
}
builder.setArrayPool(LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes))
builder.setMemoryCache(LruResourceCache(memorySizeCalculator.memoryCacheSize.toLong()))
val diskCacheSize = DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE
val internalCacheDiskCacheFactory = InternalCacheDiskCacheFactory(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize.toLong())
builder.setDiskCache(internalCacheDiskCacheFactory)
fun toMb(bytes: Int) = Formatter.formatFileSize(context, bytes.toLong())
Log.d(
LOG_TAG, "Glide disk cache size=${toMb(diskCacheSize)}" +
", memory cache size=${toMb(memorySizeCalculator.memoryCacheSize)}" +
", bitmap pool size=${toMb(memorySizeCalculator.bitmapPoolSize)}" +
", array pool size=${toMb(memorySizeCalculator.arrayPoolSizeInBytes)}"
)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
@ -34,6 +67,8 @@ class AvesAppGlideModule : AppGlideModule() {
override fun isManifestParsingEnabled(): Boolean = false
companion object {
private val LOG_TAG = LogUtils.createTag<AvesAppGlideModule>()
// request a fresh image with the highest quality format
val uncachedFullImageOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="app_name">ಎವೀಸ್</string>
<string name="app_widget_label">ಫೋಟೋ ಫ್ರೇಮ್</string>
<string name="wallpaper">ವಾಲ್ಪೇಪರ್</string>
<string name="videos_shortcut_short_label">ವೀಡಿಯೊಗಳು</string>
@ -8,4 +8,5 @@
<string name="analysis_notification_default_title">ಮೀಡಿಯಾ ಸ್ಕ್ಯಾನ್ ಮಾಡಲಾಗುತ್ತಿದೆ</string>
<string name="analysis_notification_action_stop">ನಿಲ್ಲಿಸಿ</string>
<string name="search_shortcut_short_label">ಹುಡುಕಿ</string>
<string name="map_shortcut_short_label">ನಕ್ಷೆ</string>
</resources>

35
assets/terms.txt Normal file
View file

@ -0,0 +1,35 @@
Terms of Service
================
“Aves Gallery” is an open-source gallery and metadata explorer app allowing you to access and manage your local photos and videos.
The app is designed for legal, authorized and acceptable purposes.
Disclaimer
==========
The app is released “as-is”, without any warranty, responsibility or liability. Use of the app is at your own risk.
Privacy Policy
==============
The app does not collect any personal data. We never have access to your photos and videos. This also means that we cannot get them back for you if you delete them without backing them up.
Optionally, with your consent, the app accesses the inventory of installed apps to improve album display.
Optionally, with your consent, the app collects anonymous error and diagnostic data to improve the app quality. We use Firebase Crashlytics, and the anonymous data are stored on their servers. Please note that those are anonymous data, there is absolutely nothing personal about those data.
Contact
=======
Developer: Thibault Deckers
Email: gallery.aves@gmail.com
Website: https://github.com/deckerst/aves

View file

@ -2,4 +2,4 @@
<b>Navigation und Suche</b> ist ein wichtiger Bestandteil von <i>Aves</i>. Das Ziel besteht darin, dass Benutzer problemlos von Alben zu Fotos zu Tags zu Karten usw. wechseln können.
<i>Aves</i> lässt sich mit Android mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.
<i>Aves</i> integriert sich in Android (einschließlich Android TV) mit Funktionen wie <b>Widgets</b>, <b>App-Shortcuts</b>, <b>Bildschirmschoner</b> und der <b>globalen Suche</b> integrieren. Sie funktioniert auch als <b>Medienbetrachter und -Picker</b>.

View file

@ -0,0 +1,4 @@
In v1.12.9:
- play more kinds of motion photos
- enjoy the app in Galician and Kannada
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.12.9:
- play more kinds of motion photos
- enjoy the app in Galician and Kannada
Full changelog available on GitHub

View file

@ -1,5 +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.
<i>ಏವೀಸ್</i> ನಿಮ್ಮ JPEG ಗಳು ಮತ್ತು MP4 ಗಳನ್ನು ಒಳಗೊಂಡಂತೆ ಎಲ್ಲಾ ರೀತಿಯ ಚಿತ್ರಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳನ್ನು ನಿಭಾಯಿಸಬಲ್ಲದು, ಅಲ್ಲದೆ ವಿಶಿಷ್ಟವಾದ <b>ಬಹು-ಪುಟ TIFFಗಳು, SVGಗಳು, ಹಳೆಯ AVIಗಳು ಮತ್ತು ಹಲವು ಪ್ರಕಾರಗಳನ್ನು ಕೂಡ ಬೆಂಬಲಿಸುತ್ತದೆ</b> ಇದು <b>ಚಲನೆಯ ಫೋಟೋಗಳು</b>, <b>ಪನೋರಮಾಗಳು</b> (ಫೋಟೋ ಗೋಳಗಳು) <b>360° ವೀಡಿಯೊಗಳು</b>, ಹಾಗೆಯೇ <b>GeoTIFF</b> ಕಡತಗಳನ್ನು ಗುರುತಿಸಲು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಸಂಗ್ರಹವನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.
<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.
<b>ನ್ಯಾವಿಗೇಷನ್ ಮತ್ತು ಹುಡುಕಾಟ</b> <i>ಏವೀಸ್</i>ನ ಒಂದು ಪ್ರಮುಖ ಭಾಗವಾಗಿದೆ. ಬಳಕೆದಾರರು ಆಲ್ಬಮ್‌ಗಳಿಂದ ಫೋಟೋಗಳಿಂದ ಟ್ಯಾಗ್‌ಗಳಿಗೆ ನಕ್ಷೆಗಳಿಗೆ ಸುಲಭವಾಗಿ ಹರಿಯುವುದು ಗುರಿಯಾಗಿದೆ.
<i>Aves</i> integrates with Android (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>.
<i>ಎವೀಸ್</i> ಆಂಡ್ರಾಯ್ಡ್ (ಟಿವಿ ಸೇರಿದಂತೆ) ನೊಂದಿಗೆ ಸಂಯೋಜಿಸುತ್ತದೆ, ಉದಾಹರಣೆಗೆ <b>ವಿಜೆಟ್‌ಗಳು</b>, <b>ಆ್ಯಪ್ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು</b>, <b>ಸ್ಕ್ರೀನ್ ಸೇವರ್</b> ಮತ್ತು <b>ಜಾಗತಿಕ ಹುಡುಕಾಟ</b> ನಿರ್ವಹಣೆ. ಇದು <b>ಮೀಡಿಯಾ ವೀಕ್ಷಕ ಮತ್ತು ಪಿಕ್ಕರ್</b> ಆಗಿಯೂ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

View file

@ -1 +1 @@
Gallery and metadata explorer
ಗ್ಯಾಲರಿ ಮತ್ತು ಮೆಟಾಡೇಟಾ ಎಕ್ಸ್‌ಪ್ಲೋರರ್

View file

@ -10,7 +10,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@immutable
class UriImage extends ImageProvider<UriImage> with EquatableMixin {
class FullImage extends ImageProvider<FullImage> with EquatableMixin {
final String uri, mimeType;
final int? pageId, rotationDegrees, sizeBytes;
final bool isFlipped, isAnimated;
@ -19,7 +19,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
@override
List<Object?> get props => [uri, pageId, rotationDegrees, isFlipped, isAnimated, scale];
const UriImage({
const FullImage({
required this.uri,
required this.mimeType,
required this.pageId,
@ -31,12 +31,12 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
});
@override
Future<UriImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<UriImage>(this);
Future<FullImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<FullImage>(this);
}
@override
ImageStreamCompleter loadImage(UriImage key, ImageDecoderCallback decode) {
ImageStreamCompleter loadImage(FullImage key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
@ -59,11 +59,11 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
case MimeTypes.svg:
return false;
default:
return !isAnimated;
return !isAnimated && !MimeTypes.isVideo(mimeType);
}
}
Future<ui.Codec> _loadAsync(UriImage key, ImageDecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
Future<ui.Codec> _loadAsync(FullImage key, ImageDecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
assert(key == this);
final request = ImageRequest(

View file

@ -1552,5 +1552,87 @@
"settingsViewerQuickActionEmpty": "Ingen knapper",
"@settingsViewerQuickActionEmpty": {},
"chipActionFilterOut": "Filtrer ud",
"@chipActionFilterOut": {}
"@chipActionFilterOut": {},
"mapStyleOsmHot": "Humanitært OSM",
"@mapStyleOsmHot": {},
"collectionDeselectSectionTooltip": "Fravælg sektion",
"@collectionDeselectSectionTooltip": {},
"editEntryLocationDialogImportGpx": "Importér GPX",
"@editEntryLocationDialogImportGpx": {},
"editEntryLocationDialogTimeShift": "Tidsskift",
"@editEntryLocationDialogTimeShift": {},
"videoStreamSelectionDialogTrack": "Spor",
"@videoStreamSelectionDialogTrack": {},
"albumGroupTier": "Efter kategori",
"@albumGroupTier": {},
"settingsVideoEnableHardwareAcceleration": "Hardwareacceleration",
"@settingsVideoEnableHardwareAcceleration": {},
"settingsViewerSectionTitle": "Fremviser",
"@settingsViewerSectionTitle": {},
"openMapPageTooltip": "Se på kortside",
"@openMapPageTooltip": {},
"settingsCollectionBurstPatternsTile": "Filnavnmønstre",
"@settingsCollectionBurstPatternsTile": {},
"wallpaperUseScrollEffect": "Brug rulleeffekt på startside",
"@wallpaperUseScrollEffect": {},
"editEntryDateDialogSourceFileModifiedDate": "Filens ændringsdato",
"@editEntryDateDialogSourceFileModifiedDate": {},
"editEntryDateDialogShift": "Skift",
"@editEntryDateDialogShift": {},
"chipActionDecompose": "Split",
"@chipActionDecompose": {},
"coordinateFormatDdm": "DDM",
"@coordinateFormatDdm": {},
"videoActionShowNextFrame": "Vis næste frame",
"@videoActionShowNextFrame": {},
"mapStyleStamenWatercolor": "Stamen Watercolor",
"@mapStyleStamenWatercolor": {},
"drawerCollectionAll": "Alle samlinger",
"@drawerCollectionAll": {},
"settingsThumbnailShowVideoDuration": "Vis videovarighed",
"@settingsThumbnailShowVideoDuration": {},
"settingsThemeColorHighlights": "Farvemarkeringer",
"@settingsThemeColorHighlights": {},
"viewerInfoSearchEmpty": "Ingen matchende nøgler",
"@viewerInfoSearchEmpty": {},
"removeEntryMetadataDialogAll": "Alle",
"@removeEntryMetadataDialogAll": {},
"aboutCreditsSectionTitle": "Kreditering",
"@aboutCreditsSectionTitle": {},
"settingsCollectionQuickActionTabSelecting": "Valg",
"@settingsCollectionQuickActionTabSelecting": {},
"settingsCollectionQuickActionTabBrowsing": "Browsing",
"@settingsCollectionQuickActionTabBrowsing": {},
"settingsViewerQuickActionEditorBanner": "Tryk og hold for at flytte knapper og vælge, hvilke handlinger der vises i fremviseren.",
"@settingsViewerQuickActionEditorBanner": {},
"settingsAllowInstalledAppAccess": "Tillad adgang til app-lager",
"@settingsAllowInstalledAppAccess": {},
"viewerInfoBackToViewerTooltip": "Tilbage til fremviser",
"@viewerInfoBackToViewerTooltip": {},
"videoActionShowPreviousFrame": "Vis forrige frame",
"@videoActionShowPreviousFrame": {},
"collectionSelectSectionTooltip": "Vælg sektion",
"@collectionSelectSectionTooltip": {},
"videoStreamSelectionDialogNoSelection": "Der er ingen andre spor.",
"@videoStreamSelectionDialogNoSelection": {},
"addShortcutDialogLabel": "Genvejsetiket",
"@addShortcutDialogLabel": {},
"moveUndatedConfirmationDialogMessage": "Gem elementdatoer, før du fortsætter?",
"@moveUndatedConfirmationDialogMessage": {},
"albumMimeTypeMixed": "Blandet",
"@albumMimeTypeMixed": {},
"videoActionCaptureFrame": "Tag billede af frame",
"@videoActionCaptureFrame": {},
"videoActionSelectStreams": "Vælg spor",
"@videoActionSelectStreams": {},
"videoActionABRepeat": "A-B gentagelse",
"@videoActionABRepeat": {},
"viewerActionLock": "Lås fremviser",
"@viewerActionLock": {},
"viewerActionUnlock": "Oplås fremviser",
"@viewerActionUnlock": {},
"keepScreenOnViewerOnly": "Kun fremvisningsside",
"@keepScreenOnViewerOnly": {},
"widgetOpenPageViewer": "Åbn fremviser",
"@widgetOpenPageViewer": {}
}

View file

@ -1410,5 +1410,9 @@
"chipActionDecompose": "Aufschlüsseln",
"@chipActionDecompose": {},
"editEntryLocationDialogImportGpx": "GPX importieren",
"@editEntryLocationDialogImportGpx": {}
"@editEntryLocationDialogImportGpx": {},
"editEntryLocationDialogTimeShift": "Zeitverschiebung",
"@editEntryLocationDialogTimeShift": {},
"removeEntryMetadataDialogAll": "Alle",
"@removeEntryMetadataDialogAll": {}
}

File diff suppressed because it is too large Load diff

View file

@ -1605,7 +1605,7 @@
"@mapStyleOsmLiberty": {},
"mapStyleOpenTopoMap": "OpenTopoMap",
"@mapStyleOpenTopoMap": {},
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | 地圖由 [OpenTopoMap](https://opentopomap.org/),以 [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/) 授權",
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | 地圖由 [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"sortByDuration": "按時長",
"@sortByDuration": {}

View file

@ -315,7 +315,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get entryActionRemoveFavourite => 'Fjern fra favoritter';
@override
String get videoActionCaptureFrame => 'Capture frame';
String get videoActionCaptureFrame => 'Tag billede af frame';
@override
String get videoActionMute => 'Slå lyden fra';
@ -336,19 +336,19 @@ class AppLocalizationsDa extends AppLocalizations {
String get videoActionSkip10 => 'Spol 10 sekunder frem';
@override
String get videoActionShowPreviousFrame => 'Show previous frame';
String get videoActionShowPreviousFrame => 'Vis forrige frame';
@override
String get videoActionShowNextFrame => 'Show next frame';
String get videoActionShowNextFrame => 'Vis næste frame';
@override
String get videoActionSelectStreams => 'Select tracks';
String get videoActionSelectStreams => 'Vælg spor';
@override
String get videoActionSetSpeed => 'Afspilningshastighed';
@override
String get videoActionABRepeat => 'A-B repeat';
String get videoActionABRepeat => 'A-B gentagelse';
@override
String get videoRepeatActionSetStart => 'Sæt start';
@ -360,10 +360,10 @@ class AppLocalizationsDa extends AppLocalizations {
String get viewerActionSettings => 'Indstillinger';
@override
String get viewerActionLock => 'Lock viewer';
String get viewerActionLock => 'Lås fremviser';
@override
String get viewerActionUnlock => 'Unlock viewer';
String get viewerActionUnlock => 'Oplås fremviser';
@override
String get slideshowActionResume => 'Genoptag';
@ -548,7 +548,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get keepScreenOnVideoPlayback => 'Under videoafspilning';
@override
String get keepScreenOnViewerOnly => 'Viewer page only';
String get keepScreenOnViewerOnly => 'Kun fremvisningsside';
@override
String get keepScreenOnAlways => 'Altid';
@ -575,7 +575,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get mapStyleOpenTopoMap => 'OpenTopoMap';
@override
String get mapStyleOsmHot => 'Humanitarian OSM';
String get mapStyleOsmHot => 'Humanitært OSM';
@override
String get mapStyleStamenWatercolor => 'Stamen Watercolor';
@ -701,7 +701,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get widgetOpenPageCollection => 'Åbn samling';
@override
String get widgetOpenPageViewer => 'Open viewer';
String get widgetOpenPageViewer => 'Åbn fremviser';
@override
String get widgetTapUpdateWidget => 'Opdater widget';
@ -756,7 +756,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get nameConflictDialogMultipleSourceMessage => 'Nogle filer har samme navn.';
@override
String get addShortcutDialogLabel => 'Shortcut label';
String get addShortcutDialogLabel => 'Genvejsetiket';
@override
String get addShortcutButtonLabel => 'TILFØJ';
@ -793,7 +793,7 @@ class AppLocalizationsDa extends AppLocalizations {
}
@override
String get moveUndatedConfirmationDialogMessage => 'Save item dates before proceeding?';
String get moveUndatedConfirmationDialogMessage => 'Gem elementdatoer, før du fortsætter?';
@override
String get moveUndatedConfirmationDialogSetDate => 'Gem datoer';
@ -976,10 +976,10 @@ class AppLocalizationsDa extends AppLocalizations {
String get editEntryDateDialogExtractFromTitle => 'Udtræk fra titel';
@override
String get editEntryDateDialogShift => 'Shift';
String get editEntryDateDialogShift => 'Skift';
@override
String get editEntryDateDialogSourceFileModifiedDate => 'File modified date';
String get editEntryDateDialogSourceFileModifiedDate => 'Filens ændringsdato';
@override
String get durationDialogHours => 'Timer';
@ -1000,7 +1000,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get editEntryLocationDialogChooseOnMap => 'Vælg på kort';
@override
String get editEntryLocationDialogImportGpx => 'Import GPX';
String get editEntryLocationDialogImportGpx => 'Importér GPX';
@override
String get editEntryLocationDialogLatitude => 'Breddegrad';
@ -1009,7 +1009,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get editEntryLocationDialogLongitude => 'Længdegrad';
@override
String get editEntryLocationDialogTimeShift => 'Time shift';
String get editEntryLocationDialogTimeShift => 'Tidsskift';
@override
String get locationPickerUseThisLocationButton => 'Brug denne placering';
@ -1021,7 +1021,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get removeEntryMetadataDialogTitle => 'Fjernelse af metadata';
@override
String get removeEntryMetadataDialogAll => 'All';
String get removeEntryMetadataDialogAll => 'Alle';
@override
String get removeEntryMetadataDialogMore => 'Mere';
@ -1045,10 +1045,10 @@ class AppLocalizationsDa extends AppLocalizations {
String get videoStreamSelectionDialogOff => 'Fra';
@override
String get videoStreamSelectionDialogTrack => 'Track';
String get videoStreamSelectionDialogTrack => 'Spor';
@override
String get videoStreamSelectionDialogNoSelection => 'There are no other tracks.';
String get videoStreamSelectionDialogNoSelection => 'Der er ingen andre spor.';
@override
String get genericSuccessFeedback => 'Færdig!';
@ -1174,7 +1174,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get aboutDataUsageClearCache => 'Ryd cache';
@override
String get aboutCreditsSectionTitle => 'Credits';
String get aboutCreditsSectionTitle => 'Kreditering';
@override
String get aboutCreditsWorldAtlas1 => 'Denne app bruger en TopoJSON-fil fra';
@ -1428,10 +1428,10 @@ class AppLocalizationsDa extends AppLocalizations {
String get collectionEmptyGrantAccessButtonLabel => 'Giv adgang';
@override
String get collectionSelectSectionTooltip => 'Select section';
String get collectionSelectSectionTooltip => 'Vælg sektion';
@override
String get collectionDeselectSectionTooltip => 'Deselect section';
String get collectionDeselectSectionTooltip => 'Fravælg sektion';
@override
String get drawerAboutButton => 'Om';
@ -1440,7 +1440,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get drawerSettingsButton => 'Indstillinger';
@override
String get drawerCollectionAll => 'All collection';
String get drawerCollectionAll => 'Alle samlinger';
@override
String get drawerCollectionFavourites => 'Favoritter';
@ -1530,7 +1530,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get sortOrderLongestFirst => 'Længste først';
@override
String get albumGroupTier => 'By tier';
String get albumGroupTier => 'Efter kategori';
@override
String get albumGroupType => 'Efter type';
@ -1542,7 +1542,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get albumGroupNone => 'Gruppér ikke';
@override
String get albumMimeTypeMixed => 'Mixed';
String get albumMimeTypeMixed => 'Blandet';
@override
String get albumPickPageTitleCopy => 'Kopiér til album';
@ -1794,7 +1794,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsThumbnailShowRawIcon => 'Vis RAW-ikon';
@override
String get settingsThumbnailShowVideoDuration => 'Show video duration';
String get settingsThumbnailShowVideoDuration => 'Vis videovarighed';
@override
String get settingsCollectionQuickActionsTile => 'Hurtighandlinger';
@ -1806,7 +1806,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsCollectionQuickActionTabBrowsing => 'Browsing';
@override
String get settingsCollectionQuickActionTabSelecting => 'Selecting';
String get settingsCollectionQuickActionTabSelecting => 'Valg';
@override
String get settingsCollectionBrowsingQuickActionEditorBanner => 'Tryk og hold for at flytte knapper og vælge, hvilke handlinger der vises, når du gennemser elementer.';
@ -1815,13 +1815,13 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsCollectionSelectionQuickActionEditorBanner => 'Tryk og hold for at flytte knapper og vælge, hvilke handlinger der vises, når du vælger elementer.';
@override
String get settingsCollectionBurstPatternsTile => 'Burst patterns';
String get settingsCollectionBurstPatternsTile => 'Filnavnmønstre';
@override
String get settingsCollectionBurstPatternsNone => 'Ingen';
@override
String get settingsViewerSectionTitle => 'Viewer';
String get settingsViewerSectionTitle => 'Fremviser';
@override
String get settingsViewerGestureSideTapNext => 'Tryk på skærmkanterne for at vise forrige/næste element';
@ -1845,7 +1845,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsViewerQuickActionEditorPageTitle => 'Hurtighandlinger';
@override
String get settingsViewerQuickActionEditorBanner => 'Touch and hold to move buttons and select which actions are displayed in the viewer.';
String get settingsViewerQuickActionEditorBanner => 'Tryk og hold for at flytte knapper og vælge, hvilke handlinger der vises i fremviseren.';
@override
String get settingsViewerQuickActionEditorDisplayedButtonsSectionTitle => 'Viste knapper';
@ -1938,7 +1938,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsVideoPlaybackPageTitle => 'Afspilning';
@override
String get settingsVideoEnableHardwareAcceleration => 'Hardware acceleration';
String get settingsVideoEnableHardwareAcceleration => 'Hardwareacceleration';
@override
String get settingsVideoAutoPlay => 'Afspil automatisk';
@ -2031,7 +2031,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsPrivacySectionTitle => 'Privatliv';
@override
String get settingsAllowInstalledAppAccess => 'Allow access to app inventory';
String get settingsAllowInstalledAppAccess => 'Tillad adgang til app-lager';
@override
String get settingsAllowInstalledAppAccessSubtitle => 'Bruges til at forbedre albumvisning';
@ -2106,7 +2106,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get settingsThemeBrightnessDialogTitle => 'Tema';
@override
String get settingsThemeColorHighlights => 'Color highlights';
String get settingsThemeColorHighlights => 'Farvemarkeringer';
@override
String get settingsThemeEnableDynamicColor => 'Dynamisk farve';
@ -2210,7 +2210,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get viewerInfoPageTitle => 'Info';
@override
String get viewerInfoBackToViewerTooltip => 'Back to viewer';
String get viewerInfoBackToViewerTooltip => 'Tilbage til fremviser';
@override
String get viewerInfoUnknown => 'ukendt';
@ -2279,7 +2279,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get mapAttributionStamen => 'Fliser af [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)';
@override
String get openMapPageTooltip => 'View on Map page';
String get openMapPageTooltip => 'Se på kortside';
@override
String get mapEmptyRegion => 'Ingen billeder i denne region';
@ -2297,7 +2297,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get viewerInfoSearchFieldLabel => 'Søg i metadata';
@override
String get viewerInfoSearchEmpty => 'No matching keys';
String get viewerInfoSearchEmpty => 'Ingen matchende nøgler';
@override
String get viewerInfoSearchSuggestionDate => 'Dato og tid';
@ -2315,7 +2315,7 @@ class AppLocalizationsDa extends AppLocalizations {
String get viewerInfoSearchSuggestionRights => 'Rettigheder';
@override
String get wallpaperUseScrollEffect => 'Use scroll effect on home screen';
String get wallpaperUseScrollEffect => 'Brug rulleeffekt på startside';
@override
String get tagEditorPageTitle => 'Rediger Tags';

View file

@ -1006,7 +1006,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get editEntryLocationDialogLongitude => 'Längengrad';
@override
String get editEntryLocationDialogTimeShift => 'Time shift';
String get editEntryLocationDialogTimeShift => 'Zeitverschiebung';
@override
String get locationPickerUseThisLocationButton => 'Diesen Standort verwenden';
@ -1018,7 +1018,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get removeEntryMetadataDialogTitle => 'Entfernung von Metadaten';
@override
String get removeEntryMetadataDialogAll => 'All';
String get removeEntryMetadataDialogAll => 'Alle';
@override
String get removeEntryMetadataDialogMore => 'Mehr';

File diff suppressed because it is too large Load diff

View file

@ -4548,7 +4548,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
String get mapAttributionOsmLiberty => '地圖由 [OpenMapTiles](https://www.openmaptiles.org/) 所提供,以 [CC BY](http://creativecommons.org/licenses/by/4.0) 授權 • 托管於 [OSM Americana](https://tile.ourmap.us)';
@override
String get mapAttributionOpenTopoMap => '[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | 地圖由 [OpenTopoMap](https://opentopomap.org/),以 [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/) 授權';
String get mapAttributionOpenTopoMap => '[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | 地圖由 [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)';
@override
String get mapAttributionOsmHot => '繪製於 [HOT](https://www.hotosm.org/) • 主辦方 [OSM France](https://openstreetmap.fr/)';

View file

@ -130,6 +130,8 @@ class Contributors {
Contributor('pitroig', 'ona@riseup.net'),
Contributor('Rubén Castiñeiras Lorenzo', 'rcasl@outlook.com'),
Contributor('hanyang cheng', 'cinxiafortis@tutanota.de'),
Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'),
Contributor('Prasannakumar T Bhat', 'pbhat99@gmail.com'),
// Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
@ -142,7 +144,6 @@ class Contributors {
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
// Contributor('Sartaj', 'ssaarrttaajj111@gmail.com'), // Hindi
// Contributor('Anurag Samota', 'anuragsamotasamota@gmail.com'), // Hindi
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)
// Contributor('Raman', 'xysed@tutanota.com'), // Malayalam

View file

@ -496,13 +496,8 @@ class LocalMediaDbUpgrader {
static Future<void> _upgradeFrom14(Database db) async {
debugPrint('upgrading DB from v14');
// no schema changes, but v1.12.4 may have corrupted the DB,
// so we clear rebuildable tables
final tables = [dateTakenTable, metadataTable, addressTable, trashTable, videoPlaybackTable];
await Future.forEach(tables, (table) async {
if (await db.tableExists(table)) {
await db.delete(table, where: '1');
}
});
// transitional upgrade previously used to sanitize rebuildable tables
// (dateTakenTable, metadataTable, addressTable, trashTable, videoPlaybackTable)
// for users with a potentially corrupted DB following upgrade to v1.12.4
}
}

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:aves/image_providers/full_image_provider.dart';
import 'package:aves/image_providers/thumbnail_provider.dart';
import 'package:aves/image_providers/uri_image_provider.dart';
import 'package:flutter/foundation.dart';
class EntryCache {
@ -30,7 +30,7 @@ class EntryCache {
int? pageId;
// evict fullscreen image
await UriImage(
await FullImage(
uri: uri,
mimeType: mimeType,
pageId: pageId,

View file

@ -1,8 +1,8 @@
import 'dart:math';
import 'package:aves/image_providers/full_image_provider.dart';
import 'package:aves/image_providers/region_provider.dart';
import 'package:aves/image_providers/thumbnail_provider.dart';
import 'package:aves/image_providers/uri_image_provider.dart';
import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/utils/math_utils.dart';
@ -49,7 +49,7 @@ extension ExtraAvesEntryImages on AvesEntry {
));
}
UriImage get uriImage => UriImage(
FullImage get fullImage => FullImage(
uri: uri,
mimeType: mimeType,
pageId: pageId,

View file

@ -157,7 +157,7 @@ class MappedGeoTiff with MapOverlay {
String get id => entry.uri;
@override
ImageProvider get imageProvider => entry.uriImage;
ImageProvider get imageProvider => entry.fullImage;
@override
bool get canOverlay => center != null;

View file

@ -9,6 +9,7 @@ import 'package:aves/services/common/output_buffer.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/byte_receiving_codec.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart';
@ -55,7 +56,9 @@ abstract class MediaFetchService {
int? priority,
});
Future<void> clearSizedThumbnailDiskCache();
Future<void> clearImageDiskCache();
Future<void> clearImageMemoryCache();
bool cancelRegion(Object taskKey);
@ -255,9 +258,18 @@ class PlatformMediaFetchService implements MediaFetchService {
}
@override
Future<void> clearSizedThumbnailDiskCache() async {
Future<void> clearImageDiskCache() async {
try {
return _platformObject.invokeMethod('clearSizedThumbnailDiskCache');
return _platformObject.invokeMethod('clearImageDiskCache');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
@override
Future<void> clearImageMemoryCache() async {
try {
return _platformObject.invokeMethod('clearImageMemoryCache');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -317,7 +329,7 @@ class PlatformMediaFetchService implements MediaFetchService {
}
@immutable
class ImageRequest {
class ImageRequest extends Equatable {
final String uri;
final String mimeType;
final int? rotationDegrees;
@ -327,14 +339,17 @@ class ImageRequest {
final int? sizeBytes;
final BytesReceivedCallback? onBytesReceived;
@override
List<Object?> get props => [uri, mimeType, rotationDegrees, isFlipped, isAnimated, pageId, sizeBytes, onBytesReceived];
const ImageRequest(
this.uri,
this.mimeType, {
required this.rotationDegrees,
required this.isFlipped,
required this.isAnimated,
required this.pageId,
required this.sizeBytes,
this.onBytesReceived,
});
this.uri,
this.mimeType, {
required this.rotationDegrees,
required this.isFlipped,
required this.isAnimated,
required this.pageId,
required this.sizeBytes,
this.onBytesReceived,
});
}

View file

@ -88,7 +88,7 @@ class _AboutDataUsageState extends State<AboutDataUsage> with FeedbackMixin {
onPressed: () async {
await storageService.deleteTempDirectory();
await storageService.deleteExternalCache();
await mediaFetchService.clearSizedThumbnailDiskCache();
await mediaFetchService.clearImageDiskCache();
imageCache.clear();
_reload();
setState(() {});

View file

@ -70,7 +70,6 @@ class AvesApp extends StatefulWidget {
'fi', // Finnish
'he', // Hebrew
'hi', // Hindi
'kn', // Kannada
'ml', // Malayalam
'my', // Burmese
'or', // Odia

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/model/settings/settings.dart';
@ -220,6 +221,7 @@ class AvesFloatingBar extends StatefulWidget {
class _AvesFloatingBarState extends State<AvesFloatingBar> with RouteAware {
// prevent expensive blurring when the current page is hidden
final ValueNotifier<bool> _isBlurAllowedNotifier = ValueNotifier(true);
Timer? _blurBlockTimer;
@override
void didChangeDependencies() {
@ -240,6 +242,8 @@ class _AvesFloatingBarState extends State<AvesFloatingBar> with RouteAware {
@override
void didPopNext() {
// post to prevent single frame flash during hero
_blurBlockTimer?.cancel();
_blurBlockTimer = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_isBlurAllowedNotifier.value = true;
@ -249,8 +253,9 @@ class _AvesFloatingBarState extends State<AvesFloatingBar> with RouteAware {
@override
void didPushNext() {
// post to prevent single frame flash during hero
WidgetsBinding.instance.addPostFrameCallback((_) {
// delay blur disabling, otherwise visual artifacts appear during page transition with Impeller
_blurBlockTimer?.cancel();
_blurBlockTimer = Timer(ADurations.pageTransitionLoose, () {
if (mounted) {
_isBlurAllowedNotifier.value = false;
}

View file

@ -12,6 +12,20 @@ class DebugCacheSection extends StatefulWidget {
}
class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKeepAliveClientMixin {
final TextEditingController _imageCacheSizeTextController = TextEditingController();
@override
void initState() {
super.initState();
_imageCacheSizeTextController.text = '${imageCache.maximumSizeBytes}';
}
@override
void dispose() {
_imageCacheSizeTextController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -41,6 +55,31 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
),
],
),
Row(
children: [
Expanded(
child: TextField(
controller: _imageCacheSizeTextController,
decoration: const InputDecoration(labelText: 'imageCache size bytes'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
final size = int.tryParse(_imageCacheSizeTextController.text);
if (size != null) {
imageCache.maximumSizeBytes = size;
} else {
_imageCacheSizeTextController.text = '${imageCache.maximumSizeBytes}';
}
setState(() {});
},
child: const Text('Apply'),
),
],
),
const Divider(),
Row(
children: [
const Expanded(
@ -48,7 +87,19 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: mediaFetchService.clearSizedThumbnailDiskCache,
onPressed: mediaFetchService.clearImageDiskCache,
child: const Text('Clear'),
),
],
),
Row(
children: [
const Expanded(
child: Text('Glide memory cache: ?'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: mediaFetchService.clearImageMemoryCache,
child: const Text('Clear'),
),
],

View file

@ -24,6 +24,7 @@ class SupportedLocales {
'is': 'Íslenska',
'it': 'Italiano',
'ja': '日本語',
'kn': 'ಕನ್ನಡ',
'ko': '한국어',
'lt': 'Lietuvių',
'nb': 'Norsk (Bokmål)',

View file

@ -118,7 +118,7 @@ class EntryPrinter with FeedbackMixin {
}
} else {
return pdf.Image(
await flutterImageProvider(entry.uriImage),
await flutterImageProvider(entry.fullImage),
fit: _fit,
);
}

View file

@ -172,7 +172,7 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin {
} else {
// provider image is already rotated, but not cropped
needCrop = true;
provider = entry.uriImage;
provider = entry.fullImage;
}
}
if (provider == null) return null;

View file

@ -90,7 +90,7 @@ class _PanoramaPageState extends State<PanoramaPage> {
}
},
child: Image(
image: entry.uriImage,
image: entry.fullImage,
),
),
Positioned(

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
@ -15,7 +16,7 @@ import 'package:leak_tracker/leak_tracker.dart';
class VideoConductor {
final CollectionLens? _collection;
final List<AvesVideoController> _controllers = [];
final List<StreamSubscription> _subscriptions = [];
final Map<AvesVideoController, StreamSubscription> _subscriptions = {};
final PlaybackStateHandler _playbackStateHandler = DatabasePlaybackStateHandler();
final ValueNotifier<AvesVideoController?> playingVideoControllerNotifier = ValueNotifier(null);
@ -36,9 +37,6 @@ class VideoConductor {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectDisposed(object: this);
}
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
await _disposeAll();
playingVideoControllerNotifier.dispose();
_controllers.clear();
@ -47,22 +45,24 @@ class VideoConductor {
}
}
AvesVideoController getOrCreateController(AvesEntry entry, {int? maxControllerCount}) {
Future<AvesVideoController> getOrCreateController(AvesEntry entry, {int? maxControllerCount}) async {
var controller = getController(entry);
if (controller != null) {
_controllers.remove(controller);
} else {
maxControllerCount = max(_defaultMaxControllerCount, maxControllerCount ?? 0);
while (_controllers.length >= maxControllerCount) {
await _disposeController(_controllers.removeLast());
}
await deviceService.requestGarbageCollection();
controller = videoControllerFactory.buildController(
entry,
playbackStateHandler: _playbackStateHandler,
settings: settings,
);
_subscriptions.add(controller.statusStream.listen((event) => _onControllerStatusChanged(entry, controller!, event)));
_subscriptions[controller] = controller.statusStream.listen((event) => _onControllerStatusChanged(entry, controller!, event));
}
_controllers.insert(0, controller);
while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) {
_controllers.removeLast().dispose();
}
return controller;
}
@ -99,9 +99,14 @@ class VideoConductor {
Future<void> _applyToAll(FutureOr Function(AvesVideoController controller) action) => Future.forEach<AvesVideoController>(_controllers, action);
Future<void> _disposeAll() => _applyToAll((controller) => controller.dispose());
Future<void> _disposeAll() => _applyToAll(_disposeController);
Future<void> pauseAll() => _applyToAll((controller) => controller.pause());
Future<void> muteAll(bool muted) => _applyToAll((controller) => controller.mute(muted));
Future<void> _disposeController(AvesVideoController controller) async {
await _subscriptions.remove(controller)?.cancel();
await controller.dispose();
}
}

View file

@ -130,7 +130,7 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
}
Future<void> _initVideoController(AvesEntry entry) async {
final controller = context.read<VideoConductor>().getOrCreateController(entry);
final controller = await context.read<VideoConductor>().getOrCreateController(entry);
setState(() {});
if (videoAutoPlayEnabled || entry.isAnimated) {
@ -157,7 +157,9 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
if (videoPageEntries.isNotEmpty) {
// init video controllers for all pages that could need it
final videoConductor = context.read<VideoConductor>();
videoPageEntries.forEach((entry) => videoConductor.getOrCreateController(entry, maxControllerCount: videoPageEntries.length));
await Future.forEach(videoPageEntries, (entry) async {
await videoConductor.getOrCreateController(entry, maxControllerCount: videoPageEntries.length);
});
// auto play/pause when changing page
Future<void> _onPageChanged() async {

View file

@ -43,6 +43,8 @@ class _RasterImageViewState extends State<RasterImageView> {
final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false);
ImageInfo? _fullImageInfo;
static const double _tilesByShortestSide = 2;
AvesEntry get entry => widget.entry;
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
@ -61,7 +63,7 @@ class _RasterImageViewState extends State<RasterImageView> {
region: fullImageRegion,
);
} else {
return entry.uriImage;
return entry.fullImage;
}
}
@ -158,7 +160,7 @@ class _RasterImageViewState extends State<RasterImageView> {
void _initTiling(Size viewportSize) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
_tileSide = viewportSize.shortestSide * devicePixelRatio;
_tileSide = viewportSize.shortestSide * devicePixelRatio / _tilesByShortestSide;
// scale for initial state `contained`
final containedScale = min(viewportSize.width / _displaySize.width, viewportSize.height / _displaySize.height);
_maxSampleSize = ExtraAvesEntryImages.sampleSizeForScale(magnifierScale: containedScale, devicePixelRatio: devicePixelRatio);

View file

@ -58,7 +58,7 @@ class _VideoCoverState extends State<VideoCover> {
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 : entry).uriImage;
ImageProvider get videoCoverUriImage => (mainEntry.isMotionPhoto ? mainEntry : entry).fullImage;
@override
void initState() {

View file

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "7fd72d77a7487c26faab1d274af23fb008763ddc10800261abbfb2c067f183d5"
sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422
url: "https://pub.dev"
source: hosted
version: "1.3.53"
version: "1.3.54"
analyzer:
dependency: transitive
description:
@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742"
sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12"
url: "https://pub.dev"
source: hosted
version: "4.0.4"
version: "4.0.5"
args:
dependency: transitive
description:
@ -296,10 +296,10 @@ packages:
dependency: transitive
description:
name: firebase_core
sha256: f4d8f49574a4e396f34567f3eec4d38ab9c3910818dec22ca42b2a467c685d8b
sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe"
url: "https://pub.dev"
source: hosted
version: "3.12.1"
version: "3.13.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -312,26 +312,26 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: faa5a76f6380a9b90b53bc3bdcb85bc7926a382e0709b9b5edac9f7746651493
sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23"
url: "https://pub.dev"
source: hosted
version: "2.21.1"
version: "2.22.0"
firebase_crashlytics:
dependency: transitive
description:
name: firebase_crashlytics
sha256: d672dad83e6e99b826599fef63dbe71bac70633d5c3df90c124e986e1461e79b
sha256: f3fa4a17c2f061b16b2e3ac7aaed889ae954b8952d0fd3e0009a9870cde7bbd2
url: "https://pub.dev"
source: hosted
version: "4.3.4"
version: "4.3.5"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: b2468a5cd54051dd31ca332a5c35f1bcbfb21b0135f84d4606c3275a226c0321
sha256: cedfbe39927711c0e56fc38bfecbd89e17816b21698a3d88d63298c530ed375c
url: "https://pub.dev"
source: hosted
version: "3.8.4"
version: "3.8.5"
fixnum:
dependency: transitive
description:
@ -352,10 +352,10 @@ packages:
dependency: transitive
description:
name: flex_seed_scheme
sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0
sha256: b06d8b367b84cbf7ca5c5603c858fa5edae88486c4e4da79ac1044d73b6c62ec
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.5.1"
floating:
dependency: "direct main"
description:
@ -440,10 +440,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec"
url: "https://pub.dev"
source: hosted
version: "0.7.6+2"
version: "0.7.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -535,26 +535,26 @@ packages:
dependency: transitive
description:
name: google_maps_flutter
sha256: "621125e35e81ca39ef600e45243d2be93167e61def72bc7207b0c4a635c58506"
sha256: "830d8f7b51b4a950bf0d7daa675324fed6c9beb57a7ecca2a59018270c96b4e0"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
version: "2.12.1"
google_maps_flutter_android:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: "3b3f55d6b4f2bde6bbe80dca0bf8d228313005c9ce8a97a1d24257600d8c92e5"
sha256: "0ede4ae8326335c0c007c8c7a8c9737449263123385e2bdf49f3e71103b2dc2e"
url: "https://pub.dev"
source: hosted
version: "2.14.14"
version: "2.16.0"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: "6f798adb0aa1db5adf551f2e39e24bd06c8c0fbe4de912fb2d9b5b3f48147b02"
sha256: ef72c822930ce69515cb91c10cd88cfb8b26296f765808a43cbc9a10eaffacfe
url: "https://pub.dev"
source: hosted
version: "2.13.2"
version: "2.15.0"
google_maps_flutter_platform_interface:
dependency: transitive
description:
@ -567,10 +567,10 @@ packages:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: bbeb93807a34bfeebdb7ace506bd2bc400a3915dc96736254fea721eb264caa0
sha256: a45786ea6691cc7cdbe2cf3ce2c2daf4f82a885745666b4a36baada3a4e12897
url: "https://pub.dev"
source: hosted
version: "0.5.11"
version: "0.5.12"
gpx:
dependency: "direct main"
description:
@ -623,10 +623,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3"
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
version: "4.5.4"
intl:
dependency: "direct main"
description:
@ -788,31 +788,29 @@ packages:
source: hosted
version: "7.0.7296"
media_kit:
dependency: "direct overridden"
dependency: transitive
description:
path: media_kit
ref: d2145a50f68394096845915a28874341fbf5b3fe
resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
url: "https://github.com/media-kit/media-kit.git"
source: git
version: "1.1.11"
name: media_kit
sha256: "48c10c3785df5d88f0eef970743f8c99b2e5da2b34b9d8f9876e598f62d9e776"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
media_kit_libs_android_video:
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
url: "https://pub.dev"
source: hosted
version: "1.3.6"
version: "1.3.7"
media_kit_video:
dependency: "direct overridden"
dependency: transitive
description:
path: media_kit_video
ref: d2145a50f68394096845915a28874341fbf5b3fe
resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
url: "https://github.com/media-kit/media-kit.git"
source: git
version: "1.2.5"
name: media_kit_video
sha256: a656a9463298c1adc64c57f2d012874f7f2900f0c614d9545a3e7b8bb9e2137b
url: "https://pub.dev"
source: hosted
version: "1.3.0"
meta:
dependency: transitive
description:
@ -1171,10 +1169,10 @@ packages:
dependency: "direct main"
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
url: "https://pub.dev"
source: hosted
version: "6.1.2"
version: "6.1.4"
pub_semver:
dependency: transitive
description:
@ -1267,10 +1265,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
@ -1577,10 +1575,10 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
version: "6.3.3"
url_launcher_linux:
dependency: transitive
description:
@ -1673,18 +1671,18 @@ packages:
dependency: "direct main"
description:
name: volume_controller
sha256: "30863a51338db47fe16f92902b1a6c4ee5e15c9287b46573d7c2eb6be1f197d2"
sha256: e82fd689bb8e1fe8e64be3fa5946ff8699058f8cf9f4c1679acdba20cda7f5bd
url: "https://pub.dev"
source: hosted
version: "3.3.1"
version: "3.3.3"
wakelock_plus:
dependency: transitive
description:
name: wakelock_plus
sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e"
sha256: b90fbcc8d7bdf3b883ea9706d9d76b9978cb1dfa4351fcc8014d6ec31a493354
url: "https://pub.dev"
source: hosted
version: "1.2.10"
version: "1.2.11"
wakelock_plus_platform_interface:
dependency: transitive
description:

View file

@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
version: 1.12.8+148
version: 1.12.9+149
publish_to: none
environment:
@ -130,18 +130,6 @@ dependencies:
volume_controller:
xml:
dependency_overrides:
media_kit:
git:
url: https://github.com/media-kit/media-kit.git
ref: d2145a50f68394096845915a28874341fbf5b3fe
path: media_kit
media_kit_video:
git:
url: https://github.com/media-kit/media-kit.git
ref: d2145a50f68394096845915a28874341fbf5b3fe
path: media_kit_video
dev_dependencies:
flutter_test:
sdk: flutter
@ -171,9 +159,6 @@ flutter:
################################################################################
# Test driver
# capture shaders (profile mode, real device only):
# % ./flutterw drive --flavor play -t test_driver/driver_shaders.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json
# generate screenshots (profile mode, specific collection):
# % ./flutterw drive --flavor play -t test_driver/driver_screenshots.dart --profile

File diff suppressed because one or more lines are too long

View file

@ -58,6 +58,8 @@ Future<void> configureAndLaunch() async {
..coordinateFormat = CoordinateFormat.dms
..unitSystem = UnitSystem.metric
// map
..mapStyle = EntryMapStyle.googleNormal;
..mapStyle = EntryMapStyle.googleNormal
// debug
..debugShowViewerTiles = false;
app.main();
}

View file

@ -1,4 +1,4 @@
In v1.12.8:
In v1.12.9:
- play more kinds of motion photos
- enjoy the app in Galician
- enjoy the app in Galician and Kannada
Full changelog available on GitHub