Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2022-06-18 16:53:41 +09:00
commit 8357d84c66
151 changed files with 3914 additions and 724 deletions

View file

@ -17,7 +17,7 @@ jobs:
# Available versions may lag behind https://github.com/flutter/flutter.git
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.0.1'
flutter-version: '3.0.2'
channel: 'stable'
- name: Clone the repository.

View file

@ -19,7 +19,7 @@ jobs:
# Available versions may lag behind https://github.com/flutter/flutter.git
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.0.1'
flutter-version: '3.0.2'
channel: 'stable'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
@ -56,15 +56,15 @@ jobs:
rm release.keystore.asc
mkdir outputs
(cd scripts/; ./apply_flavor_play.sh)
flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.1.sksl.json
flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json
cp build/app/outputs/bundle/playRelease/*.aab outputs
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.1.sksl.json
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json
cp build/app/outputs/apk/play/release/*.apk outputs
(cd scripts/; ./apply_flavor_huawei.sh)
flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.0.1.sksl.json
flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.0.2.sksl.json
cp build/app/outputs/apk/huawei/release/*.apk outputs
(cd scripts/; ./apply_flavor_izzy.sh)
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.0.1.sksl.json
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.0.2.sksl.json
cp build/app/outputs/apk/izzy/release/*.apk outputs
rm $AVES_STORE_FILE
env:

View file

@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.6.9"></a>[v1.6.9] - 2022-06-18
### Added
- slideshow
- set wallpaper from any media
- optional dynamic accent color on Android 12+
- Search: date/dimension/size field equality (undocumented)
- support Android 13 (API 33)
- Turkish translation (thanks metezd)
### Changed
- do not force quit on storage permission denial
- upgraded Flutter to stable v3.0.2
### Fixed
- merge ambiguously cased directories
## <a id="v1.6.8"></a>[v1.6.8] - 2022-05-27
### Fixed

View file

@ -12,6 +12,9 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
alt='Get it on Google Play'
height="80">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/huawei-appgallery-badge-english-black.png"
alt='Get it on Huawei AppGallery'
height="80">](https://appgallery.huawei.com/app/C106014023)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/amazon-appstore-badge-english-black.png"
alt='Get it on Amazon Appstore'
height="80">](https://www.amazon.com/dp/B09XQHQQ72)
@ -90,7 +93,7 @@ At this stage this project does *not* accept PRs, except for translations.
### Translations
If you want to translate this app in your language and share the result, [there is a guide](https://github.com/deckerst/aves/wiki/Contributing-to-Translations). English, Korean and French are already handled by me. Russian, German, Spanish, Portuguese, Indonesian, Japanese, Italian & Chinese are handled by generous volunteers.
If you want to translate this app in your language and share the result, [there is a guide](https://github.com/deckerst/aves/wiki/Contributing-to-Translations). English, Korean and French are already handled by me. Russian, German, Spanish, Portuguese, Indonesian, Japanese, Italian, Chinese & Turkish are handled by generous volunteers.
### Donations

View file

@ -41,7 +41,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 32
compileSdkVersion 33
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -57,7 +57,7 @@ android {
// which implementation `DocumentBuilderImpl` is provided by the OS and is not customizable on Android,
// but the implementation on API <19 is not robust enough and fails to build XMP documents
minSdkVersion 19
targetSdkVersion 32
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
@ -154,7 +154,7 @@ repositories {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'

View file

@ -6,16 +6,21 @@
Scoped storage on Android Q is inconvenient because users need to confirm edition on each individual file.
So we request `WRITE_EXTERNAL_STORAGE` until Q (29), and enable `requestLegacyExternalStorage`
-->
<!-- TODO TLAD [tiramisu] need notification permission? -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- TODO TLAD [tiramisu] READ_MEDIA_IMAGE, READ_MEDIA_VIDEO instead of READ_EXTERNAL_STORAGE? -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- to show foreground service progress via notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- to access media with original metadata with scoped storage (Android Q+) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
@ -128,6 +133,26 @@
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity
android:name=".WallpaperActivity"
android:exported="true"
android:label="@string/wallpaper"
android:theme="@style/NormalTheme">
<intent-filter>
<action android:name="android.intent.action.ATTACH_DATA" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SET_WALLPAPER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service
android:name=".AnalysisService"
android:description="@string/analysis_service_description"

View file

@ -159,7 +159,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
COMMAND_START -> {
runBlocking {
FlutterUtils.runOnUiThread {
val entryIds = data.get(KEY_ENTRY_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() }
val entryIds = data.getIntArray(KEY_ENTRY_IDS)?.toList()
backgroundChannel?.invokeMethod(
"start", hashMapOf(
"entryIds" to entryIds,

View file

@ -15,6 +15,7 @@ import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.*
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
@ -215,7 +216,7 @@ class MainActivity : FlutterActivity() {
}
}
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri))?.let { uri ->
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
return hashMapOf(
@ -332,6 +333,7 @@ class MainActivity : FlutterActivity() {
const val INTENT_ACTION_PICK = "pick"
const val INTENT_ACTION_SEARCH = "search"
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
const val INTENT_ACTION_VIEW = "view"
const val SHORTCUT_KEY_PAGE = "page"

View file

@ -0,0 +1,107 @@
package deckers.thibault.aves
import android.content.Intent
import android.net.Uri
import android.os.*
import android.util.Log
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterActivity() {
private lateinit var intentDataMap: MutableMap<String, Any?>
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(LOG_TAG, "onCreate intent=$intent")
intent.extras?.takeUnless { it.isEmpty }?.let {
Log.i(LOG_TAG, "onCreate intent extras=$it")
}
super.onCreate(savedInstanceState)
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
// - need Activity
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
// intent handling
// detail fetch: dart -> platform
intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
}
}
}
override fun onStart() {
Log.i(LOG_TAG, "onStart")
super.onStart()
// as of Flutter v3.0.1, the window `viewInsets` and `viewPadding`
// are incorrect on startup in some environments (e.g. API 29 emulator),
// so we manually request to apply the insets to update the window metrics
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
Handler(Looper.getMainLooper()).postDelayed({
window.decorView.requestApplyInsets()
}, 100)
}
}
override fun onStop() {
Log.i(LOG_TAG, "onStop")
super.onStop()
}
override fun onDestroy() {
Log.i(LOG_TAG, "onDestroy")
super.onDestroy()
}
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,
MainActivity.INTENT_DATA_KEY_URI to uri.toString(),
)
}
}
Intent.ACTION_RUN -> {
// flutter run
}
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
}
}
return HashMap()
}
companion object {
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>()
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
}
}

View file

@ -30,6 +30,8 @@ import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getApplicationInfoCompat
import deckers.thibault.aves.utils.queryIntentActivitiesCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -77,7 +79,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
val pm = context.packageManager
for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
for (resolveInfo in pm.queryIntentActivitiesCompat(intent, 0)) {
val appInfo = resolveInfo.activityInfo.applicationInfo
val packageName = appInfo.packageName
if (!packages.containsKey(packageName)) {
@ -149,7 +151,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val size = (sizeDip * density).roundToInt()
var data: ByteArray? = null
try {
val iconResourceId = context.packageManager.getApplicationInfo(packageName, 0).icon
val iconResourceId = context.packageManager.getApplicationInfoCompat(packageName, 0).icon
if (iconResourceId != Resources.ID_NULL) {
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
@ -444,4 +446,4 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
}
}
}
}

View file

@ -32,6 +32,8 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
"supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q),
)

View file

@ -1,12 +1,17 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.location.Address
import android.location.Geocoder
import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.IOException
import java.util.*
@ -48,36 +53,48 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
Geocoder(context)
}
val addresses = try {
geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
} catch (e: IOException) {
// `grpc failed`, etc.
result.error("getAddress-network", "failed to get address because of network issues", e.message)
return
} catch (e: Exception) {
result.error("getAddress-exception", "failed to get address", e.message)
return
fun processAddresses(addresses: List<Address>) {
if (addresses.isEmpty()) {
result.error("getAddress-empty", "failed to find any address for latitude=$latitude, longitude=$longitude", null)
} else {
val addressMapList: ArrayList<Map<String, String?>> = ArrayList(addresses.map { address ->
hashMapOf(
"addressLine" to (0..address.maxAddressLineIndex).joinToString(", ") { i -> address.getAddressLine(i) },
"adminArea" to address.adminArea,
"countryCode" to address.countryCode,
"countryName" to address.countryName,
"featureName" to address.featureName,
"locality" to address.locality,
"postalCode" to address.postalCode,
"subAdminArea" to address.subAdminArea,
"subLocality" to address.subLocality,
"subThoroughfare" to address.subThoroughfare,
"thoroughfare" to address.thoroughfare,
)
})
result.success(addressMapList)
}
}
if (addresses.isEmpty()) {
result.error("getAddress-empty", "failed to find any address for latitude=$latitude, longitude=$longitude", null)
} else {
val addressMapList: ArrayList<Map<String, String?>> = ArrayList(addresses.map { address ->
hashMapOf(
"addressLine" to (0..address.maxAddressLineIndex).joinToString(", ") { i -> address.getAddressLine(i) },
"adminArea" to address.adminArea,
"countryCode" to address.countryCode,
"countryName" to address.countryName,
"featureName" to address.featureName,
"locality" to address.locality,
"postalCode" to address.postalCode,
"subAdminArea" to address.subAdminArea,
"subLocality" to address.subLocality,
"subThoroughfare" to address.subThoroughfare,
"thoroughfare" to address.thoroughfare,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder!!.getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener {
override fun onGeocode(addresses: List<Address?>) = processAddresses(addresses.filterNotNull())
override fun onError(errorMessage: String?) {
result.error("getAddress-asyncerror", "failed to get address", errorMessage)
}
})
result.success(addressMapList)
} else {
try {
@Suppress("deprecation")
val addresses = geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
processAddresses(addresses)
} catch (e: IOException) {
// `grpc failed`, etc.
result.error("getAddress-network", "failed to get address because of network issues", e.message)
} catch (e: Exception) {
result.error("getAddress-exception", "failed to get address", e.message)
}
}
}

View file

@ -0,0 +1,58 @@
package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.app.WallpaperManager
import android.app.WallpaperManager.FLAG_LOCK
import android.app.WallpaperManager.FLAG_SYSTEM
import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class WallpaperHandler(private val activity: Activity) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"setWallpaper" -> ioScope.launch { safe(call, result, ::setWallpaper) }
else -> result.notImplemented()
}
}
private fun setWallpaper(call: MethodCall, result: MethodChannel.Result) {
val bytes = call.argument<ByteArray>("bytes")
val home = call.argument<Boolean>("home")
val lock = call.argument<Boolean>("lock")
if (bytes == null || home == null || lock == null) {
result.error("setWallpaper-args", "failed because of missing arguments", null)
return
}
val manager = WallpaperManager.getInstance(activity)
val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported
val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed
if (!supported || !allowed) {
result.error("setWallpaper-unsupported", "failed because setting wallpaper is not allowed", null)
return
}
bytes.inputStream().use { input ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val flags = (if (home) FLAG_SYSTEM else 0) or (if (lock) FLAG_LOCK else 0)
manager.setStream(input, null, true, flags)
} else {
manager.setStream(input)
}
}
result.success(true)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/wallpaper"
}
}

View file

@ -220,7 +220,7 @@ object ExifInterfaceHelper {
// initialize metadata-extractor directories that we will fill
// by tags converted from the ExifInterface attributes
// so that we can rely on metadata-extractor descriptions
val dirs = DirType.values().associate { Pair(it, it.createDirectory()) }
val dirs = DirType.values().associateWith { it.createDirectory() }
// exclude Exif directory when it only includes image size
val isUselessExif = fun(it: Map<String, String>): Boolean {

View file

@ -0,0 +1,37 @@
package deckers.thibault.aves.utils
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.Build
import android.os.Parcelable
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableExtra(name, T::class.java)
} else {
@Suppress("deprecation")
getParcelableExtra<Parcelable>(name) as? T
}
}
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(flags.toLong()))
} else {
@Suppress("deprecation")
getApplicationInfo(packageName, flags)
}
}
fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(flags.toLong()))
} else {
@Suppress("deprecation")
queryIntentActivities(intent, flags)
}
}

View file

@ -0,0 +1,32 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<group
android:scaleX=".44"
android:scaleY=".44"
android:translateX="28"
android:translateY="30">
<path
android:pathData="M3.925,16.034 L60.825,72.933a2.421,2.421 0.001,0 0,3.423 0l10.604,-10.603a6.789,6.789 90.001,0 0,0 -9.601L34.066,11.942A8.264,8.264 22.5,0 0,28.222 9.522H6.623A3.815,3.815 112.5,0 0,3.925 16.034Z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:pathData="m36.36,65.907v28.743a2.557,2.557 22.5,0 0,4.364 1.808L53.817,83.364a6.172,6.172 90,0 0,0 -8.729L42.532,63.35a3.616,3.616 157.5,0 0,-6.172 2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:pathData="M79.653,40.078V11.335A2.557,2.557 22.5,0 0,75.289 9.527L62.195,22.62a6.172,6.172 90,0 0,0 8.729l11.285,11.285a3.616,3.616 157.5,0 0,6.172 -2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:pathData="M96.613,16.867 L89.085,9.339a1.917,1.917 157.5,0 0,-3.273 1.356v6.172a4.629,4.629 45,0 0,4.629 4.629h4.255a2.712,2.712 112.5,0 0,1.917 -4.629z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
</group>
</vector>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_mono" />
</adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_mono" />
</adaptive-icon>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Hintergrundbild</string>
<string name="search_shortcut_short_label">Suche</string>
<string name="videos_shortcut_short_label">Videos</string>
<string name="analysis_channel_name">Analyse von Medien</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Fondo de pantalla</string>
<string name="search_shortcut_short_label">Búsqueda</string>
<string name="videos_shortcut_short_label">Videos</string>
<string name="analysis_channel_name">Explorar medios</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Fond décran</string>
<string name="search_shortcut_short_label">Recherche</string>
<string name="videos_shortcut_short_label">Vidéos</string>
<string name="analysis_channel_name">Analyse des images</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Wallpaper</string>
<string name="search_shortcut_short_label">Cari</string>
<string name="videos_shortcut_short_label">Video</string>
<string name="analysis_channel_name">Pindai media</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Sfondo</string>
<string name="search_shortcut_short_label">Ricerca</string>
<string name="videos_shortcut_short_label">Video</string>
<string name="analysis_channel_name">Scansione media</string>

View file

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="search_shortcut_short_label">検索</string>
<string name="videos_shortcut_short_label">動画</string>
<string name="analysis_channel_name">メディアスキャン</string>
<string name="analysis_service_description">画像と動画をスキャン</string>
<string name="analysis_notification_default_title">メディアをスキャン中</string>
<string name="analysis_notification_action_stop">停止</string>
<string name="app_name">Aves</string>
<string name="wallpaper">壁紙</string>
<string name="search_shortcut_short_label">検索</string>
<string name="videos_shortcut_short_label">動画</string>
<string name="analysis_channel_name">メディアスキャン</string>
<string name="analysis_service_description">画像と動画をスキャン</string>
<string name="analysis_notification_default_title">メディアをスキャン中</string>
<string name="analysis_notification_action_stop">停止</string>
</resources>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">아베스</string>
<string name="wallpaper">배경화면</string>
<string name="search_shortcut_short_label">검색</string>
<string name="videos_shortcut_short_label">동영상</string>
<string name="analysis_channel_name">미디어 분석</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Papel de parede</string>
<string name="search_shortcut_short_label">Procurar</string>
<string name="videos_shortcut_short_label">Vídeos</string>
<string name="analysis_channel_name">Digitalização de mídia</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Обои</string>
<string name="search_shortcut_short_label">Поиск</string>
<string name="videos_shortcut_short_label">Видео</string>
<string name="analysis_channel_name">Сканировать медия</string>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Duvar kağıdı</string>
<string name="search_shortcut_short_label">Arama</string>
<string name="videos_shortcut_short_label">Videolar</string>
<string name="analysis_channel_name">Medya tarama</string>
<string name="analysis_service_description">Görüntüleri ve videoları tarayın</string>
<string name="analysis_notification_default_title">Medya taranıyor</string>
<string name="analysis_notification_action_stop">Durdur</string>
</resources>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">壁纸</string>
<string name="search_shortcut_short_label">搜索</string>
<string name="videos_shortcut_short_label">视频</string>
<string name="analysis_channel_name">媒体扫描</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Wallpaper</string>
<string name="search_shortcut_short_label">Search</string>
<string name="videos_shortcut_short_label">Videos</string>
<string name="analysis_channel_name">Media scan</string>

View file

@ -1,17 +1,17 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.0'
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// GMS & Firebase Crashlytics (used by some flavors only)
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0'
// HMS (used by some flavors only)
classpath 'com.huawei.agconnect:agcp:1.5.2.300'
}

View file

@ -2,4 +2,4 @@
<b>Navigation und Suche</b> ist ein wichtiger Bestandteil von <i>Aves</i>. Das Ziel besteht darin, dass Benutzer problemlos von Alben zu Fotos zu Tags zu Karten usw. wechseln können.
<i>Aves</i> lässt sich mit Android (von <b>API 19 bis 32</b>, d. h. von KitKat bis Android 12L) mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.
<i>Aves</i> lässt sich mit Android (von <b>API 19 bis 33</b>, d. h. von KitKat bis Android 13) mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.

View file

@ -0,0 +1,5 @@
In v1.6.9:
- start slideshows
- change your wallpaper
- enjoy the app in Turkish
Full changelog available on GitHub

View file

@ -2,4 +2,4 @@
<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 <b>API 19 to 32</b>, i.e. from KitKat to Android 12L) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
<i>Aves</i> integrates with Android (from <b>API 19 to 33</b>, i.e. from KitKat to Android 13) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.

View file

@ -2,4 +2,4 @@
La <b>navegación y búsqueda</b> son partes importantes de <i>Aves</i>. Su propósito es que los usuarios puedan fácimente ir de álbumes a fotos, etiquetas, mapas, etc.
<i>Aves</i> se integra con Android (desde <b>API 19 a 32</b>, por ej. desde KitKat hasta Android 12L) con características como <b>vínculos de aplicación</b> y manejo de <b>búsqueda global</b>. También funciona como un <b>visor y seleccionador multimedia</b>.
<i>Aves</i> se integra con Android (desde <b>API 19 a 33</b>, por ej. desde KitKat hasta Android 13) con características como <b>vínculos de aplicación</b> y manejo de <b>búsqueda global</b>. También funciona como un <b>visor y seleccionador multimedia</b>.

View file

@ -2,4 +2,4 @@
<b>Navigasi dan pencarian</b> merupakan bagian penting dari <i>Aves</i>. Tujuannya adalah agar pengguna dengan mudah mengalir dari album ke foto ke tag ke peta, dll.
<i>Aves</i> terintegrasi dengan Android (dari <b>API 19 ke 32</b>, yaitu dari KitKat ke Android 12L) dengan fitur-fitur seperti <b>pintasan aplikasi</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.
<i>Aves</i> terintegrasi dengan Android (dari <b>API 19 ke 33</b>, yaitu dari KitKat ke Android 13) dengan fitur-fitur seperti <b>pintasan aplikasi</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.

View file

@ -2,4 +2,4 @@
<b>Navigazione e ricerca</b> sono una parte importante di <i>Aves</i>. L'obiettivo è che gli utenti passino facilmente dagli album alle foto, ai tag, alle mappe, ecc.
<i>Aves</i> si integra con Android (da <b>API 19 a 32</b>, cioè da KitKat ad Android 12L) con caratteristiche come <b>collegamenti alle app</b> e la gestione della <b>ricerca globale</b>. Funziona anche come <b>visualizzazione e raccolta di media</b>.
<i>Aves</i> si integra con Android (da <b>API 19 a 33</b>, cioè da KitKat ad Android 13) con caratteristiche come <b>collegamenti alle app</b> e la gestione della <b>ricerca globale</b>. Funziona anche come <b>visualizzazione e raccolta di media</b>.

View file

@ -4,4 +4,4 @@
<b>ナビゲーションと検索</b>は、Avesの重要な部分です。アルバムから写真、タグ、地図などへ簡単に移動できます。
<i>Aves</i>は、<b>アプリショートカット</b>や<b>グローバル検索</b>などの機能を、Android<b>API 19から32まで</b>、つまりAndroid 4.4から12 Lまでと統合しています。また、<b>メディアビューワー</b>や<b>メディアピッカー</b>としても機能します。
<i>Aves</i>は、<b>アプリショートカット</b>や<b>グローバル検索</b>などの機能を、Android<b>API 19から33まで</b>、つまりAndroid 4.4から13 Lまでと統合しています。また、<b>メディアビューワー</b>や<b>メディアピッカー</b>としても機能します。

View file

@ -2,4 +2,4 @@
<b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
<i>Aves</i> integra com Android (de <b>API 19 para 32</b>, i.e. de KitKat para Android 12L) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.
<i>Aves</i> integra com Android (de <b>API 19 para 33</b>, i.e. de KitKat para Android 13) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.

View file

@ -0,0 +1,5 @@
<i>Aves</i> tipik JPEG ve MP4'lerin yanı sıra <b>çok sayfalı TIFF'ler, SVG'ler, eski AVI'ler ve daha fazlası</b> gibi daha egzotik şeyler de dahil olmak üzere her türlü görüntü ve videoyu işleyebilir! <b>Hareketli fotoğrafları</b>, <b>panoramaları</b> (fotoğraf küreleri olarak da bilinir), <b>360° videoları</b> ve <b>GeoTIFF</b> dosyalarını tanımlamak için medya koleksiyonunuzu tarar.
<b>Gezinme ve arama</b> <i>Aves'in</i> önemli bir parçasıdır. Amaç, kullanıcıların albümlerden fotoğraflara, etiketlerden haritalara vb. kolayca geçmesini sağlamaktır.
<i>Aves</i>, <b>uygulama kısayolları</b> ve <b>global arama<b> işleme gibi özelliklerle Android (<b>API 19'dan 33'ye</b>, yani KitKat'tan Android 13'ye kadar) ile entegre olur. Ayrıca bir <b>medya görüntüleyici ve alıcı</b> olarak da çalışır.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

View file

@ -0,0 +1 @@
Galeri ve meta veri gezgini

View file

@ -2,4 +2,4 @@
<b>导航与搜索</b>是 <i>Aves</i> 的核心功能之一,旨在帮助用户在相册、照片、标签、地图等之间轻松切换。
<i> Aves</i> 与 Android<b>API 19-32</b>,即从 KitKat 到 Android 12L)集成,具有<b>快捷方式</b>和<b>全局搜索</b>等功能。它还可用作<b>媒体查看器和选择器<b>。
<i> Aves</i> 与 Android<b>API 19-33</b>,即从 KitKat 到 Android 13)集成,具有<b>快捷方式</b>和<b>全局搜索</b>等功能。它还可用作<b>媒体查看器和选择器<b>。

View file

@ -1,4 +1,13 @@
enum AppMode { main, pickSingleMediaExternal, pickMultipleMediaExternal, pickMediaInternal, pickFilterInternal, view }
enum AppMode {
main,
pickSingleMediaExternal,
pickMultipleMediaExternal,
pickMediaInternal,
pickFilterInternal,
setWallpaper,
slideshow,
view,
}
extension ExtraAppMode on AppMode {
bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal;

View file

@ -50,6 +50,7 @@
"entryActionDelete": "Löschen",
"entryActionConvert": "Konvertieren",
"entryActionExport": "Exportieren",
"entryActionInfo": "Info",
"entryActionRename": "Umbenennen",
"entryActionRestore": "Wiederherstellen",
"entryActionRotateCCW": "Drehen gegen den Uhrzeigersinn",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "Wiedergabegeschwindigkeit",
"videoActionSettings": "Einstellungen",
"slideshowActionResume": "Wiedergabe",
"slideshowActionShowInCollection": "In Sammlung anzeigen",
"entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten",
"entryInfoActionEditLocation": "Standort bearbeiten",
"entryInfoActionEditRating": "Bewertung bearbeiten",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "Höchste Rate",
"displayRefreshRatePreferLowest": "Niedrigste Rate",
"slideshowVideoPlaybackSkip": "Überspringen",
"slideshowVideoPlaybackMuted": "Stumm abspielen",
"slideshowVideoPlaybackWithSound": "Mit Ton abspielen",
"themeBrightnessLight": "Hell",
"themeBrightnessDark": "Dunkel",
"themeBrightnessBlack": "Schwarz",
"viewerTransitionSlide": "Dia",
"viewerTransitionParallax": "Parallaxe",
"viewerTransitionFade": "Ausblenden",
"viewerTransitionZoomIn": "Heranzoomen",
"wallpaperTargetHome": "Startbildschirm",
"wallpaperTargetLock": "Sperrbildschirm",
"wallpaperTargetHomeLock": "Start- und Sperrbildschirm",
"albumTierNew": "Neu",
"albumTierPinned": "Angeheftet",
"albumTierSpecial": "Häufig verwendet",
@ -262,6 +279,7 @@
"menuActionSelectAll": "Alle auswählen",
"menuActionSelectNone": "Keine auswählen",
"menuActionMap": "Karte",
"menuActionSlideshow": "Diashow",
"menuActionStats": "Statistiken",
"viewDialogTabSort": "Sortieren",
@ -349,6 +367,7 @@
"collectionEmptyFavourites": "Keine Favoriten",
"collectionEmptyVideos": "Keine Videos",
"collectionEmptyImages": "Keine Bilder",
"collectionEmptyGrantAccessButtonLabel": "Zugriff gewähren",
"collectionSelectSectionTooltip": "Bereich auswählen",
"collectionDeselectSectionTooltip": "Bereich abwählen",
@ -479,6 +498,17 @@
"settingsViewerShowOverlayThumbnails": "Vorschaubilder anzeigen",
"settingsViewerEnableOverlayBlurEffect": "Unschärfe-Effekt",
"settingsViewerSlideshowTile": "Diashow",
"settingsViewerSlideshowTitle": "Diashow",
"settingsSlideshowRepeat": "Wiederholung",
"settingsSlideshowShuffle": "Mischen",
"settingsSlideshowTransitionTile": "Übergang",
"settingsSlideshowTransitionTitle": "Übergang",
"settingsSlideshowIntervalTile": "Intervall",
"settingsSlideshowIntervalTitle": "Intervall",
"settingsSlideshowVideoPlaybackTile": "Videowiedergabe",
"settingsSlideshowVideoPlaybackTitle": "Videowiedergabe",
"settingsVideoPageTitle": "Video-Einstellungen",
"settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Videos anzeigen",
@ -543,6 +573,7 @@
"settingsSectionDisplay": "Anzeige",
"settingsThemeBrightness": "Thema",
"settingsThemeColorHighlights": "Farbige Highlights",
"settingsThemeEnableDynamicColor": "Dynamische Farben",
"settingsDisplayRefreshRateModeTile": "Bildwiederholrate der Anzeige",
"settingsDisplayRefreshRateModeTitle": "Bildwiederholrate",
@ -560,6 +591,7 @@
"statsTopTags": "Top-Tags",
"viewerOpenPanoramaButtonLabel": "ÖFFNE PANORAMA",
"viewerSetWallpaperButtonLabel": "HINTERGRUNDBILD EINSTELLEN",
"viewerErrorUnknown": "Ups!",
"viewerErrorDoesNotExist": "Die Datei existiert nicht mehr.",

View file

@ -78,6 +78,7 @@
"entryActionDelete": "Delete",
"entryActionConvert": "Convert",
"entryActionExport": "Export",
"entryActionInfo": "Info",
"entryActionRename": "Rename",
"entryActionRestore": "Restore",
"entryActionRotateCCW": "Rotate counterclockwise",
@ -108,6 +109,9 @@
"videoActionSetSpeed": "Playback speed",
"videoActionSettings": "Settings",
"slideshowActionResume": "Resume",
"slideshowActionShowInCollection": "Show in Collection",
"entryInfoActionEditDate": "Edit date & time",
"entryInfoActionEditLocation": "Edit location",
"entryInfoActionEditRating": "Edit rating",
@ -184,10 +188,23 @@
"displayRefreshRatePreferHighest": "Highest rate",
"displayRefreshRatePreferLowest": "Lowest rate",
"slideshowVideoPlaybackSkip": "Skip",
"slideshowVideoPlaybackMuted": "Play muted",
"slideshowVideoPlaybackWithSound": "Play with sound",
"themeBrightnessLight": "Light",
"themeBrightnessDark": "Dark",
"themeBrightnessBlack": "Black",
"viewerTransitionSlide": "Slide",
"viewerTransitionParallax": "Parallax",
"viewerTransitionFade": "Fade",
"viewerTransitionZoomIn": "Zoom in",
"wallpaperTargetHome": "Home screen",
"wallpaperTargetLock": "Lock screen",
"wallpaperTargetHomeLock": "Home and lock screens",
"albumTierNew": "New",
"albumTierPinned": "Pinned",
"albumTierSpecial": "Common",
@ -392,6 +409,7 @@
"menuActionSelectAll": "Select all",
"menuActionSelectNone": "Select none",
"menuActionMap": "Map",
"menuActionSlideshow": "Slideshow",
"menuActionStats": "Stats",
"viewDialogTabSort": "Sort",
@ -529,6 +547,7 @@
"collectionEmptyFavourites": "No favorites",
"collectionEmptyVideos": "No videos",
"collectionEmptyImages": "No images",
"collectionEmptyGrantAccessButtonLabel": "Grant access",
"collectionSelectSectionTooltip": "Select section",
"collectionDeselectSectionTooltip": "Deselect section",
@ -659,6 +678,17 @@
"settingsViewerShowOverlayThumbnails": "Show thumbnails",
"settingsViewerEnableOverlayBlurEffect": "Blur effect",
"settingsViewerSlideshowTile": "Slideshow",
"settingsViewerSlideshowTitle": "Slideshow",
"settingsSlideshowRepeat": "Repeat",
"settingsSlideshowShuffle": "Shuffle",
"settingsSlideshowTransitionTile": "Transition",
"settingsSlideshowTransitionTitle": "Transition",
"settingsSlideshowIntervalTile": "Interval",
"settingsSlideshowIntervalTitle": "Interval",
"settingsSlideshowVideoPlaybackTile": "Video playback",
"settingsSlideshowVideoPlaybackTitle": "Video Playback",
"settingsVideoPageTitle": "Video Settings",
"settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Show videos",
@ -723,6 +753,7 @@
"settingsSectionDisplay": "Display",
"settingsThemeBrightness": "Theme",
"settingsThemeColorHighlights": "Color highlights",
"settingsThemeEnableDynamicColor": "Dynamic color",
"settingsDisplayRefreshRateModeTile": "Display refresh rate",
"settingsDisplayRefreshRateModeTitle": "Refresh Rate",
@ -745,6 +776,7 @@
"statsTopTags": "Top Tags",
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
"viewerSetWallpaperButtonLabel": "SET WALLPAPER",
"viewerErrorUnknown": "Oops!",
"viewerErrorDoesNotExist": "The file no longer exists.",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "Borrar",
"entryActionConvert": "Convertir",
"entryActionExport": "Exportar",
"entryActionInfo": "Información",
"entryActionRename": "Renombrar",
"entryActionRestore": "Restaurar",
"entryActionRotateCCW": "Rotar en sentido antihorario",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "Velocidad de reproducción",
"videoActionSettings": "Ajustes",
"slideshowActionResume": "Reanudar",
"slideshowActionShowInCollection": "Mostrar en Colección",
"entryInfoActionEditDate": "Editar fecha y hora",
"entryInfoActionEditLocation": "Editar ubicación",
"entryInfoActionEditRating": "Editar clasificación",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "Alta tasa",
"displayRefreshRatePreferLowest": "Baja tasa",
"slideshowVideoPlaybackSkip": "Saltear",
"slideshowVideoPlaybackMuted": "Reproducir sin sonido",
"slideshowVideoPlaybackWithSound": "Reproducir con sonido",
"themeBrightnessLight": "Claro",
"themeBrightnessDark": "Obscuro",
"themeBrightnessBlack": "Negro",
"viewerTransitionSlide": "Diapositiva",
"viewerTransitionParallax": "Paralaje",
"viewerTransitionFade": "Desvanecer",
"viewerTransitionZoomIn": "Acercar",
"wallpaperTargetHome": "Pantalla de inicio",
"wallpaperTargetLock": "Pantalla de bloqueo",
"wallpaperTargetHomeLock": "Pantallas de inicio y bloqueo",
"albumTierNew": "Nuevo",
"albumTierPinned": "Fijado",
"albumTierSpecial": "Común",
@ -262,6 +279,7 @@
"menuActionSelectAll": "Seleccionar todo",
"menuActionSelectNone": "Deseleccionar",
"menuActionMap": "Mapa",
"menuActionSlideshow": "Presentación",
"menuActionStats": "Estadísticas",
"viewDialogTabSort": "Ordenar",
@ -278,7 +296,6 @@
"appPickDialogTitle": "Escoger aplicación",
"appPickDialogNone": "Ninguna",
"aboutPageTitle": "Acerca de",
"aboutLinkSources": "Fuentes",
"aboutLinkLicense": "Licencia",
@ -296,7 +313,6 @@
"aboutCreditsWorldAtlas1": "Esta aplicación usa un archivo TopoJSON de",
"aboutCreditsWorldAtlas2": "bajo licencia ISC.",
"aboutCreditsTranslators": "Traductores:",
"aboutCreditsTranslatorLine": "{language}: {names}",
"aboutLicenses": "Licencias de código abierto",
"aboutLicensesBanner": "Esta aplicación usa los siguientes paquetes y librerías de código abierto.",
@ -351,6 +367,7 @@
"collectionEmptyFavourites": "Sin favoritos",
"collectionEmptyVideos": "Sin videos",
"collectionEmptyImages": "Sin imágenes",
"collectionEmptyGrantAccessButtonLabel": "Otorgar accceso",
"collectionSelectSectionTooltip": "Seleccionar sección",
"collectionDeselectSectionTooltip": "Deseleccionar sección",
@ -421,6 +438,7 @@
"settingsSectionNavigation": "Navegación",
"settingsHome": "Inicio",
"settingsShowBottomNavigationBar": "Mostrar barra de navegación inferior",
"settingsKeepScreenOnTile": "Mantener pantalla encendida",
"settingsKeepScreenOnTitle": "Mantener pantalla encendida",
"settingsDoubleBackExit": "Presione «atrás» dos veces para salir",
@ -443,6 +461,7 @@
"settingsThumbnailOverlayTile": "Incrustaciones",
"settingsThumbnailOverlayTitle": "Incrustaciones",
"settingsThumbnailShowFavouriteIcon": "Mostrar icono de favoritos",
"settingsThumbnailShowTagIcon": "Mostrar ícono de etiqueta",
"settingsThumbnailShowLocationIcon": "Mostrar icono de ubicación",
"settingsThumbnailShowMotionPhotoIcon": "Mostrar icono de foto en movimiento",
"settingsThumbnailShowRating": "Mostrar clasificación",
@ -476,9 +495,20 @@
"settingsViewerShowInformation": "Mostrar información",
"settingsViewerShowInformationSubtitle": "Mostrar título, fecha, ubicación, etc.",
"settingsViewerShowShootingDetails": "Mostrar detalles de toma",
"settingsViewerShowOverlayThumbnails": "Mostrar miniaturas",
"settingsViewerShowOverlayThumbnails": "Mostrar miniaturas",
"settingsViewerEnableOverlayBlurEffect": "Efecto de difuminado",
"settingsViewerSlideshowTile": "Presentación",
"settingsViewerSlideshowTitle": "Presentación",
"settingsSlideshowRepeat": "Repetir",
"settingsSlideshowShuffle": "Mezclar",
"settingsSlideshowTransitionTile": "Transición",
"settingsSlideshowTransitionTitle": "Transición",
"settingsSlideshowIntervalTile": "Intervalo",
"settingsSlideshowIntervalTitle": "Intervalo",
"settingsSlideshowVideoPlaybackTile": "Reproducción de video",
"settingsSlideshowVideoPlaybackTitle": "Reproducción de video",
"settingsVideoPageTitle": "Ajustes de video",
"settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Mostrar videos",
@ -486,8 +516,6 @@
"settingsVideoEnableAutoPlay": "Reproducción automática",
"settingsVideoLoopModeTile": "Modo bucle",
"settingsVideoLoopModeTitle": "Modo bucle",
"settingsVideoQuickActionsTile": "Acciones rápidas para videos",
"settingsVideoQuickActionEditorTitle": "Acciones rápidas",
"settingsSubtitleThemeTile": "Subtítulos",
"settingsSubtitleThemeTitle": "Subtítulos",
@ -545,6 +573,7 @@
"settingsSectionDisplay": "Pantalla",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Acentos de color",
"settingsThemeEnableDynamicColor": "Color dinámico",
"settingsDisplayRefreshRateModeTile": "Tasa de refresco de la pantalla",
"settingsDisplayRefreshRateModeTitle": "Tasa de refresco",
@ -562,6 +591,7 @@
"statsTopTags": "Etiquetas principales",
"viewerOpenPanoramaButtonLabel": "ABRIR PANORÁMICA",
"viewerSetWallpaperButtonLabel": "ESTABLECER FONDO",
"viewerErrorUnknown": "¡Ups!",
"viewerErrorDoesNotExist": "El archivo no existe.",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "Supprimer",
"entryActionConvert": "Convertir",
"entryActionExport": "Exporter",
"entryActionInfo": "Détails",
"entryActionRename": "Renommer",
"entryActionRestore": "Restaurer",
"entryActionRotateCCW": "Pivoter à gauche",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "Vitesse de lecture",
"videoActionSettings": "Préférences",
"slideshowActionResume": "Reprendre",
"slideshowActionShowInCollection": "Afficher dans Collection",
"entryInfoActionEditDate": "Modifier la date",
"entryInfoActionEditLocation": "Modifier le lieu",
"entryInfoActionEditRating": "Modifier la notation",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "Fréquence maximale",
"displayRefreshRatePreferLowest": "Fréquence minimale",
"slideshowVideoPlaybackSkip": "Passer",
"slideshowVideoPlaybackMuted": "Jouer sans son",
"slideshowVideoPlaybackWithSound": "Jouer avec son",
"themeBrightnessLight": "Clair",
"themeBrightnessDark": "Sombre",
"themeBrightnessBlack": "Noir",
"viewerTransitionSlide": "Défilement",
"viewerTransitionParallax": "Parallaxe",
"viewerTransitionFade": "Fondu",
"viewerTransitionZoomIn": "Zoom",
"wallpaperTargetHome": "Écran daccueil",
"wallpaperTargetLock": "Écran de verrouillage",
"wallpaperTargetHomeLock": "Écrans accueil et verrouillage",
"albumTierNew": "Nouveaux",
"albumTierPinned": "Épinglés",
"albumTierSpecial": "Standards",
@ -262,6 +279,7 @@
"menuActionSelectAll": "Tout sélectionner",
"menuActionSelectNone": "Tout désélectionner",
"menuActionMap": "Carte",
"menuActionSlideshow": "Diaporama",
"menuActionStats": "Statistiques",
"viewDialogTabSort": "Tri",
@ -349,6 +367,7 @@
"collectionEmptyFavourites": "Aucun favori",
"collectionEmptyVideos": "Aucune vidéo",
"collectionEmptyImages": "Aucune image",
"collectionEmptyGrantAccessButtonLabel": "Autoriser laccès",
"collectionSelectSectionTooltip": "Sélectionner la section",
"collectionDeselectSectionTooltip": "Désélectionner la section",
@ -479,6 +498,17 @@
"settingsViewerShowOverlayThumbnails": "Afficher les vignettes",
"settingsViewerEnableOverlayBlurEffect": "Effets de flou",
"settingsViewerSlideshowTile": "Diaporama",
"settingsViewerSlideshowTitle": "Diaporama",
"settingsSlideshowRepeat": "Répéter",
"settingsSlideshowShuffle": "Aléatoire",
"settingsSlideshowTransitionTile": "Transition",
"settingsSlideshowTransitionTitle": "Transition",
"settingsSlideshowIntervalTile": "Intervalle",
"settingsSlideshowIntervalTitle": "Intervalle",
"settingsSlideshowVideoPlaybackTile": "Lecture de vidéos",
"settingsSlideshowVideoPlaybackTitle": "Lecture de vidéos",
"settingsVideoPageTitle": "Réglages vidéo",
"settingsSectionVideo": "Vidéo",
"settingsVideoShowVideos": "Afficher les vidéos",
@ -543,6 +573,7 @@
"settingsSectionDisplay": "Affichage",
"settingsThemeBrightness": "Thème",
"settingsThemeColorHighlights": "Surlignages colorés",
"settingsThemeEnableDynamicColor": "Couleur dynamique",
"settingsDisplayRefreshRateModeTile": "Fréquence dactualisation de l'écran",
"settingsDisplayRefreshRateModeTitle": "Fréquence dactualisation",
@ -560,6 +591,7 @@
"statsTopTags": "Top libellés",
"viewerOpenPanoramaButtonLabel": "OUVRIR LE PANORAMA",
"viewerSetWallpaperButtonLabel": "APPLIQUER",
"viewerErrorUnknown": "Zut !",
"viewerErrorDoesNotExist": "Le fichier nexiste plus.",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "Hapus",
"entryActionConvert": "Ubah",
"entryActionExport": "Ekspor",
"entryActionInfo": "Info",
"entryActionRename": "Ganti nama",
"entryActionRestore": "Pulihkan",
"entryActionRotateCCW": "Putar berlawanan arah jarum jam",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "Elimina",
"entryActionConvert": "Converti",
"entryActionExport": "Esportazione",
"entryActionInfo": "Info",
"entryActionRename": "Rinomina",
"entryActionRestore": "Ripristina",
"entryActionRotateCCW": "Ruota in senso antiorario",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "Velocità di riproduzione",
"videoActionSettings": "Impostazioni",
"slideshowActionResume": "Riprendi",
"slideshowActionShowInCollection": "Mostra nella Collezione",
"entryInfoActionEditDate": "Modifica data e ora",
"entryInfoActionEditLocation": "Modifica posizione",
"entryInfoActionEditRating": "Modifica valutazione",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "Frequenza massima",
"displayRefreshRatePreferLowest": "Frequenza minima",
"slideshowVideoPlaybackSkip": "Salta",
"slideshowVideoPlaybackMuted": "Riproduci senza audio",
"slideshowVideoPlaybackWithSound": "Riproduci con audio",
"themeBrightnessLight": "Chiaro",
"themeBrightnessDark": "Scuro",
"themeBrightnessBlack": "Nero",
"viewerTransitionSlide": "Diapositiva",
"viewerTransitionParallax": "Parallasse",
"viewerTransitionFade": "Dissolvenza",
"viewerTransitionZoomIn": "Ingrandisci",
"wallpaperTargetHome": "Schermata iniziale",
"wallpaperTargetLock": "Schermata di blocco",
"wallpaperTargetHomeLock": "Schermata iniziale e di blocco",
"albumTierNew": "Nuovi",
"albumTierPinned": "Fissati",
"albumTierSpecial": "Frequenti",
@ -262,6 +279,7 @@
"menuActionSelectAll": "Seleziona tutto",
"menuActionSelectNone": "Deseleziona tutto",
"menuActionMap": "Mappa",
"menuActionSlideshow": "Presentazione",
"menuActionStats": "Statistiche",
"viewDialogTabSort": "Ordina",
@ -349,6 +367,7 @@
"collectionEmptyFavourites": "Nessun preferito",
"collectionEmptyVideos": "Nessun video",
"collectionEmptyImages": "Nessuna immagine",
"collectionEmptyGrantAccessButtonLabel": "Consenti accesso",
"collectionSelectSectionTooltip": "Seleziona sezione",
"collectionDeselectSectionTooltip": "Deseleziona sezione",
@ -479,6 +498,17 @@
"settingsViewerShowOverlayThumbnails": "Mostra le miniature",
"settingsViewerEnableOverlayBlurEffect": "Effetto sfocatura",
"settingsViewerSlideshowTile": "Presentazione",
"settingsViewerSlideshowTitle": "Presentazione",
"settingsSlideshowRepeat": "Ripeti",
"settingsSlideshowShuffle": "Ordine casuale",
"settingsSlideshowTransitionTile": "Transizione",
"settingsSlideshowTransitionTitle": "Transizione",
"settingsSlideshowIntervalTile": "Intervallo",
"settingsSlideshowIntervalTitle": "Intervallo",
"settingsSlideshowVideoPlaybackTile": "Riproduzione video",
"settingsSlideshowVideoPlaybackTitle": "Riproduzione video",
"settingsVideoPageTitle": "Impostazioni video",
"settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Mostra video",
@ -543,6 +573,7 @@
"settingsSectionDisplay": "Schermo",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Colori evidenziati",
"settingsThemeEnableDynamicColor": "Colori dinamici",
"settingsDisplayRefreshRateModeTile": "Frequenza di aggiornamento dello schermo",
"settingsDisplayRefreshRateModeTitle": "Frequenza di aggiornamento",
@ -560,6 +591,7 @@
"statsTopTags": "Etichette più frequenti",
"viewerOpenPanoramaButtonLabel": "APRI PANORAMA",
"viewerSetWallpaperButtonLabel": "IMPOSTA SFONDO",
"viewerErrorUnknown": "Ops!",
"viewerErrorDoesNotExist": "Il file non esiste più",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "削除",
"entryActionConvert": "変換",
"entryActionExport": "エクスポート",
"entryActionInfo": "情報",
"entryActionRename": "名前を変更",
"entryActionRestore": "元に戻す",
"entryActionRotateCCW": "反時計回りに回転",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "삭제",
"entryActionConvert": "변환",
"entryActionExport": "내보내기",
"entryActionInfo": "상세정보",
"entryActionRename": "이름 변경",
"entryActionRestore": "복원",
"entryActionRotateCCW": "좌회전",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "재생 배속",
"videoActionSettings": "설정",
"slideshowActionResume": "이어서",
"slideshowActionShowInCollection": "미디어 페이지에서 보기",
"entryInfoActionEditDate": "날짜 및 시간 수정",
"entryInfoActionEditLocation": "위치 수정",
"entryInfoActionEditRating": "별점 수정",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "가장 높은 재생률",
"displayRefreshRatePreferLowest": "가장 낮은 재생률",
"slideshowVideoPlaybackSkip": "생략",
"slideshowVideoPlaybackMuted": "음소거 재생",
"slideshowVideoPlaybackWithSound": "일반 재생",
"themeBrightnessLight": "라이트",
"themeBrightnessDark": "다크",
"themeBrightnessBlack": "검은색",
"viewerTransitionSlide": "좌우",
"viewerTransitionParallax": "시차",
"viewerTransitionFade": "페이드",
"viewerTransitionZoomIn": "확대",
"wallpaperTargetHome": "홈 화면",
"wallpaperTargetLock": "잠금화면",
"wallpaperTargetHomeLock": "홈 및 잠금화면",
"albumTierNew": "신규",
"albumTierPinned": "고정",
"albumTierSpecial": "기본",
@ -262,6 +279,7 @@
"menuActionSelectAll": "모두 선택",
"menuActionSelectNone": "모두 해제",
"menuActionMap": "지도",
"menuActionSlideshow": "슬라이드쇼",
"menuActionStats": "통계",
"viewDialogTabSort": "정렬",
@ -349,6 +367,7 @@
"collectionEmptyFavourites": "즐겨찾기가 없습니다",
"collectionEmptyVideos": "동영상이 없습니다",
"collectionEmptyImages": "사진이 없습니다",
"collectionEmptyGrantAccessButtonLabel": "접근 허용",
"collectionSelectSectionTooltip": "묶음 선택",
"collectionDeselectSectionTooltip": "묶음 선택 해제",
@ -479,6 +498,17 @@
"settingsViewerShowOverlayThumbnails": "섬네일 표시",
"settingsViewerEnableOverlayBlurEffect": "흐림 효과",
"settingsViewerSlideshowTile": "슬라이드쇼",
"settingsViewerSlideshowTitle": "슬라이드쇼",
"settingsSlideshowRepeat": "반복",
"settingsSlideshowShuffle": "순서섞기",
"settingsSlideshowTransitionTile": "전환 효과",
"settingsSlideshowTransitionTitle": "전환 효과",
"settingsSlideshowIntervalTile": "교체 주기",
"settingsSlideshowIntervalTitle": "교체 주기",
"settingsSlideshowVideoPlaybackTile": "동영상 재생",
"settingsSlideshowVideoPlaybackTitle": "동영상 재생",
"settingsVideoPageTitle": "동영상 설정",
"settingsSectionVideo": "동영상",
"settingsVideoShowVideos": "미디어에 동영상 표시",
@ -543,6 +573,7 @@
"settingsSectionDisplay": "디스플레이",
"settingsThemeBrightness": "테마",
"settingsThemeColorHighlights": "색 강조",
"settingsThemeEnableDynamicColor": "동적 색상",
"settingsDisplayRefreshRateModeTile": "화면 재생률",
"settingsDisplayRefreshRateModeTitle": "화면 재생률",
@ -560,6 +591,7 @@
"statsTopTags": "태그 랭킹",
"viewerOpenPanoramaButtonLabel": "파노라마 열기",
"viewerSetWallpaperButtonLabel": "설정",
"viewerErrorUnknown": "아이구!",
"viewerErrorDoesNotExist": "파일이 존재하지 않습니다.",

View file

@ -49,6 +49,7 @@
"entryActionCopyToClipboard": "Copiar para área de transferência",
"entryActionDelete": "Excluir",
"entryActionExport": "Exportar",
"entryActionInfo": "Informações",
"entryActionConvert": "Converter",
"entryActionRename": "Renomear",
"entryActionRestore": "Restaurar",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "Velocidade de reprodução",
"videoActionSettings": "Configurações",
"slideshowActionResume": "Retomar",
"slideshowActionShowInCollection": "Mostrar na Coleção",
"entryInfoActionEditDate": "Editar data e hora",
"entryInfoActionEditLocation": "Editar localização",
"entryInfoActionEditRating": "Editar classificação",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "Taxa mais alta",
"displayRefreshRatePreferLowest": "Taxa mais baixa",
"slideshowVideoPlaybackSkip": "Pular",
"slideshowVideoPlaybackMuted": "Reproduzir sem som",
"slideshowVideoPlaybackWithSound": "Reproduzir com som",
"themeBrightnessLight": "Claro",
"themeBrightnessDark": "Escuro",
"themeBrightnessBlack": "Preto",
"viewerTransitionSlide": "Deslizar",
"viewerTransitionParallax": "Parallax",
"viewerTransitionFade": "Desvaneça",
"viewerTransitionZoomIn": "Mais zoom",
"wallpaperTargetHome": "Tela inicial",
"wallpaperTargetLock": "Tela de bloqueio",
"wallpaperTargetHomeLock": "Telas iniciais e de bloqueio",
"albumTierNew": "Novo",
"albumTierPinned": "Fixada",
"albumTierSpecial": "Comum",
@ -262,6 +279,7 @@
"menuActionSelectAll": "Selecionar tudo",
"menuActionSelectNone": "Selecione nenhum",
"menuActionMap": "Mapa",
"menuActionSlideshow": "Apresentação de slides",
"menuActionStats": "Estatísticas",
"viewDialogTabSort": "Organizar",
@ -349,6 +367,7 @@
"collectionEmptyFavourites": "Nenhum favorito",
"collectionEmptyVideos": "Nenhum video",
"collectionEmptyImages": "Nenhuma image",
"collectionEmptyGrantAccessButtonLabel": "Garantir acesso",
"collectionSelectSectionTooltip": "Selecionar seção",
"collectionDeselectSectionTooltip": "Desmarcar seção",
@ -479,6 +498,17 @@
"settingsViewerShowOverlayThumbnails": "Mostrar miniaturas",
"settingsViewerEnableOverlayBlurEffect": "Efeito de desfoque",
"settingsViewerSlideshowTile": "Apresentação de slides",
"settingsViewerSlideshowTitle": "Apresentação de slides",
"settingsSlideshowRepeat": "Repetir",
"settingsSlideshowShuffle": "Embaralhar",
"settingsSlideshowTransitionTile": "Transição",
"settingsSlideshowTransitionTitle": "Transição",
"settingsSlideshowIntervalTile": "Intervalo",
"settingsSlideshowIntervalTitle": "Intervalo",
"settingsSlideshowVideoPlaybackTile": "Reprodução de vídeo",
"settingsSlideshowVideoPlaybackTitle": "Reprodução de vídeo",
"settingsVideoPageTitle": "Configurações de vídeo",
"settingsSectionVideo": "Vídeo",
"settingsVideoShowVideos": "Mostrar vídeos",
@ -543,6 +573,7 @@
"settingsSectionDisplay": "Tela",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Destaques de cores",
"settingsThemeEnableDynamicColor": "Cor dinâmica",
"settingsDisplayRefreshRateModeTile": "Taxa de atualização de exibição",
"settingsDisplayRefreshRateModeTitle": "Taxa de atualização",
@ -560,6 +591,7 @@
"statsTopTags": "Principais Etiquetas",
"viewerOpenPanoramaButtonLabel": "ABRIR PANORAMA",
"viewerSetWallpaperButtonLabel": "DEFINIR PAPEL DE PAREDE",
"viewerErrorUnknown": "Algo não está certo!",
"viewerErrorDoesNotExist": "O arquivo não existe mais.",

View file

@ -50,6 +50,7 @@
"entryActionDelete": "Удалить",
"entryActionConvert": "Конвертировать",
"entryActionExport": "Экспорт",
"entryActionInfo": "Информация",
"entryActionRename": "Переименовать",
"entryActionRestore": "Восстановить",
"entryActionRotateCCW": "Повернуть против часовой стрелки",

639
lib/l10n/app_tr.arb Normal file
View file

@ -0,0 +1,639 @@
{
"appName": "Aves",
"welcomeMessage": "Aves'e Hoş Geldiniz",
"welcomeOptional": "İsteğe bağlı",
"welcomeTermsToggle": "Hüküm ve koşulları kabul ediyorum",
"itemCount": "{count, plural, =1{1 öğe} other{{count} öğe}}",
"timeSeconds": "{seconds, plural, =1{1 saniye} other{{seconds} saniye}}",
"timeMinutes": "{minutes, plural, =1{1 dakika} other{{minutes} dakika}}",
"timeDays": "{days, plural, =1{1 gün} other{{days} gün}}",
"focalLength": "{length} mm",
"applyButtonLabel": "UYGULA",
"deleteButtonLabel": "SİL",
"nextButtonLabel": "SONRAKİ",
"showButtonLabel": "GÖSTER",
"hideButtonLabel": "GİZLE",
"continueButtonLabel": "DEVAM ET",
"cancelTooltip": "İptal et",
"changeTooltip": "Değiştir",
"clearTooltip": "Temizle",
"previousTooltip": "Önceki",
"nextTooltip": "Sonraki",
"showTooltip": "Göster",
"hideTooltip": "Gizle",
"actionRemove": "Kaldır",
"resetButtonTooltip": "Sıfırla",
"doubleBackExitMessage": "Çıkmak için tekrar “geri”, düğmesine dokunun.",
"doNotAskAgain": "Bir daha sorma",
"sourceStateLoading": "Yükleniyor",
"sourceStateCataloguing": "Kataloglanıyor",
"sourceStateLocatingCountries": "Ülkeler konumlandırılıyor",
"sourceStateLocatingPlaces": "Konum belirleniyor",
"chipActionDelete": "Sil",
"chipActionGoToAlbumPage": "Albümlerde göster",
"chipActionGoToCountryPage": "Ülkelerde göster",
"chipActionGoToTagPage": "Etiketlerde göster",
"chipActionHide": "Gizle",
"chipActionPin": "Başa sabitle",
"chipActionUnpin": "Baştan çıkar",
"chipActionRename": "Yeniden adlandır",
"chipActionSetCover": "Kapağı ayarla",
"chipActionCreateAlbum": "Albüm oluştur",
"entryActionCopyToClipboard": "Panoya kopyala",
"entryActionDelete": "Sil",
"entryActionConvert": "Dönüştür",
"entryActionExport": "Dışa aktar",
"entryActionInfo": "Bilgi",
"entryActionRename": "Yeniden adlandır",
"entryActionRestore": "Dışa aktar",
"entryActionRotateCCW": "Saat yönünün tersine döndür",
"entryActionRotateCW": "Saat yönünde döndür",
"entryActionFlip": "Yatay olarak çevir",
"entryActionPrint": "Yazdır",
"entryActionShare": "Paylaş",
"entryActionViewSource": "Kaynağı görüntüle",
"entryActionShowGeoTiffOnMap": "Harita katmanı olarak göster",
"entryActionConvertMotionPhotoToStillImage": "Hareketsiz görüntüye dönüştür",
"entryActionViewMotionPhotoVideo": "Videoyu aç",
"entryActionEdit": "Düzenle",
"entryActionOpen": "Şununla aç",
"entryActionSetAs": "Olarak ayarla",
"entryActionOpenMap": "Harita uygulamasında göster",
"entryActionRotateScreen": "Ekranı döndür",
"entryActionAddFavourite": "Favorilere ekle",
"entryActionRemoveFavourite": "Favorilerden kaldır",
"videoActionCaptureFrame": "Çerçeve yakala",
"videoActionMute": "Sustur",
"videoActionUnmute": "Susturmayı kaldır",
"videoActionPause": "Duraklat",
"videoActionPlay": "Oynat",
"videoActionReplay10": "10 saniye geri git",
"videoActionSkip10": "10 saniye ileri git",
"videoActionSelectStreams": "Parça seç",
"videoActionSetSpeed": "Oynatma hızı",
"videoActionSettings": "Ayarlar",
"entryInfoActionEditDate": "Tarih ve saati düzenle",
"entryInfoActionEditLocation": "Konumu düzenle",
"entryInfoActionEditRating": "Derecelendirmeyi düzenle",
"entryInfoActionEditTags": "Etiketleri düzenle",
"entryInfoActionRemoveMetadata": "Meta verileri kaldır",
"filterBinLabel": "Geri dönüşüm kutusu",
"filterFavouriteLabel": "Favori",
"filterLocationEmptyLabel": "Konumsuz",
"filterTagEmptyLabel": "Etiketsiz",
"filterRatingUnratedLabel": "Derecelendirilmemiş",
"filterRatingRejectedLabel": "Reddedilmiş",
"filterTypeAnimatedLabel": "Hareketli",
"filterTypeMotionPhotoLabel": "Hareketli Fotoğraf",
"filterTypePanoramaLabel": "Panorama",
"filterTypeRawLabel": "Raw",
"filterTypeSphericalVideoLabel": "360° Video",
"filterTypeGeotiffLabel": "GeoTIFF",
"filterMimeImageLabel": "Resim",
"filterMimeVideoLabel": "Video",
"coordinateFormatDms": "DMS",
"coordinateFormatDecimal": "Ondalık dereceler",
"coordinateDms": "{coordinate} {direction}",
"coordinateDmsNorth": "K",
"coordinateDmsSouth": "G",
"coordinateDmsEast": "D",
"coordinateDmsWest": "B",
"unitSystemMetric": "Metrik",
"unitSystemImperial": "İngiliz",
"videoLoopModeNever": "Asla",
"videoLoopModeShortOnly": "Yalnızca kısa videolar",
"videoLoopModeAlways": "Her zaman",
"videoControlsPlay": "Oynat",
"videoControlsPlaySeek": "Oynat ve ileri/geri git",
"videoControlsPlayOutside": "Başka bir oynatıcı ile aç",
"videoControlsNone": "Hiçbiri",
"mapStyleGoogleNormal": "Google Haritalar",
"mapStyleGoogleHybrid": "Google Haritalar (Hibrit)",
"mapStyleGoogleTerrain": "Google Haritalar (Arazi)",
"mapStyleHuaweiNormal": "Petal Haritalar",
"mapStyleHuaweiTerrain": "Petal Haritalar (Arazi)",
"mapStyleOsmHot": "İnsancıl OSM",
"mapStyleStamenToner": "Stamen Tonik",
"mapStyleStamenWatercolor": "Stamen Suluboya",
"nameConflictStrategyRename": "Yeniden adlandır",
"nameConflictStrategyReplace": "Değiştir",
"nameConflictStrategySkip": "Atla",
"keepScreenOnNever": "Asla",
"keepScreenOnViewerOnly": "Yalnızca görüntüleyici sayfası",
"keepScreenOnAlways": "Her zaman",
"accessibilityAnimationsRemove": "Ekran efektlerini önle",
"accessibilityAnimationsKeep": "Ekran efektlerini koru",
"displayRefreshRatePreferHighest": "En yüksek oran",
"displayRefreshRatePreferLowest": "En düşük oran",
"themeBrightnessLight": "Açık",
"themeBrightnessDark": "Koyu",
"themeBrightnessBlack": "Siyah",
"albumTierNew": "Yeni",
"albumTierPinned": "Sabitlenmiş",
"albumTierSpecial": "Genel",
"albumTierApps": "Uygulamalar",
"albumTierRegular": "Diğer",
"storageVolumeDescriptionFallbackPrimary": "Dahili depolama",
"storageVolumeDescriptionFallbackNonPrimary": "SD kart",
"rootDirectoryDescription": "kök dizin",
"otherDirectoryDescription": "“{name}” dizin",
"storageAccessDialogTitle": "Depolama Erişimi",
"storageAccessDialogMessage": "Bu uygulamaya erişim sağlamak için lütfen bir sonraki ekranda “{volume}” öğesinin {directory} dizinini seçin.",
"restrictedAccessDialogTitle": "Kısıtlı Erişim",
"restrictedAccessDialogMessage": "Bu uygulamanın “{volume}” içindeki {directory} dosyaları değiştirmesine izin verilmiyor.\n\nÖğeleri başka bir dizine taşımak için lütfen önceden yüklenmiş bir dosya yöneticisi veya galeri uygulaması kullanın.",
"notEnoughSpaceDialogTitle": "Yeterli Yer Yok",
"notEnoughSpaceDialogMessage": "Bu işlemin tamamlanması için “{volume}” üzerinde {needSize} boş alana ihtiyaç var, ancak yalnızca {freeSize} kaldı.",
"missingSystemFilePickerDialogTitle": "Eksik Sistem Dosya Seçicisi",
"missingSystemFilePickerDialogMessage": "Sistem dosya seçicisi eksik veya devre dışı. Lütfen etkinleştirin ve tekrar deneyin.",
"unsupportedTypeDialogTitle": "Desteklenmeyen Türler",
"unsupportedTypeDialogMessage": "{count, plural, =1{Bu işlem aşağıdaki türdeki öğeler için desteklenmez: {types}.} other{Bu işlem aşağıdaki türlerdeki öğeler için desteklenmez: {types}.}}",
"nameConflictDialogSingleSourceMessage": "Hedef klasördeki bazı dosyalar aynı ada sahip.",
"nameConflictDialogMultipleSourceMessage": "Bazı dosyalar aynı ada sahip.",
"addShortcutDialogLabel": "Kısayol etiketi",
"addShortcutButtonLabel": "EKLE",
"noMatchingAppDialogTitle": "Eşleşen Uygulama Yok",
"noMatchingAppDialogMessage": "Bununla ilgilenebilecek bir uygulama yok.",
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Bu öğe geri dönüşüm kutusuna taşınsın mı?} other{Bu {count} madde geri dönüşüm kutusuna atılsın mı?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Bu öğe silinsin mi?} other{Bu {count} öğe silinsin mi?}}",
"moveUndatedConfirmationDialogMessage": "Devam etmeden önce öğe tarihleri kaydedilsin mi?",
"moveUndatedConfirmationDialogSetDate": "Tarihleri kaydet",
"videoResumeDialogMessage": "{time} itibarıyla oynatmaya devam etmek istiyor musunuz?",
"videoStartOverButtonLabel": "BAŞTAN BAŞLA",
"videoResumeButtonLabel": "SÜRDÜR",
"setCoverDialogLatest": "Son öğe",
"setCoverDialogAuto": "Otomatik",
"setCoverDialogCustom": "Özel",
"hideFilterConfirmationDialogMessage": "Eşleşen fotoğraf ve videolar koleksiyonunuzdan gizlenecektir. Bunları “Gizlilik”, ayarlarından tekrar gösterebilirsiniz.\n\nBunları gizlemek istediğinizden emin misiniz?",
"newAlbumDialogTitle": "Yeni Albüm",
"newAlbumDialogNameLabel": "Albüm adı",
"newAlbumDialogNameLabelAlreadyExistsHelper": "Dizin zaten var",
"newAlbumDialogStorageLabel": "Depolama:",
"renameAlbumDialogLabel": "Yeni ad",
"renameAlbumDialogLabelAlreadyExistsHelper": "Dizin zaten var",
"renameEntrySetPageTitle": "Yeniden adlandır",
"renameEntrySetPagePatternFieldLabel": "İsimlendirme şekli",
"renameEntrySetPageInsertTooltip": "Alan ekle",
"renameEntrySetPagePreview": "Önizleme",
"renameProcessorCounter": "Sayaç",
"renameProcessorName": "Ad",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albüm ve öğesi silinsin mi?} other{Bu albüm ve {count} öğesi silinsin mi?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albümler ve öğeleri silinsin mi?} other{Bu albümler ve {count} öğesi silinsin mi?}}",
"exportEntryDialogFormat": "Biçim:",
"exportEntryDialogWidth": "Genişlik",
"exportEntryDialogHeight": "Yükseklik",
"renameEntryDialogLabel": "Yeni ad",
"editEntryDateDialogTitle": "Tarih ve Saat",
"editEntryDateDialogSetCustom": "Özel tarih ayarla",
"editEntryDateDialogCopyField": "Başka bir tarihten kopyala",
"editEntryDateDialogCopyItem": "Başka bir öğeden kopyala",
"editEntryDateDialogExtractFromTitle": "Başlıktan ayıkla",
"editEntryDateDialogShift": "Değişim",
"editEntryDateDialogSourceFileModifiedDate": "Dosya değiştirilme tarihi",
"editEntryDateDialogTargetFieldsHeader": "Değiştirilecek alanlar",
"editEntryDateDialogHours": "Saat",
"editEntryDateDialogMinutes": "Dakika",
"editEntryLocationDialogTitle": "Konum",
"editEntryLocationDialogChooseOnMapTooltip": "Harita üzerinde seç",
"editEntryLocationDialogLatitude": "Enlem",
"editEntryLocationDialogLongitude": "Boylam",
"locationPickerUseThisLocationButton": "Bu konumu kullan",
"editEntryRatingDialogTitle": "Derecelendirme",
"removeEntryMetadataDialogTitle": "Meta veri kaldırma",
"removeEntryMetadataDialogMore": "Daha fazla",
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "Hareketli bir fotoğrafın içindeki videoyu oynatmak için XMP gereklidir.\n\nKaldırmak istediğinizden emin misiniz?",
"convertMotionPhotoToStillImageWarningDialogMessage": "Emin misiniz?",
"videoSpeedDialogLabel": "Oynatma hızı",
"videoStreamSelectionDialogVideo": "Video",
"videoStreamSelectionDialogAudio": "Ses",
"videoStreamSelectionDialogText": "Altyazı",
"videoStreamSelectionDialogOff": "Kapalı",
"videoStreamSelectionDialogTrack": "Parça",
"videoStreamSelectionDialogNoSelection": "Başka parça yok.",
"genericSuccessFeedback": "Başarılı!",
"genericFailureFeedback": "Başarısız",
"menuActionConfigureView": "Görünüm",
"menuActionSelect": "Seç",
"menuActionSelectAll": "Hepsini seç",
"menuActionSelectNone": "Hiçbirini seçme",
"menuActionMap": "Harita",
"menuActionStats": "İstatistikler",
"viewDialogTabSort": "Sırala",
"viewDialogTabGroup": "Grup",
"viewDialogTabLayout": "Düzen",
"tileLayoutGrid": "Izgara",
"tileLayoutList": "Liste",
"coverDialogTabCover": "Kapak",
"coverDialogTabApp": "Uygulama",
"coverDialogTabColor": "Renk",
"appPickDialogTitle": "Uygulama seç",
"appPickDialogNone": "Yok",
"aboutPageTitle": "Hakkında",
"aboutLinkSources": "Kaynaklar",
"aboutLinkLicense": "Lisans",
"aboutLinkPolicy": "Gizlilik Politikası",
"aboutBug": "Hata Bildirimi",
"aboutBugSaveLogInstruction": "Uygulama günlüklerini bir dosyaya kaydet",
"aboutBugSaveLogButton": "Kaydet",
"aboutBugCopyInfoInstruction": "Sistem bilgilerini kopyala",
"aboutBugCopyInfoButton": "Kopyala",
"aboutBugReportInstruction": "GitHub'da günlükleri ve sistem bilgilerini içeren bir rapor oluştur",
"aboutBugReportButton": "Raporla",
"aboutCredits": "Kredi",
"aboutCreditsWorldAtlas1": "Bu uygulama bir TopoJSON dosyası kullanır",
"aboutCreditsWorldAtlas2": "ISC Lisansı kapsamında.",
"aboutCreditsTranslators": "Tercümanlar",
"aboutLicenses": "Açık Kaynak Lisansları",
"aboutLicensesBanner": "Bu uygulama aşağıdaki açık kaynaklı paketleri ve kütüphaneleri kullanır.",
"aboutLicensesAndroidLibraries": "Android Kütüphaneleri",
"aboutLicensesFlutterPlugins": "Flutter Eklentileri",
"aboutLicensesFlutterPackages": "Flutter Paketleri",
"aboutLicensesDartPackages": "Dart Paketleri",
"aboutLicensesShowAllButtonLabel": "Tüm Lisansları Göster",
"policyPageTitle": "Gizlilik Politikası",
"collectionPageTitle": "Koleksiyon",
"collectionPickPageTitle": "Seç",
"collectionSelectPageTitle": "Öğeleri seç",
"collectionActionShowTitleSearch": "Başlık filtresini göster",
"collectionActionHideTitleSearch": "Başlık filtresini gizle",
"collectionActionAddShortcut": "Kısayol ekle",
"collectionActionEmptyBin": "Boş çöp kutusu",
"collectionActionCopy": "Albüme kopyala",
"collectionActionMove": "Albüme taşı",
"collectionActionRescan": "Yeniden tara",
"collectionActionEdit": "Düzenle",
"collectionSearchTitlesHintText": "Başlıkları ara",
"collectionSortDate": "Tarihe göre",
"collectionSortSize": "Boyuta göre",
"collectionSortName": "Albüm ve dosya adına göre",
"collectionSortRating": "Derecelendirmeye göre",
"collectionGroupAlbum": "Albüme göre",
"collectionGroupMonth": "Aya göre",
"collectionGroupDay": "Güne göre",
"collectionGroupNone": "Gruplama",
"sectionUnknown": "Bilinmeyen",
"dateToday": "Bugün",
"dateYesterday": "Dün",
"dateThisMonth": "Bu ay",
"collectionDeleteFailureFeedback": "{count, plural, =1{1 öğe silinemedi} other{{count} öğe silinemedi}}",
"collectionCopyFailureFeedback": "{count, plural, =1{1 öğe kopyalanamadı} other{{count} öğe kopyalanamadı}}",
"collectionMoveFailureFeedback": "{count, plural, =1{1 öğe taşınamadı} other{{count} öğe taşınamadı}}",
"collectionRenameFailureFeedback": "{count, plural, =1{1 öğenin adı değiştirilemedi} other{{count} öğenin adı değiştirilemedi}}",
"collectionEditFailureFeedback": "{count, plural, =1{1 öğe düzenlenemedi} other{{count} öğe düzenlenemedi}}",
"collectionExportFailureFeedback": "{count, plural, =1{1 sayfa dışa aktarılamadı} other{{count} sayfa dışa aktarılamadı}",
"collectionCopySuccessFeedback": "{count, plural, =1{1 öğe kopyalandı} other{{count} öğe kopyalandı}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{1 öğe taşındı} other{{count} öğe taşındı}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{1 öğenin adı değiştirildi} other{{count} öğenin adı değiştirildi}}",
"collectionEditSuccessFeedback": "{count, plural, =1{1 öğe düzenlendi} other{{count} öğe düzenlendi}}",
"collectionEmptyFavourites": "Favori yok",
"collectionEmptyVideos": "Video yok",
"collectionEmptyImages": "Resim yok",
"collectionEmptyGrantAccessButtonLabel": "Erişim izni",
"collectionSelectSectionTooltip": "Bölüm seç",
"collectionDeselectSectionTooltip": "Bölüm seçimini kaldır",
"drawerCollectionAll": "Tüm koleksiyon",
"drawerCollectionFavourites": "Favoriler",
"drawerCollectionImages": "Resimler",
"drawerCollectionVideos": "Videolar",
"drawerCollectionAnimated": "Hareketli",
"drawerCollectionMotionPhotos": "Hareketli fotoğraflar",
"drawerCollectionPanoramas": "Panoramalar",
"drawerCollectionRaws": "Raw fotoğraflar",
"drawerCollectionSphericalVideos": "360° Videolar",
"chipSortDate": "Tarihe göre",
"chipSortName": "Adına göre",
"chipSortCount": "Öğe sayısına göre",
"albumGroupTier": "Kademeye göre",
"albumGroupVolume": "Depolama hacmine göre",
"albumGroupNone": "Gruplama",
"albumPickPageTitleCopy": "Albüme kopyala",
"albumPickPageTitleExport": "Albüme aktar",
"albumPickPageTitleMove": "Albüme taşı",
"albumPickPageTitlePick": "Albüm seç",
"albumCamera": "Kamera",
"albumDownload": "İndir",
"albumScreenshots": "Ekran görüntüleri",
"albumScreenRecordings": "Ekran kayıtları",
"albumVideoCaptures": "Video çekimleri",
"albumPageTitle": "Albümler",
"albumEmpty": "Albüm yok",
"createAlbumTooltip": "Albüm oluştur",
"createAlbumButtonLabel": "OLUŞTUR",
"newFilterBanner": "yeni",
"countryPageTitle": "Ülkeler",
"countryEmpty": "Ülke yok",
"tagPageTitle": "Etiketler",
"tagEmpty": "Etiket yok",
"binPageTitle": "Geri Dönüşüm Kutusu",
"searchCollectionFieldHint": "Koleksiyonu ara",
"searchSectionRecent": "Yakın zamanda",
"searchSectionAlbums": "Albümler",
"searchSectionCountries": "Ülkeler",
"searchSectionPlaces": "Yerler",
"searchSectionTags": "Etiketler",
"searchSectionRating": "Derecelendirmeler",
"settingsPageTitle": "Ayarlar",
"settingsSystemDefault": "Sistem",
"settingsDefault": "Varsayılan",
"settingsSearchFieldLabel": "Ayarlarda ara",
"settingsSearchEmpty": "Eşleşen ayar bulunamadı",
"settingsActionExport": "Dışa aktar",
"settingsActionImport": "İçe aktar",
"appExportCovers": "Kapaklar",
"appExportFavourites": "Favoriler",
"appExportSettings": "Ayarlar",
"settingsSectionNavigation": "Gezinti",
"settingsHome": "Anasayfa",
"settingsShowBottomNavigationBar": "Alt gezinti çubuğunu göster",
"settingsKeepScreenOnTile": "Ekranıık tut",
"settingsKeepScreenOnTitle": "Ekranıık Tut",
"settingsDoubleBackExit": "Çıkmak için iki kez “geri” düğmesine dokunun",
"settingsConfirmationDialogTile": "Onaylama diyalogları",
"settingsConfirmationDialogTitle": "Onaylama Diyalogları",
"settingsConfirmationDialogDeleteItems": "Öğeleri sonsuza dek silmeden önce sor",
"settingsConfirmationDialogMoveToBinItems": "Eşyaları geri dönüşüm kutusuna atmadan önce sor",
"settingsConfirmationDialogMoveUndatedItems": "Tarihsiz eşyaları taşımadan önce sor",
"settingsNavigationDrawerTile": "Gezinti menüsü",
"settingsNavigationDrawerEditorTitle": "Gezinti Menüsü",
"settingsNavigationDrawerBanner": "Menü öğelerini taşımak ve yeniden sıralamak için dokunun ve basılı tutun.",
"settingsNavigationDrawerTabTypes": "Türler",
"settingsNavigationDrawerTabAlbums": "Albümler",
"settingsNavigationDrawerTabPages": "Sayfalar",
"settingsNavigationDrawerAddAlbum": "Albüm ekle",
"settingsSectionThumbnails": "Küçük resimler",
"settingsThumbnailOverlayTile": "Kaplama",
"settingsThumbnailOverlayTitle": "Kaplama",
"settingsThumbnailShowFavouriteIcon": "Favori simgeyi göster",
"settingsThumbnailShowTagIcon": "Etiket simgesini göster",
"settingsThumbnailShowLocationIcon": "Konum simgesini göster",
"settingsThumbnailShowMotionPhotoIcon": "Hareketli fotoğraf simgesini göster",
"settingsThumbnailShowRating": "Derecelendirmeyi göster",
"settingsThumbnailShowRawIcon": "Raw simgesini göster",
"settingsThumbnailShowVideoDuration": "Video süresini göster",
"settingsCollectionQuickActionsTile": "Hızlı eylemler",
"settingsCollectionQuickActionEditorTitle": "Hızlı Eylemler",
"settingsCollectionQuickActionTabBrowsing": "Gözatma",
"settingsCollectionQuickActionTabSelecting": "Seçme",
"settingsCollectionBrowsingQuickActionEditorBanner": "Düğmeleri hareket ettirmek ve öğelere göz atarken hangi eylemlerin görüntüleneceğini seçmek için dokunun ve basılı tutun.",
"settingsCollectionSelectionQuickActionEditorBanner": "Düğmeleri hareket ettirmek ve öğeleri seçerken hangi eylemlerin görüntüleneceğini seçmek için dokunun ve basılı tutun.",
"settingsSectionViewer": "Görüntüleyici",
"settingsViewerUseCutout": "Kesim alanını kullan",
"settingsViewerMaximumBrightness": "Maksimum parlaklık",
"settingsMotionPhotoAutoPlay": "Hareketli fotoğrafları otomatik oynat",
"settingsImageBackground": "Resim arka planı",
"settingsViewerQuickActionsTile": "Hızlı eylemler",
"settingsViewerQuickActionEditorTitle": "Hızlı Eylemler",
"settingsViewerQuickActionEditorBanner": "Düğmeleri hareket ettirmek ve görüntüleyicide hangi eylemlerin görüntüleneceğini seçmek için dokunun ve basılı tutun.",
"settingsViewerQuickActionEditorDisplayedButtons": "Gösterilen Düğmeler",
"settingsViewerQuickActionEditorAvailableButtons": "Mevcut Düğmeler",
"settingsViewerQuickActionEmpty": "Düğme yok",
"settingsViewerOverlayTile": "Kaplama",
"settingsViewerOverlayTitle": "Kaplama",
"settingsViewerShowOverlayOnOpening": "Açılışta göster",
"settingsViewerShowMinimap": "Mini haritayı göster",
"settingsViewerShowInformation": "Bilgileri göster",
"settingsViewerShowInformationSubtitle": "Başlığı, tarihi, konumu vb. göster.",
"settingsViewerShowShootingDetails": "Çekim ayrıntılarını göster",
"settingsViewerShowOverlayThumbnails": "Küçük resimleri göster",
"settingsViewerEnableOverlayBlurEffect": "Bulanıklık efekti",
"settingsVideoPageTitle": "Video Ayarları",
"settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Videoları göster",
"settingsVideoEnableHardwareAcceleration": "Donanım hızlandırma",
"settingsVideoEnableAutoPlay": "Otomatik oynat",
"settingsVideoLoopModeTile": "Döngü modu",
"settingsVideoLoopModeTitle": "Döngü Modu",
"settingsSubtitleThemeTile": "Altyazılar",
"settingsSubtitleThemeTitle": "Altyazılar",
"settingsSubtitleThemeSample": "Bu bir örnek.",
"settingsSubtitleThemeTextAlignmentTile": "Metin hizalama",
"settingsSubtitleThemeTextAlignmentTitle": "Metin Hizalama",
"settingsSubtitleThemeTextSize": "Metin boyutu",
"settingsSubtitleThemeShowOutline": "Dış çizgiyi ve gölgeyi göster",
"settingsSubtitleThemeTextColor": "Metin rengi",
"settingsSubtitleThemeTextOpacity": "Metin opaklığı",
"settingsSubtitleThemeBackgroundColor": "Arka plan rengi",
"settingsSubtitleThemeBackgroundOpacity": "Arka plan opaklığı",
"settingsSubtitleThemeTextAlignmentLeft": "Sol",
"settingsSubtitleThemeTextAlignmentCenter": "Merkez",
"settingsSubtitleThemeTextAlignmentRight": "Sağ",
"settingsVideoControlsTile": "Kontroller",
"settingsVideoControlsTitle": "Kontroller",
"settingsVideoButtonsTile": "Düğmeler",
"settingsVideoButtonsTitle": "Düğmeler",
"settingsVideoGestureDoubleTapTogglePlay": "Oynatmak/duraklatmak için çift dokunun",
"settingsVideoGestureSideDoubleTapSeek": "Geri/ileri aramak için ekran kenarlarına çift dokunun",
"settingsSectionPrivacy": "Gizlilik",
"settingsAllowInstalledAppAccess": "Uygulama envanterine erişime izin ver",
"settingsAllowInstalledAppAccessSubtitle": "Albüm görüntüsünü iyileştirmek için kullanılır",
"settingsAllowErrorReporting": "Anonim hata raporlamasına izin ver",
"settingsSaveSearchHistory": "Arama geçmişini kaydet",
"settingsEnableBin": "Geri dönüşüm kutusunu kullan",
"settingsEnableBinSubtitle": "Silinen öğeleri 30 gün boyunca saklar",
"settingsHiddenItemsTile": "Gizli öğeler",
"settingsHiddenItemsTitle": "Gizli Öğeler",
"settingsHiddenFiltersTitle": "Gizli Filtreler",
"settingsHiddenFiltersBanner": "Gizli filtrelerle eşleşen fotoğraflar ve videolar koleksiyonunuzda görünmeyecektir.",
"settingsHiddenFiltersEmpty": "Gizli filtre yok",
"settingsHiddenPathsTitle": "Gizli Yollar",
"settingsHiddenPathsBanner": "Bu klasörlerdeki veya alt klasörlerindeki fotoğraflar ve videolar koleksiyonunuzda görünmeyecektir.",
"addPathTooltip": "Yol ekle",
"settingsStorageAccessTile": "Depolama erişimi",
"settingsStorageAccessTitle": "Depolama Erişimi",
"settingsStorageAccessBanner": "Bazı dizinler, içlerindeki dosyaları değiştirmek için açık bir erişim izni gerektirir. Daha önce erişim izni verdiğiniz dizinleri buradan inceleyebilirsiniz.",
"settingsStorageAccessEmpty": "Erişim izni yok",
"settingsStorageAccessRevokeTooltip": "Geri al",
"settingsSectionAccessibility": "Erişilebilirlik",
"settingsRemoveAnimationsTile": "Animasyonları kaldır",
"settingsRemoveAnimationsTitle": "Animasyonları Kaldır",
"settingsTimeToTakeActionTile": "Harekete geçme zamanı",
"settingsTimeToTakeActionTitle": "Harekete Geçme Zamanı",
"settingsSectionDisplay": "Ekran",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Renk vurguları",
"settingsThemeEnableDynamicColor": "Dinamik renk",
"settingsDisplayRefreshRateModeTile": "Görüntü yenileme hızı",
"settingsDisplayRefreshRateModeTitle": "Yenileme Hızı",
"settingsSectionLanguage": "Dil ve Biçimler",
"settingsLanguage": "Dil",
"settingsCoordinateFormatTile": "Koordinat formatı",
"settingsCoordinateFormatTitle": "Koordinat Formatı",
"settingsUnitSystemTile": "Birimler",
"settingsUnitSystemTitle": "Birimler",
"statsPageTitle": "İstatistikler",
"statsWithGps": "{count, plural, =1{1 konuma sahip öğe} other{{count} konuma sahip öğe}}",
"statsTopCountries": "Başlıca Ülkeler",
"statsTopPlaces": "Başlıca Yerler",
"statsTopTags": "Başlıca Etiketler",
"viewerOpenPanoramaButtonLabel": "PANORAMAYI AÇ",
"viewerErrorUnknown": "Tüh!",
"viewerErrorDoesNotExist": "Dosya artık mevcut değil.",
"viewerInfoPageTitle": "Bilgi",
"viewerInfoBackToViewerTooltip": "Görüntüleyiciye geri dön",
"viewerInfoUnknown": "bilinmeyen",
"viewerInfoLabelTitle": "Başlık",
"viewerInfoLabelDate": "Tarih",
"viewerInfoLabelResolution": "Çözünürlük",
"viewerInfoLabelSize": "Boyut",
"viewerInfoLabelUri": "URI",
"viewerInfoLabelPath": "Yol",
"viewerInfoLabelDuration": "Süre",
"viewerInfoLabelOwner": "Sahibi",
"viewerInfoLabelCoordinates": "Koordinatlar",
"viewerInfoLabelAddress": "Adres",
"mapStyleTitle": "Harita Şekli",
"mapStyleTooltip": "Harita şeklini seç",
"mapZoomInTooltip": "Yakınlaştır",
"mapZoomOutTooltip": "Uzaklaştır",
"mapPointNorthUpTooltip": "Kuzeyi göster",
"mapAttributionOsmHot": "Harita verileri © [OpenStreetMap](https://www.openstreetmap.org/copyright) katkıda bulunanlar - Kutucuklar [HOT](https://www.hotosm.org/) tarafından hazırlanmıştır - [OSM France](https://openstreetmap.fr/) tarafından barındırılmaktadır",
"mapAttributionStamen": "Harita verileri © [OpenStreetMap](https://www.openstreetmap.org/copyright) katkıda bulunanlar - Döşemeler [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
"openMapPageTooltip": "Harita sayfasında görüntüle",
"mapEmptyRegion": "Bu bölgede resim yok",
"viewerInfoOpenEmbeddedFailureFeedback": "Gömülü veriler ayıklanamadı",
"viewerInfoOpenLinkText": "Aç",
"viewerInfoViewXmlLinkText": "XML'i Görüntüle",
"viewerInfoSearchFieldLabel": "Meta verileri ara",
"viewerInfoSearchEmpty": "Eşleşen anahtar yok",
"viewerInfoSearchSuggestionDate": "Tarih ve saat",
"viewerInfoSearchSuggestionDescription": "Açıklama",
"viewerInfoSearchSuggestionDimensions": "Boyutlar",
"viewerInfoSearchSuggestionResolution": "Çözünürlük",
"viewerInfoSearchSuggestionRights": "Haklar",
"tagEditorPageTitle": "Etiketleri Düzenle",
"tagEditorPageNewTagFieldLabel": "Yeni etiket",
"tagEditorPageAddTagTooltip": "Etiket ekle",
"tagEditorSectionRecent": "Yakın zamanda",
"panoramaEnableSensorControl": "Sensör kontrolünü etkinleştir",
"panoramaDisableSensorControl": "Sensör kontrolünü devre dışı bırak",
"sourceViewerPageTitle": "Kaynak",
"filePickerShowHiddenFiles": "Gizli dosyaları göster",
"filePickerDoNotShowHiddenFiles": "Gizli dosyaları gösterme",
"filePickerOpenFrom": "Şuradan aç",
"filePickerNoItems": "Öğe yok",
"filePickerUseThisFolder": "Bu klasörü kullan"
}

View file

@ -50,6 +50,7 @@
"entryActionDelete": "删除",
"entryActionConvert": "转换",
"entryActionExport": "导出",
"entryActionInfo": "信息",
"entryActionRename": "重命名",
"entryActionRestore": "恢复",
"entryActionRotateCCW": "逆时针旋转",
@ -80,6 +81,9 @@
"videoActionSetSpeed": "播放速度",
"videoActionSettings": "设置",
"slideshowActionResume": "继续",
"slideshowActionShowInCollection": "在媒体集中显示",
"entryInfoActionEditDate": "编辑日期和时间",
"entryInfoActionEditLocation": "编辑位置",
"entryInfoActionEditRating": "修改评分",
@ -144,10 +148,23 @@
"displayRefreshRatePreferHighest": "最高刷新率",
"displayRefreshRatePreferLowest": "最低刷新率",
"slideshowVideoPlaybackSkip": "跳过",
"slideshowVideoPlaybackMuted": "静音播放",
"slideshowVideoPlaybackWithSound": "带音播放",
"themeBrightnessLight": "浅色",
"themeBrightnessDark": "深色",
"themeBrightnessBlack": "黑色",
"viewerTransitionSlide": "滑动",
"viewerTransitionParallax": "视差滚动",
"viewerTransitionFade": "淡入淡出",
"viewerTransitionZoomIn": "放大",
"wallpaperTargetHome": "主屏幕",
"wallpaperTargetLock": "锁屏界面",
"wallpaperTargetHomeLock": "主屏幕 + 锁屏界面",
"albumTierNew": "新的",
"albumTierPinned": "钉选",
"albumTierSpecial": "普通",
@ -262,6 +279,7 @@
"menuActionSelectAll": "全选",
"menuActionSelectNone": "全不选",
"menuActionMap": "地图",
"menuActionSlideshow": "幻灯片",
"menuActionStats": "统计",
"viewDialogTabSort": "排序",
@ -349,6 +367,7 @@
"collectionEmptyFavourites": "无收藏项",
"collectionEmptyVideos": "无视频",
"collectionEmptyImages": "无图像",
"collectionEmptyGrantAccessButtonLabel": "授予访问权限",
"collectionSelectSectionTooltip": "选择部分",
"collectionDeselectSectionTooltip": "取消选择部分",
@ -479,6 +498,17 @@
"settingsViewerShowOverlayThumbnails": "显示缩略图",
"settingsViewerEnableOverlayBlurEffect": "模糊特效",
"settingsViewerSlideshowTile": "幻灯片",
"settingsViewerSlideshowTitle": "幻灯片",
"settingsSlideshowRepeat": "重复",
"settingsSlideshowShuffle": "随机播放",
"settingsSlideshowTransitionTile": "过渡动画",
"settingsSlideshowTransitionTitle": "过渡动画",
"settingsSlideshowIntervalTile": "时间间隔",
"settingsSlideshowIntervalTitle": "时间间隔",
"settingsSlideshowVideoPlaybackTile": "视频回放",
"settingsSlideshowVideoPlaybackTitle": "视频回放",
"settingsVideoPageTitle": "视频设置",
"settingsSectionVideo": "视频",
"settingsVideoShowVideos": "显示视频",
@ -543,6 +573,7 @@
"settingsSectionDisplay": "显示",
"settingsThemeBrightness": "主题",
"settingsThemeColorHighlights": "色彩强调",
"settingsThemeEnableDynamicColor": "动态色彩",
"settingsDisplayRefreshRateModeTile": "显示刷新率",
"settingsDisplayRefreshRateModeTitle": "刷新率",
@ -560,6 +591,7 @@
"statsTopTags": "热门标签",
"viewerOpenPanoramaButtonLabel": "打开全景",
"viewerSetWallpaperButtonLabel": "设置壁纸",
"viewerErrorUnknown": "糟糕!",
"viewerErrorDoesNotExist": "该文件不存在",

View file

@ -13,6 +13,7 @@ enum ChipSetAction {
createAlbum,
// browsing or selecting
map,
slideshow,
stats,
// selecting (single/multiple filters)
delete,
@ -36,6 +37,7 @@ class ChipSetActions {
ChipSetAction.search,
ChipSetAction.createAlbum,
ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats,
];
@ -47,6 +49,7 @@ class ChipSetActions {
ChipSetAction.rename,
ChipSetAction.hide,
ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats,
];
}
@ -71,6 +74,8 @@ extension ExtraChipSetAction on ChipSetAction {
// browsing or selecting
case ChipSetAction.map:
return context.l10n.menuActionMap;
case ChipSetAction.slideshow:
return context.l10n.menuActionSlideshow;
case ChipSetAction.stats:
return context.l10n.menuActionStats;
// selecting (single/multiple filters)
@ -111,6 +116,8 @@ extension ExtraChipSetAction on ChipSetAction {
// browsing or selecting
case ChipSetAction.map:
return AIcons.map;
case ChipSetAction.slideshow:
return AIcons.slideshow;
case ChipSetAction.stats:
return AIcons.stats;
// selecting (single/multiple filters)

View file

@ -4,6 +4,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
enum EntryAction {
info,
addShortcut,
copyToClipboard,
delete,
@ -43,6 +44,7 @@ enum EntryAction {
class EntryActions {
static const topLevel = [
EntryAction.info,
EntryAction.share,
EntryAction.edit,
EntryAction.rename,
@ -102,6 +104,8 @@ class EntryActions {
extension ExtraEntryAction on EntryAction {
String getText(BuildContext context) {
switch (this) {
case EntryAction.info:
return context.l10n.entryActionInfo;
case EntryAction.addShortcut:
return context.l10n.collectionActionAddShortcut;
case EntryAction.copyToClipboard:
@ -188,6 +192,8 @@ extension ExtraEntryAction on EntryAction {
IconData getIconData() {
switch (this) {
case EntryAction.info:
return AIcons.info;
case EntryAction.addShortcut:
return AIcons.addShortcut;
case EntryAction.copyToClipboard:

View file

@ -15,6 +15,7 @@ enum EntrySetAction {
emptyBin,
// browsing or selecting
map,
slideshow,
stats,
rescan,
// selecting
@ -48,6 +49,7 @@ class EntrySetActions {
EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut,
EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats,
EntrySetAction.rescan,
EntrySetAction.emptyBin,
@ -59,6 +61,7 @@ class EntrySetActions {
EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut,
EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats,
EntrySetAction.rescan,
];
@ -72,6 +75,7 @@ class EntrySetActions {
EntrySetAction.rename,
EntrySetAction.toggleFavourite,
EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats,
EntrySetAction.rescan,
// editing actions are in their subsection
@ -86,6 +90,7 @@ class EntrySetActions {
EntrySetAction.rename,
EntrySetAction.toggleFavourite,
EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats,
EntrySetAction.rescan,
// editing actions are in their subsection
@ -125,6 +130,8 @@ extension ExtraEntrySetAction on EntrySetAction {
// browsing or selecting
case EntrySetAction.map:
return context.l10n.menuActionMap;
case EntrySetAction.slideshow:
return context.l10n.menuActionSlideshow;
case EntrySetAction.stats:
return context.l10n.menuActionStats;
case EntrySetAction.rescan:
@ -190,6 +197,8 @@ extension ExtraEntrySetAction on EntrySetAction {
// browsing or selecting
case EntrySetAction.map:
return AIcons.map;
case EntrySetAction.slideshow:
return AIcons.slideshow;
case EntrySetAction.stats:
return AIcons.stats;
case EntrySetAction.rescan:

View file

@ -0,0 +1,30 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
enum SlideshowAction {
resume,
showInCollection,
}
extension ExtraSlideshowAction on SlideshowAction {
String getText(BuildContext context) {
switch (this) {
case SlideshowAction.resume:
return context.l10n.slideshowActionResume;
case SlideshowAction.showInCollection:
return context.l10n.slideshowActionShowInCollection;
}
}
Widget getIcon() => Icon(_getIconData());
IconData _getIconData() {
switch (this) {
case SlideshowAction.resume:
return AIcons.play;
case SlideshowAction.showInCollection:
return AIcons.allCollection;
}
}
}

View file

@ -5,8 +5,8 @@ final Device device = Device._private();
class Device {
late final String _userAgent;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis;
late final bool _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canSetLockScreenWallpaper;
late final bool _isDynamicColorAvailable, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
String get userAgent => _userAgent;
@ -18,6 +18,10 @@ class Device {
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
bool get showPinShortcutFeedback => _showPinShortcutFeedback;
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
@ -33,6 +37,8 @@ class Device {
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canPrint = capabilities['canPrint'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false;
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;
_supportEdgeToEdgeUIMode = capabilities['supportEdgeToEdgeUIMode'] ?? false;
}

View file

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:aves/geo/countries.dart';
import 'package:aves/model/entry_cache.dart';
import 'package:aves/model/entry_dirs.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/geotiff.dart';
import 'package:aves/model/metadata/address.dart';
@ -31,7 +32,8 @@ class AvesEntry {
// `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode
int id;
String uri;
String? _path, _directory, _filename, _extension, _sourceTitle;
String? _path, _filename, _extension, _sourceTitle;
EntryDir? _directory;
int? pageId, contentId;
final String sourceMimeType;
int width, height, sourceRotationDegrees;
@ -175,8 +177,8 @@ class AvesEntry {
// directory path, without the trailing separator
String? get directory {
_directory ??= path != null ? pContext.dirname(path!) : null;
return _directory;
_directory ??= entryDirRepo.getOrCreate(path != null ? pContext.dirname(path!) : null);
return _directory!.resolved;
}
String? get filenameWithoutExtension {

68
lib/model/entry_dirs.dart Normal file
View file

@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:io';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart';
final entryDirRepo = EntryDirRepo._private();
class EntryDirRepo {
EntryDirRepo._private();
// mapping between the raw entry directory path to a resolvable directory
final Map<String?, EntryDir> _dirs = {};
final StreamController<EntryDir> _ambiguousDirStreamController = StreamController.broadcast();
Stream<EntryDir> get ambiguousDirStream => _ambiguousDirStreamController.stream;
// get a resolvable directory for a raw entry directory path
EntryDir getOrCreate(String? asIs) {
var entryDir = _dirs[asIs];
if (entryDir != null) return entryDir;
final asIsLower = asIs?.toLowerCase();
entryDir = _dirs.values.firstWhereOrNull((dir) => dir.asIsLower == asIsLower);
if (entryDir != null && !entryDir.ambiguous) {
entryDir.ambiguous = true;
_ambiguousDirStreamController.add(entryDir);
}
return _dirs.putIfAbsent(asIs, () => entryDir ?? EntryDir(asIs));
}
}
// Some directories are ambiguous because they use different cases,
// but the OS merge and present them as one directory.
// This class resolves ambiguous directories to get the directory path
// with the right case, as presented by the OS.
class EntryDir {
final String? asIs, asIsLower;
bool ambiguous = false;
String? _resolved;
EntryDir(this.asIs) : asIsLower = asIs?.toLowerCase();
String? get resolved {
if (!ambiguous) return asIs;
if (asIs == null) return null;
_resolved ??= _resolve();
return _resolved;
}
String? _resolve() {
final vrl = VolumeRelativeDirectory.fromPath(asIs!);
if (vrl == null || vrl.relativeDir.isEmpty) return asIs;
var resolved = vrl.volumePath;
final parts = pContext.split(vrl.relativeDir);
for (final part in parts) {
final partLower = part.toLowerCase();
final childrenDirs = Directory(resolved).listSync().where((v) => v.absolute is Directory).toSet();
final found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower);
resolved = found?.path ?? '$resolved${pContext.separator}$part';
}
return resolved;
}
}

View file

@ -5,6 +5,7 @@ import 'package:aves/image_providers/thumbnail_provider.dart';
import 'package:aves/image_providers/uri_image_provider.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_cache.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
@ -63,4 +64,15 @@ extension ExtraAvesEntryImages on AvesEntry {
final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady);
return sizedThumbnailKey != null ? ThumbnailProvider(sizedThumbnailKey) : getThumbnail();
}
// magic number used to derive sample size from scale
static const scaleFactor = 2.0;
static int sampleSizeForScale(double scale) {
var sample = 0;
if (0 < scale && scale < 1) {
sample = highestPowerOf2((1 / scale) / scaleFactor);
}
return max<int>(1, sample);
}
}

View file

@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
@ -19,11 +20,25 @@ class QueryFilter extends CollectionFilter {
@override
List<Object?> get props => [query, live];
static final _fieldPattern = RegExp(r'(.+)([=<>])(.+)');
static final _fileSizePattern = RegExp(r'(\d+)([KMG])?');
static const keyContentId = 'ID';
static const keyContentYear = 'YEAR';
static const keyContentMonth = 'MONTH';
static const keyContentDay = 'DAY';
static const keyContentWidth = 'WIDTH';
static const keyContentHeight = 'HEIGHT';
static const keyContentSize = 'SIZE';
static const opEqual = '=';
static const opLower = '<';
static const opGreater = '>';
QueryFilter(this.query, {this.colorful = true, this.live = false}) {
var upQuery = query.toUpperCase();
if (upQuery.startsWith('ID:')) {
final id = int.tryParse(upQuery.substring(3));
_test = (entry) => entry.id == id;
final test = fieldTest(upQuery);
if (test != null) {
_test = test;
return;
}
@ -82,4 +97,114 @@ class QueryFilter extends CollectionFilter {
@override
String get key => '$type-$query';
EntryFilter? fieldTest(String upQuery) {
var match = _fieldPattern.firstMatch(upQuery);
if (match == null) return null;
final key = match.group(1)?.trim();
final op = match.group(2)?.trim();
var valueString = match.group(3)?.trim();
if (key == null || op == null || valueString == null) return null;
final valueInt = int.tryParse(valueString);
switch (key) {
case keyContentId:
if (valueInt == null) return null;
if (op == opEqual) {
return (entry) => entry.contentId == valueInt;
}
break;
case keyContentYear:
if (valueInt == null) return null;
switch (op) {
case opEqual:
return (entry) => (entry.bestDate?.year ?? 0) == valueInt;
case opLower:
return (entry) => (entry.bestDate?.year ?? 0) < valueInt;
case opGreater:
return (entry) => (entry.bestDate?.year ?? 0) > valueInt;
}
break;
case keyContentMonth:
if (valueInt == null) return null;
switch (op) {
case opEqual:
return (entry) => (entry.bestDate?.month ?? 0) == valueInt;
case opLower:
return (entry) => (entry.bestDate?.month ?? 0) < valueInt;
case opGreater:
return (entry) => (entry.bestDate?.month ?? 0) > valueInt;
}
break;
case keyContentDay:
if (valueInt == null) return null;
switch (op) {
case opEqual:
return (entry) => (entry.bestDate?.day ?? 0) == valueInt;
case opLower:
return (entry) => (entry.bestDate?.day ?? 0) < valueInt;
case opGreater:
return (entry) => (entry.bestDate?.day ?? 0) > valueInt;
}
break;
case keyContentWidth:
if (valueInt == null) return null;
switch (op) {
case opEqual:
return (entry) => entry.displaySize.width == valueInt;
case opLower:
return (entry) => entry.displaySize.width < valueInt;
case opGreater:
return (entry) => entry.displaySize.width > valueInt;
}
break;
case keyContentHeight:
if (valueInt == null) return null;
switch (op) {
case opEqual:
return (entry) => entry.displaySize.height == valueInt;
case opLower:
return (entry) => entry.displaySize.height < valueInt;
case opGreater:
return (entry) => entry.displaySize.height > valueInt;
}
break;
case keyContentSize:
match = _fileSizePattern.firstMatch(valueString);
if (match == null) return null;
valueString = match.group(1)?.trim();
if (valueString == null) return null;
final valueInt = int.tryParse(valueString);
if (valueInt == null) return null;
var bytes = valueInt;
final multiplierString = match.group(2)?.trim();
switch (multiplierString) {
case 'K':
bytes *= kilo;
break;
case 'M':
bytes *= mega;
break;
case 'G':
bytes *= giga;
break;
}
switch (op) {
case opEqual:
return (entry) => (entry.sizeBytes ?? 0) == bytes;
case opLower:
return (entry) => (entry.sizeBytes ?? 0) < bytes;
case opGreater:
return (entry) => (entry.sizeBytes ?? 0) > bytes;
}
break;
}
return null;
}
}

View file

@ -17,11 +17,15 @@ class SettingsDefaults {
static const canUseAnalysisService = true;
static const isInstalledAppAccessAllowed = false;
static const isErrorReportingAllowed = false;
static const tileLayout = TileLayout.grid;
static const entryRenamingPattern = '<${DateNamingProcessor.key}, yyyyMMdd-HHmmss> <${NameNamingProcessor.key}>';
// display
static const displayRefreshRateMode = DisplayRefreshRateMode.auto;
static const themeBrightness = AvesThemeBrightness.system;
static const themeColorMode = AvesThemeColorMode.polychrome;
static const tileLayout = TileLayout.grid;
static const entryRenamingPattern = '<${DateNamingProcessor.key}, yyyyMMdd-HHmmss> <${NameNamingProcessor.key}>';
static const enableDynamicColor = false;
static const enableBlurEffect = true; // `enableBlurEffect` has a contextual default value
// navigation
static const mustBackTwiceToExit = true;
@ -79,7 +83,6 @@ class SettingsDefaults {
static const showOverlayInfo = true;
static const showOverlayShootingDetails = false;
static const showOverlayThumbnailPreview = false;
static const enableOverlayBlurEffect = true; // `enableOverlayBlurEffect` has a contextual default value
static const viewerUseCutout = true;
static const viewerMaxBrightness = false;
static const enableMotionPhotoAutoPlay = false;
@ -122,6 +125,13 @@ class SettingsDefaults {
// file picker
static const filePickerShowHiddenFiles = false;
// slideshow
static const slideshowRepeat = false;
static const slideshowShuffle = false;
static const slideshowTransition = ViewerTransition.fade;
static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted;
static const slideshowInterval = SlideshowInterval.s5;
// platform settings
static const isRotationLocked = false;
static const areAnimationsRemoved = false;

View file

@ -2,24 +2,30 @@ enum AccessibilityAnimations { system, disabled, enabled }
enum AccessibilityTimeout { system, appDefault, s3, s10, s30, s60, s120 }
enum AvesThemeColorMode { monochrome, polychrome }
enum AvesThemeBrightness { system, light, dark, black }
enum AvesThemeColorMode { monochrome, polychrome }
enum ConfirmationDialog { deleteForever, moveToBin, moveUndatedItems }
enum CoordinateFormat { dms, decimal }
enum DisplayRefreshRateMode { auto, highest, lowest }
enum EntryBackground { black, white, checkered }
enum HomePageSetting { collection, albums }
enum KeepScreenOn { never, viewerOnly, always }
enum DisplayRefreshRateMode { auto, highest, lowest }
enum SlideshowInterval { s3, s5, s10, s30, s60 }
enum SlideshowVideoPlayback { skip, playMuted, playWithSound }
enum UnitSystem { metric, imperial }
enum VideoControls { play, playSeek, playOutside, none }
enum VideoLoopMode { never, shortOnly, always }
enum VideoControls { play, playSeek, playOutside, none }
enum ViewerTransition { slide, parallax, fade, zoomIn }

View file

@ -0,0 +1,36 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSlideshowInterval on SlideshowInterval {
String getName(BuildContext context) {
switch (this) {
case SlideshowInterval.s3:
return context.l10n.timeSeconds(3);
case SlideshowInterval.s5:
return context.l10n.timeSeconds(5);
case SlideshowInterval.s10:
return context.l10n.timeSeconds(10);
case SlideshowInterval.s30:
return context.l10n.timeSeconds(30);
case SlideshowInterval.s60:
return context.l10n.timeMinutes(1);
}
}
Duration getDuration() {
switch (this) {
case SlideshowInterval.s3:
return const Duration(seconds: 3);
case SlideshowInterval.s5:
return const Duration(seconds: 5);
case SlideshowInterval.s10:
return const Duration(seconds: 10);
case SlideshowInterval.s30:
return const Duration(seconds: 30);
case SlideshowInterval.s60:
return const Duration(minutes: 1);
}
}
}

View file

@ -0,0 +1,17 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSlideshowVideoPlayback on SlideshowVideoPlayback {
String getName(BuildContext context) {
switch (this) {
case SlideshowVideoPlayback.skip:
return context.l10n.slideshowVideoPlaybackSkip;
case SlideshowVideoPlayback.playMuted:
return context.l10n.slideshowVideoPlaybackMuted;
case SlideshowVideoPlayback.playWithSound:
return context.l10n.slideshowVideoPlaybackWithSound;
}
}
}

View file

@ -0,0 +1,33 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraViewerTransition on ViewerTransition {
String getName(BuildContext context) {
switch (this) {
case ViewerTransition.slide:
return context.l10n.viewerTransitionSlide;
case ViewerTransition.parallax:
return context.l10n.viewerTransitionParallax;
case ViewerTransition.fade:
return context.l10n.viewerTransitionFade;
case ViewerTransition.zoomIn:
return context.l10n.viewerTransitionZoomIn;
}
}
TransitionBuilder builder(PageController pageController, int index) {
switch (this) {
case ViewerTransition.slide:
return PageTransitionEffects.slide(pageController, index, parallax: false);
case ViewerTransition.parallax:
return PageTransitionEffects.slide(pageController, index, parallax: true);
case ViewerTransition.fade:
return PageTransitionEffects.fade(pageController, index, zoomIn: false);
case ViewerTransition.zoomIn:
return PageTransitionEffects.fade(pageController, index, zoomIn: true);
}
}
}

View file

@ -10,6 +10,7 @@ import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/accessibility_service.dart';
import 'package:aves/services/common/optional_event_channel.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves_map/aves_map.dart';
import 'package:collection/collection.dart';
@ -19,7 +20,7 @@ import 'package:flutter/services.dart';
final Settings settings = Settings._private();
class Settings extends ChangeNotifier {
final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settings_change');
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
Stream<SettingsChangedEvent> get updateStream => _updateStreamController.stream;
@ -42,15 +43,19 @@ class Settings extends ChangeNotifier {
static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed';
static const isErrorReportingAllowedKey = 'is_crashlytics_enabled';
static const localeKey = 'locale';
static const displayRefreshRateModeKey = 'display_refresh_rate_mode';
static const themeBrightnessKey = 'theme_brightness';
static const themeColorModeKey = 'theme_color_mode';
static const catalogTimeZoneKey = 'catalog_time_zone';
static const tileExtentPrefixKey = 'tile_extent_';
static const tileLayoutPrefixKey = 'tile_layout_';
static const entryRenamingPatternKey = 'entry_renaming_pattern';
static const topEntryIdsKey = 'top_entry_ids';
// display
static const displayRefreshRateModeKey = 'display_refresh_rate_mode';
static const themeBrightnessKey = 'theme_brightness';
static const themeColorModeKey = 'theme_color_mode';
static const enableDynamicColorKey = 'dynamic_color';
static const enableBlurEffectKey = 'enable_overlay_blur_effect';
// navigation
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const keepScreenOnKey = 'keep_screen_on';
@ -92,7 +97,6 @@ class Settings extends ChangeNotifier {
static const showOverlayInfoKey = 'show_overlay_info';
static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details';
static const showOverlayThumbnailPreviewKey = 'show_overlay_thumbnail_preview';
static const enableOverlayBlurEffectKey = 'enable_overlay_blur_effect';
static const viewerUseCutoutKey = 'viewer_use_cutout';
static const viewerMaxBrightnessKey = 'viewer_max_brightness';
static const enableMotionPhotoAutoPlayKey = 'motion_photo_auto_play';
@ -134,6 +138,13 @@ class Settings extends ChangeNotifier {
// file picker
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
// slideshow
static const slideshowRepeatKey = 'slideshow_loop';
static const slideshowShuffleKey = 'slideshow_shuffle';
static const slideshowTransitionKey = 'slideshow_transition';
static const slideshowVideoPlaybackKey = 'slideshow_video_playback';
static const slideshowIntervalKey = 'slideshow_interval';
// platform settings
// cf Android `Settings.System.ACCELEROMETER_ROTATION`
static const platformAccelerometerRotationKey = 'accelerometer_rotation';
@ -161,7 +172,7 @@ class Settings extends ChangeNotifier {
Future<void> setContextualDefaults() async {
// performance
final performanceClass = await deviceService.getPerformanceClass();
enableOverlayBlurEffect = performanceClass >= 29;
enableBlurEffect = performanceClass >= 29;
// availability
final defaultMapStyle = mobileServices.defaultMapStyle;
@ -187,8 +198,7 @@ class Settings extends ChangeNotifier {
set canUseAnalysisService(bool newValue) => setAndNotify(canUseAnalysisServiceKey, newValue);
// TODO TLAD use `true` for transition (it's unset in v1.5.4), but replace by `SettingsDefaults.isInstalledAppAccessAllowed` in a later release
bool get isInstalledAppAccessAllowed => getBoolOrDefault(isInstalledAppAccessAllowedKey, true);
bool get isInstalledAppAccessAllowed => getBoolOrDefault(isInstalledAppAccessAllowedKey, SettingsDefaults.isInstalledAppAccessAllowed);
set isInstalledAppAccessAllowed(bool newValue) => setAndNotify(isInstalledAppAccessAllowedKey, newValue);
@ -249,18 +259,6 @@ class Settings extends ChangeNotifier {
return _appliedLocale!;
}
DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values);
set displayRefreshRateMode(DisplayRefreshRateMode newValue) => setAndNotify(displayRefreshRateModeKey, newValue.toString());
AvesThemeBrightness get themeBrightness => getEnumOrDefault(themeBrightnessKey, SettingsDefaults.themeBrightness, AvesThemeBrightness.values);
set themeBrightness(AvesThemeBrightness newValue) => setAndNotify(themeBrightnessKey, newValue.toString());
AvesThemeColorMode get themeColorMode => getEnumOrDefault(themeColorModeKey, SettingsDefaults.themeColorMode, AvesThemeColorMode.values);
set themeColorMode(AvesThemeColorMode newValue) => setAndNotify(themeColorModeKey, newValue.toString());
String get catalogTimeZone => getString(catalogTimeZoneKey) ?? '';
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
@ -281,6 +279,28 @@ class Settings extends ChangeNotifier {
set topEntryIds(List<int>? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList());
// display
DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values);
set displayRefreshRateMode(DisplayRefreshRateMode newValue) => setAndNotify(displayRefreshRateModeKey, newValue.toString());
AvesThemeBrightness get themeBrightness => getEnumOrDefault(themeBrightnessKey, SettingsDefaults.themeBrightness, AvesThemeBrightness.values);
set themeBrightness(AvesThemeBrightness newValue) => setAndNotify(themeBrightnessKey, newValue.toString());
AvesThemeColorMode get themeColorMode => getEnumOrDefault(themeColorModeKey, SettingsDefaults.themeColorMode, AvesThemeColorMode.values);
set themeColorMode(AvesThemeColorMode newValue) => setAndNotify(themeColorModeKey, newValue.toString());
bool get enableDynamicColor => getBoolOrDefault(enableDynamicColorKey, SettingsDefaults.enableDynamicColor);
set enableDynamicColor(bool newValue) => setAndNotify(enableDynamicColorKey, newValue);
bool get enableBlurEffect => getBoolOrDefault(enableBlurEffectKey, SettingsDefaults.enableBlurEffect);
set enableBlurEffect(bool newValue) => setAndNotify(enableBlurEffectKey, newValue);
// navigation
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, SettingsDefaults.mustBackTwiceToExit);
@ -441,10 +461,6 @@ class Settings extends ChangeNotifier {
set showOverlayThumbnailPreview(bool newValue) => setAndNotify(showOverlayThumbnailPreviewKey, newValue);
bool get enableOverlayBlurEffect => getBoolOrDefault(enableOverlayBlurEffectKey, SettingsDefaults.enableOverlayBlurEffect);
set enableOverlayBlurEffect(bool newValue) => setAndNotify(enableOverlayBlurEffectKey, newValue);
bool get viewerUseCutout => getBoolOrDefault(viewerUseCutoutKey, SettingsDefaults.viewerUseCutout);
set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue);
@ -567,6 +583,28 @@ class Settings extends ChangeNotifier {
set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue);
// slideshow
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
set slideshowRepeat(bool newValue) => setAndNotify(slideshowRepeatKey, newValue);
bool get slideshowShuffle => getBoolOrDefault(slideshowShuffleKey, SettingsDefaults.slideshowShuffle);
set slideshowShuffle(bool newValue) => setAndNotify(slideshowShuffleKey, newValue);
ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString());
SlideshowVideoPlayback get slideshowVideoPlayback => getEnumOrDefault(slideshowVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values);
set slideshowVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(slideshowVideoPlaybackKey, newValue.toString());
SlideshowInterval get slideshowInterval => getEnumOrDefault(slideshowIntervalKey, SettingsDefaults.slideshowInterval, SlideshowInterval.values);
set slideshowInterval(SlideshowInterval newValue) => setAndNotify(slideshowIntervalKey, newValue.toString());
// convenience methods
int? getInt(String key) => settingsStore.getInt(key);
@ -695,6 +733,8 @@ class Settings extends ChangeNotifier {
break;
case isInstalledAppAccessAllowedKey:
case isErrorReportingAllowedKey:
case enableDynamicColorKey:
case enableBlurEffectKey:
case showBottomNavigationBarKey:
case mustBackTwiceToExitKey:
case confirmDeleteForeverKey:
@ -713,7 +753,6 @@ class Settings extends ChangeNotifier {
case showOverlayInfoKey:
case showOverlayShootingDetailsKey:
case showOverlayThumbnailPreviewKey:
case enableOverlayBlurEffectKey:
case viewerUseCutoutKey:
case viewerMaxBrightnessKey:
case enableMotionPhotoAutoPlayKey:
@ -724,6 +763,8 @@ class Settings extends ChangeNotifier {
case subtitleShowOutlineKey:
case saveSearchHistoryKey:
case filePickerShowHiddenFilesKey:
case slideshowRepeatKey:
case slideshowShuffleKey:
if (newValue is bool) {
settingsStore.setBool(key, newValue);
} else {
@ -751,6 +792,9 @@ class Settings extends ChangeNotifier {
case unitSystemKey:
case accessibilityAnimationsKey:
case timeToTakeActionKey:
case slideshowTransitionKey:
case slideshowVideoPlaybackKey:
case slideshowIntervalKey:
if (newValue is String) {
settingsStore.setString(key, newValue);
} else {

View file

@ -32,7 +32,7 @@ class CollectionLens with ChangeNotifier {
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = [];
int? id;
bool listenToSource, groupBursts;
bool listenToSource, groupBursts, fixedSort;
List<AvesEntry>? fixedSelection;
List<AvesEntry> _filteredSortedEntries = [];
@ -45,6 +45,7 @@ class CollectionLens with ChangeNotifier {
this.id,
this.listenToSource = true,
this.groupBursts = true,
this.fixedSort = false,
this.fixedSelection,
}) : filters = (filters ?? {}).whereNotNull().toSet(),
sectionFactor = settings.collectionSectionFactor,
@ -203,6 +204,8 @@ class CollectionLens with ChangeNotifier {
}
void _applySort() {
if (fixedSort) return;
switch (sortFactor) {
case EntrySortFactor.date:
_filteredSortedEntries.sort(AvesEntry.compareByDate);
@ -220,37 +223,43 @@ class CollectionLens with ChangeNotifier {
}
void _applySection() {
switch (sortFactor) {
case EntrySortFactor.date:
switch (sectionFactor) {
case EntryGroupFactor.album:
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
break;
case EntryGroupFactor.month:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
break;
case EntryGroupFactor.day:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
break;
case EntryGroupFactor.none:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
}
break;
case EntrySortFactor.name:
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!));
break;
case EntrySortFactor.rating:
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
break;
case EntrySortFactor.size:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
if (fixedSort) {
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
} else {
switch (sortFactor) {
case EntrySortFactor.date:
switch (sectionFactor) {
case EntryGroupFactor.album:
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
break;
case EntryGroupFactor.month:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
break;
case EntryGroupFactor.day:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
break;
case EntryGroupFactor.none:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
}
break;
case EntrySortFactor.name:
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!));
break;
case EntrySortFactor.rating:
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
break;
case EntrySortFactor.size:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
}
}
sections = Map.unmodifiable(sections);
_sortedEntries = null;

View file

@ -416,6 +416,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
}
}
if (startAnalysisService) {
// TODO TLAD [tiramisu] explain foreground service and request POST_NOTIFICATIONS permission
await AnalysisService.startService(
force: force,
entryIds: entries?.map((entry) => entry.id).toList(),

View file

@ -0,0 +1,17 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
enum WallpaperTarget { home, lock, homeLock }
extension ExtraWallpaperTarget on WallpaperTarget {
String getName(BuildContext context) {
switch (this) {
case WallpaperTarget.home:
return context.l10n.wallpaperTargetHome;
case WallpaperTarget.lock:
return context.l10n.wallpaperTargetLock;
case WallpaperTarget.homeLock:
return context.l10n.wallpaperTargetHomeLock;
}
}
}

View file

@ -44,6 +44,8 @@ class MimeTypes {
static const anyVideo = 'video/*';
static const v3gpp = 'video/3gpp';
static const asf = 'video/x-ms-asf';
static const avi = 'video/avi';
static const aviVnd = 'video/vnd.avi';
static const flv = 'video/flv';
@ -56,6 +58,7 @@ class MimeTypes {
static const mpeg = 'video/mpeg';
static const ogv = 'video/ogg';
static const webm = 'video/webm';
static const wmv = 'video/x-ms-wmv';
static const json = 'application/json';
static const plainText = 'text/plain';
@ -76,7 +79,7 @@ class MimeTypes {
static const Set<String> _knownOpaqueImages = {jpeg};
static const Set<String> _knownVideos = {avi, aviVnd, flv, flvX, mkv, mov, mp2t, mp2ts, mp4, mpeg, ogv, webm};
static const Set<String> _knownVideos = {v3gpp, asf, avi, aviVnd, flv, flvX, mkv, mov, mp2t, mp2ts, mp4, mpeg, ogv, webm, wmv};
static final Set<String> knownMediaTypes = {
anyImage,

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
// adapted from Flutter `EventChannel` in `/services/platform_channel.dart`
// to use an `OptionalMethodChannel` when subscribing to events
class OptionalEventChannel extends EventChannel {
const OptionalEventChannel(super.name, [super.codec = const StandardMethodCodec(), super.binaryMessenger]);
@override
Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
final MethodChannel methodChannel = OptionalMethodChannel(name, codec);
late StreamController<dynamic> controller;
controller = StreamController<dynamic>.broadcast(onListen: () async {
binaryMessenger.setMessageHandler(name, (reply) async {
if (reply == null) {
await controller.close();
} else {
try {
controller.add(codec.decodeEnvelope(reply));
} on PlatformException catch (e) {
controller.addError(e);
}
}
return null;
});
try {
await methodChannel.invokeMethod<void>('listen', arguments);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('while activating platform stream on channel $name'),
));
}
}, onCancel: () async {
binaryMessenger.setMessageHandler(name, null);
try {
await methodChannel.invokeMethod<void>('cancel', arguments);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('while de-activating platform stream on channel $name'),
));
}
});
return controller.stream;
}
}

View file

@ -22,7 +22,11 @@ class GeocodingService {
});
return (result as List).cast<Map>().map(Address.fromMap).toList();
} on PlatformException catch (e, stack) {
if (e.code != 'getAddress-empty' && e.code != 'getAddress-network') {
if (!{
'getAddress-empty',
'getAddress-network',
'getAddress-unavailable',
}.contains(e.code)) {
await reportService.recordError(e, stack);
}
}

View file

@ -0,0 +1,23 @@
import 'dart:typed_data';
import 'package:aves/model/wallpaper_target.dart';
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class WallpaperService {
static const platform = MethodChannel('deckers.thibault/aves/wallpaper');
static Future<bool> set(Uint8List bytes, WallpaperTarget target) async {
try {
await platform.invokeMethod('setWallpaper', <String, dynamic>{
'bytes': bytes,
'home': {WallpaperTarget.home, WallpaperTarget.homeLock}.contains(target),
'lock': {WallpaperTarget.lock, WallpaperTarget.homeLock}.contains(target),
});
return true;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
}

View file

@ -104,6 +104,7 @@ class AIcons {
static const IconData setCover = MdiIcons.imageEditOutline;
static const IconData share = Icons.share_outlined;
static const IconData show = Icons.visibility_outlined;
static const IconData slideshow = Icons.slideshow_outlined;
static const IconData speed = Icons.speed_outlined;
static const IconData stats = Icons.pie_chart_outline_outlined;
static const IconData streams = Icons.translate_outlined;

View file

@ -7,7 +7,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class Themes {
static const _accentColor = Colors.indigoAccent;
static const defaultAccent = Colors.indigoAccent;
static const _tooltipTheme = TooltipThemeData(
verticalOffset: 32,
@ -19,10 +19,10 @@ class Themes {
fontFeatures: [FontFeature.enable('smcp')],
);
static const _snackBarTheme = SnackBarThemeData(
actionTextColor: _accentColor,
behavior: SnackBarBehavior.floating,
);
static SnackBarThemeData _snackBarTheme(Color accentColor) => SnackBarThemeData(
actionTextColor: accentColor,
behavior: SnackBarBehavior.floating,
);
static final _typography = Typography.material2018(platform: TargetPlatform.android);
@ -35,49 +35,49 @@ class Themes {
static const _lightSecondLayer = Color(0xFFF5F5F5); // aka `Colors.grey[100]`
static const _lightThirdLayer = Color(0xFFEEEEEE); // aka `Colors.grey[200]`
static final lightTheme = ThemeData(
colorScheme: ColorScheme.light(
primary: _accentColor,
secondary: _accentColor,
onPrimary: _lightBodyColor,
onSecondary: _lightBodyColor,
),
brightness: Brightness.light,
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _lightSecondLayer,
scaffoldBackgroundColor: _lightFirstLayer,
// `cardColor` is used by `ExpansionPanel`
cardColor: _lightSecondLayer,
dialogBackgroundColor: _lightSecondLayer,
indicatorColor: _accentColor,
toggleableActiveColor: _accentColor,
typography: _typography,
appBarTheme: AppBarTheme(
backgroundColor: _lightFirstLayer,
// `foregroundColor` is used by icons
foregroundColor: _lightActionIconColor,
// `titleTextStyle.color` is used by text
titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor),
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
listTileTheme: const ListTileThemeData(
iconColor: _lightActionIconColor,
),
popupMenuTheme: const PopupMenuThemeData(
color: _lightSecondLayer,
),
snackBarTheme: _snackBarTheme,
tabBarTheme: TabBarTheme(
labelColor: _lightTitleColor,
unselectedLabelColor: Colors.black54,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
primary: _lightLabelColor,
),
),
tooltipTheme: _tooltipTheme,
);
static ThemeData lightTheme(Color accentColor) => ThemeData(
colorScheme: ColorScheme.light(
primary: accentColor,
secondary: accentColor,
onPrimary: _lightBodyColor,
onSecondary: _lightBodyColor,
),
brightness: Brightness.light,
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _lightSecondLayer,
scaffoldBackgroundColor: _lightFirstLayer,
// `cardColor` is used by `ExpansionPanel`
cardColor: _lightSecondLayer,
dialogBackgroundColor: _lightSecondLayer,
indicatorColor: accentColor,
toggleableActiveColor: accentColor,
typography: _typography,
appBarTheme: AppBarTheme(
backgroundColor: _lightFirstLayer,
// `foregroundColor` is used by icons
foregroundColor: _lightActionIconColor,
// `titleTextStyle.color` is used by text
titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor),
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
listTileTheme: const ListTileThemeData(
iconColor: _lightActionIconColor,
),
popupMenuTheme: const PopupMenuThemeData(
color: _lightSecondLayer,
),
snackBarTheme: _snackBarTheme(accentColor),
tabBarTheme: TabBarTheme(
labelColor: _lightTitleColor,
unselectedLabelColor: Colors.black54,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
primary: _lightLabelColor,
),
),
tooltipTheme: _tooltipTheme,
);
static final _darkThemeTypo = _typography.white;
static final _darkTitleColor = _darkThemeTypo.titleMedium!.color!;
@ -87,71 +87,74 @@ class Themes {
static const _darkSecondLayer = Color(0xFF363636);
static const _darkThirdLayer = Color(0xFF424242); // aka `Colors.grey[800]`
static final darkTheme = ThemeData(
colorScheme: ColorScheme.dark(
primary: _accentColor,
secondary: _accentColor,
// surface color is used by the date/time pickers
surface: Colors.grey.shade800,
onPrimary: _darkBodyColor,
onSecondary: _darkBodyColor,
),
brightness: Brightness.dark,
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _darkSecondLayer,
scaffoldBackgroundColor: _darkFirstLayer,
// `cardColor` is used by `ExpansionPanel`
cardColor: _darkSecondLayer,
dialogBackgroundColor: _darkSecondLayer,
indicatorColor: _accentColor,
toggleableActiveColor: _accentColor,
typography: _typography,
appBarTheme: AppBarTheme(
backgroundColor: _darkFirstLayer,
// `foregroundColor` is used by icons
foregroundColor: _darkTitleColor,
// `titleTextStyle.color` is used by text
titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor),
systemOverlayStyle: SystemUiOverlayStyle.light,
),
popupMenuTheme: const PopupMenuThemeData(
color: _darkSecondLayer,
),
snackBarTheme: _snackBarTheme.copyWith(
backgroundColor: Colors.grey.shade800,
contentTextStyle: TextStyle(
color: _darkBodyColor,
),
),
tabBarTheme: TabBarTheme(
labelColor: _darkTitleColor,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
primary: _darkLabelColor,
),
),
tooltipTheme: _tooltipTheme,
);
static ThemeData darkTheme(Color accentColor) => ThemeData(
colorScheme: ColorScheme.dark(
primary: accentColor,
secondary: accentColor,
// surface color is used by the date/time pickers
surface: Colors.grey.shade800,
onPrimary: _darkBodyColor,
onSecondary: _darkBodyColor,
),
brightness: Brightness.dark,
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _darkSecondLayer,
scaffoldBackgroundColor: _darkFirstLayer,
// `cardColor` is used by `ExpansionPanel`
cardColor: _darkSecondLayer,
dialogBackgroundColor: _darkSecondLayer,
indicatorColor: accentColor,
toggleableActiveColor: accentColor,
typography: _typography,
appBarTheme: AppBarTheme(
backgroundColor: _darkFirstLayer,
// `foregroundColor` is used by icons
foregroundColor: _darkTitleColor,
// `titleTextStyle.color` is used by text
titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor),
systemOverlayStyle: SystemUiOverlayStyle.light,
),
popupMenuTheme: const PopupMenuThemeData(
color: _darkSecondLayer,
),
snackBarTheme: _snackBarTheme(accentColor).copyWith(
backgroundColor: Colors.grey.shade800,
contentTextStyle: TextStyle(
color: _darkBodyColor,
),
),
tabBarTheme: TabBarTheme(
labelColor: _darkTitleColor,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
primary: _darkLabelColor,
),
),
tooltipTheme: _tooltipTheme,
);
static const _blackFirstLayer = Colors.black;
static const _blackSecondLayer = Color(0xFF212121); // aka `Colors.grey[900]`
static const _blackThirdLayer = Color(0xFF303030); // aka `Colors.grey[850]`
static final blackTheme = darkTheme.copyWith(
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _blackSecondLayer,
scaffoldBackgroundColor: _blackFirstLayer,
// `cardColor` is used by `ExpansionPanel`
cardColor: _blackSecondLayer,
dialogBackgroundColor: _blackSecondLayer,
appBarTheme: darkTheme.appBarTheme.copyWith(
backgroundColor: _blackFirstLayer,
),
popupMenuTheme: darkTheme.popupMenuTheme.copyWith(
color: _blackSecondLayer,
),
);
static ThemeData blackTheme(Color accentColor) {
final baseTheme = darkTheme(accentColor);
return baseTheme.copyWith(
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _blackSecondLayer,
scaffoldBackgroundColor: _blackFirstLayer,
// `cardColor` is used by `ExpansionPanel`
cardColor: _blackSecondLayer,
dialogBackgroundColor: _blackSecondLayer,
appBarTheme: baseTheme.appBarTheme.copyWith(
backgroundColor: _blackFirstLayer,
),
popupMenuTheme: baseTheme.popupMenuTheme.copyWith(
color: _blackSecondLayer,
),
);
}
static Color overlayBackgroundColor({
required Brightness brightness,

View file

@ -108,6 +108,11 @@ class Constants {
licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/device_info_plus/device_info_plus/LICENSE',
sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus',
),
Dependency(
name: 'Dynamic Color',
license: 'BSD 3-Clause',
sourceUrl: 'https://github.com/material-foundation/material-dynamic-color-flutter',
),
Dependency(
name: 'fijkplayer (Aves fork)',
license: 'MIT',
@ -337,6 +342,12 @@ class Constants {
license: 'Apache 2.0',
sourceUrl: 'https://github.com/jifalops/dart-latlong',
),
Dependency(
name: 'Material Color Utilities',
license: 'Apache 2.0',
licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE',
sourceUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart',
),
Dependency(
name: 'Path',
license: 'BSD 3-Clause',

View file

@ -1,16 +1,16 @@
import 'package:intl/intl.dart';
const _kiloDivider = 1024;
const _megaDivider = _kiloDivider * _kiloDivider;
const _gigaDivider = _megaDivider * _kiloDivider;
const _teraDivider = _gigaDivider * _kiloDivider;
const kilo = 1024;
const mega = kilo * kilo;
const giga = mega * kilo;
const tera = giga * kilo;
String formatFileSize(String locale, int size, {int round = 2}) {
if (size < _kiloDivider) return '$size B';
if (size < kilo) return '$size B';
final formatter = NumberFormat('0${round > 0 ? '.${'0' * round}' : ''}', locale);
if (size < _megaDivider) return '${formatter.format(size / _kiloDivider)} KB';
if (size < _gigaDivider) return '${formatter.format(size / _megaDivider)} MB';
if (size < _teraDivider) return '${formatter.format(size / _gigaDivider)} GB';
return '${formatter.format(size / _teraDivider)} TB';
if (size < mega) return '${formatter.format(size / kilo)} KB';
if (size < giga) return '${formatter.format(size / mega)} MB';
if (size < tera) return '${formatter.format(size / giga)} GB';
return '${formatter.format(size / tera)} TB';
}

View file

@ -13,6 +13,7 @@ class AboutCredits extends StatelessWidget {
'Español (México)': 'n-berenice',
'Italiano': 'glemco',
'Português (Brasil)': 'Jonatas De Almeida Barros',
'Türkçe': 'metezd',
'Русский': 'D3ZOXY',
'日本語': 'Maki',
'简体中文': '小默, Aerowolf',

View file

@ -5,6 +5,7 @@ import 'package:aves/app_flavor.dart';
import 'package:aves/app_mode.dart';
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart';
import 'package:aves/model/settings/enums/enums.dart';
@ -15,6 +16,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/accessibility_service.dart';
import 'package:aves/services/common/optional_event_channel.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart';
@ -30,11 +32,13 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:equatable/equatable.dart';
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
@ -70,6 +74,7 @@ class AvesApp extends StatefulWidget {
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
final ValueNotifier<AppMode> appModeNotifier = ValueNotifier(AppMode.main);
late Future<void> _appSetup;
late Future<CorePalette?> _dynamicColorPaletteLoader;
final _mediaStoreSource = MediaStoreSource();
final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.mediaContentChangeDebounceDelay);
final Set<String> changedUris = {};
@ -77,10 +82,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// observers are not registered when using the same list object with different items
// the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = [AvesApp.pageRouteObserver];
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/media_store_change');
final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent');
final EventChannel _analysisCompletionChannel = const EventChannel('deckers.thibault/aves/analysis_events');
final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error');
final EventChannel _mediaStoreChangeChannel = const OptionalEventChannel('deckers.thibault/aves/media_store_change');
final EventChannel _newIntentChannel = const OptionalEventChannel('deckers.thibault/aves/intent');
final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events');
final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error');
Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
@ -89,6 +94,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
super.initState();
EquatableConfig.stringify = true;
_appSetup = _setup();
_dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette();
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?));
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?));
_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion());
@ -120,16 +126,18 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
: Scaffold(
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
);
return Selector<Settings, Tuple3<Locale?, bool, AvesThemeBrightness>>(
selector: (context, s) => Tuple3(
return Selector<Settings, Tuple4<Locale?, bool, AvesThemeBrightness, bool>>(
selector: (context, s) => Tuple4(
s.locale,
s.initialized ? s.accessibilityAnimations.animate : true,
s.initialized ? s.themeBrightness : AvesThemeBrightness.system,
s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final areAnimationsEnabled = s.item2;
final themeBrightness = s.item3;
final enableDynamicColor = s.item4;
final pageTransitionsTheme = areAnimationsEnabled
// Flutter has various page transition implementations for Android:
@ -144,27 +152,42 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// strip page transitions used by `MaterialPageRoute`
: const DirectPageTransitionsTheme();
return MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => AvesColorsProvider(
child: Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: pageTransitionsTheme,
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
return MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => AvesColorsProvider(
child: Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: pageTransitionsTheme,
),
child: child!,
),
),
child: child!,
),
),
onGenerateTitle: (context) => context.l10n.appName,
theme: Themes.lightTheme,
darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme : Themes.darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
onGenerateTitle: (context) => context.l10n.appName,
theme: Themes.lightTheme(lightAccent),
darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent) : Themes.darkTheme(darkAccent),
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
);
},
);
},
);
@ -207,6 +230,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
break;
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
case AppMode.setWallpaper:
case AppMode.slideshow:
case AppMode.view:
break;
}
@ -223,7 +248,14 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
if (!settings.initialized) return;
final stopwatch = Stopwatch()..start();
final screenSize = window.physicalSize / window.devicePixelRatio;
final Size screenSize;
try {
screenSize = window.physicalSize / window.devicePixelRatio;
} catch (error) {
// view may no longer be usable
return;
}
var tileExtent = settings.getTileExtent(CollectionPage.routeName);
if (tileExtent == 0) {
tileExtent = screenSize.shortestSide / CollectionGrid.columnCountDefault;

View file

@ -284,6 +284,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
padding: EdgeInsets.zero,
child: PopupMenuItemExpansionPanel<EntrySetAction>(
enabled: canApplyEditActions,
value: 'edit',
icon: AIcons.edit,
title: context.l10n.collectionActionEdit,
items: [
@ -477,6 +478,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case EntrySetAction.addShortcut:
// browsing or selecting
case EntrySetAction.map:
case EntrySetAction.slideshow:
case EntrySetAction.stats:
case EntrySetAction.rescan:
case EntrySetAction.emptyBin:

View file

@ -29,6 +29,7 @@ import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:aves/widgets/common/grid/selector.dart';
import 'package:aves/widgets/common/grid/sliver.dart';
import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/identity/buttons.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';
@ -39,6 +40,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
@ -97,6 +99,7 @@ class _CollectionGridContent extends StatelessWidget {
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, thumbnailExtent, child) {
assert(thumbnailExtent > 0);
return Selector<TileExtentController, Tuple4<double, int, double, double>>(
selector: (context, c) => Tuple4(c.viewportSize.width, c.columnCount, c.spacing, c.horizontalPadding),
builder: (context, c, child) {
@ -305,13 +308,15 @@ class _CollectionScrollView extends StatefulWidget {
State<_CollectionScrollView> createState() => _CollectionScrollViewState();
}
class _CollectionScrollViewState extends State<_CollectionScrollView> {
class _CollectionScrollViewState extends State<_CollectionScrollView> with WidgetsBindingObserver {
Timer? _scrollMonitoringTimer;
bool _checkingStoragePermission = false;
@override
void initState() {
super.initState();
_registerWidget(widget);
WidgetsBinding.instance.addObserver(this);
}
@override
@ -323,6 +328,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_unregisterWidget(widget);
_stopScrollMonitoringTimer();
super.dispose();
@ -340,6 +346,26 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
widget.scrollController.removeListener(_onScrollChange);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
break;
case AppLifecycleState.resumed:
if (_checkingStoragePermission) {
_checkingStoragePermission = false;
_isStoragePermissionGranted.then((granted) {
if (granted) {
widget.collection.source.init();
}
});
}
break;
}
}
@override
Widget build(BuildContext context) {
final scrollView = _buildScrollView(widget.appBar, widget.collection);
@ -423,23 +449,47 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
valueListenable: collection.source.stateNotifier,
builder: (context, sourceState, child) {
if (sourceState == SourceState.loading) {
return const SizedBox.shrink();
return const SizedBox();
}
if (collection.filters.any((filter) => filter is FavouriteFilter)) {
return EmptyContent(
icon: AIcons.favourite,
text: context.l10n.collectionEmptyFavourites,
);
}
if (collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.anyVideo)) {
return EmptyContent(
icon: AIcons.video,
text: context.l10n.collectionEmptyVideos,
);
}
return EmptyContent(
icon: AIcons.image,
text: context.l10n.collectionEmptyImages,
return FutureBuilder<bool>(
future: _isStoragePermissionGranted,
builder: (context, snapshot) {
final granted = snapshot.data ?? true;
Widget? bottom = granted
? null
: Padding(
padding: const EdgeInsets.only(top: 16),
child: AvesOutlinedButton(
label: context.l10n.collectionEmptyGrantAccessButtonLabel,
onPressed: () async {
if (await openAppSettings()) {
_checkingStoragePermission = true;
}
},
),
);
if (collection.filters.any((filter) => filter is FavouriteFilter)) {
return EmptyContent(
icon: AIcons.favourite,
text: context.l10n.collectionEmptyFavourites,
bottom: bottom,
);
}
if (collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.anyVideo)) {
return EmptyContent(
icon: AIcons.video,
text: context.l10n.collectionEmptyVideos,
bottom: bottom,
);
}
return EmptyContent(
icon: AIcons.image,
text: context.l10n.collectionEmptyImages,
bottom: bottom,
);
},
);
},
);
@ -519,4 +569,6 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
}
return crumbs;
}
Future<bool> get _isStoragePermissionGranted => Permission.storage.status.then((status) => status.isGranted);
}

View file

@ -35,6 +35,7 @@ import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart'
import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/stats/stats_page.dart';
import 'package:aves/widgets/viewer/slideshow_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -73,6 +74,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
return appMode == AppMode.main && isTrash;
// browsing or selecting
case EntrySetAction.map:
case EntrySetAction.slideshow:
case EntrySetAction.stats:
return appMode == AppMode.main;
case EntrySetAction.rescan:
@ -124,6 +126,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.emptyBin:
return !isSelecting && hasItems;
case EntrySetAction.map:
case EntrySetAction.slideshow:
case EntrySetAction.stats:
case EntrySetAction.rescan:
return (!isSelecting && hasItems) || (isSelecting && hasSelection);
@ -169,6 +172,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.map:
_goToMap(context);
break;
case EntrySetAction.slideshow:
_goToSlideshow(context);
break;
case EntrySetAction.stats:
_goToStats(context);
break;
@ -543,6 +549,27 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
);
}
void _goToSlideshow(BuildContext context) {
final collection = context.read<CollectionLens>();
final entries = _getTargetItems(context);
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: SlideshowPage.routeName),
builder: (context) {
return SlideshowPage(
collection: CollectionLens(
source: collection.source,
filters: collection.filters,
fixedSelection: entries.toList(),
),
);
},
),
);
}
void _goToStats(BuildContext context) {
final collection = context.read<CollectionLens>();
final entries = _getTargetItems(context);

View file

@ -54,6 +54,8 @@ class InteractiveTile extends StatelessWidget {
Navigator.pop(context, entry);
break;
case AppMode.pickFilterInternal:
case AppMode.setWallpaper:
case AppMode.slideshow:
case AppMode.view:
break;
}

View file

@ -122,7 +122,7 @@ mixin FeedbackMixin {
Future<void> showOpReport<T>({
required BuildContext context,
required Stream<T> opStream,
required int itemCount,
int? itemCount,
VoidCallback? onCancel,
void Function(Set<T> processed)? onDone,
}) {
@ -144,7 +144,7 @@ mixin FeedbackMixin {
class ReportOverlay<T> extends StatefulWidget {
final Stream<T> opStream;
final int itemCount;
final int? itemCount;
final VoidCallback? onCancel;
final void Function(Set<T> processed) onDone;
@ -212,7 +212,7 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
builder: (context, snapshot) {
final processedCount = processed.length.toDouble();
final total = widget.itemCount;
final percent = total != 0 ? min(1.0, processedCount / total) : 1.0;
final percent = total == null || total == 0 ? 0.0 : min(1.0, processedCount / total);
return FadeTransition(
opacity: _animation,
child: Stack(
@ -243,10 +243,12 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2),
progressColor: progressColor,
animation: animate,
center: Text(
NumberFormat.percentPattern().format(percent),
style: const TextStyle(fontSize: fontSize),
),
center: total != null
? Text(
NumberFormat.percentPattern().format(percent),
style: const TextStyle(fontSize: fontSize),
)
: null,
animateFromLastPercent: true,
),
if (widget.onCancel != null)
@ -305,12 +307,13 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
if (start != null && stop != null) {
_totalDurationMillis = stop.difference(start).inMilliseconds;
final remainingDuration = stop.difference(DateTime.now());
final effectiveDuration = remainingDuration > Duration.zero ? remainingDuration : const Duration(milliseconds: 1);
_animationController = AnimationController(
duration: remainingDuration,
duration: effectiveDuration,
vsync: this,
);
_remainingDurationMillis = IntTween(
begin: remainingDuration.inMilliseconds,
begin: effectiveDuration.inMilliseconds,
end: 0,
).animate(CurvedAnimation(
parent: _animationController!,

View file

@ -38,7 +38,7 @@ class AvesHighlightView extends StatelessWidget {
this.padding,
this.textStyle,
int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087
}) : source = input.replaceAll('\t', ' ' * tabSize);
}) : source = input.replaceAll('\t', ' ' * tabSize);
List<TextSpan> _convert(List<Node> nodes) {
final spans = <TextSpan>[];

View file

@ -55,25 +55,27 @@ class MenuIconTheme extends StatelessWidget {
class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
final bool enabled;
final String value;
final ValueNotifier<String?> expandedNotifier;
final IconData icon;
final String title;
final List<PopupMenuEntry<T>> items;
const PopupMenuItemExpansionPanel({
PopupMenuItemExpansionPanel({
super.key,
this.enabled = true,
required this.value,
ValueNotifier<String?>? expandedNotifier,
required this.icon,
required this.title,
required this.items,
});
}) : expandedNotifier = expandedNotifier ?? ValueNotifier(null);
@override
State<PopupMenuItemExpansionPanel<T>> createState() => _PopupMenuItemExpansionPanelState<T>();
}
class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionPanel<T>> {
bool _isExpanded = false;
// ref `_kMenuHorizontalPadding` used in `PopupMenuItem`
static const double _horizontalPadding = 16;
@ -86,38 +88,43 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
}
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
Widget child = ExpansionPanelList(
expansionCallback: (index, isExpanded) {
setState(() => _isExpanded = !isExpanded);
},
animationDuration: animationDuration,
expandedHeaderPadding: EdgeInsets.zero,
elevation: 0,
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) => DefaultTextStyle(
style: style,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
child: MenuRow(
text: widget.title,
icon: Icon(widget.icon),
Widget child = ValueListenableBuilder<String?>(
valueListenable: widget.expandedNotifier,
builder: (context, expandedValue, child) {
return ExpansionPanelList(
expansionCallback: (index, isExpanded) {
widget.expandedNotifier.value = isExpanded ? null : widget.value;
},
animationDuration: animationDuration,
expandedHeaderPadding: EdgeInsets.zero,
elevation: 0,
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) => DefaultTextStyle(
style: style,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
child: MenuRow(
text: widget.title,
icon: Icon(widget.icon),
),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const PopupMenuDivider(height: 0),
...widget.items,
const PopupMenuDivider(height: 0),
],
),
isExpanded: expandedValue == widget.value,
canTapOnHeader: true,
backgroundColor: Colors.transparent,
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const PopupMenuDivider(height: 0),
...widget.items,
const PopupMenuDivider(height: 0),
],
),
isExpanded: _isExpanded,
canTapOnHeader: true,
backgroundColor: Colors.transparent,
),
],
],
);
},
);
if (!widget.enabled) {
child = IgnorePointer(child: child);

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
class QueryBar extends StatefulWidget {
final ValueNotifier<String> queryNotifier;
final FocusNode? focusNode;
final EdgeInsetsGeometry? leadingPadding;
final IconData? icon;
final String? hintText;
final bool editable;
@ -15,6 +16,7 @@ class QueryBar extends StatefulWidget {
super.key,
required this.queryNotifier,
this.focusNode,
this.leadingPadding,
this.icon,
this.hintText,
this.editable = true,
@ -60,7 +62,7 @@ class _QueryBarState extends State<QueryBar> {
focusNode: widget.focusNode ?? FocusNode(),
decoration: InputDecoration(
icon: Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
padding: widget.leadingPadding ?? const EdgeInsetsDirectional.only(start: 16),
child: Icon(widget.icon ?? AIcons.filter),
),
hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel,

View file

@ -35,7 +35,7 @@ class ReselectableRadioListTile<T> extends StatelessWidget {
this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform,
this.autofocus = false,
}) : assert(!isThreeLine || subtitle != null);
}) : assert(!isThreeLine || subtitle != null);
@override
Widget build(BuildContext context) {

View file

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

Some files were not shown because too many files have changed in this diff Show more