Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-09-16 00:14:08 +02:00
commit ce3afd87a0
92 changed files with 1327 additions and 1058 deletions

@ -1 +1 @@
Subproject commit 5874a72aa4c779a02553007c47dacbefba2374dc
Subproject commit 2663184aa79047d0a33a14a3b607954f8fdd8730

View file

@ -1,36 +0,0 @@
name: Quality check
on:
push:
branches:
- develop
pull_request:
types: [ opened, synchronize, reopened ]
permissions:
contents: read
jobs:
build:
name: Check code quality.
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit
- name: Clone the repository.
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get packages for the Flutter project.
run: scripts/pub_get_all.sh
- name: Update the flutter version file.
run: scripts/update_flutter_version.sh
- name: Static analysis.
run: ./flutterw analyze
- name: Unit tests.
run: ./flutterw test

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit

88
.github/workflows/quality-check.yml vendored Normal file
View file

@ -0,0 +1,88 @@
name: Quality check
on:
push:
branches: [ "develop", "main" ]
pull_request:
branches: [ "develop", "main" ]
types: [ opened, synchronize, reopened ]
schedule:
- cron: '17 8 * * 3'
# Declare default permissions as read only.
permissions: read-all
jobs:
analyze_flutter:
name: Flutter analysis
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get Flutter packages
run: scripts/pub_get_all.sh
- name: Static analysis.
run: ./flutterw analyze
- name: Unit tests.
run: ./flutterw test
analyze_codeql:
name: CodeQL analysis (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
strategy:
fail-fast: false
matrix:
include:
- language: java-kotlin
build-mode: manual
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
# Building relies on the Android Gradle plugin,
# which requires a modern Java version (not the default one).
- name: Set up JDK for Android Gradle plugin
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
with:
distribution: 'temurin'
java-version: '21'
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- if: matrix.build-mode == 'manual'
shell: bash
# build in profile mode, instead of release,
# so that setting up signing environment variables is not required
run: |
scripts/apply_flavor_play.sh
./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7
with:
category: "/language:${{matrix.language}}"

View file

@ -5,37 +5,39 @@ on:
tags:
- v*
# Declare default permissions as read only.
permissions: read-all
jobs:
build:
name: Build and release artifacts.
release_github:
name: GitHub release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
# Building relies on the Android Gradle plugin,
# which requires a modern Java version (not the default one).
- name: Set up JDK for Android Gradle plugin
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
with:
distribution: 'zulu'
java-version: '17'
distribution: 'temurin'
java-version: '21'
- name: Clone the repository.
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get packages for the Flutter project.
- name: Get Flutter packages
run: scripts/pub_get_all.sh
- name: Update the flutter version file.
- name: Update Flutter version file
run: scripts/update_flutter_version.sh
- name: Static analysis.
run: ./flutterw analyze
- name: Unit tests.
run: ./flutterw test
- name: Build signed artifacts.
- name: Build signed artifacts
# `KEY_JKS` should contain the result of:
# gpg -c --armor keystore.jks
# `KEY_JKS_PASSPHRASE` should contain the passphrase used for the command above
@ -70,7 +72,7 @@ jobs:
AVES_KEY_PASSWORD: ${{ secrets.AVES_KEY_PASSWORD }}
AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }}
- name: Create a release with the APK and App Bundle.
- name: Create GitHub release
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
with:
artifacts: "outputs/*"
@ -78,29 +80,30 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: appbundle
path: outputs/app-play-release.aab
release:
name: Create beta release on Play Store.
release_play:
name: Play Store beta release
needs: [ build ]
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get appbundle from artifacts.
- name: Get appbundle from artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: appbundle
- name: Release app to beta channel.
- name: Release to beta channel
uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}

View file

@ -31,7 +31,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
@ -63,7 +63,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: SARIF file
path: results.sarif
@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7
with:
sarif_file: results.sarif

View file

@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.11"></a>[v1.11.11] - 2024-09-16
### Added
- support opening from the lock screen
### Changed
- upgraded Flutter to stable v3.24.3
### Fixed
- crash when cataloguing some malformed MP4 files
- inconsistent launch screen
## <a id="v1.11.10"></a>[v1.11.10] - 2024-09-01
### Added

View file

@ -49,8 +49,8 @@ android {
ndkVersion '26.1.10909125'
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
lint {
@ -156,12 +156,12 @@ android {
}
tasks.withType(KotlinCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
jvmToolchain(17)
jvmToolchain(21)
}
flutter {
@ -189,11 +189,11 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.7.0"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
implementation 'androidx.lifecycle:lifecycle-process:2.8.5'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.work:work-runtime-ktx:2.9.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
@ -208,14 +208,14 @@ dependencies {
// - https://jitpack.io/p/deckerst/mp4parser
// - https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:Android-TiffBitmapFactory:90c06eebf4'
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c'
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c'
implementation 'com.github.deckerst.mp4parser:isoparser:86d4b6baa1'
implementation 'com.github.deckerst.mp4parser:muxer:86d4b6baa1'
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.3'
kapt 'androidx.annotation:annotation:1.8.1'
kapt 'androidx.annotation:annotation:1.8.2'
ksp "com.github.bumptech.glide:ksp:$glide_version"
compileOnly rootProject.findProject(':streams_channel')

View file

@ -128,6 +128,7 @@
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:showWhenLocked="true"
android:supportsPictureInPicture="true"
android:theme="@style/NormalTheme"
android:windowSoftInputMode="adjustResize">

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.SearchManager
import android.appwidget.AppWidgetManager
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -229,6 +230,7 @@ open class MainActivity : FlutterFragmentActivity() {
intentStreamHandler.notifyNewIntent(extractIntentData(intent))
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
@ -316,6 +318,13 @@ open class MainActivity : FlutterFragmentActivity() {
INTENT_DATA_KEY_URI to uri.toString(),
)
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager
val isLocked = keyguardManager.isKeyguardLocked
if (isLocked) {
// device is locked, so access to content is limited to intent URI by default
fields[INTENT_DATA_KEY_SECURE_URIS] = listOf(uri.toString())
}
if (action == MediaStore.ACTION_REVIEW_SECURE) {
val uris = ArrayList<String>()
intent.clipData?.let { clipData ->
@ -323,8 +332,10 @@ open class MainActivity : FlutterFragmentActivity() {
clipData.getItemAt(i).uri?.let { uris.add(it.toString()) }
}
}
if (uris.isNotEmpty()) {
fields[INTENT_DATA_KEY_SECURE_URIS] = uris
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && intent.hasExtra(MediaStore.EXTRA_BRIGHTNESS)) {
fields[INTENT_DATA_KEY_BRIGHTNESS] = intent.getFloatExtra(MediaStore.EXTRA_BRIGHTNESS, 0f)
}

View file

@ -33,6 +33,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
"getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isLocked" -> safe(call, result, ::isLocked)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
"requestMediaManagePermission" -> safe(call, result, ::requestMediaManagePermission)
"getAvailableHeapSize" -> safe(call, result, ::getAvailableHeapSize)
@ -49,13 +50,11 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
val sdkInt = Build.VERSION.SDK_INT
result.success(
hashMapOf(
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
"canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"hasGeocoder" to Geocoder.isPresent(),
"isDynamicColorAvailable" to DynamicColors.isDynamicColorAvailable(),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
@ -100,6 +99,12 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(Build.VERSION.SDK_INT)
}
private fun isLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager
val isLocked = keyguardManager.isKeyguardLocked
result.success(isLocked)
}
private fun isSystemFilePickerEnabled(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val enabled = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).resolveActivity(context.packageManager) != null
result.success(enabled)

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="NormalTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowBackground">@color/window_background_night</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/window_background_night</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher</item>
<!-- API28+, draws next to the notch in fullscreen -->
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="28">shortEdges</item>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_widget_label">Okvir Slike</string>
<string name="wallpaper">Pozadina</string>
<string name="safe_mode_shortcut_short_label">Siguran režim</string>
<string name="videos_shortcut_short_label">Snimci</string>
<string name="analysis_channel_name">Pretraga medija</string>
<string name="analysis_notification_default_title">Skeniranje medija</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="app_name">Aves</string>
<string name="search_shortcut_short_label">Pretraga</string>
</resources>

View file

@ -4,4 +4,6 @@
<color name="ic_shortcut_background">#FFFFFF</color>
<color name="ic_shortcut_foreground">#455A64</color>
<color name="ic_launcher_flavour">#1cc8eb</color>
<color name="window_background_day">#FFFFFF</color>
<color name="window_background_night">#262626</color>
</resources>

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowBackground">@color/window_background_day</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/window_background_day</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher</item>
<!-- API28+, draws next to the notch in fullscreen -->
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="28">shortEdges</item>

View file

@ -4,10 +4,10 @@ plugins {
android {
namespace 'androidx.exifinterface.media'
compileSdk 34
compileSdk 35
defaultConfig {
minSdk 19
minSdk 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@ -20,11 +20,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
dependencies {
implementation 'androidx.annotation:annotation:1.8.0'
implementation 'androidx.annotation:annotation:1.8.2'
}

View file

@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20"
settings.ext.agp_version = '8.5.1'
settings.ext.agp_version = '8.6.0'
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

View file

@ -0,0 +1,3 @@
In v1.11.11:
- review photos from the lock screen
Full changelog available on GitHub

View file

@ -0,0 +1,3 @@
In v1.11.11:
- review photos from the lock screen
Full changelog available on GitHub

View file

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

View file

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

View file

@ -1 +1,119 @@
{}
{
"welcomeMessage": "Velkommen til Aves",
"@welcomeMessage": {},
"welcomeOptional": "Valgfri",
"@welcomeOptional": {},
"appName": "Aves",
"@appName": {},
"welcomeTermsToggle": "Jeg accepterer vilkårene og betingelserne",
"@welcomeTermsToggle": {},
"itemCount": "{count, plural, =1{{count} fil} other{{count} filer}}",
"@itemCount": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"columnCount": "{count, plural, =1{{count} kolonne} other{{count} kolonner}}",
"@columnCount": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"timeMinutes": "{count, plural, =1{{count} minut} other{{count} minutter}}",
"@timeMinutes": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"timeDays": "{count, plural, =1{{count} dag} other{{count} dage}}",
"@timeDays": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"focalLength": "{length} mm",
"@focalLength": {
"placeholders": {
"length": {
"type": "String",
"example": "5.4"
}
}
},
"applyButtonLabel": "UDFØR",
"@applyButtonLabel": {},
"deleteButtonLabel": "SLET",
"@deleteButtonLabel": {},
"nextButtonLabel": "NÆSTE",
"@nextButtonLabel": {},
"showButtonLabel": "VIS",
"@showButtonLabel": {},
"hideButtonLabel": "SKJUL",
"@hideButtonLabel": {},
"saveCopyButtonLabel": "GEM KOPI",
"@saveCopyButtonLabel": {},
"applyTooltip": "Udfør",
"@applyTooltip": {},
"cancelTooltip": "Fortryd",
"@cancelTooltip": {},
"nextTooltip": "Næste",
"@nextTooltip": {},
"showTooltip": "Vis",
"@showTooltip": {},
"hideTooltip": "Skjul",
"@hideTooltip": {},
"actionRemove": "Fjern",
"@actionRemove": {},
"resetTooltip": "Nulstil",
"@resetTooltip": {},
"saveTooltip": "Gem",
"@saveTooltip": {},
"chipActionDelete": "Slet",
"@chipActionDelete": {},
"chipActionGoToAlbumPage": "Vis i Album",
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Vis i Lande",
"@chipActionGoToCountryPage": {},
"chipActionGoToPlacePage": "Vis i Steder",
"@chipActionGoToPlacePage": {},
"chipActionGoToTagPage": "Vis i Tags",
"@chipActionGoToTagPage": {},
"timeSeconds": "{count, plural, =1{{count} sekund} other{{count} sekunder}}",
"@timeSeconds": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"continueButtonLabel": "FORTSÆT",
"@continueButtonLabel": {},
"clearTooltip": "Ryd",
"@clearTooltip": {},
"previousTooltip": "Forrige",
"@previousTooltip": {},
"stopTooltip": "Stop",
"@stopTooltip": {},
"pickTooltip": "Vælg",
"@pickTooltip": {},
"chipActionShowCollection": "Vis i Samling",
"@chipActionShowCollection": {},
"doubleBackExitMessage": "Tryk \"tilbage\" igen for at gå ud.",
"@doubleBackExitMessage": {},
"doNotAskAgain": "Spørg ikke igen",
"@doNotAskAgain": {},
"sourceStateLoading": "Loader",
"@sourceStateLoading": {},
"sourceStateLocatingCountries": "Lokaliserer lande",
"@sourceStateLocatingCountries": {},
"sourceStateLocatingPlaces": "Lokaliserer steder",
"@sourceStateLocatingPlaces": {}
}

View file

@ -1,7 +1,7 @@
{
"welcomeOptional": "वैकल्पिक",
"@welcomeOptional": {},
"welcomeTermsToggle": "मैं नियमों और शर्तों पर सहमत हुं",
"welcomeTermsToggle": "मैं नियमों और शर्तों से सहमत हूं",
"@welcomeTermsToggle": {},
"columnCount": "{count, plural, other{{count} कॉलम}}",
"@columnCount": {
@ -246,7 +246,7 @@
"@displayRefreshRatePreferLowest": {},
"nameConflictStrategyRename": "नाम बदलें",
"@nameConflictStrategyRename": {},
"unitSystemMetric": "Metric",
"unitSystemMetric": "मात्रिक",
"@unitSystemMetric": {},
"viewerTransitionSlide": "स्लाइड",
"@viewerTransitionSlide": {},
@ -262,9 +262,9 @@
"@filterTaggedLabel": {},
"mapStyleGoogleTerrain": "गूगल मैप्स (टेरेन)",
"@mapStyleGoogleTerrain": {},
"themeBrightnessDark": "Dark",
"themeBrightnessDark": "डार्क",
"@themeBrightnessDark": {},
"themeBrightnessBlack": "Black",
"themeBrightnessBlack": "काला",
"@themeBrightnessBlack": {},
"videoControlsPlaySeek": "पिछड़े / आगे की तलाश करें",
"@videoControlsPlaySeek": {},
@ -284,7 +284,7 @@
"@mapStyleGoogleHybrid": {},
"mapStyleStamenWatercolor": "Stamen Watercolor",
"@mapStyleStamenWatercolor": {},
"unitSystemImperial": "Imperial",
"unitSystemImperial": "इम्पीरियल",
"@unitSystemImperial": {},
"passwordDialogEnter": "पासवर्ड दर्ज करें",
"@passwordDialogEnter": {},
@ -298,7 +298,7 @@
"@albumTierRegular": {},
"coordinateFormatDms": "DMS",
"@coordinateFormatDms": {},
"coordinateFormatDecimal": "Decimal degrees",
"coordinateFormatDecimal": "दशमलव डिग्री",
"@coordinateFormatDecimal": {},
"coordinateDms": "{coordinate} {direction}",
"@coordinateDms": {
@ -491,7 +491,7 @@
"@lengthUnitPercent": {},
"nameConflictStrategyReplace": "बदलें",
"@nameConflictStrategyReplace": {},
"themeBrightnessLight": "Light",
"themeBrightnessLight": "लाइट",
"@themeBrightnessLight": {},
"vaultLockTypePassword": "पासवर्ड",
"@vaultLockTypePassword": {},
@ -554,5 +554,421 @@
"videoActionABRepeat": "A-B दोहराव",
"@videoActionABRepeat": {},
"videoRepeatActionSetStart": "स्टार्ट सेट करे",
"@videoRepeatActionSetStart": {}
"@videoRepeatActionSetStart": {},
"keepScreenOnNever": "कभी नहीं",
"@keepScreenOnNever": {},
"editEntryDialogCopyFromItem": "अन्य आइटम से कॉपी करें",
"@editEntryDialogCopyFromItem": {},
"durationDialogSeconds": "सेकंड",
"@durationDialogSeconds": {},
"entryInfoActionEditTitleDescription": "शीर्षक और विवरण संपादित करें",
"@entryInfoActionEditTitleDescription": {},
"widgetOpenPageCollection": "संग्रह खोलें",
"@widgetOpenPageCollection": {},
"vaultBinUsageDialogMessage": "कुछ वॉल्ट्स रीसायकल बिन का उपयोग कर रहे हैं।।",
"@vaultBinUsageDialogMessage": {},
"editEntryLocationDialogSetCustom": "कस्टम स्थान सेट करें",
"@editEntryLocationDialogSetCustom": {},
"editEntryLocationDialogChooseOnMap": "मानचित्र पर चुनें",
"@editEntryLocationDialogChooseOnMap": {},
"sourceStateCataloguing": "Cataloguing",
"@sourceStateCataloguing": {},
"editEntryLocationDialogLongitude": "देशांतर",
"@editEntryLocationDialogLongitude": {},
"videoStreamSelectionDialogVideo": "वीडियो",
"@videoStreamSelectionDialogVideo": {},
"keepScreenOnViewerOnly": "केवल व्यूअर पेज",
"@keepScreenOnViewerOnly": {},
"keepScreenOnAlways": "हमेशा",
"@keepScreenOnAlways": {},
"renameEntrySetPageInsertTooltip": "फ़ील्ड डालें",
"@renameEntrySetPageInsertTooltip": {},
"renameEntrySetPagePreviewSectionTitle": "पूर्वावलोकन",
"@renameEntrySetPagePreviewSectionTitle": {},
"setCoverDialogAuto": "ऑटो",
"@setCoverDialogAuto": {},
"exportEntryDialogWidth": "चौड़ाई",
"@exportEntryDialogWidth": {},
"editEntryDialogTargetFieldsHeader": "संशोधित करने के लिए फ़ील्ड",
"@editEntryDialogTargetFieldsHeader": {},
"editEntryDateDialogTitle": "तारीख और समय",
"@editEntryDateDialogTitle": {},
"editEntryDateDialogSetCustom": "कस्टम तिथि सेट करें",
"@editEntryDateDialogSetCustom": {},
"durationDialogHours": "घंटे",
"@durationDialogHours": {},
"editEntryLocationDialogTitle": "स्थान",
"@editEntryLocationDialogTitle": {},
"renameProcessorHash": "हैश",
"@renameProcessorHash": {},
"renameProcessorName": "नाम",
"@renameProcessorName": {},
"exportEntryDialogFormat": "फॉर्मेट:",
"@exportEntryDialogFormat": {},
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "मोशन फोटो के अंदर वीडियो चलाने के लिए XMP की जरूरत है\n\nक्या आप वास्तव में इसे हटाना चाहते हैं?",
"@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
"videoSpeedDialogLabel": "चलाने की गति",
"@videoSpeedDialogLabel": {},
"editEntryLocationDialogLatitude": "अक्षांश",
"@editEntryLocationDialogLatitude": {},
"removeEntryMetadataDialogTitle": "मेटाडाटा हटाना",
"@removeEntryMetadataDialogTitle": {},
"filterNoLocationLabel": "लॉकेट नही किया गया",
"@filterNoLocationLabel": {},
"accessibilityAnimationsRemove": "स्क्रीन प्रभाव को रोकें",
"@accessibilityAnimationsRemove": {},
"maxBrightnessNever": "कभी नहीं",
"@maxBrightnessNever": {},
"maxBrightnessAlways": "हमेशा",
"@maxBrightnessAlways": {},
"widgetTapUpdateWidget": "विजेट अपडेट करें",
"@widgetTapUpdateWidget": {},
"storageVolumeDescriptionFallbackPrimary": "आंतरिक भंडारण",
"@storageVolumeDescriptionFallbackPrimary": {},
"editEntryDateDialogExtractFromTitle": "शीर्षक से निकालें",
"@editEntryDateDialogExtractFromTitle": {},
"editEntryRatingDialogTitle": "रेटिंग",
"@editEntryRatingDialogTitle": {},
"removeEntryMetadataDialogMore": "अधिक",
"@removeEntryMetadataDialogMore": {},
"filterLocatedLabel": "लोकेट किया गया",
"@filterLocatedLabel": {},
"locationPickerUseThisLocationButton": "इस स्थान का उपयोग करें",
"@locationPickerUseThisLocationButton": {},
"viewerActionLock": "व्यूअर को लॉक करे",
"@viewerActionLock": {},
"renameEntrySetPagePatternFieldLabel": "नामकरण पैटर्न",
"@renameEntrySetPagePatternFieldLabel": {},
"videoResumptionModeNever": "कभी नहीं",
"@videoResumptionModeNever": {},
"videoResumptionModeAlways": "हमेशा",
"@videoResumptionModeAlways": {},
"renameProcessorCounter": "काउंटर",
"@renameProcessorCounter": {},
"editEntryDateDialogCopyField": "अन्य तारीख से कॉपी करें",
"@editEntryDateDialogCopyField": {},
"viewerActionUnlock": "व्यूअर को अनलॉक करे",
"@viewerActionUnlock": {},
"editorTransformCrop": "क्रॉप",
"@editorTransformCrop": {},
"cropAspectRatioFree": "फ्री",
"@cropAspectRatioFree": {},
"nameConflictStrategySkip": "छोड़े",
"@nameConflictStrategySkip": {},
"albumTierPinned": "पिन किया गया",
"@albumTierPinned": {},
"albumTierSpecial": "कॉमन",
"@albumTierSpecial": {},
"overlayHistogramNone": "कोई नहीं",
"@overlayHistogramNone": {},
"overlayHistogramRGB": "RGB",
"@overlayHistogramRGB": {},
"overlayHistogramLuminance": "चमक",
"@overlayHistogramLuminance": {},
"widgetOpenPageViewer": "व्यूअर खोलें",
"@widgetOpenPageViewer": {},
"addShortcutButtonLabel": "ADD",
"@addShortcutButtonLabel": {},
"setCoverDialogCustom": "कस्टम",
"@setCoverDialogCustom": {},
"exportEntryDialogHeight": "ऊंचाई",
"@exportEntryDialogHeight": {},
"exportEntryDialogQuality": "गुणवत्ता",
"@exportEntryDialogQuality": {},
"exportEntryDialogWriteMetadata": "मेटाडाटा लिखें",
"@exportEntryDialogWriteMetadata": {},
"renameEntryDialogLabel": "नया नाम",
"@renameEntryDialogLabel": {},
"editEntryDateDialogShift": "शिफ्ट",
"@editEntryDateDialogShift": {},
"durationDialogMinutes": "मिनट",
"@durationDialogMinutes": {},
"videoStreamSelectionDialogText": "उपशीर्षक",
"@videoStreamSelectionDialogText": {},
"genericFailureFeedback": "असफल",
"@genericFailureFeedback": {},
"menuActionSlideshow": "स्लाइड शो",
"@menuActionSlideshow": {},
"viewDialogReverseSortOrder": "क्रम उलटा करे",
"@viewDialogReverseSortOrder": {},
"tileLayoutMosaic": "मौज़ेक",
"@tileLayoutMosaic": {},
"cropAspectRatioSquare": "वर्ग",
"@cropAspectRatioSquare": {},
"widgetOpenPageHome": "घर खोलें",
"@widgetOpenPageHome": {},
"viewDialogSortSectionTitle": "क्रम से लगाए",
"@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "समूह में रखे",
"@viewDialogGroupSectionTitle": {},
"videoStreamSelectionDialogAudio": "ऑडियो",
"@videoStreamSelectionDialogAudio": {},
"videoStreamSelectionDialogTrack": "ट्रैक",
"@videoStreamSelectionDialogTrack": {},
"genericSuccessFeedback": "हो गया!",
"@genericSuccessFeedback": {},
"menuActionMap": "मैप",
"@menuActionMap": {},
"aboutLinkLicense": "लाइसेंस",
"@aboutLinkLicense": {},
"editEntryDateDialogSourceFileModifiedDate": "फ़ाइल संशोधित दिनांक",
"@editEntryDateDialogSourceFileModifiedDate": {},
"coverDialogTabApp": "ऐप",
"@coverDialogTabApp": {},
"coverDialogTabCover": "ढखें",
"@coverDialogTabCover": {},
"aboutBugReportButton": "रिपोर्ट दे",
"@aboutBugReportButton": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{यह एल्बम और इसमें मौजूद आइटम हटाएं?} other{यह एल्बम और इसमें मौजूद {count} हटाएं?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{ये एल्बम और उनमें मौजूद आइटम हटाएं?} other{ये एल्बम और उनमें मौजूद {count} आइटम हटाएं?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"menuActionSelect": "चूने",
"@menuActionSelect": {},
"genericDangerWarningDialogMessage": "क्या आपको यकीन है?",
"@genericDangerWarningDialogMessage": {},
"tooManyItemsErrorDialogMessage": "कम आइटम के साथ पुनः प्रयास करें।",
"@tooManyItemsErrorDialogMessage": {},
"menuActionSelectAll": "सभी चुने",
"@menuActionSelectAll": {},
"menuActionSelectNone": "कुछ मत चुने",
"@menuActionSelectNone": {},
"tileLayoutGrid": "ग्रिड",
"@tileLayoutGrid": {},
"tileLayoutList": "सूची",
"@tileLayoutList": {},
"castDialogTitle": "कास्ट डिवाइस",
"@castDialogTitle": {},
"coverDialogTabColor": "रंग",
"@coverDialogTabColor": {},
"appPickDialogTitle": "ऐप चुनें",
"@appPickDialogTitle": {},
"aboutBugCopyInfoButton": "कोपी",
"@aboutBugCopyInfoButton": {},
"aboutBugReportInstruction": "लॉग और सिस्टम जानकारी के साथ GitHub पर रिपोर्ट करें",
"@aboutBugReportInstruction": {},
"aboutBugCopyInfoInstruction": "सिस्टम जानकारी कॉपी करें",
"@aboutBugCopyInfoInstruction": {},
"videoStreamSelectionDialogOff": "बंद",
"@videoStreamSelectionDialogOff": {},
"aboutBugSectionTitle": "बग रिपोर्ट",
"@aboutBugSectionTitle": {},
"aboutLinkPolicy": "गोपनीयता नीति",
"@aboutLinkPolicy": {},
"menuActionConfigureView": "देखें",
"@menuActionConfigureView": {},
"menuActionStats": "आँकड़े",
"@menuActionStats": {},
"aboutBugSaveLogInstruction": "ऐप लॉग को फ़ाइल में सहेजें",
"@aboutBugSaveLogInstruction": {},
"aboutDataUsageSectionTitle": "डेटा उपयोग",
"@aboutDataUsageSectionTitle": {},
"aboutDataUsageData": "डेटा",
"@aboutDataUsageData": {},
"aboutDataUsageCache": "कैश",
"@aboutDataUsageCache": {},
"aboutDataUsageDatabase": "डेटाबेस",
"@aboutDataUsageDatabase": {},
"editorActionTransform": "परिवर्तन",
"@editorActionTransform": {},
"videoStreamSelectionDialogNoSelection": "कोई अन्य ट्रैक नहीं हैं।",
"@videoStreamSelectionDialogNoSelection": {},
"viewDialogLayoutSectionTitle": "लेआउट",
"@viewDialogLayoutSectionTitle": {},
"appPickDialogNone": "कोई नहीं",
"@appPickDialogNone": {},
"aboutPageTitle": "बारे में",
"@aboutPageTitle": {},
"collectionEditFailureFeedback": "{count, plural, =1{1 आइटम एडिट करने में विफल} other{{count} आइटम एडिट करने में विफल}}",
"@collectionEditFailureFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"drawerCollectionVideos": "वीडियो",
"@drawerCollectionVideos": {},
"collectionActionSetHome": "घर के रूप में सेट करें",
"@collectionActionSetHome": {},
"collectionActionMove": "एल्बम पर जाएँ",
"@collectionActionMove": {},
"collectionActionRescan": "पुन: स्कैन करे",
"@collectionActionRescan": {},
"collectionGroupDay": "दिन के अनुसार",
"@collectionGroupDay": {},
"collectionCopySuccessFeedback": "{count, plural, =1{1 आइटम कॉपी किया गया} other{{count} आइटम कॉपी किए गए}}",
"@collectionCopySuccessFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"collectionEmptyImages": "कोई इमेज नहीं",
"@collectionEmptyImages": {},
"collectionEmptyGrantAccessButtonLabel": "पहुंच प्रदान करें",
"@collectionEmptyGrantAccessButtonLabel": {},
"collectionActionAddShortcut": "शॉर्टकट जोड़ें",
"@collectionActionAddShortcut": {},
"collectionRenameFailureFeedback": "{count, plural, =1{1 आइटम का नाम बदलने में विफल} other{{count} आइटम का नाम बदलने में विफल}}",
"@collectionRenameFailureFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"drawerCollectionAnimated": "एनिमेटेड",
"@drawerCollectionAnimated": {},
"aboutCreditsSectionTitle": "क्रेडिट",
"@aboutCreditsSectionTitle": {},
"aboutLicensesSectionTitle": "ओपन सोर्स लाइसेंस",
"@aboutLicensesSectionTitle": {},
"aboutLicensesFlutterPackagesSectionTitle": "फ्लटर पैकेज",
"@aboutLicensesFlutterPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "सभी लाइसेंस दिखाएं",
"@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "गोपनीयता नीति",
"@policyPageTitle": {},
"collectionActionEmptyBin": "बीन खाली करे",
"@collectionActionEmptyBin": {},
"collectionActionCopy": "एल्बम में कॉपी करें",
"@collectionActionCopy": {},
"collectionGroupAlbum": "एल्बम के अनुसार",
"@collectionGroupAlbum": {},
"aboutCreditsWorldAtlas1": "यह ऐप TopoJSON फ़ाइल का उपयोग करता है",
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "आईएससी लाइसेंस के तहत।",
"@aboutCreditsWorldAtlas2": {},
"collectionPageTitle": "संग्रह",
"@collectionPageTitle": {},
"collectionGroupMonth": "महीने के अनुसार",
"@collectionGroupMonth": {},
"collectionGroupNone": "समूह न बनाएं",
"@collectionGroupNone": {},
"sectionUnknown": "अज्ञात",
"@sectionUnknown": {},
"dateYesterday": "कल",
"@dateYesterday": {},
"dateThisMonth": "इस महीने",
"@dateThisMonth": {},
"collectionDeleteFailureFeedback": "{count, plural, =1{1 आइटम हटाने में विफल} other{{count} आइटम हटाने में विफल}}",
"@collectionDeleteFailureFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"collectionExportFailureFeedback": "{count, plural, =1{1 पन्ना निर्यात करने में विफल} other{{count} पन्ने निर्यात करने में विफल}}",
"@collectionExportFailureFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"collectionRenameSuccessFeedback": "{count, plural, =1{1 आइटम का नाम बदला गया} other{{count} आइटम का नाम बदला गया}}",
"@collectionRenameSuccessFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"aboutLicensesAndroidLibrariesSectionTitle": "एंड्रॉइड लाइब्रेरीज़",
"@aboutLicensesAndroidLibrariesSectionTitle": {},
"dateToday": "आज",
"@dateToday": {},
"collectionActionHideTitleSearch": "शीर्षक फ़िल्टर छुपाएं",
"@collectionActionHideTitleSearch": {},
"drawerCollectionFavourites": "पसंदीदा",
"@drawerCollectionFavourites": {},
"drawerCollectionMotionPhotos": "मोशन तस्वीरें",
"@drawerCollectionMotionPhotos": {},
"collectionMoveFailureFeedback": "{count, plural, =1{1 आइटम ले जाने में विफल} other{{count} आइटम ले जाने में विफल}}",
"@collectionMoveFailureFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"collectionEditSuccessFeedback": "{count, plural, =1{1 आइटम संपादित किया गया} other{{count} आइटम संपादित किए गए}}",
"@collectionEditSuccessFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"collectionEmptyFavourites": "कोई पसंदीदा नहीं",
"@collectionEmptyFavourites": {},
"aboutDataUsageClearCache": "कैश साफ़ करें",
"@aboutDataUsageClearCache": {},
"aboutTranslatorsSectionTitle": "अनुवादक",
"@aboutTranslatorsSectionTitle": {},
"aboutLicensesBanner": "यह ऐप निम्नलिखित ओपन सोर्स पैकेज और पुस्तकालयों का उपयोग करता है।",
"@aboutLicensesBanner": {},
"aboutLicensesFlutterPluginsSectionTitle": "फ्लटर प्लगइन्स",
"@aboutLicensesFlutterPluginsSectionTitle": {},
"aboutLicensesDartPackagesSectionTitle": "डार्ट पैकेज",
"@aboutLicensesDartPackagesSectionTitle": {},
"collectionCopyFailureFeedback": "{count, plural, =1{1 आइटम कॉपी करने में विफल} other{{count} आइटम कॉपी करने में विफल}}",
"@collectionCopyFailureFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"aboutDataUsageInternal": "आंतरिक",
"@aboutDataUsageInternal": {},
"aboutDataUsageExternal": "बाहरी",
"@aboutDataUsageExternal": {},
"collectionSelectPageTitle": "आइटम चुनें",
"@collectionSelectPageTitle": {},
"collectionActionShowTitleSearch": "शीर्षक फ़िल्टर दिखाएं",
"@collectionActionShowTitleSearch": {},
"collectionActionEdit": "एडिट करे",
"@collectionActionEdit": {},
"collectionSearchTitlesHintText": "शीर्षक खोजें",
"@collectionSearchTitlesHintText": {},
"collectionMoveSuccessFeedback": "{count, plural, =1{1 आइटम स्थानांतरित किया गया} other{{count} आइटम स्थानांतरित किए गए}}",
"@collectionMoveSuccessFeedback": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"collectionEmptyVideos": "कोई वीडियो नहीं",
"@collectionEmptyVideos": {},
"collectionSelectSectionTooltip": "अनुभाग चुनें",
"@collectionSelectSectionTooltip": {},
"collectionDeselectSectionTooltip": "अनुभाग का चयन रद्द करें",
"@collectionDeselectSectionTooltip": {},
"drawerAboutButton": "के बारे में",
"@drawerAboutButton": {},
"drawerSettingsButton": "सेटिंग्स",
"@drawerSettingsButton": {},
"drawerCollectionAll": "सभी संग्रह",
"@drawerCollectionAll": {},
"drawerCollectionImages": "इमेजेस",
"@drawerCollectionImages": {},
"aboutDataUsageMisc": "विविध",
"@aboutDataUsageMisc": {}
}

View file

@ -1467,7 +1467,7 @@
"@settingsVideoBackgroundModeDialogTitle": {},
"tagEditorDiscardDialogMessage": "Forkast endringer?",
"@tagEditorDiscardDialogMessage": {},
"tagPlaceholderState": "Tilstand?",
"tagPlaceholderState": "Delstat",
"@tagPlaceholderState": {},
"chipActionShowCountryStates": "Vis tilstander",
"@chipActionShowCountryStates": {},

1
lib/l10n/app_sr.arb Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -7,7 +7,7 @@
"@videoActionPlay": {},
"viewerActionSettings": "Inställningar",
"@viewerActionSettings": {},
"albumTierSpecial": "Vanligt förekommande",
"albumTierSpecial": "Vanliga",
"@albumTierSpecial": {},
"displayRefreshRatePreferLowest": "Lägsta intervall",
"@displayRefreshRatePreferLowest": {},
@ -132,7 +132,7 @@
"@chipActionRename": {},
"chipActionSetCover": "Välj som omslag",
"@chipActionSetCover": {},
"chipActionShowCountryStates": "Visa landskap",
"chipActionShowCountryStates": "Visa delstater",
"@chipActionShowCountryStates": {},
"chipActionCreateAlbum": "Skapa album",
"@chipActionCreateAlbum": {},
@ -216,7 +216,7 @@
"@entryInfoActionEditDate": {},
"entryInfoActionEditLocation": "Redigera plats",
"@entryInfoActionEditLocation": {},
"entryInfoActionEditTitleDescription": "Redigera filnamn och beskrivning",
"entryInfoActionEditTitleDescription": "Redigera titel & beskrivning",
"@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Redigera omdöme",
"@entryInfoActionEditRating": {},
@ -337,7 +337,7 @@
"@mapStyleGoogleTerrain": {},
"mapStyleStamenWatercolor": "Stamen Watercolor (Akvarell)",
"@mapStyleStamenWatercolor": {},
"maxBrightnessNever": "Alldrig",
"maxBrightnessNever": "Aldrig",
"@maxBrightnessNever": {},
"maxBrightnessAlways": "Alltid",
"@maxBrightnessAlways": {},
@ -395,7 +395,7 @@
"@videoPlaybackMuted": {},
"videoPlaybackWithSound": "Spela med ljud",
"@videoPlaybackWithSound": {},
"videoResumptionModeNever": "Alldrig",
"videoResumptionModeNever": "Aldrig",
"@videoResumptionModeNever": {},
"viewerTransitionSlide": "Glid",
"@viewerTransitionSlide": {},
@ -407,7 +407,7 @@
"@wallpaperTargetHome": {},
"widgetDisplayedItemRandom": "Slumpartat",
"@widgetDisplayedItemRandom": {},
"widgetDisplayedItemMostRecent": "Senaste",
"widgetDisplayedItemMostRecent": "Det senaste",
"@widgetDisplayedItemMostRecent": {},
"widgetOpenPageHome": "Öppna startsida",
"@widgetOpenPageHome": {},
@ -449,7 +449,7 @@
"@hideFilterConfirmationDialogMessage": {},
"newAlbumDialogTitle": "Nytt Album",
"@newAlbumDialogTitle": {},
"newAlbumDialogNameLabelAlreadyExistsHelper": "Mappen existerar redan",
"newAlbumDialogNameLabelAlreadyExistsHelper": "Katalogen existerar redan",
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
"newAlbumDialogStorageLabel": "Lagring:",
"@newAlbumDialogStorageLabel": {},
@ -475,7 +475,7 @@
"@welcomeMessage": {},
"nextButtonLabel": "NÄSTA",
"@nextButtonLabel": {},
"nextTooltip": "Nästs",
"nextTooltip": "Nästa",
"@nextTooltip": {},
"doNotAskAgain": "Fråga inte igen",
"@doNotAskAgain": {},
@ -546,7 +546,7 @@
"@patternDialogEnter": {},
"patternDialogConfirm": "Bekräfta mönster",
"@patternDialogConfirm": {},
"renameEntrySetPagePatternFieldLabel": "Namnge mönster",
"renameEntrySetPagePatternFieldLabel": "Namngivningsmönster",
"@renameEntrySetPagePatternFieldLabel": {},
"renameEntrySetPageInsertTooltip": "Infoga fällt",
"@renameEntrySetPageInsertTooltip": {},
@ -570,7 +570,7 @@
"@editEntryDialogCopyFromItem": {},
"editEntryDateDialogTitle": "Datum & Tid",
"@editEntryDateDialogTitle": {},
"editEntryDateDialogExtractFromTitle": "Kopiera från filnamn",
"editEntryDateDialogExtractFromTitle": "Extrahera från titel",
"@editEntryDateDialogExtractFromTitle": {},
"editEntryDateDialogShift": "Förskjut",
"@editEntryDateDialogShift": {},
@ -654,7 +654,7 @@
"@collectionActionMove": {},
"viewDialogSortSectionTitle": "Sortera",
"@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grupp",
"viewDialogGroupSectionTitle": "Gruppera",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Layout",
"@viewDialogLayoutSectionTitle": {},
@ -682,7 +682,7 @@
"@aboutLinkLicense": {},
"aboutLinkPolicy": "IntegritetsPolicy",
"@aboutLinkPolicy": {},
"aboutBugSectionTitle": "FelRapport",
"aboutBugSectionTitle": "Felrapport",
"@aboutBugSectionTitle": {},
"aboutBugSaveLogInstruction": "Spara appens log till en fil",
"@aboutBugSaveLogInstruction": {},
@ -692,7 +692,7 @@
"@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "IntegritetsPolicy",
"@policyPageTitle": {},
"collectionActionHideTitleSearch": "Göm filnamnsfilter",
"collectionActionHideTitleSearch": "Göm titelfilter",
"@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Lägg till genväg",
"@collectionActionAddShortcut": {},
@ -746,7 +746,7 @@
"@entryActionCast": {},
"filterTaggedLabel": "Taggad",
"@filterTaggedLabel": {},
"keepScreenOnNever": "Alldrig",
"keepScreenOnNever": "Aldrig",
"@keepScreenOnNever": {},
"viewerTransitionFade": "Tona ut",
"@viewerTransitionFade": {},
@ -905,7 +905,7 @@
"@editEntryDateDialogCopyField": {},
"castDialogTitle": "Uppspelningsenheter",
"@castDialogTitle": {},
"renameEntrySetPageTitle": "Döpa om",
"renameEntrySetPageTitle": "Ändra namn",
"@renameEntrySetPageTitle": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Vill du ta bort denna fil?} other{Vill du ta bort dessa {count} filer?}}",
"@deleteEntriesConfirmationDialogMessage": {
@ -921,7 +921,7 @@
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
"aboutLicensesDartPackagesSectionTitle": "Dart-paket",
"@aboutLicensesDartPackagesSectionTitle": {},
"collectionActionShowTitleSearch": "Filtrera på filnamn",
"collectionActionShowTitleSearch": "Visa titelfilter",
"@collectionActionShowTitleSearch": {},
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytta denna fil tillpapperskorgen?} other{Flytta dessa {count} filer till papperskorgen?}}",
"@binEntriesConfirmationDialogMessage": {
@ -991,11 +991,11 @@
"@albumPickPageTitleMove": {},
"albumPickPageTitlePick": "Välj Album",
"@albumPickPageTitlePick": {},
"statePageTitle": "Landskap",
"statePageTitle": "Delstater",
"@statePageTitle": {},
"countryEmpty": "Inga länder",
"@countryEmpty": {},
"stateEmpty": "Inga landskap",
"stateEmpty": "Inga delstater",
"@stateEmpty": {},
"placePageTitle": "Platser",
"@placePageTitle": {},
@ -1057,9 +1057,9 @@
"@tagEmpty": {},
"binPageTitle": "Papperskorgen",
"@binPageTitle": {},
"explorerActionSelectStorageVolume": "Välj lagringsplatts",
"explorerActionSelectStorageVolume": "Välj lagringsplats",
"@explorerActionSelectStorageVolume": {},
"selectStorageVolumeDialogTitle": "Välj Lagringsplatts",
"selectStorageVolumeDialogTitle": "Välj Lagringsplats",
"@selectStorageVolumeDialogTitle": {},
"collectionEditSuccessFeedback": "{count, plural, =1{Ändrat 1 objekt} other{Ändrat {count} objekt}}",
"@collectionEditSuccessFeedback": {
@ -1089,7 +1089,7 @@
"@searchDateSectionTitle": {},
"searchAlbumsSectionTitle": "Album",
"@searchAlbumsSectionTitle": {},
"searchStatesSectionTitle": "Landskap",
"searchStatesSectionTitle": "Delstater",
"@searchStatesSectionTitle": {},
"albumPickPageTitleExport": "Exportera till Album",
"@albumPickPageTitleExport": {},
@ -1201,9 +1201,9 @@
"@settingsViewerShowMinimap": {},
"settingsViewerShowInformation": "Visa information",
"@settingsViewerShowInformation": {},
"settingsViewerShowInformationSubtitle": "Visa filnamn, datum, plats osv …",
"settingsViewerShowInformationSubtitle": "Visa titel, datum, plats, etc.",
"@settingsViewerShowInformationSubtitle": {},
"settingsViewerShowOverlayThumbnails": "Visa minityrbilder",
"settingsViewerShowOverlayThumbnails": "Visa miniatyrbilder",
"@settingsViewerShowOverlayThumbnails": {},
"settingsViewerShowDescription": "Visa beskrivning",
"@settingsViewerShowDescription": {},
@ -1287,7 +1287,7 @@
"@settingsWidgetDisplayedItem": {},
"statsPageTitle": "Statistik",
"@statsPageTitle": {},
"statsTopCountriesSectionTitle": "Vanligast Länder",
"statsTopCountriesSectionTitle": "Populära Länder",
"@statsTopCountriesSectionTitle": {},
"settingsCollectionTile": "Samling",
"@settingsCollectionTile": {},
@ -1301,7 +1301,7 @@
"@viewerErrorDoesNotExist": {},
"viewerInfoPageTitle": "Info",
"@viewerInfoPageTitle": {},
"viewerInfoLabelTitle": "Filnamn",
"viewerInfoLabelTitle": "Titel",
"@viewerInfoLabelTitle": {},
"viewerInfoLabelDate": "Datum",
"@viewerInfoLabelDate": {},
@ -1391,9 +1391,9 @@
"@settingsCollectionQuickActionEditorPageTitle": {},
"settingsCollectionSelectionQuickActionEditorBanner": "Tryck och håll nere för att flytta knappar och välj på så vis vilka åtgärder som skall visas när objekt väljs.",
"@settingsCollectionSelectionQuickActionEditorBanner": {},
"settingsCollectionBurstPatternsTile": "Namngivningsmönster",
"settingsCollectionBurstPatternsTile": "Seriefotograferingsmönster",
"@settingsCollectionBurstPatternsTile": {},
"settingsCollectionBurstPatternsNone": "Ingen",
"settingsCollectionBurstPatternsNone": "Inget",
"@settingsCollectionBurstPatternsNone": {},
"settingsViewerSectionTitle": "Bildvisare",
"@settingsViewerSectionTitle": {},
@ -1403,7 +1403,7 @@
"@settingsViewerUseCutout": {},
"settingsViewerMaximumBrightness": "Maximal ljusstyrka",
"@settingsViewerMaximumBrightness": {},
"settingsMotionPhotoAutoPlay": "Spela automatiskt upp Rörelsefoton",
"settingsMotionPhotoAutoPlay": "Spela automatiskt upp rörelsefoton",
"@settingsMotionPhotoAutoPlay": {},
"settingsImageBackground": "Bakgrund för bilder",
"@settingsImageBackground": {},
@ -1419,7 +1419,7 @@
"@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {},
"settingsViewerQuickActionEmpty": "Inga knappar",
"@settingsViewerQuickActionEmpty": {},
"settingsViewerOverlayTile": "Överblick",
"settingsViewerOverlayTile": "Overlay",
"@settingsViewerOverlayTile": {},
"settingsViewerEnableOverlayBlurEffect": "Effekt för oskärpa",
"@settingsViewerEnableOverlayBlurEffect": {},
@ -1467,7 +1467,7 @@
"@settingsHiddenItemsTabFilters": {},
"settingsHiddenFiltersEmpty": "Inga dolda filter",
"@settingsHiddenFiltersEmpty": {},
"addPathTooltip": "Lägg till katalog",
"addPathTooltip": "Lägg till sökväg",
"@addPathTooltip": {},
"settingsRemoveAnimationsTile": "Inaktivera animationer",
"@settingsRemoveAnimationsTile": {},
@ -1507,7 +1507,7 @@
"@settingsEnableBin": {},
"settingsEnableBinSubtitle": "Behåll borttagna objekt i 30 dagar",
"@settingsEnableBinSubtitle": {},
"settingsViewerOverlayPageTitle": "Överblick",
"settingsViewerOverlayPageTitle": "Overlay",
"@settingsViewerOverlayPageTitle": {},
"settingsHomeTile": "Startsida",
"@settingsHomeTile": {},
@ -1531,13 +1531,13 @@
"@settingsUnitSystemDialogTitle": {},
"settingsScreenSaverPageTitle": "Skärmsläckare",
"@settingsScreenSaverPageTitle": {},
"statsTopStatesSectionTitle": "Vanligast Delstater",
"statsTopStatesSectionTitle": "Populära Delstater",
"@statsTopStatesSectionTitle": {},
"statsTopPlacesSectionTitle": "Vanligast Platser",
"statsTopPlacesSectionTitle": "Populära Platser",
"@statsTopPlacesSectionTitle": {},
"statsTopTagsSectionTitle": "Vanligast Etiketter",
"statsTopTagsSectionTitle": "Populära Etiketter",
"@statsTopTagsSectionTitle": {},
"statsTopAlbumsSectionTitle": "Vanligast Album",
"statsTopAlbumsSectionTitle": "Populära Album",
"@statsTopAlbumsSectionTitle": {},
"viewerInfoLabelResolution": "Upplösning",
"@viewerInfoLabelResolution": {},
@ -1569,6 +1569,6 @@
"@tagEditorDiscardDialogMessage": {},
"tagPlaceholderCountry": "Land",
"@tagPlaceholderCountry": {},
"tagPlaceholderState": "Delstat",
"tagPlaceholderState": "Stat",
"@tagPlaceholderState": {}
}

View file

@ -105,18 +105,21 @@ class Contributors {
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
// Contributor('Grooty12', 'Rasmus@rosendahl-kaa.name'), // Danish
// Contributor('Victor M', 'victormorita@tuta.io'), // Danish
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// 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
// Contributor('Subham Jena', 'subhamjena8465@gmail.com'), // Odia
// Contributor('Prasanta-Hembram', 'Prasantahembram720@gmail.com'), // Santali
// Contributor('Enenra', 'nnra2210@gmail.com'), // Serbian
// Contributor('mytja', 'mamnju21@gmail.com'), // Slovenian
// Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
};

View file

@ -6,8 +6,12 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
abstract class AvesAvailability {
Future<void> onNewIntent();
void onResume();
bool get isLocked;
Future<bool> get isConnected;
Future<bool> get canLocatePlaces;
@ -16,15 +20,24 @@ abstract class AvesAvailability {
}
class LiveAvesAvailability implements AvesAvailability {
bool? _isConnected;
bool? _isConnected, _isLocked;
LiveAvesAvailability() {
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
}
@override
Future<void> onNewIntent() async {
_isLocked = await deviceService.isLocked();
debugPrint('Device is locked=$_isLocked');
}
@override
void onResume() => _isConnected = null;
@override
bool get isLocked => _isLocked ?? false;
@override
Future<bool> get isConnected async {
if (_isConnected != null) return SynchronousFuture(_isConnected!);

View file

@ -9,8 +9,8 @@ final Device device = Device._private();
class Device {
late final String _packageName, _packageVersion, _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut;
late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _canAuthenticateUser, _canPinShortcut;
late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
String get packageName => _packageName;
@ -21,8 +21,6 @@ class Device {
bool get canAuthenticateUser => _canAuthenticateUser;
bool get canGrantDirectoryAccess => _canGrantDirectoryAccess;
bool get canPinShortcut => _canPinShortcut;
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
@ -33,10 +31,6 @@ class Device {
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
bool get canUseCrypto => _canUseCrypto;
bool get canUseVaults => canAuthenticateUser || canUseCrypto;
bool get hasGeocoder => _hasGeocoder;
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
@ -71,13 +65,11 @@ class Device {
}
final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
_hasGeocoder = capabilities['hasGeocoder'] ?? false;
_isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false;
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;

View file

@ -9,22 +9,24 @@ class PathFilter extends CollectionFilter {
static const type = 'path';
// including trailing separator
final String path;
late final String path;
// without trailing separator
final String _rootAlbum;
late final String _rootAlbum;
late final EntryFilter _test;
@override
List<Object?> get props => [path, reversed];
PathFilter(this.path, {super.reversed = false}) : _rootAlbum = path.substring(0, path.length - 1) {
PathFilter(String path, {super.reversed = false}) {
this.path = path.endsWith(pContext.separator) ? path : '$path${pContext.separator}';
_rootAlbum = this.path.substring(0, this.path.length - 1);
_test = (entry) {
final dir = entry.directory;
if (dir == null) return false;
// avoid string building in most cases
return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path);
return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(this.path);
};
}

View file

@ -37,6 +37,7 @@ class MediaStoreSource extends CollectionSource {
bool loadTopEntriesFirst = false,
bool canAnalyze = true,
}) async {
await reportService.log('$runtimeType init directory=$directory');
if (_initState == SourceInitializationState.none) {
await _loadEssentials();
}
@ -81,7 +82,7 @@ class MediaStoreSource extends CollectionSource {
required bool loadTopEntriesFirst,
required bool canAnalyze,
}) async {
debugPrint('$runtimeType refresh start');
unawaited(reportService.log('$runtimeType load start'));
final stopwatch = Stopwatch()..start();
state = SourceState.loading;
clearEntries();
@ -90,17 +91,17 @@ class MediaStoreSource extends CollectionSource {
if (loadTopEntriesFirst) {
final topIds = settings.topEntryIds?.toSet();
if (topIds != null) {
debugPrint('$runtimeType refresh ${stopwatch.elapsed} load ${topIds.length} top entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} load ${topIds.length} top entries');
topEntries.addAll(await localMediaDb.loadEntriesById(topIds));
addEntries(topEntries);
}
}
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch known entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries');
final knownEntries = await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: directory);
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs)));
final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
@ -112,14 +113,14 @@ class MediaStoreSource extends CollectionSource {
knownEntries.removeAll(removedEntries);
// show known entries
debugPrint('$runtimeType refresh ${stopwatch.elapsed} add known entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} add known entries');
// add entries without notifying, so that the collection is not refreshed
// with items that may be hidden right away because of their metadata
addEntries(knownEntries, notify: false);
await _loadVaultEntries(directory);
debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata');
debugPrint('$runtimeType load ${stopwatch.elapsed} load metadata');
if (directory != null) {
final ids = knownLiveEntries.map((entry) => entry.id).toSet();
await loadCatalogMetadata(ids: ids);
@ -144,12 +145,12 @@ class MediaStoreSource extends CollectionSource {
// clean up obsolete entries
if (removedEntries.isNotEmpty) {
debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} remove obsolete entries');
await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet());
}
// verify paths because some apps move files without updating their `last modified date`
debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete paths');
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete paths');
final knownPathByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.path)));
final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathByContentId)).toSet();
movedContentIds.forEach((contentId) {
@ -161,13 +162,13 @@ class MediaStoreSource extends CollectionSource {
final newEntries = <AvesEntry>{};
// recover untracked trash items
debugPrint('$runtimeType refresh ${stopwatch.elapsed} recover untracked entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} recover untracked entries');
if (directory == null) {
newEntries.addAll(await recoverUntrackedTrashItems());
}
// fetch new & modified entries
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch new entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch new entries');
mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
(entry) {
// when discovering modified entry with known content ID,
@ -180,7 +181,7 @@ class MediaStoreSource extends CollectionSource {
},
onDone: () async {
if (newEntries.isNotEmpty) {
debugPrint('$runtimeType refresh ${stopwatch.elapsed} save new entries');
debugPrint('$runtimeType load ${stopwatch.elapsed} save new entries');
await localMediaDb.insertEntries(newEntries);
// TODO TLAD find duplication cause
@ -203,7 +204,7 @@ class MediaStoreSource extends CollectionSource {
updateDirectories();
}
debugPrint('$runtimeType refresh ${stopwatch.elapsed} analyze');
debugPrint('$runtimeType load ${stopwatch.elapsed} analyze');
Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds;
if (analysisIds != null) {
@ -220,8 +221,7 @@ class MediaStoreSource extends CollectionSource {
// so we manually notify change for potential home screen filters
notifyAlbumsChanged();
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done');
unawaited(reportService.log('Source refresh complete in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${newEntries.length} new, ${removedEntries.length} removed'));
unawaited(reportService.log('$runtimeType load done in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${newEntries.length} new, ${removedEntries.length} removed'));
},
onError: (error) => debugPrint('$runtimeType stream error=$error'),
);
@ -238,7 +238,7 @@ class MediaStoreSource extends CollectionSource {
state = SourceState.loading;
debugPrint('$runtimeType refreshUris ${changedUris.length} uris');
unawaited(reportService.log('$runtimeType refresh start for ${changedUris.length} uris'));
final changedUriByContentId = Map.fromEntries(changedUris.map((uri) {
final pathSegments = Uri.parse(uri).pathSegments;
// e.g. URI `content://media/` has no path segment
@ -297,8 +297,6 @@ class MediaStoreSource extends CollectionSource {
invalidateAlbumFilterSummary(directories: existingDirectories);
state = SourceState.ready;
if (newEntries.isNotEmpty) {
await localMediaDb.insertEntries(newEntries);
@ -323,6 +321,10 @@ class MediaStoreSource extends CollectionSource {
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
}
unawaited(reportService.log('$runtimeType refresh end for ${changedUris.length} uris'));
state = SourceState.ready;
return tempUris;
}

View file

@ -14,6 +14,8 @@ abstract class DeviceService {
Future<int> getPerformanceClass();
Future<bool> isLocked();
Future<bool> isSystemFilePickerEnabled();
Future<void> requestMediaManagePermission();
@ -89,6 +91,17 @@ class PlatformDeviceService implements DeviceService {
return 0;
}
@override
Future<bool> isLocked() async {
try {
final result = await _platform.invokeMethod('isLocked');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
@override
Future<bool> isSystemFilePickerEnabled() async {
try {

View file

@ -5,10 +5,11 @@ class ADurations {
static const transitionMarginMillis = 20;
// page transition duration also available via `ModalRoute.of(context)!.transitionDuration * timeDilation`
static const pageTransitionAnimation = Duration(milliseconds: 300 + transitionMarginMillis); // ref `transitionDuration` used in `MaterialRouteTransitionMixin`
static const dialogTransitionAnimation = Duration(milliseconds: 150 + transitionMarginMillis); // ref `transitionDuration` used in `DialogRoute`
static const drawerTransitionAnimation = Duration(milliseconds: 246 + transitionMarginMillis); // ref `_kBaseSettleDuration` used in `DrawerControllerState`
static const toggleableTransitionAnimation = Duration(milliseconds: 200 + transitionMarginMillis); // ref `_kToggleDuration` used in `ToggleableStateMixin`
static const pageTransitionExact = Duration(milliseconds: 300); // ref `transitionDuration` used in `MaterialRouteTransitionMixin`
static const pageTransitionLoose = Duration(milliseconds: 300 + transitionMarginMillis); // ref `transitionDuration` used in `MaterialRouteTransitionMixin`
static const dialogTransitionLoose = Duration(milliseconds: 150 + transitionMarginMillis); // ref `transitionDuration` used in `DialogRoute`
static const drawerTransitionLoose = Duration(milliseconds: 246 + transitionMarginMillis); // ref `_kBaseSettleDuration` used in `DrawerControllerState`
static const toggleableTransitionLoose = Duration(milliseconds: 200 + transitionMarginMillis); // ref `_kToggleDuration` used in `ToggleableStateMixin`
// common animations
static const sweeperOpacityAnimation = Duration(milliseconds: 150);

View file

@ -32,6 +32,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/durations_provider.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/navigation/tv_page_transitions.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
@ -224,6 +225,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Provider<TvRailController>.value(value: _tvRailController),
DurationsProvider(),
HighlightInfoProvider(),
ViewerEntryProvider(),
],
child: NotificationListener<PopExitNotification>(
onNotification: (notification) {
@ -411,7 +413,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
reportService.log('App memory pressure');
debugPrint('App memory pressure');
imageCache.clear();
}

View file

@ -691,7 +691,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
routeSettings: const RouteSettings(name: TileViewDialog.routeName),
);
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
if (value != null && initialValue != value) {
settings.collectionSortFactor = value.$1!;
settings.collectionSectionFactor = value.$2!;

View file

@ -40,6 +40,7 @@ import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
@ -49,6 +50,7 @@ import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
@ -116,6 +118,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
final ValueNotifier<AppMode> _selectingAppModeNotifier = ValueNotifier(AppMode.pickFilteredMediaInternal);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => context.read<ViewerEntryNotifier>().value = null);
}
@override
void dispose() {
_focusedItemNotifier.dispose();
@ -238,9 +246,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
);
}
void _goToViewer(CollectionLens collection, AvesEntry entry) {
Future<void> _goToViewer(CollectionLens collection, AvesEntry entry) async {
// track viewer entry for dynamic hero placeholder
WidgetsBinding.instance.addPostFrameCallback((_) => context.read<ViewerEntryNotifier>().value = entry);
final selection = context.read<Selection<AvesEntry>>();
Navigator.maybeOf(context)?.push(
await Navigator.maybeOf(context)?.push(
TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
@ -266,6 +277,14 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
},
),
);
// reset track viewer entry
final animate = context.read<Settings>().animate;
if (animate) {
// TODO TLAD fix timing when transition is incomplete, e.g. when going back while going to the viewer
await Future.delayed(ADurations.pageTransitionExact * timeDilation);
}
context.read<ViewerEntryNotifier>().value = null;
}
}

View file

@ -510,7 +510,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
if (confirmed == null || !confirmed) return null;
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
return supported;
}

View file

@ -6,8 +6,10 @@ import 'package:aves/services/intent_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -62,10 +64,7 @@ class InteractiveTile extends StatelessWidget {
selectable: true,
highlightable: true,
isScrollingNotifier: isScrollingNotifier,
// hero tag should include a collection identifier, so that it animates
// between different views of the entry in the same collection (e.g. thumbnails <-> viewer)
// but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer)
heroTagger: () => Object.hashAll([collection.id, entry.id]),
heroTagger: () => EntryHeroInfo(collection, entry).tag,
),
),
);
@ -126,5 +125,20 @@ class Tile extends StatelessWidget {
selectable: selectable,
highlightable: highlightable,
heroTagger: heroTagger,
// do not use a hero placeholder but hide the thumbnail matching the viewer entry,
// so that it can hero out on an entry and come back with a hero to a different entry
heroPlaceholderBuilder: (context, heroSize, child) => child,
imageDecorator: (context, child) {
return Selector<ViewerEntryNotifier, bool>(
selector: (context, v) => v.value == entry,
builder: (context, isViewerEntry, child) {
return Visibility.maintain(
visible: !isViewerEntry,
child: child!,
);
},
child: child,
);
},
);
}

View file

@ -81,7 +81,7 @@ mixin FeedbackMixin {
final margin = (marginComputer ?? snackBarMarginDefault).call(context);
return AnimatedPadding(
padding: margin,
duration: ADurations.pageTransitionAnimation,
duration: ADurations.pageTransitionLoose,
child: child,
);
},

View file

@ -0,0 +1,17 @@
import 'package:aves/model/entry/entry.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class ViewerEntryProvider extends ListenableProvider<ViewerEntryNotifier> {
ViewerEntryProvider({
super.key,
super.child,
}) : super(
create: (context) => ViewerEntryNotifier(null),
dispose: (context, value) => value.dispose(),
);
}
class ViewerEntryNotifier extends ValueNotifier<AvesEntry?> {
ViewerEntryNotifier(super.value);
}

View file

@ -76,7 +76,7 @@ class _SearchPageState extends State<SearchPage> {
return;
}
widget.animation.removeStatusListener(_onAnimationStatusChanged);
Future.delayed(ADurations.pageTransitionAnimation * timeDilation).then((_) {
Future.delayed(ADurations.pageTransitionLoose * timeDilation).then((_) {
if (!mounted) return;
_searchFieldFocusNode.requestFocus();
});

View file

@ -13,6 +13,8 @@ class DecoratedThumbnail extends StatelessWidget {
final ValueNotifier<bool>? cancellableNotifier;
final bool isMosaic, selectable, highlightable;
final Object? Function()? heroTagger;
final HeroPlaceholderBuilder? heroPlaceholderBuilder;
final TransitionBuilder? imageDecorator;
static Color borderColor(BuildContext context) => Theme.of(context).dividerColor;
@ -27,6 +29,8 @@ class DecoratedThumbnail extends StatelessWidget {
this.selectable = true,
this.highlightable = true,
this.heroTagger,
this.heroPlaceholderBuilder,
this.imageDecorator,
});
@override
@ -50,12 +54,13 @@ class DecoratedThumbnail extends StatelessWidget {
isMosaic: isMosaic,
cancellableNotifier: cancellableNotifier,
heroTag: heroTagger?.call(),
heroPlaceholderBuilder: heroPlaceholderBuilder,
);
child = Stack(
fit: StackFit.passthrough,
children: [
child,
imageDecorator?.call(context, child) ?? child,
ThumbnailEntryOverlay(entry: entry),
if (selectable) ...[
GridItemSelectionOverlay<AvesEntry>(

View file

@ -25,6 +25,7 @@ class ThumbnailImage extends StatefulWidget {
final bool showLoadingBackground;
final ValueNotifier<bool>? cancellableNotifier;
final Object? heroTag;
final HeroPlaceholderBuilder? heroPlaceholderBuilder;
const ThumbnailImage({
super.key,
@ -37,6 +38,7 @@ class ThumbnailImage extends StatefulWidget {
this.showLoadingBackground = true,
this.cancellableNotifier,
this.heroTag,
this.heroPlaceholderBuilder,
});
@override
@ -261,11 +263,12 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
},
);
if (animate && widget.heroTag != null) {
final heroTag = widget.heroTag;
if (animate && heroTag != null) {
final background = settings.imageBackground;
final backgroundColor = background.isColor ? background.color : null;
image = Hero(
tag: widget.heroTag!,
tag: heroTag,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
Widget child = TransitionImage(
image: entry.bestCachedThumbnail,
@ -282,6 +285,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
}
return child;
},
placeholderBuilder: widget.heroPlaceholderBuilder,
transitionOnUserGestures: true,
child: image,
);
@ -296,9 +300,10 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
extent: extent,
);
if (animate && widget.heroTag != null) {
final heroTag = widget.heroTag;
if (animate && heroTag != null) {
child = Hero(
tag: widget.heroTag!,
tag: heroTag,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
return MediaQueryDataProvider(
child: DefaultTextStyle(

View file

@ -25,14 +25,11 @@ class _DebugDeviceSectionState extends State<DebugDeviceSection> with AutomaticK
'packageVersion': device.packageVersion,
'userAgent': device.userAgent,
'canAuthenticateUser': '${device.canAuthenticateUser}',
'canGrantDirectoryAccess': '${device.canGrantDirectoryAccess}',
'canPinShortcut': '${device.canPinShortcut}',
'canRenderFlagEmojis': '${device.canRenderFlagEmojis}',
'canRenderSubdivisionFlagEmojis': '${device.canRenderSubdivisionFlagEmojis}',
'canRequestManageMedia': '${device.canRequestManageMedia}',
'canSetLockScreenWallpaper': '${device.canSetLockScreenWallpaper}',
'canUseCrypto': '${device.canUseCrypto}',
'canUseVaults': '${device.canUseVaults}',
'hasGeocoder': '${device.hasGeocoder}',
'isDynamicColorAvailable': '${device.isDynamicColorAvailable}',
'isTelevision': '${device.isTelevision}',

View file

@ -42,11 +42,9 @@ class _EditVaultDialogState extends State<EditVaultDialog> with FeedbackMixin, V
final List<VaultLockType> _lockTypeOptions = [
if (device.canAuthenticateUser) VaultLockType.system,
if (device.canUseCrypto) ...[
VaultLockType.pattern,
VaultLockType.pin,
VaultLockType.password,
],
];
VaultDetails? get initialDetails => widget.initialDetails;

View file

@ -248,7 +248,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
if (directory == null) return;
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
_pickAlbum(directory);
}
@ -270,7 +270,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
if (details == null) return;
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
await vaults.create(details);
_pickAlbum(details.path);

View file

@ -77,7 +77,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
if (ExtraEntryMapStyle.isHeavy(settings.mapStyle)) {
_isPageAnimatingNotifier = ValueNotifier(true);
Future.delayed(ADurations.pageTransitionAnimation * timeDilation).then((_) {
Future.delayed(ADurations.pageTransitionLoose * timeDilation).then((_) {
if (!mounted) return;
_isPageAnimatingNotifier.value = false;
});

View file

@ -14,7 +14,7 @@ Future<void> showSelectionDialog<T>({
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
);
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
if (value != null) {
onSelection(value);
}

View file

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
@ -78,12 +77,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
final selectedSingleItem = selectedFilters.length == 1;
final isMain = appMode == AppMode.main;
final canCreate = !settings.isReadOnly && appMode.canCreateFilter && !isSelecting;
switch (action) {
case ChipSetAction.createAlbum:
return canCreate;
case ChipSetAction.createVault:
return canCreate && device.canUseVaults;
return !settings.isReadOnly && appMode.canCreateFilter && !isSelecting;
case ChipSetAction.delete:
case ChipSetAction.rename:
return isMain && isSelecting && !settings.isReadOnly;
@ -190,7 +187,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
routeSettings: const RouteSettings(name: TileViewDialog.routeName),
);
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
if (value != null && initialValue != value) {
sortFactor = value.$1!;
settings.albumGroupFactor = value.$2!;

View file

@ -250,7 +250,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
routeSettings: const RouteSettings(name: TileViewDialog.routeName),
);
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
if (value != null && initialValue != value) {
sortFactor = value.$1!;
tileLayout = value.$3!;

View file

@ -90,13 +90,15 @@ class _HomePageState extends State<HomePage> {
}
var appMode = AppMode.main;
var error = false;
final intentData = widget.intentData ?? await IntentService.getIntentData();
final safeMode = intentData[IntentDataKeys.safeMode] ?? false;
final intentAction = intentData[IntentDataKeys.action];
final safeMode = (intentData[IntentDataKeys.safeMode] as bool?) ?? false;
final intentAction = intentData[IntentDataKeys.action] as String?;
_initialFilters = null;
_initialExplorerPath = null;
_secureUris = null;
await availability.onNewIntent();
await androidFileUtils.init();
if (!{
IntentActions.edit,
@ -109,61 +111,22 @@ class _HomePageState extends State<HomePage> {
if (intentData.values.whereNotNull().isNotEmpty) {
await reportService.log('Intent data=$intentData');
var intentUri = intentData[IntentDataKeys.uri] as String?;
final intentMimeType = intentData[IntentDataKeys.mimeType] as String?;
switch (intentAction) {
case IntentActions.view:
case IntentActions.widgetOpen:
String? uri, mimeType;
final widgetId = intentData[IntentDataKeys.widgetId];
if (widgetId != null) {
// widget settings may be modified in a different process after channel setup
await settings.reload();
final page = settings.getWidgetOpenPage(widgetId);
switch (page) {
case WidgetOpenPage.home:
case WidgetOpenPage.updateWidget:
break;
case WidgetOpenPage.collection:
_initialFilters = settings.getWidgetCollectionFilters(widgetId);
case WidgetOpenPage.viewer:
uri = settings.getWidgetUri(widgetId);
}
unawaited(WidgetService.update(widgetId));
} else {
uri = intentData[IntentDataKeys.uri];
mimeType = intentData[IntentDataKeys.mimeType];
}
_secureUris = intentData[IntentDataKeys.secureUris];
if (uri != null) {
_viewerEntry = await _initViewerEntry(
uri: uri,
mimeType: mimeType,
);
if (_viewerEntry != null) {
appMode = AppMode.view;
}
}
_secureUris = (intentData[IntentDataKeys.secureUris] as List?)?.cast<String>();
case IntentActions.edit:
_viewerEntry = await _initViewerEntry(
uri: intentData[IntentDataKeys.uri],
mimeType: intentData[IntentDataKeys.mimeType],
);
if (_viewerEntry != null) {
appMode = AppMode.edit;
}
case IntentActions.setWallpaper:
_viewerEntry = await _initViewerEntry(
uri: intentData[IntentDataKeys.uri],
mimeType: intentData[IntentDataKeys.mimeType],
);
if (_viewerEntry != null) {
appMode = AppMode.setWallpaper;
}
case IntentActions.pickItems:
// TODO TLAD apply pick mimetype(s)
// some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?)
String? pickMimeTypes = intentData[IntentDataKeys.mimeType];
final multiple = intentData[IntentDataKeys.allowMultiple] ?? false;
debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple');
final multiple = (intentData[IntentDataKeys.allowMultiple] as bool?) ?? false;
debugPrint('pick mimeType=$intentMimeType multiple=$multiple');
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
case IntentActions.pickCollectionFilters:
appMode = AppMode.pickCollectionFiltersExternal;
@ -174,23 +137,64 @@ class _HomePageState extends State<HomePage> {
_initialRouteName = ScreenSaverSettingsPage.routeName;
case IntentActions.search:
_initialRouteName = SearchPage.routeName;
_initialSearchQuery = intentData[IntentDataKeys.query];
_initialSearchQuery = intentData[IntentDataKeys.query] as String?;
case IntentActions.widgetSettings:
_initialRouteName = HomeWidgetSettingsPage.routeName;
_widgetId = intentData[IntentDataKeys.widgetId] ?? 0;
_widgetId = (intentData[IntentDataKeys.widgetId] as int?) ?? 0;
case IntentActions.widgetOpen:
final widgetId = intentData[IntentDataKeys.widgetId] as int?;
if (widgetId == null) {
error = true;
} else {
// widget settings may be modified in a different process after channel setup
await settings.reload();
final page = settings.getWidgetOpenPage(widgetId);
switch (page) {
case WidgetOpenPage.collection:
_initialFilters = settings.getWidgetCollectionFilters(widgetId);
case WidgetOpenPage.viewer:
appMode = AppMode.view;
intentUri = settings.getWidgetUri(widgetId);
case WidgetOpenPage.home:
case WidgetOpenPage.updateWidget:
break;
}
unawaited(WidgetService.update(widgetId));
}
default:
// do not use 'route' as extra key, as the Flutter framework acts on it
final extraRoute = intentData[IntentDataKeys.page];
final extraRoute = intentData[IntentDataKeys.page] as String?;
if (allowedShortcutRoutes.contains(extraRoute)) {
_initialRouteName = extraRoute;
}
}
if (_initialFilters == null) {
final extraFilters = intentData[IntentDataKeys.filters];
_initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
final extraFilters = (intentData[IntentDataKeys.filters] as List?)?.cast<String>();
_initialFilters = extraFilters?.map(CollectionFilter.fromJson).whereNotNull().toSet();
}
_initialExplorerPath = intentData[IntentDataKeys.explorerPath];
_initialExplorerPath = intentData[IntentDataKeys.explorerPath] as String?;
switch (appMode) {
case AppMode.view:
case AppMode.edit:
case AppMode.setWallpaper:
if (intentUri != null) {
_viewerEntry = await _initViewerEntry(
uri: intentUri,
mimeType: intentMimeType,
);
}
error = _viewerEntry == null;
default:
break;
}
}
if (error) {
debugPrint('Failed to init app mode=$appMode for intent data=$intentData. Fallback to main mode.');
appMode = AppMode.main;
}
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms');

View file

@ -118,7 +118,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
if (ExtraEntryMapStyle.isHeavy(settings.mapStyle)) {
_isPageAnimatingNotifier.value = true;
Future.delayed(ADurations.pageTransitionAnimation * timeDilation).then((_) {
Future.delayed(ADurations.pageTransitionLoose * timeDilation).then((_) {
if (!mounted) return;
_isPageAnimatingNotifier.value = false;
});
@ -142,7 +142,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
_subscriptions.add(openingCollection.source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _updateRegionCollection()));
_selectedIndexNotifier.addListener(_onThumbnailIndexChanged);
Future.delayed(ADurations.pageTransitionAnimation * timeDilation + const Duration(seconds: 1), () {
Future.delayed(ADurations.pageTransitionLoose * timeDilation + const Duration(seconds: 1), () {
final regionEntries = regionCollection?.sortedEntries ?? [];
final initialEntry = widget.initialEntry ?? regionEntries.firstOrNull;
if (initialEntry != null) {

View file

@ -6,6 +6,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:aves/widgets/map/info_row.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:flutter/material.dart';
class MapEntryScroller extends StatefulWidget {
@ -85,7 +86,7 @@ class _MapEntryScrollerState extends State<MapEntryScroller> {
entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null,
indexNotifier: widget.selectedIndexNotifier,
onTap: widget.onTap,
heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]),
heroTagger: (entry) => EntryHeroInfo(regionCollection, entry).tag,
highlightable: true,
showLocation: false,
);

View file

@ -115,7 +115,7 @@ class _AppDrawerState extends State<AppDrawer> {
Future<void> goTo(String routeName, WidgetBuilder pageBuilder) async {
Navigator.maybeOf(context)?.pop();
await Future.delayed(ADurations.drawerTransitionAnimation);
await Future.delayed(ADurations.drawerTransitionLoose);
await Navigator.maybeOf(context)?.push(MaterialPageRoute(
settings: RouteSettings(name: routeName),
builder: pageBuilder,

View file

@ -68,7 +68,7 @@ class SettingsSwitchListTile extends StatelessWidget {
Expanded(child: titleWidget),
AnimatedOpacity(
opacity: current ? 1 : disabledOpacity,
duration: ADurations.toggleableTransitionAnimation,
duration: ADurations.toggleableTransitionLoose,
child: trailing,
),
],

View file

@ -32,7 +32,7 @@ class LocaleTile extends StatelessWidget {
),
);
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.pageTransitionAnimation * timeDilation);
await Future.delayed(ADurations.pageTransitionLoose * timeDilation);
if (value != null) {
settings.locale = value == systemLocaleOption ? null : value;
}

View file

@ -182,7 +182,7 @@ class _FilePickerPageState extends State<FilePickerPage> {
title: Text(v.getDescription(context)),
onTap: () async {
Navigator.maybeOf(context)?.pop();
await Future.delayed(ADurations.drawerTransitionAnimation);
await Future.delayed(ADurations.drawerTransitionLoose);
_goTo(v.path);
setState(() {});
},

View file

@ -185,7 +185,7 @@ class _HiddenPaths extends StatelessWidget {
),
);
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.pageTransitionAnimation * timeDilation);
await Future.delayed(ADurations.pageTransitionLoose * timeDilation);
if (path != null && path.isNotEmpty) {
settings.changeFilterVisibility({PathFilter(path)}, false);
}

View file

@ -43,7 +43,7 @@ class PrivacySection extends SettingsSection {
SettingsTilePrivacySaveSearchHistory(),
if (!settings.useTvLayout) SettingsTilePrivacyEnableBin(),
SettingsTilePrivacyHiddenItems(),
if (!settings.useTvLayout && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(),
if (!settings.useTvLayout) SettingsTilePrivacyStorageAccess(),
];
}
}

View file

@ -68,7 +68,7 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix
super.initState();
_isPageAnimatingNotifier = ValueNotifier(true);
Future.delayed(ADurations.pageTransitionAnimation * timeDilation).then((_) {
Future.delayed(ADurations.pageTransitionLoose * timeDilation).then((_) {
if (!mounted) return;
_isPageAnimatingNotifier.value = false;
});

View file

@ -159,6 +159,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.convertMotionPhotoToStillImage:
case EntryAction.viewMotionPhotoVideo:
return _metadataActionDelegate.canApply(targetEntry, action);
case EntryAction.convert:
case EntryAction.copy:
case EntryAction.move:
return !availability.isLocked;
default:
return true;
}
@ -471,7 +475,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
if (newName == null || newName.isEmpty || newName == targetEntry.filenameWithoutExtension) return;
// wait for the dialog to hide as applying the change may block the UI
await Future.delayed(ADurations.dialogTransitionAnimation * timeDilation);
await Future.delayed(ADurations.dialogTransitionLoose * timeDilation);
await rename(
context,
entriesToNewName: {targetEntry: '$newName${targetEntry.extension}'},

View file

@ -18,6 +18,7 @@ import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/viewer/action/video_action_delegate.dart';
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
@ -75,7 +76,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
late Animation<Offset> _overlayTopOffset;
EdgeInsets? _frozenViewInsets, _frozenViewPadding;
late VideoActionDelegate _videoActionDelegate;
final ValueNotifier<HeroInfo?> _heroInfoNotifier = ValueNotifier(null);
final ValueNotifier<EntryHeroInfo?> _heroInfoNotifier = ValueNotifier(null);
bool _isEntryTracked = true;
Timer? _overlayHidingTimer, _appInactiveReactionTimer;
@ -116,7 +117,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
final initialEntry = widget.initialEntry;
final entry = entries.firstWhereOrNull((entry) => entry.id == initialEntry.id) ?? entries.firstOrNull;
// opening hero, with viewer as target
_heroInfoNotifier.value = HeroInfo(collection?.id, entry);
_heroInfoNotifier.value = EntryHeroInfo(collection, entry);
entryNotifier = viewerController.entryNotifier;
entryNotifier.value = entry;
_currentEntryIndex = max(0, entry != null ? entries.indexOf(entry) : -1);
@ -224,7 +225,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
_onPopInvoked();
},
child: ValueListenableProvider<HeroInfo?>.value(
child: ValueListenableProvider<EntryHeroInfo?>.value(
value: _heroInfoNotifier,
child: NotificationListener(
onNotification: _handleNotification,
@ -412,17 +413,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
}
Widget _buildSlideshowBottomOverlay(Size availableSize) {
return SizedBox.fromSize(
size: availableSize,
child: Align(
alignment: AlignmentDirectional.bottomEnd,
child: TooltipTheme(
return TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: SlideshowButtons(
child: Align(
alignment: AlignmentDirectional.bottomEnd,
child: SlideshowBottomOverlay(
animationController: _overlayAnimationController,
),
availableSize: availableSize,
viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding,
),
),
);
@ -867,7 +868,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
}
// closing hero, with viewer as source
final heroInfo = HeroInfo(collection?.id, entryNotifier.value);
final heroInfo = EntryHeroInfo(collection, entryNotifier.value);
if (_heroInfoNotifier.value != heroInfo) {
_heroInfoNotifier.value = heroInfo;
// we post closing the viewer page so that hero animation source is ready
@ -900,6 +901,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
predicate: (v) => v < 1,
animate: false,
);
context.read<ViewerEntryNotifier>().value = entry;
}
}

View file

@ -1,17 +1,20 @@
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@immutable
class HeroInfo extends Equatable {
class EntryHeroInfo extends Equatable {
// hero tag should include a collection identifier, so that it animates
// between different views of the entry in the same collection (e.g. thumbnails <-> viewer)
// but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer)
final int? collectionId;
final CollectionLens? collection;
final AvesEntry? entry;
@override
List<Object?> get props => [collectionId, entry?.uri];
List<Object?> get props => [collection?.id, entry?.uri];
const HeroInfo(this.collectionId, this.entry);
const EntryHeroInfo(this.collection, this.entry);
int get tag => Object.hashAll([collection?.id, entry?.uri]);
}

View file

@ -123,7 +123,7 @@ class _InfoPageState extends State<InfoPage> {
ShowImageNotification().dispatch(context);
_scrollController.animateTo(
0,
duration: ADurations.pageTransitionAnimation,
duration: ADurations.pageTransitionLoose,
curve: Curves.easeInOut,
);
}
@ -276,7 +276,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
}
void _onActionDelegateEvent(ActionEvent<EntryAction> event) {
Future.delayed(ADurations.dialogTransitionAnimation).then((_) {
Future.delayed(ADurations.dialogTransitionLoose).then((_) {
if (event is ActionStartedEvent) {
_isEditingMetadataNotifier.value = event.action;
} else if (event is ActionEndedEvent) {

View file

@ -21,7 +21,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class ViewerBottomOverlay extends StatefulWidget {
class ViewerBottomOverlay extends StatelessWidget {
final List<AvesEntry> entries;
final int index;
final CollectionLens? collection;
@ -33,6 +33,10 @@ class ViewerBottomOverlay extends StatefulWidget {
// always keep action buttons in the lower right corner, even with RTL locales
static const actionsDirection = TextDirection.ltr;
AvesEntry? get entry {
return index < entries.length ? entries[index] : null;
}
const ViewerBottomOverlay({
super.key,
required this.entries,
@ -45,27 +49,6 @@ class ViewerBottomOverlay extends StatefulWidget {
required this.multiPageController,
});
@override
State<StatefulWidget> createState() => _ViewerBottomOverlayState();
static double actionSafeHeight(BuildContext context) {
final mqPaddingBottom = context.select<MediaQueryData, double>((mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom));
final buttonHeight = ViewerButtons.preferredHeight(context);
final thumbnailHeight = (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0);
return mqPaddingBottom + buttonHeight + thumbnailHeight;
}
}
class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
List<AvesEntry> get entries => widget.entries;
AvesEntry? get entry {
final index = widget.index;
return index < entries.length ? entries[index] : null;
}
MultiPageController? get multiPageController => widget.multiPageController;
@override
Widget build(BuildContext context) {
final mainEntry = entry;
@ -73,15 +56,15 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
entries: entries,
index: widget.index,
index: index,
mainEntry: mainEntry,
pageEntry: pageEntry ?? mainEntry,
collection: widget.collection,
availableSize: widget.availableSize,
viewInsets: widget.viewInsets,
viewPadding: widget.viewPadding,
collection: collection,
availableSize: availableSize,
viewInsets: viewInsets,
viewPadding: viewPadding,
multiPageController: multiPageController,
animationController: widget.animationController,
animationController: animationController,
);
Widget child = multiPageController != null
@ -102,6 +85,13 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
child: child,
);
}
static double actionSafeHeight(BuildContext context) {
final mqPaddingBottom = context.select<MediaQueryData, double>((mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom));
final buttonHeight = ViewerButtons.preferredHeight(context);
final thumbnailHeight = (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0);
return mqPaddingBottom + buttonHeight + thumbnailHeight;
}
}
class _BottomOverlayContent extends StatefulWidget {

View file

@ -221,7 +221,7 @@ class ViewerDetailOverlayContent extends StatelessWidget {
rows.add(_buildRatingTagsFullRow(context));
}
if (showDescription) {
rows.add(_buildDescriptionFullRow(context));
rows.add(_buildDescriptionFullRow(context, infoMaxWidth));
}
return rows;
}
@ -243,13 +243,18 @@ class ViewerDetailOverlayContent extends StatelessWidget {
),
);
Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher(
Widget _buildDescriptionFullRow(BuildContext context, double infoMaxWidth) => _buildFullRowSwitcher(
context: context,
visible: details.description != null,
builder: (context) => OverlayRowExpander(
builder: (context) => SizedBox(
// size it so that a long description with multiple short lines
// expands to the full width and the scroll bar is at the edge
width: infoMaxWidth,
child: OverlayRowExpander(
expandedNotifier: expandedNotifier,
child: OverlayDescriptionRow(description: details.description!),
),
),
);
Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher(
@ -286,8 +291,14 @@ class ViewerDetailOverlayContent extends StatelessWidget {
required double subRowWidth,
required bool visible,
required WidgetBuilder builder,
}) =>
AnimatedSwitcher(
}) {
final child = visible
? SizedBox(
width: subRowWidth,
child: builder(context),
)
: const SizedBox();
return AnimatedSwitcher(
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation),
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
@ -295,36 +306,34 @@ class ViewerDetailOverlayContent extends StatelessWidget {
opacity: animation,
child: child,
),
child: visible
? SizedBox(
width: subRowWidth,
child: builder(context),
)
: const SizedBox(),
child: child,
);
}
Widget _buildFullRowSwitcher({
required BuildContext context,
required bool visible,
required WidgetBuilder builder,
}) =>
AnimatedSwitcher(
}) {
final child = visible
? Padding(
padding: const EdgeInsets.only(top: _interRowPadding),
child: builder(context),
)
: const SizedBox();
return AnimatedSwitcher(
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation),
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SizeTransition(
axisAlignment: 1,
sizeFactor: animation,
axisAlignment: 1,
child: child,
),
),
child: visible
? Padding(
padding: const EdgeInsets.only(top: _interRowPadding),
child: builder(context),
)
: const SizedBox(),
child: child,
);
}
}

View file

@ -1,21 +1,64 @@
import 'dart:math';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/viewer/controls/intents.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves/widgets/viewer/slideshow_page.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class SlideshowBottomOverlay extends StatelessWidget {
final AnimationController animationController;
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding;
const SlideshowBottomOverlay({
super.key,
required this.animationController,
required this.availableSize,
this.viewInsets,
this.viewPadding,
});
@override
Widget build(BuildContext context) {
return Selector<MediaQueryData, double>(
selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
builder: (context, mqPaddingBottom, child) {
return Padding(
padding: EdgeInsets.only(bottom: mqPaddingBottom),
child: child,
);
},
child: SlideshowButtons(
availableSize: availableSize,
viewInsets: viewInsets,
viewPadding: viewPadding,
animationController: animationController,
),
);
}
}
class SlideshowButtons extends StatefulWidget {
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding;
final AnimationController animationController;
const SlideshowButtons({
super.key,
required this.availableSize,
required this.viewInsets,
required this.viewPadding,
required this.animationController,
});
@ -70,7 +113,8 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
final viewerButtonRow = FocusableActionDetector(
focusNode: _buttonRowFocusScopeNode,
shortcuts: settings.useTvLayout
? const {
@ -80,26 +124,53 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
actions: {
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
},
child: settings.useTvLayout
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: _actions.map((action) {
return CaptionedButton(
scale: _buttonScale,
icon: action.getIcon(),
caption: action.getText(context),
onPressed: () => _onAction(context, action),
child: SafeArea(
top: false,
bottom: false,
minimum: EdgeInsets.only(
left: viewInsetsPadding.left,
right: viewInsetsPadding.right,
),
child: _buildButtons(context),
),
);
}).toList(),
)
: SafeArea(
final availableWidth = widget.availableSize.width;
return SizedBox(
width: availableWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
viewerButtonRow,
],
),
);
}
Widget _buildButtons(BuildContext context) {
if (settings.useTvLayout) {
return _buildTvButtonRowContent(context);
}
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.only(left: _padding / 2, right: _padding / 2, bottom: _padding),
child: Row(
mainAxisSize: MainAxisSize.min,
children: _actions
.map((action) => Padding(
textDirection: ViewerBottomOverlay.actionsDirection,
children: [
const Spacer(),
..._actions.map((action) => _buildOverlayButton(context, action)),
],
),
),
);
}
Widget _buildOverlayButton(BuildContext context, SlideshowAction action) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: _padding / 2),
child: OverlayButton(
scale: _buttonScale,
@ -109,11 +180,22 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
tooltip: action.getText(context),
),
),
))
.toList(),
),
),
),
);
}
Widget _buildTvButtonRowContent(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
textDirection: ViewerBottomOverlay.actionsDirection,
children: _actions.map((action) {
return CaptionedButton(
scale: _buttonScale,
icon: action.getIcon(),
caption: action.getText(context),
onPressed: () => _onAction(context, action),
);
}).toList(),
);
}

View file

@ -7,6 +7,7 @@ import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/move_button.dart';
@ -278,6 +279,7 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
...topLevelActions.map((action) => _buildPopupMenuItem(context, action, videoController)),
if (exportActions.isNotEmpty)
PopupMenuExpansionPanel<EntryAction>(
enabled: !availability.isLocked,
value: 'export',
expandedNotifier: _popupExpandedNotifier,
icon: AIcons.export,
@ -345,18 +347,18 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
}
PopupMenuItem<EntryAction> _buildPopupMenuItem(BuildContext context, EntryAction action, AvesVideoController? videoController) {
late final bool enabled;
var enabled = widget.actionDelegate.canApply(action);
switch (action) {
case EntryAction.videoCaptureFrame:
enabled = videoController?.canCaptureFrameNotifier.value ?? false;
enabled &= videoController?.canCaptureFrameNotifier.value ?? false;
case EntryAction.videoToggleMute:
enabled = videoController?.canMuteNotifier.value ?? false;
enabled &= videoController?.canMuteNotifier.value ?? false;
case EntryAction.videoSelectStreams:
enabled = videoController?.canSelectStreamNotifier.value ?? false;
enabled &= videoController?.canSelectStreamNotifier.value ?? false;
case EntryAction.videoSetSpeed:
enabled = videoController?.canSetSpeedNotifier.value ?? false;
enabled &= videoController?.canSetSpeedNotifier.value ?? false;
default:
enabled = true;
break;
}
Widget? child;

View file

@ -150,9 +150,9 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
final animate = context.select<Settings, bool>((v) => v.animate);
if (animate) {
child = Consumer<HeroInfo?>(
child = Consumer<EntryHeroInfo?>(
builder: (context, info, child) => Hero(
tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.id]) : hashCode,
tag: info != null && info.entry == mainEntry ? info.tag : hashCode,
transitionOnUserGestures: true,
child: child!,
),
@ -414,7 +414,11 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onFling: _onFling,
onTap: (c, s, a, p) => _onTap(alignment: a),
onTap: (c, s, a, p) {
if (c.mounted) {
_onTap(alignment: a);
}
},
onDoubleTap: onDoubleTap,
child: child,
);

View file

@ -1,9 +1,13 @@
*.iml
.gradle
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View file

@ -33,8 +33,8 @@ android {
compileSdk 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
lintOptions {

View file

@ -1,160 +0,0 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

View file

@ -1,90 +0,0 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7"
sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692
url: "https://pub.dev"
source: hosted
version: "1.3.41"
version: "1.3.42"
async:
dependency: transitive
description:
@ -68,10 +68,10 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff"
sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1"
firebase_core_platform_interface:
dependency: transitive
description:
@ -84,26 +84,26 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25
url: "https://pub.dev"
source: hosted
version: "2.17.5"
version: "2.18.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "4c9872020c0d97a161362ee6af7000cfdb8666234ddc290a15252ad379bb235a"
sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.1.1"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: ede8a199ff03378857d3c8cbb7fa58d37c27bb5a6b75faf8415ff6925dcaae2a
sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac"
url: "https://pub.dev"
source: hosted
version: "3.6.41"
version: "3.6.42"
flutter:
dependency: "direct main"
description: flutter
@ -272,10 +272,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.22.0"

View file

@ -1,9 +1,13 @@
*.iml
.gradle
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View file

@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript {
ext {
kotlin_version = '1.9.24'
agp_version = '8.5.1'
agp_version = '8.6.0'
}
repositories {
@ -30,11 +30,11 @@ apply plugin: 'kotlin-android'
android {
namespace 'deckers.thibault.aves.aves_screen_state'
compileSdk 34
compileSdk 35
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
lintOptions {

View file

@ -1,234 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View file

@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -211,10 +211,10 @@ packages:
dependency: "direct main"
description:
name: google_maps_flutter_android
sha256: "60a005bf1ba8d178144e442f6e2d734b0ffc2cc800a05415388472f934ad6d6a"
sha256: "36e75af1d0bd4c7391eacdaedf9ca7632c5b9709c5ec618b04489b79ea2b3f82"
url: "https://pub.dev"
source: hosted
version: "2.14.4"
version: "2.14.6"
google_maps_flutter_ios:
dependency: transitive
description:
@ -227,10 +227,10 @@ packages:
dependency: "direct main"
description:
name: google_maps_flutter_platform_interface
sha256: "4f6930fd668bf5d40feb2695d5695dbc0c35e5542b557a34ad35be491686d2ba"
sha256: "099874463dc4c9bff04fe4b2b8cf7284d2455c2deead8f9a59a87e1b9f028c69"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
version: "2.9.2"
google_maps_flutter_web:
dependency: transitive
description:

View file

@ -385,10 +385,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
url: "https://pub.dev"
source: hosted
version: "4.4.2"
version: "4.5.0"
vector_math:
dependency: transitive
description:

View file

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7"
sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692
url: "https://pub.dev"
source: hosted
version: "1.3.41"
version: "1.3.42"
_macros:
dependency: transitive
description: dart
@ -239,18 +239,18 @@ packages:
dependency: "direct main"
description:
name: country_code
sha256: f69ccd5163b1ca43011be9632e33ebe7ffac65e49ce2afcd3e3e5228af5d91fc
sha256: af9f06f6ccf873ff447214c7820509867ee6f791780cc3061f0e51f7f358ee2b
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
coverage:
dependency: transitive
description:
name: coverage
sha256: "7b594a150942e0d3be99cd45a1d0b5caff27ba5a27f292ed8e8d904ba3f167b5"
sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.9.2"
crypto:
dependency: transitive
description:
@ -401,10 +401,10 @@ packages:
dependency: transitive
description:
name: firebase_core
sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff"
sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1"
firebase_core_platform_interface:
dependency: transitive
description:
@ -417,26 +417,26 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25
url: "https://pub.dev"
source: hosted
version: "2.17.5"
version: "2.18.0"
firebase_crashlytics:
dependency: transitive
description:
name: firebase_crashlytics
sha256: "4c9872020c0d97a161362ee6af7000cfdb8666234ddc290a15252ad379bb235a"
sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.1.1"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: ede8a199ff03378857d3c8cbb7fa58d37c27bb5a6b75faf8415ff6925dcaae2a
sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac"
url: "https://pub.dev"
source: hosted
version: "3.6.41"
version: "3.6.42"
fixnum:
dependency: transitive
description:
@ -457,10 +457,10 @@ packages:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "86470c8dc470f55dd3e28a6d30e3253a1c176df32903263d7daeabfc0c77dbd4"
sha256: "7d97ba5c20f0e5cb1e3e2c17c865e1f797d129de31fc1f75d2dcce9470d6373c"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.3.0"
floating:
dependency: "direct main"
description:
@ -648,10 +648,10 @@ packages:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: "60a005bf1ba8d178144e442f6e2d734b0ffc2cc800a05415388472f934ad6d6a"
sha256: "36e75af1d0bd4c7391eacdaedf9ca7632c5b9709c5ec618b04489b79ea2b3f82"
url: "https://pub.dev"
source: hosted
version: "2.14.4"
version: "2.14.6"
google_maps_flutter_ios:
dependency: transitive
description:
@ -664,10 +664,10 @@ packages:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
sha256: "4f6930fd668bf5d40feb2695d5695dbc0c35e5542b557a34ad35be491686d2ba"
sha256: "099874463dc4c9bff04fe4b2b8cf7284d2455c2deead8f9a59a87e1b9f028c69"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
version: "2.9.2"
google_maps_flutter_web:
dependency: transitive
description:
@ -1154,10 +1154,10 @@ packages:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
@ -1218,10 +1218,10 @@ packages:
dependency: "direct main"
description:
name: printing
sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3
sha256: de1889f30b34029fc46e5de6a9841498850b23d32942a9ee810ca36b0cb1b234
url: "https://pub.dev"
source: hosted
version: "5.13.1"
version: "5.13.2"
process:
dependency: transitive
description:
@ -1402,10 +1402,10 @@ packages:
dependency: transitive
description:
name: shelf_static
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
@ -1672,10 +1672,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
url: "https://pub.dev"
source: hosted
version: "4.4.2"
version: "4.5.0"
vector_math:
dependency: "direct main"
description:
@ -1728,10 +1728,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
web_socket:
dependency: transitive
description:
@ -1814,4 +1814,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.1"
flutter: ">=3.24.3"

View file

@ -7,13 +7,13 @@ 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.11.10+129
version: 1.11.11+130
publish_to: none
environment:
# this project bundles Flutter SDK via `flutter_wrapper`
# cf https://github.com/passsy/flutter_wrapper
flutter: 3.24.1
flutter: 3.24.3
sdk: '>=3.5.0 <4.0.0'
# use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor
@ -119,10 +119,6 @@ dependency_overrides:
# media_kit_video v1.2.4 depends on a specific old version of screen_brightness
media_kit_video: ^1.0.0
screen_brightness: ^1.0.0
# Because printing >=5.13.2 depends on web ^1.0.0 and firebase_core_web >=2.12.0 depends on web ^0.5.1, printing >=5.13.2 is incompatible with firebase_core_web >=2.12.0.
# And because firebase_core 3.3.0 depends on firebase_core_web ^2.17.4, printing >=5.13.2 is incompatible with firebase_core 3.3.0.
# So, because aves depends on both firebase_core 3.3.0 and printing 5.13.2, version solving failed.
printing: "5.13.1"
dev_dependencies:
flutter_test:

File diff suppressed because one or more lines are too long

View file

@ -93,9 +93,14 @@ void main() {
final subImage = FakeMediaStoreService.newImage(subAlbum, 'image1');
final siblingImage = FakeMediaStoreService.newImage(siblingAlbum, 'image1');
final path = PathFilter('$rootAlbum/');
expect(path.test(rootImage), true);
expect(path.test(subImage), true);
expect(path.test(siblingImage), false);
final untrailedPath = PathFilter(rootAlbum);
expect(untrailedPath.test(rootImage), true);
expect(untrailedPath.test(subImage), true);
expect(untrailedPath.test(siblingImage), false);
final trailedPath = PathFilter('$rootAlbum/');
expect(trailedPath.test(rootImage), true);
expect(trailedPath.test(subImage), true);
expect(trailedPath.test(siblingImage), false);
});
}

View file

@ -1,3 +1,3 @@
In v1.11.10:
- enjoy the app in Swedish
In v1.11.11:
- review photos from the lock screen
Full changelog available on GitHub