Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-11-24 23:16:41 +01:00
commit 1d7deac84d
62 changed files with 357 additions and 124 deletions

View file

@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with: with:
egress-policy: audit egress-policy: audit
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0 uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with: with:
egress-policy: audit egress-policy: audit
@ -52,7 +52,7 @@ jobs:
build-mode: manual build-mode: manual
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with: with:
egress-policy: audit egress-policy: audit
@ -69,7 +69,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -83,6 +83,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play ./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View file

@ -18,7 +18,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with: with:
egress-policy: audit egress-policy: audit
@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with: with:
egress-policy: audit egress-policy: audit

View file

@ -31,7 +31,7 @@ jobs:
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with: with:
egress-policy: audit egress-policy: audit
@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.19"></a>[v1.11.19] - 2024-11-24
### Added
- integrate with OS app language settings on Android >=14
### Changed
- remember title filter visibility by page
## <a id="v1.11.18"></a>[v1.11.18] - 2024-11-18 ## <a id="v1.11.18"></a>[v1.11.18] - 2024-11-18
### Changed ### Changed

View file

@ -1,11 +1,14 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.LocaleConfig
import android.app.LocaleManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.location.Geocoder import android.location.Geocoder
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.LocaleList
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.Settings import android.provider.Settings
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
@ -30,8 +33,8 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
when (call.method) { when (call.method) {
"canManageMedia" -> safe(call, result, ::canManageMedia) "canManageMedia" -> safe(call, result, ::canManageMedia)
"getCapabilities" -> defaultScope.launch { safe(call, result, ::getCapabilities) } "getCapabilities" -> defaultScope.launch { safe(call, result, ::getCapabilities) }
"getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
"getLocales" -> safe(call, result, ::getLocales) "getLocales" -> safe(call, result, ::getLocales)
"setLocaleConfig" -> safe(call, result, ::setLocaleConfig)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isLocked" -> safe(call, result, ::isLocked) "isLocked" -> safe(call, result, ::isLocked)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled) "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
@ -63,10 +66,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
) )
} }
private fun getDefaultTimeZoneRawOffsetMillis(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().rawOffset)
}
private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
fun toMap(locale: Locale): FieldMap = hashMapOf( fun toMap(locale: Locale): FieldMap = hashMapOf(
"language" to locale.language, "language" to locale.language,
@ -88,6 +87,21 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(locales) result.success(locales)
} }
private fun setLocaleConfig(call: MethodCall, result: MethodChannel.Result) {
val locales = call.argument<List<String>>("locales")
if (locales.isNullOrEmpty()) {
result.error("setLocaleConfig-args", "missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val lm = context.getSystemService(Context.LOCALE_SERVICE) as? LocaleManager
lm?.overrideLocaleConfig = LocaleConfig(LocaleList.forLanguageTags(locales.joinToString(",")))
}
result.success(true)
}
private fun getPerformanceClass(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getPerformanceClass(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">मीडिया जाँचे</string> <string name="analysis_channel_name">मीडिया जाँचे</string>
<string name="app_name">ऐवीज</string> <string name="app_name">ऐवीज</string>
<string name="videos_shortcut_short_label">वीडियो</string> <string name="videos_shortcut_short_label">वीडियो</string>
<string name="map_shortcut_short_label">मैप</string>
</resources> </resources>

View file

@ -0,0 +1,5 @@
In v1.11.19:
- peruse your videos frame by frame
- create map shortcuts to filtered collections
- enjoy the app in Shavian
Full changelog available on GitHub

View file

@ -0,0 +1,5 @@
In v1.11.19:
- peruse your videos frame by frame
- create map shortcuts to filtered collections
- enjoy the app in Shavian
Full changelog available on GitHub

View file

@ -1589,5 +1589,7 @@
"videoActionShowPreviousFrame": "𐑖𐑴 𐑐𐑮𐑰𐑝𐑾𐑕 𐑓𐑮𐑱𐑥", "videoActionShowPreviousFrame": "𐑖𐑴 𐑐𐑮𐑰𐑝𐑾𐑕 𐑓𐑮𐑱𐑥",
"@videoActionShowPreviousFrame": {}, "@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "𐑖𐑴 𐑯𐑧𐑒𐑕𐑑 𐑓𐑮𐑱𐑥", "videoActionShowNextFrame": "𐑖𐑴 𐑯𐑧𐑒𐑕𐑑 𐑓𐑮𐑱𐑥",
"@videoActionShowNextFrame": {} "@videoActionShowNextFrame": {},
"newAlbumDialogAlbumAlreadyExistsHelper": "𐑨𐑤𐑚𐑩𐑥 𐑷𐑤𐑮𐑧𐑛𐑦 𐑦𐑜𐑟𐑦𐑕𐑑𐑕",
"@newAlbumDialogAlbumAlreadyExistsHelper": {}
} }

View file

@ -464,5 +464,178 @@
"videoLoopModeAlways": "Alati", "videoLoopModeAlways": "Alati",
"@videoLoopModeAlways": {}, "@videoLoopModeAlways": {},
"unitSystemMetric": "Meetermõõdustik", "unitSystemMetric": "Meetermõõdustik",
"@unitSystemMetric": {} "@unitSystemMetric": {},
"viewerTransitionZoomIn": "Sissesuumimine",
"@viewerTransitionZoomIn": {},
"wallpaperTargetLock": "Lukustusvaade",
"@wallpaperTargetLock": {},
"wallpaperTargetHomeLock": "Avaleht ja lukustusvaade",
"@wallpaperTargetHomeLock": {},
"widgetDisplayedItemRandom": "Juhuslik",
"@widgetDisplayedItemRandom": {},
"widgetDisplayedItemMostRecent": "Viimane",
"@widgetDisplayedItemMostRecent": {},
"widgetOpenPageHome": "Mine avalehele",
"@widgetOpenPageHome": {},
"widgetOpenPageCollection": "Ava kogumik",
"@widgetOpenPageCollection": {},
"widgetOpenPageViewer": "Ava sirvija",
"@widgetOpenPageViewer": {},
"widgetTapUpdateWidget": "Värskenda vidinat",
"@widgetTapUpdateWidget": {},
"storageVolumeDescriptionFallbackPrimary": "Sisemine andmehoidla",
"@storageVolumeDescriptionFallbackPrimary": {},
"rootDirectoryDescription": "juurkaust",
"@rootDirectoryDescription": {},
"viewerTransitionNone": "Määratlemata",
"@viewerTransitionNone": {},
"wallpaperTargetHome": "Avaleht",
"@wallpaperTargetHome": {},
"storageVolumeDescriptionFallbackNonPrimary": "SD-kaart",
"@storageVolumeDescriptionFallbackNonPrimary": {},
"unitSystemImperial": "Inglise mõõdustik",
"@unitSystemImperial": {},
"viewerTransitionSlide": "Äraliuglemine",
"@viewerTransitionSlide": {},
"viewerTransitionFade": "Hajumine",
"@viewerTransitionFade": {},
"viewerTransitionParallax": "Parallaks",
"@viewerTransitionParallax": {},
"otherDirectoryDescription": "„{name}“ kaust",
"@otherDirectoryDescription": {
"placeholders": {
"name": {
"type": "String",
"example": "Pictures",
"description": "the name of a specific directory"
}
}
},
"notEnoughSpaceDialogMessage": "See tegevus vajab „{volume}“ andmeruumis „{neededSize}“ vaba andmemahtu, kuid alles on vaid „{freeSize}“.",
"@notEnoughSpaceDialogMessage": {
"placeholders": {
"neededSize": {
"type": "String",
"example": "314 MB"
},
"freeSize": {
"type": "String",
"example": "123 MB"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"missingSystemFilePickerDialogMessage": "Süsteemi failihaldur/failivalija on puudu või kasutuselt eemaldatud. Palun pane ta tööle ja proovi siis uuesti.",
"@missingSystemFilePickerDialogMessage": {},
"nameConflictDialogSingleSourceMessage": "Mõnedel sihtkausta failidel on sama nimi.",
"@nameConflictDialogSingleSourceMessage": {},
"videoStartOverButtonLabel": "ALUSTA UUESTI",
"@videoStartOverButtonLabel": {},
"videoResumeButtonLabel": "JÄTKA",
"@videoResumeButtonLabel": {},
"setCoverDialogLatest": "Viimane objekt",
"@setCoverDialogLatest": {},
"setCoverDialogAuto": "Automaatne",
"@setCoverDialogAuto": {},
"nameConflictDialogMultipleSourceMessage": "Mõnedel failidel on sama nimi.",
"@nameConflictDialogMultipleSourceMessage": {},
"addShortcutDialogLabel": "Kiirnupu silt",
"@addShortcutDialogLabel": {},
"addShortcutButtonLabel": "LISA",
"@addShortcutButtonLabel": {},
"noMatchingAppDialogMessage": "Pole rakendusi, mis oskaks seda kasutada.",
"@noMatchingAppDialogMessage": {},
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Kas viskame selle objekti prügikasti?} other{Kas viskame need {count} objekti prügikasti?}}",
"@binEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"moveUndatedConfirmationDialogMessage": "Kas enne jätkamist salvestame objekti kuupäevad?",
"@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Salvesta kuupäevad",
"@moveUndatedConfirmationDialogSetDate": {},
"videoResumeDialogMessage": "Kas sa soovid jätkata esitamist {time} kohalt?",
"@videoResumeDialogMessage": {
"placeholders": {
"time": {
"type": "String",
"example": "13:37"
}
}
},
"patternDialogEnter": "Sisesta viipemuster",
"@patternDialogEnter": {},
"patternDialogConfirm": "Korda viipemustrit",
"@patternDialogConfirm": {},
"pinDialogEnter": "Sisesta PIN-kood",
"@pinDialogEnter": {},
"pinDialogConfirm": "Korda PIN-koodi",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Sisesta salasõna",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Korda salasõna",
"@passwordDialogConfirm": {},
"renameAlbumDialogLabel": "Uus nimi",
"@renameAlbumDialogLabel": {},
"renameAlbumDialogLabelAlreadyExistsHelper": "Selline kaust on juba olemas",
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
"renameEntrySetPageTitle": "Muuda nime",
"@renameEntrySetPageTitle": {},
"renameEntrySetPagePatternFieldLabel": "Failide nimemuster",
"@renameEntrySetPagePatternFieldLabel": {},
"renameEntrySetPageInsertTooltip": "Lisa väli",
"@renameEntrySetPageInsertTooltip": {},
"renameEntrySetPagePreviewSectionTitle": "Eelvaade",
"@renameEntrySetPagePreviewSectionTitle": {},
"renameProcessorCounter": "Loendur",
"@renameProcessorCounter": {},
"renameProcessorHash": "Räsi",
"@renameProcessorHash": {},
"renameProcessorName": "Nimi",
"@renameProcessorName": {},
"storageAccessDialogMessage": "Palun anna sellele rakendusele järgmises ekraanivaates õigused „{directory}“ kaustale „{volume}“ andmekogus.",
"@storageAccessDialogMessage": {
"placeholders": {
"directory": {
"type": "String",
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"restrictedAccessDialogMessage": "Sellel rakendusel pole õigusi muuta faile „{volume}“ andmekogu „{directory}“ kaustas.\n\nPalun kasuta failihaldurit või galeriirakendust failide tõstmiseks muude asukohta.",
"@restrictedAccessDialogMessage": {
"placeholders": {
"directory": {
"type": "String",
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"setCoverDialogCustom": "Sinu valik",
"@setCoverDialogCustom": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Kas kustutame selle objekti?} other{Kas kustutame need {count} objekti?}}",
"@deleteEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
}
} }

View file

@ -964,5 +964,31 @@
"drawerCollectionImages": "इमेजेस", "drawerCollectionImages": "इमेजेस",
"@drawerCollectionImages": {}, "@drawerCollectionImages": {},
"aboutDataUsageMisc": "विविध", "aboutDataUsageMisc": "विविध",
"@aboutDataUsageMisc": {} "@aboutDataUsageMisc": {},
"sortByName": "नाम से",
"@sortByName": {},
"sortByDate": "दिनांक से",
"@sortByDate": {},
"videoActionShowPreviousFrame": "पिछला फ्रेम दिखाए",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "अगला फ्रेम दिखाए",
"@videoActionShowNextFrame": {},
"newAlbumDialogAlbumAlreadyExistsHelper": "एल्बम पहले से उपलब्ध हैं",
"@newAlbumDialogAlbumAlreadyExistsHelper": {},
"drawerCountryPage": "देश",
"@drawerCountryPage": {},
"collectionPickPageTitle": "चुने",
"@collectionPickPageTitle": {},
"drawerCollectionPanoramas": "पैनोरामा",
"@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "रॉ फ़ोटो",
"@drawerCollectionRaws": {},
"drawerAlbumPage": "एल्बम",
"@drawerAlbumPage": {},
"drawerCollectionSphericalVideos": "360⁰ वीडियो",
"@drawerCollectionSphericalVideos": {},
"drawerPlacePage": "स्थान",
"@drawerPlacePage": {},
"drawerTagPage": "टैग्स",
"@drawerTagPage": {}
} }

View file

@ -1390,5 +1390,7 @@
"mapStyleOsmLiberty": "OSM Liberty", "mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {}, "@mapStyleOsmLiberty": {},
"mapAttributionOsmLiberty": "Tiles by [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Hosted by [OSM Americana](https://tile.ourmap.us)", "mapAttributionOsmLiberty": "Tiles by [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Hosted by [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {} "@mapAttributionOsmLiberty": {},
"newAlbumDialogAlbumAlreadyExistsHelper": "Альбом уже существует",
"@newAlbumDialogAlbumAlreadyExistsHelper": {}
} }

View file

@ -32,7 +32,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final appliedModifier = await _applyDateModifierToEntry(userModifier); final appliedModifier = await _applyDateModifierToEntry(userModifier);
if (appliedModifier == null) { if (appliedModifier == null) {
if (isValid && userModifier.action != DateEditAction.copyField) { if (isValid && userModifier.action != DateEditAction.copyField) {
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this');
} }
return {}; return {};
} }
@ -65,7 +65,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final shiftedDate = date.add(Duration(seconds: appliedModifier.shiftSeconds!)); final shiftedDate = date.add(Duration(seconds: appliedModifier.shiftSeconds!));
editCreateDateXmp(descriptions, shiftedDate); editCreateDateXmp(descriptions, shiftedDate);
} else { } else {
reportService.recordError('failed to parse XMP date=$xmpDate', null); reportService.recordError('failed to parse XMP date=$xmpDate');
} }
} }
case DateEditAction.remove: case DateEditAction.remove:

View file

@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/media/video/channel_layouts.dart'; import 'package:aves/model/media/video/channel_layouts.dart';
import 'package:aves/model/media/video/codecs.dart'; import 'package:aves/model/media/video/codecs.dart';
import 'package:aves/model/media/video/profiles/aac.dart'; import 'package:aves/model/media/video/profiles/aac.dart';
import 'package:aves/model/media/video/profiles/h264.dart'; import 'package:aves/model/media/video/profiles/h264.dart';
import 'package:aves/model/media/video/profiles/hevc.dart'; import 'package:aves/model/media/video/profiles/hevc.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/ref/languages.dart'; import 'package:aves/ref/languages.dart';
import 'package:aves/ref/locales.dart'; import 'package:aves/ref/locales.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
@ -99,7 +99,7 @@ class VideoMetadataFormatter {
if (isDefined(dateString)) { if (isDefined(dateString)) {
dateMillis = parseVideoDate(dateString); dateMillis = parseVideoDate(dateString);
if (dateMillis == null && !isAmbiguousDate(dateString)) { if (dateMillis == null && !isAmbiguousDate(dateString)) {
await reportService.recordError('getCatalogMetadata failed to parse date=$dateString for mimeType=${entry.mimeType} entry=$entry', null); await reportService.recordError('getCatalogMetadata failed to parse date=$dateString for mimeType=${entry.mimeType} entry=$entry');
} }
} }

View file

@ -87,9 +87,9 @@ mixin AppSettings on SettingsAccess {
set forceWesternArabicNumerals(bool newValue) => set(SettingKeys.forceWesternArabicNumeralsKey, newValue); set forceWesternArabicNumerals(bool newValue) => set(SettingKeys.forceWesternArabicNumeralsKey, newValue);
int get catalogTimeZoneRawOffsetMillis => getInt(SettingKeys.catalogTimeZoneRawOffsetMillisKey) ?? 0; int get catalogTimeZoneOffsetMillis => getInt(SettingKeys.catalogTimeZoneOffsetMillisKey) ?? 0;
set catalogTimeZoneRawOffsetMillis(int newValue) => set(SettingKeys.catalogTimeZoneRawOffsetMillisKey, newValue); set catalogTimeZoneOffsetMillis(int newValue) => set(SettingKeys.catalogTimeZoneOffsetMillisKey, newValue);
double getTileExtent(String routeName) => getDouble(SettingKeys.tileExtentPrefixKey + routeName) ?? 0; double getTileExtent(String routeName) => getDouble(SettingKeys.tileExtentPrefixKey + routeName) ?? 0;

View file

@ -51,7 +51,7 @@ mixin FilterGridsSettings on SettingsAccess {
set pinnedFilters(Set<CollectionFilter> newValue) => set(SettingKeys.pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); set pinnedFilters(Set<CollectionFilter> newValue) => set(SettingKeys.pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
bool get showAlbumPickQuery => getBool(SettingKeys.showAlbumPickQueryKey) ?? false; bool getShowTitleQuery(String routeName) => getBool(SettingKeys.showTitleQueryPrefixKey + routeName) ?? false;
set showAlbumPickQuery(bool newValue) => set(SettingKeys.showAlbumPickQueryKey, newValue); void setShowTitleQuery(String routeName, bool newValue) => set(SettingKeys.showTitleQueryPrefixKey + routeName, newValue);
} }

View file

@ -360,6 +360,12 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
} else { } else {
debugPrint('failed to import key=$key, value=$newValue is not a string'); debugPrint('failed to import key=$key, value=$newValue is not a string');
} }
} else if (key.startsWith(SettingKeys.showTitleQueryPrefixKey)) {
if (newValue is bool) {
store.setBool(key, newValue);
} else {
debugPrint('failed to import key=$key, value=$newValue is not a bool');
}
} else { } else {
switch (key) { switch (key) {
case SettingKeys.subtitleTextColorKey: case SettingKeys.subtitleTextColorKey:
@ -404,7 +410,6 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
case SettingKeys.stateSortReverseKey: case SettingKeys.stateSortReverseKey:
case SettingKeys.placeSortReverseKey: case SettingKeys.placeSortReverseKey:
case SettingKeys.tagSortReverseKey: case SettingKeys.tagSortReverseKey:
case SettingKeys.showAlbumPickQueryKey:
case SettingKeys.showOverlayOnOpeningKey: case SettingKeys.showOverlayOnOpeningKey:
case SettingKeys.showOverlayMinimapKey: case SettingKeys.showOverlayMinimapKey:
case SettingKeys.showOverlayInfoKey: case SettingKeys.showOverlayInfoKey:

View file

@ -59,15 +59,13 @@ class MediaStoreSource extends CollectionSource {
await vaults.init(); await vaults.init();
await favourites.init(); await favourites.init();
await covers.init(); await covers.init();
final currentTimeZoneOffset = await deviceService.getDefaultTimeZoneRawOffsetMillis(); final currentTimeZoneOffset = DateTime.now().timeZoneOffset.inMilliseconds;
if (currentTimeZoneOffset != null) { final catalogTimeZoneOffset = settings.catalogTimeZoneOffsetMillis;
final catalogTimeZoneOffset = settings.catalogTimeZoneRawOffsetMillis;
if (currentTimeZoneOffset != catalogTimeZoneOffset) { if (currentTimeZoneOffset != catalogTimeZoneOffset) {
unawaited(reportService.recordError('Time zone offset change: $currentTimeZoneOffset -> $catalogTimeZoneOffset. Clear catalog metadata to get correct date/times.', null)); unawaited(reportService.recordError('Time zone offset change: $currentTimeZoneOffset -> $catalogTimeZoneOffset. Clear catalog metadata to get correct date/times.'));
await localMediaDb.clearDates(); await localMediaDb.clearDates();
await localMediaDb.clearCatalogMetadata(); await localMediaDb.clearCatalogMetadata();
settings.catalogTimeZoneRawOffsetMillis = currentTimeZoneOffset; settings.catalogTimeZoneOffsetMillis = currentTimeZoneOffset;
}
} }
await loadDates(); await loadDates();
debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms'); debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
@ -214,7 +212,7 @@ class MediaStoreSource extends CollectionSource {
// TODO TLAD find duplication cause // TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries); final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) { if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}'), StackTrace.current)); unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup // post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet()); await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) { for (final duplicate in duplicates) {
@ -327,7 +325,7 @@ class MediaStoreSource extends CollectionSource {
// TODO TLAD find duplication cause // TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries); final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) { if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}'), StackTrace.current)); unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup // post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet()); await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) { for (final duplicate in duplicates) {

View file

@ -76,7 +76,7 @@ mixin TrashMixin on SourceBase {
sourceEntry.trashDetails = _buildTrashDetails(id); sourceEntry.trashDetails = _buildTrashDetails(id);
newEntries.add(sourceEntry); newEntries.add(sourceEntry);
} else { } else {
await reportService.recordError('Failed to recover untracked bin item at uri=$uri', null); await reportService.recordError('Failed to recover untracked bin item at uri=$uri');
} }
} }
}); });

View file

@ -175,7 +175,7 @@ class Vaults extends ChangeNotifier {
sourceEntry.origin = EntryOrigins.vault; sourceEntry.origin = EntryOrigins.vault;
newEntries.add(sourceEntry); newEntries.add(sourceEntry);
} else { } else {
await reportService.recordError('Failed to recover untracked vault item at uri=$uri', null); await reportService.recordError('Failed to recover untracked vault item at uri=$uri');
} }
}); });
} }

View file

@ -8,10 +8,10 @@ abstract class DeviceService {
Future<Map<String, dynamic>> getCapabilities(); Future<Map<String, dynamic>> getCapabilities();
Future<int?> getDefaultTimeZoneRawOffsetMillis();
Future<List<Locale>> getLocales(); Future<List<Locale>> getLocales();
Future<void> setLocaleConfig(List<Locale> locales);
Future<int> getPerformanceClass(); Future<int> getPerformanceClass();
Future<bool> isLocked(); Future<bool> isLocked();
@ -50,16 +50,6 @@ class PlatformDeviceService implements DeviceService {
return {}; return {};
} }
@override
Future<int?> getDefaultTimeZoneRawOffsetMillis() async {
try {
return await _platform.invokeMethod('getDefaultTimeZoneRawOffsetMillis');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
@override @override
Future<List<Locale>> getLocales() async { Future<List<Locale>> getLocales() async {
try { try {
@ -80,6 +70,17 @@ class PlatformDeviceService implements DeviceService {
return []; return [];
} }
@override
Future<void> setLocaleConfig(List<Locale> locales) async {
try {
await _platform.invokeMethod('setLocaleConfig', <String, dynamic>{
'locales': locales.map((v) => v.toLanguageTag()).toList(),
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
@override @override
Future<int> getPerformanceClass() async { Future<int> getPerformanceClass() async {
try { try {

View file

@ -6,10 +6,8 @@ import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:aves_report/aves_report.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
abstract class MetadataEditService { abstract class MetadataEditService {
Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise}); Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise});
@ -135,24 +133,22 @@ class PlatformMetadataEditService implements MetadataEditService {
} }
} }
StackTrace? _currentStack() => ReportService.buildReportStack(Trace.current(), level: 1);
// distinct exceptions to convince Crashlytics to split reports into distinct issues // distinct exceptions to convince Crashlytics to split reports into distinct issues
// The distinct debug statement is there to make the body unique, so that the methods are not merged at compile time. // The distinct debug statement is there to make the body unique, so that the methods are not merged at compile time.
Future<void> mp4LargeMoov(CustomPlatformException e) { Future<void> mp4LargeMoov(CustomPlatformException e) {
debugPrint('mp4LargeMoov $e'); debugPrint('mp4LargeMoov $e');
return reportService.recordError(e, _currentStack()); return reportService.recordError(e);
} }
Future<void> mp4LargeOther(CustomPlatformException e) { Future<void> mp4LargeOther(CustomPlatformException e) {
debugPrint('mp4LargeOther $e'); debugPrint('mp4LargeOther $e');
return reportService.recordError(e, _currentStack()); return reportService.recordError(e);
} }
Future<void> fileNotFound(CustomPlatformException e) { Future<void> fileNotFound(CustomPlatformException e) {
debugPrint('fileNotFound $e'); debugPrint('fileNotFound $e');
return reportService.recordError(e, _currentStack()); return reportService.recordError(e);
} }
} }

View file

@ -423,13 +423,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
} }
} }
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
debugPrint('App memory pressure');
imageCache.clear();
}
@override @override
void didChangeMetrics() => _updateCutoutInsets(); void didChangeMetrics() => _updateCutoutInsets();
@ -502,6 +495,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
_monitorSettings(); _monitorSettings();
videoControllerFactory.init(); videoControllerFactory.init();
unawaited(deviceService.setLocaleConfig(AvesApp.supportedLocales));
unawaited(storageService.deleteTempDirectory()); unawaited(storageService.deleteTempDirectory());
unawaited(_setupErrorReporting()); unawaited(_setupErrorReporting());
@ -692,7 +686,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
_mediaStoreSource.updateDerivedFilters(); _mediaStoreSource.updateDerivedFilters();
} }
void _onError(String? error) => reportService.recordError(error, null); void _onError(String? error) => reportService.recordError(error);
void _onAppModeChanged() { void _onAppModeChanged() {
final appMode = _appModeNotifier.value; final appMode = _appModeNotifier.value;

View file

@ -399,7 +399,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
(action) => _buildButtonIcon(context, action, enabled: canApply(action), selection: selection), (action) => _buildButtonIcon(context, action, enabled: canApply(action), selection: selection),
); );
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return [ return [
...quickActionButtons, ...quickActionButtons,
PopupMenuButton<EntrySetAction>( PopupMenuButton<EntrySetAction>(

View file

@ -88,7 +88,7 @@ class _CollectionGridState extends State<CollectionGrid> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final spacing = context.select<Settings, double>((s) => s.getTileLayout(settingsRouteKey) == TileLayout.mosaic ? CollectionGrid.mosaicLayoutSpacing : CollectionGrid.fixedExtentLayoutSpacing); final spacing = context.select<Settings, double>((v) => v.getTileLayout(settingsRouteKey) == TileLayout.mosaic ? CollectionGrid.mosaicLayoutSpacing : CollectionGrid.fixedExtentLayoutSpacing);
if (_tileExtentController?.spacing != spacing) { if (_tileExtentController?.spacing != spacing) {
_tileExtentController = TileExtentController( _tileExtentController = TileExtentController(
settingsRouteKey: settingsRouteKey, settingsRouteKey: settingsRouteKey,
@ -136,7 +136,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectable = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canSelectMedia); final selectable = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canSelectMedia);
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey; final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey)); final tileLayout = context.select<Settings, TileLayout>((v) => v.getTileLayout(settingsRouteKey));
return Consumer<CollectionLens>( return Consumer<CollectionLens>(
builder: (context, collection, child) { builder: (context, collection, child) {
final sectionedListLayoutProvider = ValueListenableBuilder<double>( final sectionedListLayoutProvider = ValueListenableBuilder<double>(

View file

@ -91,6 +91,7 @@ class _CollectionPageState extends State<CollectionPage> {
selector: (context, selection) => selection.selectedItems.isNotEmpty, selector: (context, selection) => selection.selectedItems.isNotEmpty,
builder: (context, hasSelection, child) { builder: (context, hasSelection, child) {
final body = QueryProvider( final body = QueryProvider(
startEnabled: settings.getShowTitleQuery(context.currentRouteName!),
initialQuery: liveFilter?.query, initialQuery: liveFilter?.query,
child: Builder( child: Builder(
builder: (context) { builder: (context) {

View file

@ -181,6 +181,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.searchCollection: case EntrySetAction.searchCollection:
_goToSearch(context); _goToSearch(context);
case EntrySetAction.toggleTitleSearch: case EntrySetAction.toggleTitleSearch:
final routeName = context.currentRouteName!;
settings.setShowTitleQuery(routeName, !settings.getShowTitleQuery(routeName));
context.read<Query>().toggle(); context.read<Query>().toggle();
case EntrySetAction.addShortcut: case EntrySetAction.addShortcut:
_addShortcut(context); _addShortcut(context);

View file

@ -14,7 +14,7 @@ class TvEdgeFocus extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final useTvLayout = context.select<Settings, bool>((s) => s.useTvLayout); final useTvLayout = context.select<Settings, bool>((v) => v.useTvLayout);
return useTvLayout return useTvLayout
? Focus( ? Focus(
focusNode: focusNode, focusNode: focusNode,

View file

@ -20,7 +20,7 @@ class DoubleBackPopHandler extends PopHandler {
@override @override
bool canPop(BuildContext context) { bool canPop(BuildContext context) {
if (context.select<Settings, bool>((s) => !s.mustBackTwiceToExit)) return true; if (context.select<Settings, bool>((v) => !v.mustBackTwiceToExit)) return true;
if (Navigator.canPop(context)) return true; if (Navigator.canPop(context)) return true;
return false; return false;
} }

View file

@ -20,7 +20,7 @@ class TvNavigationPopHandler implements PopHandler {
@override @override
bool canPop(BuildContext context) { bool canPop(BuildContext context) {
if (context.select<Settings, bool>((s) => !s.useTvLayout)) return true; if (context.select<Settings, bool>((v) => !v.useTvLayout)) return true;
if (_isHome(context)) return true; if (_isHome(context)) return true;
return false; return false;
} }

View file

@ -260,7 +260,7 @@ class _AvesFloatingBarState extends State<AvesFloatingBar> with RouteAware {
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
valueListenable: _isBlurAllowedNotifier, valueListenable: _isBlurAllowedNotifier,
builder: (context, isBlurAllowed, child) { builder: (context, isBlurAllowed, child) {
final blurred = isBlurAllowed && context.select<Settings, bool>((s) => s.enableBlurEffect); final blurred = isBlurAllowed && context.select<Settings, bool>((v) => v.enableBlurEffect);
return Container( return Container(
foregroundDecoration: BoxDecoration( foregroundDecoration: BoxDecoration(
border: Border.all( border: Border.all(

View file

@ -3,13 +3,13 @@ import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class QueryProvider extends StatelessWidget { class QueryProvider extends StatelessWidget {
final bool enabled; final bool startEnabled;
final String? initialQuery; final String? initialQuery;
final Widget child; final Widget child;
const QueryProvider({ const QueryProvider({
super.key, super.key,
this.enabled = false, this.startEnabled = false,
this.initialQuery, this.initialQuery,
required this.child, required this.child,
}); });
@ -18,7 +18,7 @@ class QueryProvider extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<Query>( return ChangeNotifierProvider<Query>(
create: (context) => Query( create: (context) => Query(
enabled: enabled, enabled: startEnabled,
initialValue: initialQuery, initialValue: initialQuery,
), ),
child: child, child: child,

View file

@ -37,7 +37,7 @@ class AppDebugPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return Directionality( return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: AvesScaffold( child: AvesScaffold(

View file

@ -55,7 +55,7 @@ class _DebugSettingsSectionState extends State<DebugSettingsSection> with Automa
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup( child: InfoRowGroup(
info: { info: {
'catalogTimeZoneRawOffsetMillis': '${settings.catalogTimeZoneRawOffsetMillis}', 'catalogTimeZoneRawOffsetMillis': '${settings.catalogTimeZoneOffsetMillis}',
'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}', 'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}',
'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}', 'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}',
'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}', 'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}',

View file

@ -71,7 +71,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
final l10n = context.l10n; final l10n = context.l10n;
final textScaler = MediaQuery.textScalerOf(context); final textScaler = MediaQuery.textScalerOf(context);
final effectiveThumbnailExtent = max(thumbnailExtent, textScaler.scale(thumbnailExtent)); final effectiveThumbnailExtent = max(thumbnailExtent, textScaler.scale(thumbnailExtent));
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return AvesScaffold( return AvesScaffold(
appBar: AppBar( appBar: AppBar(
title: Text(l10n.renameEntrySetPageTitle), title: Text(l10n.renameEntrySetPageTitle),

View file

@ -152,7 +152,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
builder: (context, value, child) { builder: (context, value, child) {
return IconButton( return IconButton(
icon: const Icon(AIcons.add), icon: const Icon(AIcons.add),
onPressed: value.text.isEmpty ? null : () => _addCustomTag(_newTagTextController.text), onPressed: value.text.trim().isEmpty ? null : () => _addCustomTag(_newTagTextController.text),
tooltip: l10n.tagEditorPageAddTagTooltip, tooltip: l10n.tagEditorPageAddTagTooltip,
); );
}, },
@ -296,6 +296,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
} }
void _addCustomTag(String newTag) { void _addCustomTag(String newTag) {
newTag = newTag.trim();
if (newTag.isNotEmpty) { if (newTag.isNotEmpty) {
_addTag(TagFilter(newTag)); _addTag(TagFilter(newTag));
} }

View file

@ -114,13 +114,13 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
final gridItems = AlbumListPage.getAlbumGridItems(context, source); final gridItems = AlbumListPage.getAlbumGridItems(context, source);
return SelectionProvider<FilterGridItem<AlbumFilter>>( return SelectionProvider<FilterGridItem<AlbumFilter>>(
child: QueryProvider( child: QueryProvider(
enabled: settings.showAlbumPickQuery, startEnabled: settings.getShowTitleQuery(context.currentRouteName!),
child: FilterGridPage<AlbumFilter>( child: FilterGridPage<AlbumFilter>(
settingsRouteKey: AlbumListPage.routeName, settingsRouteKey: AlbumListPage.routeName,
appBar: FilterGridAppBar( appBar: FilterGridAppBar(
source: source, source: source,
title: title, title: title,
actionDelegate: _AlbumChipSetPickActionDelegate(gridItems), actionDelegate: AlbumChipSetActionDelegate(gridItems),
actionsBuilder: _buildActions, actionsBuilder: _buildActions,
isEmpty: false, isEmpty: false,
appBarHeightNotifier: _appBarHeightNotifier, appBarHeightNotifier: _appBarHeightNotifier,
@ -211,7 +211,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
required bool Function(ChipSetAction action) isVisible, required bool Function(ChipSetAction action) isVisible,
required void Function(ChipSetAction action) onActionSelected, required void Function(ChipSetAction action) onActionSelected,
}) { }) {
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return [ return [
if (widget.moveType != null) if (widget.moveType != null)
..._quickActions.where(isVisible).map( ..._quickActions.where(isVisible).map(
@ -287,15 +287,3 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
Navigator.maybeOf(context)?.pop<AlbumFilter>(filter); Navigator.maybeOf(context)?.pop<AlbumFilter>(filter);
} }
} }
class _AlbumChipSetPickActionDelegate extends AlbumChipSetActionDelegate {
_AlbumChipSetPickActionDelegate(super.items);
@override
void onActionSelected(BuildContext context, ChipSetAction action) {
if (action == ChipSetAction.toggleTitleSearch) {
settings.showAlbumPickQuery = !settings.showAlbumPickQuery;
}
super.onActionSelected(context, action);
}
}

View file

@ -1,11 +1,13 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -49,6 +51,7 @@ class _ItemPickPageState extends State<ItemPickPage> {
child: AvesScaffold( child: AvesScaffold(
body: SelectionProvider<AvesEntry>( body: SelectionProvider<AvesEntry>(
child: QueryProvider( child: QueryProvider(
startEnabled: settings.getShowTitleQuery(context.currentRouteName!),
initialQuery: liveFilter?.query, initialQuery: liveFilter?.query,
child: GestureAreaProtectorStack( child: GestureAreaProtectorStack(
child: SafeArea( child: SafeArea(

View file

@ -104,7 +104,7 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
} }
List<Widget> _buildActions(BuildContext context, double maxWidth) { List<Widget> _buildActions(BuildContext context, double maxWidth) {
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return [ return [
IconButton( IconButton(
icon: const Icon(AIcons.search), icon: const Icon(AIcons.search),

View file

@ -179,6 +179,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.search: case ChipSetAction.search:
_goToSearch(context); _goToSearch(context);
case ChipSetAction.toggleTitleSearch: case ChipSetAction.toggleTitleSearch:
final routeName = context.currentRouteName!;
settings.setShowTitleQuery(routeName, !settings.getShowTitleQuery(routeName));
context.read<Query>().toggle(); context.read<Query>().toggle();
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
case ChipSetAction.createVault: case ChipSetAction.createVault:

View file

@ -329,7 +329,7 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
(action) => _buildButtonIcon(context, actionDelegate, action, enabled: canApply(action)), (action) => _buildButtonIcon(context, actionDelegate, action, enabled: canApply(action)),
); );
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return [ return [
...quickActionButtons, ...quickActionButtons,
PopupMenuButton<ChipSetAction>( PopupMenuButton<ChipSetAction>(

View file

@ -286,7 +286,7 @@ class _FilterGridContentState<T extends CollectionFilter> extends State<_FilterG
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey; final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey)); final tileLayout = context.select<Settings, TileLayout>((v) => v.getTileLayout(settingsRouteKey));
return Selector<Query, bool>( return Selector<Query, bool>(
selector: (context, query) => query.enabled, selector: (context, query) => query.enabled,
builder: (context, queryEnabled, child) { builder: (context, queryEnabled, child) {

View file

@ -1,6 +1,8 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart';
@ -110,6 +112,7 @@ class _FilterNavigationPageState<T extends CollectionFilter, CSAD extends ChipSe
return SelectionProvider<FilterGridItem<T>>( return SelectionProvider<FilterGridItem<T>>(
child: Builder( child: Builder(
builder: (context) => QueryProvider( builder: (context) => QueryProvider(
startEnabled: settings.getShowTitleQuery(context.currentRouteName!),
child: FilterGridPage<T>( child: FilterGridPage<T>(
appBar: FilterGridAppBar<T, CSAD>( appBar: FilterGridAppBar<T, CSAD>(
source: widget.source, source: widget.source,

View file

@ -66,7 +66,7 @@ class _AppBottomNavBarState extends State<AppBottomNavBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final showVideo = context.select<Settings, bool>((s) => !s.hiddenFilters.contains(MimeFilter.video)); final showVideo = context.select<Settings, bool>((v) => !v.hiddenFilters.contains(MimeFilter.video));
final items = [ final items = [
const AvesBottomNavItem(route: CollectionPage.routeName), const AvesBottomNavItem(route: CollectionPage.routeName),
@ -91,6 +91,7 @@ class _AppBottomNavBarState extends State<AppBottomNavBar> {
.toList(), .toList(),
onTap: (index) => _goTo(context, items, index), onTap: (index) => _goTo(context, items, index),
currentIndex: _getCurrentIndex(context, items), currentIndex: _getCurrentIndex(context, items),
elevation: 0,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
showSelectedLabels: false, showSelectedLabels: false,

View file

@ -59,7 +59,7 @@ class _FilePickerPageState extends State<FilePickerPage> {
return !isHidden; return !isHidden;
} }
}).toList(); }).toList();
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return PopScope( return PopScope(
canPop: _directory.relativeDir.isEmpty, canPop: _directory.relativeDir.isEmpty,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {

View file

@ -46,7 +46,7 @@ class _SettingsMobilePageState extends State<SettingsMobilePage> with FeedbackMi
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return AvesScaffold( return AvesScaffold(
appBar: AppBar( appBar: AppBar(
title: InteractiveAppBarTitle( title: InteractiveAppBarTitle(

View file

@ -143,7 +143,7 @@ class SettingsTileThumbnailLocationIcon extends SettingsTile {
); );
Widget _buildTrailing(BuildContext context) { Widget _buildTrailing(BuildContext context) {
final iconType = context.select<Settings, ThumbnailOverlayLocationIcon>((s) => s.thumbnailLocationIcon); final iconType = context.select<Settings, ThumbnailOverlayLocationIcon>((v) => v.thumbnailLocationIcon);
return ThumbnailOverlayPage.buildTrailingIcon( return ThumbnailOverlayPage.buildTrailingIcon(
context: context, context: context,
key: iconType, key: iconType,
@ -168,7 +168,7 @@ class SettingsTileThumbnailTagIcon extends SettingsTile {
); );
Widget _buildTrailing(BuildContext context) { Widget _buildTrailing(BuildContext context) {
final iconType = context.select<Settings, ThumbnailOverlayTagIcon>((s) => s.thumbnailTagIcon); final iconType = context.select<Settings, ThumbnailOverlayTagIcon>((v) => v.thumbnailTagIcon);
return ThumbnailOverlayPage.buildTrailingIcon( return ThumbnailOverlayPage.buildTrailingIcon(
context: context, context: context,
key: iconType, key: iconType,

View file

@ -61,8 +61,8 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin {
} else { } else {
showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback);
} }
} catch (error, stack) { } catch (e, stack) {
await reportService.recordError(error, stack); await reportService.recordError(e, stack);
} }
source?.resumeMonitoring(); source?.resumeMonitoring();
} }

View file

@ -96,8 +96,8 @@ mixin CastMixin {
); );
debugPrint('cast: play entry=$entry'); debugPrint('cast: play entry=$entry');
unawaited(renderer.play()); unawaited(renderer.play());
} catch (error, stack) { } catch (e, stack) {
await reportService.recordError(error, stack); await reportService.recordError(e, stack);
} }
} }

View file

@ -50,7 +50,7 @@ class InfoAppBar extends StatelessWidget {
final commonActions = EntryActions.commonMetadataActions.where(isVisible); final commonActions = EntryActions.commonMetadataActions.where(isVisible);
final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where(isVisible); final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where(isVisible);
final useTvLayout = settings.useTvLayout; final useTvLayout = settings.useTvLayout;
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return SliverAppBar( return SliverAppBar(
leading: useTvLayout leading: useTvLayout
? null ? null

View file

@ -252,7 +252,7 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
final exportActions = widget.exportActions; final exportActions = widget.exportActions;
final videoActions = widget.videoActions; final videoActions = widget.videoActions;
final hasOverflowMenu = pageEntry.canRotate || pageEntry.canFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty || videoActions.isNotEmpty; final hasOverflowMenu = pageEntry.canRotate || pageEntry.canFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty || videoActions.isNotEmpty;
final animations = context.select<Settings, AccessibilityAnimations>((s) => s.accessibilityAnimations); final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
return Selector<VideoConductor, AvesVideoController?>( return Selector<VideoConductor, AvesVideoController?>(
selector: (context, vc) => vc.getController(pageEntry), selector: (context, vc) => vc.getController(pageEntry),
builder: (context, videoController, child) { builder: (context, videoController, child) {

View file

@ -3,7 +3,7 @@ class SettingKeys {
static const Set<String> _internalKeys = { static const Set<String> _internalKeys = {
hasAcceptedTermsKey, hasAcceptedTermsKey,
catalogTimeZoneRawOffsetMillisKey, catalogTimeZoneOffsetMillisKey,
searchHistoryKey, searchHistoryKey,
platformAccelerometerRotationKey, platformAccelerometerRotationKey,
platformTransitionAnimationScaleKey, platformTransitionAnimationScaleKey,
@ -21,7 +21,7 @@ class SettingKeys {
static const isErrorReportingAllowedKey = 'is_crashlytics_enabled'; static const isErrorReportingAllowedKey = 'is_crashlytics_enabled';
static const localeKey = 'locale'; static const localeKey = 'locale';
static const forceWesternArabicNumeralsKey = 'force_western_arabic_numerals'; static const forceWesternArabicNumeralsKey = 'force_western_arabic_numerals';
static const catalogTimeZoneRawOffsetMillisKey = 'catalog_time_zone_raw_offset_millis'; static const catalogTimeZoneOffsetMillisKey = 'catalog_time_zone_raw_offset_millis';
static const tileExtentPrefixKey = 'tile_extent_'; static const tileExtentPrefixKey = 'tile_extent_';
static const tileLayoutPrefixKey = 'tile_layout_'; static const tileLayoutPrefixKey = 'tile_layout_';
static const entryRenamingPatternKey = 'entry_renaming_pattern'; static const entryRenamingPatternKey = 'entry_renaming_pattern';
@ -86,7 +86,7 @@ class SettingKeys {
static const pinnedFiltersKey = 'pinned_filters'; static const pinnedFiltersKey = 'pinned_filters';
static const hiddenFiltersKey = 'hidden_filters'; static const hiddenFiltersKey = 'hidden_filters';
static const deactivatedHiddenFiltersKey = 'deactivated_hidden_filters'; static const deactivatedHiddenFiltersKey = 'deactivated_hidden_filters';
static const showAlbumPickQueryKey = 'show_album_pick_query'; static const showTitleQueryPrefixKey = 'show_title_query_';
// viewer // viewer
static const viewerQuickActionsKey = 'viewer_quick_actions'; static const viewerQuickActionsKey = 'viewer_quick_actions';

View file

@ -15,7 +15,7 @@ abstract class ReportService {
Future<void> setCustomKeys(Map<String, Object> map); Future<void> setCustomKeys(Map<String, Object> map);
Future<void> recordError(dynamic exception, StackTrace? stack); Future<void> recordError(dynamic exception, [StackTrace? stack]);
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails); Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails);

View file

@ -9,7 +9,7 @@ class PlatformReportService extends ReportService {
Future<void> log(String message) async => debugPrint('Report log with message=$message'); Future<void> log(String message) async => debugPrint('Report log with message=$message');
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) async => debugPrint('Report error with exception=$exception, stack=$stack'); Future<void> recordError(dynamic exception, [StackTrace? stack]) async => debugPrint('Report error with exception=$exception, stack=$stack');
@override @override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async => debugPrint('Report Flutter error with details=$flutterErrorDetails'); Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async => debugPrint('Report Flutter error with details=$flutterErrorDetails');

View file

@ -5,6 +5,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
class PlatformReportService extends ReportService { class PlatformReportService extends ReportService {
FirebaseCrashlytics? get _instance { FirebaseCrashlytics? get _instance {
@ -65,11 +66,12 @@ class PlatformReportService extends ReportService {
} }
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) async { Future<void> recordError(dynamic exception, [StackTrace? stack]) async {
if (exception is PlatformException && stack != null) { if (exception is PlatformException && stack != null) {
stack = ReportService.buildReportStack(stack, level: 2); stack = ReportService.buildReportStack(stack, level: 2);
} }
if (exception is! UnreportedStateError) { if (exception is! UnreportedStateError) {
stack ??= ReportService.buildReportStack(Trace.current(), level: 1);
return _instance?.recordError(exception, stack); return _instance?.recordError(exception, stack);
} }
} }

View file

@ -213,7 +213,7 @@ packages:
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
stack_trace: stack_trace:
dependency: transitive dependency: "direct main"
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"

View file

@ -14,6 +14,7 @@ dependencies:
# so that the transitive `path` gets upgraded to v1.8.3 # so that the transitive `path` gets upgraded to v1.8.3
firebase_core: ">=2.10.0" firebase_core: ">=2.10.0"
firebase_crashlytics: firebase_crashlytics:
stack_trace:
dev_dependencies: dev_dependencies:
flutter_lints: flutter_lints:

View file

@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US # - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
version: 1.11.18+137 version: 1.11.19+138
publish_to: none publish_to: none
environment: environment:

View file

@ -3,9 +3,6 @@ import 'package:flutter/foundation.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
class FakeDeviceService extends Fake implements DeviceService { class FakeDeviceService extends Fake implements DeviceService {
@override
Future<int> getDefaultTimeZoneRawOffsetMillis() => SynchronousFuture(3600000);
@override @override
Future<int> getAvailableHeapSize() => SynchronousFuture(0x7fffffff); Future<int> getAvailableHeapSize() => SynchronousFuture(0x7fffffff);

View file

@ -21,7 +21,7 @@ class FakeReportService extends ReportService {
Future<void> setCustomKeys(Map<String, Object> map) => SynchronousFuture(null); Future<void> setCustomKeys(Map<String, Object> map) => SynchronousFuture(null);
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) => SynchronousFuture(null); Future<void> recordError(dynamic exception, [StackTrace? stack]) => SynchronousFuture(null);
@override @override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) => SynchronousFuture(null); Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) => SynchronousFuture(null);

View file

@ -1,4 +1,4 @@
In v1.11.18: In v1.11.19:
- peruse your videos frame by frame - peruse your videos frame by frame
- create map shortcuts to filtered collections - create map shortcuts to filtered collections
- enjoy the app in Shavian - enjoy the app in Shavian