? = null,
fileMimeType: String? = null,
+ safe: Boolean = false,
): Boolean {
var found = false
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
@@ -299,7 +302,7 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
- val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
+ val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe)
entryMap = entry.toMap()
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt
index ffcc2400f..8c8e29ddf 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt
@@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() {
return
}
- val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
+ val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())
} else {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt
index 7002c07f0..8935cd712 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt
@@ -5,5 +5,5 @@ import kotlin.math.pow
object MathUtils {
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
- fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
+ private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
index 1430472b0..c240a5df2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
@@ -1,7 +1,7 @@
package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap
-import androidx.exifinterface.media.ExifInterface
+import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.decoder.MultiPageImage
object MimeTypes {
@@ -17,8 +17,8 @@ object MimeTypes {
private const val ICO = "image/x-icon"
const val JPEG = "image/jpeg"
const val PNG = "image/png"
- const val PSD_VND = "image/vnd.adobe.photoshop"
- const val PSD_X = "image/x-photoshop"
+ private const val PSD_VND = "image/vnd.adobe.photoshop"
+ private const val PSD_X = "image/x-photoshop"
const val TIFF = "image/tiff"
private const val WBMP = "image/vnd.wap.wbmp"
const val WEBP = "image/webp"
diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml
index 8cb4384cc..4e5ba3da4 100644
--- a/android/app/src/main/res/values-nl/strings.xml
+++ b/android/app/src/main/res/values-nl/strings.xml
@@ -1,12 +1,12 @@
Aves
- Foto Lijstje
+ Fotolijst
Achtergrond
Zoeken
Video’s
Media indexeren
Indexeren van media
- Stop
+ Stoppen
Veilige modus
\ No newline at end of file
diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
similarity index 99%
rename from android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
rename to android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
index cb1817043..9beaaf01d 100644
--- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
@@ -16,12 +16,12 @@
package androidx.exifinterface.media;
-import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor;
-import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly;
-import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
-import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
-import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
-import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
+import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor;
+import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly;
+import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray;
+import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
+import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
+import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
@@ -41,8 +41,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl;
-import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl;
+import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
+import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -84,6 +84,7 @@ import java.util.zip.CRC32;
/*
* Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21
+ * Named differently to let ExifInterface be loaded as subdependency.
*/
/**
@@ -97,7 +98,7 @@ import java.util.zip.CRC32;
* it. This class will search both locations for XMP data, but if XMP data exist both inside and
* outside Exif, will favor the XMP data inside Exif over the one outside.
*/
-public class ExifInterface {
+public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
@@ -3949,7 +3950,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
- public ExifInterface(@NonNull File file) throws IOException {
+ public ExifInterfaceFork(@NonNull File file) throws IOException {
if (file == null) {
throw new NullPointerException("file cannot be null");
}
@@ -3964,7 +3965,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
- public ExifInterface(@NonNull String filename) throws IOException {
+ public ExifInterfaceFork(@NonNull String filename) throws IOException {
if (filename == null) {
throw new NullPointerException("filename cannot be null");
}
@@ -3980,7 +3981,7 @@ public class ExifInterface {
* @throws NullPointerException if file descriptor is null
* @throws IOException if an error occurs while duplicating the file descriptor.
*/
- public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException {
+ public ExifInterfaceFork(@NonNull FileDescriptor fileDescriptor) throws IOException {
if (fileDescriptor == null) {
throw new NullPointerException("fileDescriptor cannot be null");
}
@@ -4023,7 +4024,7 @@ public class ExifInterface {
* @param inputStream the input stream that contains the image data
* @throws NullPointerException if the input stream is null
*/
- public ExifInterface(@NonNull InputStream inputStream) throws IOException {
+ public ExifInterfaceFork(@NonNull InputStream inputStream) throws IOException {
this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA);
}
@@ -4039,7 +4040,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
- public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType)
+ public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType)
throws IOException {
if (inputStream == null) {
throw new NullPointerException("inputStream cannot be null");
@@ -5071,7 +5072,7 @@ public class ExifInterface {
if (location == null) {
return;
}
- setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
+ setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setLatLong(location.getLatitude(), location.getLongitude());
setAltitude(location.getAltitude());
// Location objects store speeds in m/sec. Translates it to km/hr here.
@@ -5080,8 +5081,8 @@ public class ExifInterface {
* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1);
- setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
- setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
+ setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
+ setAttribute(ExifInterfaceFork.TAG_GPS_TIMESTAMP, dateTime[1]);
}
/**
@@ -5158,11 +5159,11 @@ public class ExifInterface {
}
/**
- * Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
+ * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME} value as number of milliseconds since
* Jan. 1, 1970, midnight local time.
*
* Note: The return value includes the first three digits (or less depending on the length
- * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
+ * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME}.
*
* @return null if date time information is unavailable or invalid.
*/
@@ -5175,11 +5176,11 @@ public class ExifInterface {
}
/**
- * Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
+ * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_DIGITIZED} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
*
*
Note: The return value includes the first three digits (or less depending on the length
- * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
+ * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_DIGITIZED}.
*
* @return null if digitized date time information is unavailable or invalid.
*/
@@ -5192,11 +5193,11 @@ public class ExifInterface {
}
/**
- * Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
+ * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_ORIGINAL} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
*
*
Note: The return value includes the first three digits (or less depending on the length
- * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
+ * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_ORIGINAL}.
*
* @return null if original date time information is unavailable or invalid.
*/
@@ -5910,18 +5911,18 @@ public class ExifInterface {
}
if (rotation != null) {
- int orientation = ExifInterface.ORIENTATION_NORMAL;
+ int orientation = ExifInterfaceFork.ORIENTATION_NORMAL;
// all rotation angles in CW
switch (Integer.parseInt(rotation)) {
case 90:
- orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90;
break;
case 180:
- orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180;
break;
case 270:
- orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270;
break;
}
@@ -6175,7 +6176,11 @@ public class ExifInterface {
// IEND marks the end of the image.
break;
} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
- // TODO: Need to handle potential OutOfMemoryError
+ // TLAD start
+ if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
+ throw new IOException("dangerous exif chunk size=" + length);
+ }
+ // TLAD end
byte[] data = new byte[length];
in.readFully(data);
@@ -6976,9 +6981,11 @@ public class ExifInterface {
}
final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
- if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
+ // TLAD start
+ if (byteCount > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous attribute size=" + byteCount);
}
+ // TLAD end
final byte[] bytes = new byte[(int) byteCount];
dataInputStream.readFully(bytes);
ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,
diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
similarity index 98%
rename from android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
rename to android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
index a7033b4ae..df7ed9320 100644
--- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
+++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
@@ -32,10 +32,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-class ExifInterfaceUtils {
+class ExifInterfaceUtilsFork {
private static final String TAG = "ExifInterfaceUtils";
- private ExifInterfaceUtils() {
+ private ExifInterfaceUtilsFork() {
// Prevent instantiation
}
/**
diff --git a/android/settings.gradle b/android/settings.gradle
index 5ad529b31..3480028f5 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20"
- settings.ext.agp_version = '8.5.0'
+ settings.ext.agp_version = '8.5.1'
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
diff --git a/fastlane/metadata/android/en-US/changelogs/125.txt b/fastlane/metadata/android/en-US/changelogs/125.txt
new file mode 100644
index 000000000..3ab2a9554
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/125.txt
@@ -0,0 +1,4 @@
+In v1.11.6:
+- explore your collection with the... explorer
+- convert your motion photos to stills in bulk
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/12501.txt b/fastlane/metadata/android/en-US/changelogs/12501.txt
new file mode 100644
index 000000000..3ab2a9554
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/12501.txt
@@ -0,0 +1,4 @@
+In v1.11.6:
+- explore your collection with the... explorer
+- convert your motion photos to stills in bulk
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt
index 6b96ec3ea..ec60c4d99 100644
--- a/fastlane/metadata/android/hi/full_description.txt
+++ b/fastlane/metadata/android/hi/full_description.txt
@@ -1,4 +1,4 @@
-Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files.
+Aves आपके ठेठ JPEGs और MP4s सम्मिलित करते हुए, लगभग सभी प्रकार के Photos और Videos को सम्भाल सकता है, साथ के साथ यह multi-page TIFFs, SVGs, old AVIs और भी बहुत कुछ संभालता है ! यह आपके Media संग्रह की जाँच करता है, ताकि यह motion photos, panoramas (aka photo spheres), 360° videos, और GeoTIFF files की पहचान कर सके ।
Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
diff --git a/fastlane/metadata/android/nl/full_description.txt b/fastlane/metadata/android/nl/full_description.txt
index 11f348f36..b09754ce3 100644
--- a/fastlane/metadata/android/nl/full_description.txt
+++ b/fastlane/metadata/android/nl/full_description.txt
@@ -1,5 +1,5 @@
-Aves kan allerlei soorten afbeeldingen en video's aan, waaronder de typische JPEG's en MP4's, maar ook minder gangbare formaten zoals multi-pagina TIFF's, SVG's, oude AVI's en meer! Het scant uw media collectie om bewegende foto's, panorama's, 360° video's, evenals GeoTIFF bestanden te herkennen.
+Aves kan allerlei soorten afbeeldingen en video's aan, waaronder de veelgebruikte JPEG's en MP4's, maar ook minder gangbare formaten zoals multi-pagina TIFF's, SVG's, oude AVI's en meer! Het scant jouw mediacollectie om bewegende foto's, panorama's, 360° video's, evenals GeoTIFF-bestanden te herkennen.
-Navigatie en zoeken is een belangrijk onderdeel van Aves. Het doel is dat gebruikers gemakkelijk van albums naar foto's naar tags naar kaarten enz. kunnen gaan.
+Navigatie en zoeken is een belangrijk onderdeel van Aves. Het doel is dat gebruikers eenvoudig kunnen wisselen van albums naar foto's naar labels naar kaarten enz.
-Aves integrates with Android (from KitKat to Android 14, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker.
+Aves integreert met Android (van KitKat t/m Android 14, inclusief Android TV) met functies zoals widgets, app-snelkoppelingen, screensaver en algemene zoekopdrachten. Het werkt ook als een mediaviewer en -kiezer.
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 3e6aeb9eb..7caea6927 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -1519,8 +1519,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "تعيين كخلفية",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "مجموعة مخصصة",
- "@setHomeCustomCollection": {},
"videoActionABRepeat": "تكرار A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",
diff --git a/lib/l10n/app_be.arb b/lib/l10n/app_be.arb
index 8d58141ab..3b4f8304c 100644
--- a/lib/l10n/app_be.arb
+++ b/lib/l10n/app_be.arb
@@ -1517,8 +1517,6 @@
},
"collectionActionSetHome": "Усталяваць як галоўную",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Уласная калекцыя",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
"@settingsThumbnailShowHdrIcon": {},
"videoRepeatActionSetEnd": "Усталяваць канец",
diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb
index 2ed333c02..a588f254f 100644
--- a/lib/l10n/app_ca.arb
+++ b/lib/l10n/app_ca.arb
@@ -1467,8 +1467,6 @@
"@tagPlaceholderState": {},
"tagPlaceholderPlace": "Lloc",
"@tagPlaceholderPlace": {},
- "setHomeCustomCollection": "Coŀlecció personalitzada",
- "@setHomeCustomCollection": {},
"settingsConfirmationBeforeMoveToBinItems": "Pregunta abans de moure elements a la paperera de reciclatge",
"@settingsConfirmationBeforeMoveToBinItems": {},
"settingsNavigationDrawerBanner": "Mantén premut per moure i reordenar els elements del menú.",
diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb
index 3fa3155b8..23d4f5ccd 100644
--- a/lib/l10n/app_cs.arb
+++ b/lib/l10n/app_cs.arb
@@ -1515,8 +1515,6 @@
"@entryActionCast": {},
"castDialogTitle": "Zařízení pro promítání",
"@castDialogTitle": {},
- "setHomeCustomCollection": "Vlastní sbírka",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Zobrazit ikonu HDR",
"@settingsThumbnailShowHdrIcon": {},
"settingsForceWesternArabicNumeralsTile": "Vynutit arabské číslice",
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 8142332c4..760769d20 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -1355,8 +1355,6 @@
"@overlayHistogramNone": {},
"collectionActionSetHome": "Als Startseite setzen",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Benutzerdefinierte Sammlung",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "HDR-Symbol anzeigen",
"@settingsThumbnailShowHdrIcon": {},
"entryActionCast": "Übertragen",
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index ef0394040..f7dbac54c 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -771,6 +771,9 @@
"binPageTitle": "Recycle Bin",
"explorerPageTitle": "Explorer",
+ "explorerActionSelectStorageVolume": "Select storage",
+
+ "selectStorageVolumeDialogTitle": "Select Storage",
"searchCollectionFieldHint": "Search collection",
"searchRecentSectionTitle": "Recent",
@@ -804,7 +807,7 @@
"settingsNavigationSectionTitle": "Navigation",
"settingsHomeTile": "Home",
"settingsHomeDialogTitle": "Home",
- "setHomeCustomCollection": "Custom collection",
+ "setHomeCustom": "Custom",
"settingsShowBottomNavigationBar": "Show bottom navigation bar",
"settingsKeepScreenOnTile": "Keep screen on",
"settingsKeepScreenOnDialogTitle": "Keep Screen On",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 5302177ff..e222d0467 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Fijar como inicio",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Colección personalizada",
- "@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "Fijar el inicio",
"@videoRepeatActionSetStart": {},
"stopTooltip": "Parar",
@@ -1380,5 +1378,11 @@
"explorerPageTitle": "Explorar",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Mostrar en el explorador",
- "@chipActionGoToExplorerPage": {}
+ "@chipActionGoToExplorerPage": {},
+ "selectStorageVolumeDialogTitle": "Seleccionar almacenamiento",
+ "@selectStorageVolumeDialogTitle": {},
+ "setHomeCustom": "Personalizado",
+ "@setHomeCustom": {},
+ "explorerActionSelectStorageVolume": "Seleccionar almacenamiento",
+ "@explorerActionSelectStorageVolume": {}
}
diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb
index 396081e89..e3309950a 100644
--- a/lib/l10n/app_eu.arb
+++ b/lib/l10n/app_eu.arb
@@ -1519,8 +1519,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Ezarri hasiera gisa",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Bilduma pertsonalizatua",
- "@setHomeCustomCollection": {},
"renameProcessorHash": "Hash-a",
"@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak",
diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb
index 5a0273eca..a101850a3 100644
--- a/lib/l10n/app_fa.arb
+++ b/lib/l10n/app_fa.arb
@@ -1099,8 +1099,6 @@
"@settingsSystemDefault": {},
"settingsConfirmationTile": "درخواست های تایید",
"@settingsConfirmationTile": {},
- "setHomeCustomCollection": "مجموعه سفارشی",
- "@setHomeCustomCollection": {},
"settingsKeepScreenOnDialogTitle": "صفحه را روشن نگه دار",
"@settingsKeepScreenOnDialogTitle": {},
"settingsShowBottomNavigationBar": "نمایش گزینهگان پیمایش پایین",
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index e89591e7e..0b9c7ed12 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1359,8 +1359,6 @@
"@castDialogTitle": {},
"collectionActionSetHome": "Définir comme page d’accueil",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Collection personnalisée",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afficher l’icône HDR",
"@settingsThumbnailShowHdrIcon": {},
"videoRepeatActionSetEnd": "Définir la fin",
@@ -1380,5 +1378,11 @@
"explorerPageTitle": "Explorateur",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Afficher dans Explorateur",
- "@chipActionGoToExplorerPage": {}
+ "@chipActionGoToExplorerPage": {},
+ "setHomeCustom": "Personnalisé",
+ "@setHomeCustom": {},
+ "explorerActionSelectStorageVolume": "Choisir le stockage",
+ "@explorerActionSelectStorageVolume": {},
+ "selectStorageVolumeDialogTitle": "Volumes de stockage",
+ "@selectStorageVolumeDialogTitle": {}
}
diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb
index 8d12567fa..e11454b4b 100644
--- a/lib/l10n/app_hi.arb
+++ b/lib/l10n/app_hi.arb
@@ -41,7 +41,7 @@
"count": {}
}
},
- "deleteButtonLabel": "डिलीट",
+ "deleteButtonLabel": "मिटाए",
"@deleteButtonLabel": {},
"timeMinutes": "{count, plural, other{{count} मिनट}}",
"@timeMinutes": {
@@ -88,7 +88,7 @@
"@chipActionGoToTagPage": {},
"resetTooltip": "रिसेट",
"@resetTooltip": {},
- "saveTooltip": "सेव करें",
+ "saveTooltip": "सहेजें",
"@saveTooltip": {},
"pickTooltip": "चुनें",
"@pickTooltip": {},
diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb
index 0b1b2919f..d34a5e814 100644
--- a/lib/l10n/app_hu.arb
+++ b/lib/l10n/app_hu.arb
@@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "HDR ikon megjelenítése",
"@settingsThumbnailShowHdrIcon": {},
- "setHomeCustomCollection": "Egyéni gyűjtemény",
- "@setHomeCustomCollection": {},
"collectionActionSetHome": "Kezdőlapnak beállít",
"@collectionActionSetHome": {},
"stopTooltip": "Állj",
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index ce5523a09..0c3b4b82e 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -1355,8 +1355,6 @@
"@aboutDataUsageClearCache": {},
"entryActionCast": "Siarkan",
"@entryActionCast": {},
- "setHomeCustomCollection": "Koleksi kustom",
- "@setHomeCustomCollection": {},
"collectionActionSetHome": "Tetapkan sebagai beranda",
"@collectionActionSetHome": {},
"settingsThumbnailShowHdrIcon": "Tampilkan ikon HDR",
diff --git a/lib/l10n/app_is.arb b/lib/l10n/app_is.arb
index ca42651c2..2d93d709e 100644
--- a/lib/l10n/app_is.arb
+++ b/lib/l10n/app_is.arb
@@ -1519,8 +1519,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Setja sem upphafsskjá",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Sérsniðið safn",
- "@setHomeCustomCollection": {},
"renameProcessorHash": "Tætigildi",
"@renameProcessorHash": {},
"videoRepeatActionSetStart": "Stilla byrjun",
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index 13ee7c516..273aa5d65 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -1369,8 +1369,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Imposta come pagina iniziale",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Collezione personalizzata",
- "@setHomeCustomCollection": {},
"chipActionShowCollection": "Mostra nella Collezione",
"@chipActionShowCollection": {},
"renameProcessorHash": "Hash",
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index fc30a33c7..78fd2ba3e 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -1357,8 +1357,6 @@
"@overlayHistogramLuminance": {},
"settingsModificationWarningDialogMessage": "他の設定は変更されます。",
"@settingsModificationWarningDialogMessage": {},
- "setHomeCustomCollection": "カスタムコレクション",
- "@setHomeCustomCollection": {},
"settingsAccessibilityShowPinchGestureAlternatives": "マルチタッチジェスチャーの選択肢を表示する",
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"chipActionCreateVault": "保管庫を作成",
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index fe77febf2..a48694609 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "홈으로 설정",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "지정 미디어",
- "@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "시작 지점 설정",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "종료 지점 설정",
@@ -1380,5 +1378,11 @@
"explorerPageTitle": "탐색기",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "탐색기 페이지에서 보기",
- "@chipActionGoToExplorerPage": {}
+ "@chipActionGoToExplorerPage": {},
+ "setHomeCustom": "직접 설정",
+ "@setHomeCustom": {},
+ "explorerActionSelectStorageVolume": "저장공간 선택",
+ "@explorerActionSelectStorageVolume": {},
+ "selectStorageVolumeDialogTitle": "저장공간",
+ "@selectStorageVolumeDialogTitle": {}
}
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index bf0731de9..e42a5f496 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -101,9 +101,9 @@
"@entryActionRename": {},
"entryActionRestore": "Herstellen",
"@entryActionRestore": {},
- "entryActionRotateCCW": "Roteren tegen de klok in",
+ "entryActionRotateCCW": "Linksom roteren",
"@entryActionRotateCCW": {},
- "entryActionRotateCW": "Roteren met de klok mee",
+ "entryActionRotateCW": "Rechtsom roteren",
"@entryActionRotateCW": {},
"entryActionFlip": "Horizontaal omdraaien",
"@entryActionFlip": {},
@@ -163,25 +163,25 @@
"@entryInfoActionEditLocation": {},
"entryInfoActionEditTitleDescription": "Wijzig titel & omschrijving",
"@entryInfoActionEditTitleDescription": {},
- "entryInfoActionEditRating": "Bewerk waardering",
+ "entryInfoActionEditRating": "Waardering bewerken",
"@entryInfoActionEditRating": {},
- "entryInfoActionEditTags": "Bewerk labels",
+ "entryInfoActionEditTags": "Labels bewerken",
"@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Verwijder metadata",
"@entryInfoActionRemoveMetadata": {},
"filterBinLabel": "Prullenbak",
"@filterBinLabel": {},
- "filterFavouriteLabel": "Favorieten",
+ "filterFavouriteLabel": "Favoriet",
"@filterFavouriteLabel": {},
- "filterNoDateLabel": "Geen datum",
+ "filterNoDateLabel": "Zonder datum",
"@filterNoDateLabel": {},
- "filterNoLocationLabel": "Geen locatie",
+ "filterNoLocationLabel": "Zonder plaats",
"@filterNoLocationLabel": {},
- "filterNoRatingLabel": "Geen rating",
+ "filterNoRatingLabel": "Zonder waardering",
"@filterNoRatingLabel": {},
- "filterNoTagLabel": "Geen label",
+ "filterNoTagLabel": "Zonder label",
"@filterNoTagLabel": {},
- "filterNoTitleLabel": "Geen titel",
+ "filterNoTitleLabel": "Zonder titel",
"@filterNoTitleLabel": {},
"filterOnThisDayLabel": "Op deze dag",
"@filterOnThisDayLabel": {},
@@ -347,7 +347,7 @@
"@videoResumeDialogMessage": {},
"videoStartOverButtonLabel": "OPNIEUW BEGINNEN",
"@videoStartOverButtonLabel": {},
- "videoResumeButtonLabel": "HERVAT",
+ "videoResumeButtonLabel": "HERVATTEN",
"@videoResumeButtonLabel": {},
"setCoverDialogLatest": "Laatste item",
"@setCoverDialogLatest": {},
@@ -355,7 +355,7 @@
"@setCoverDialogAuto": {},
"setCoverDialogCustom": "Aangepast",
"@setCoverDialogCustom": {},
- "hideFilterConfirmationDialogMessage": "Overeenkomende foto’s en video’s worden verborgen binnen uw verzameling. Je kunt ze opnieuw weergeven via de “Privacy”-instellingen.\n\nWeet je zeker dat je ze wilt verbergen?",
+ "hideFilterConfirmationDialogMessage": "Overeenkomende foto’s en video’s worden verborgen binnen jouw verzameling. Je kunt ze opnieuw weergeven via de “Privacy”-instellingen.\n\nWeet je zeker dat je ze wilt verbergen?",
"@hideFilterConfirmationDialogMessage": {},
"newAlbumDialogTitle": "Nieuw Album",
"@newAlbumDialogTitle": {},
@@ -423,7 +423,7 @@
"@editEntryLocationDialogLongitude": {},
"locationPickerUseThisLocationButton": "Gebruik deze locatie",
"@locationPickerUseThisLocationButton": {},
- "editEntryRatingDialogTitle": "Beoordeling",
+ "editEntryRatingDialogTitle": "Waardering",
"@editEntryRatingDialogTitle": {},
"removeEntryMetadataDialogTitle": "Verwijderen metadata",
"@removeEntryMetadataDialogTitle": {},
@@ -505,11 +505,11 @@
"@aboutBugReportInstruction": {},
"aboutBugReportButton": "Reporteer",
"@aboutBugReportButton": {},
- "aboutCreditsSectionTitle": "Credits",
+ "aboutCreditsSectionTitle": "Dankbetuiging",
"@aboutCreditsSectionTitle": {},
"aboutCreditsWorldAtlas1": "Deze applicatie gebruikt een TopoJSON-bestand van",
"@aboutCreditsWorldAtlas1": {},
- "aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
+ "aboutCreditsWorldAtlas2": "onder ISC-licentie.",
"@aboutCreditsWorldAtlas2": {},
"aboutTranslatorsSectionTitle": "Vertalers",
"@aboutTranslatorsSectionTitle": {},
@@ -525,7 +525,7 @@
"@aboutLicensesFlutterPackagesSectionTitle": {},
"aboutLicensesDartPackagesSectionTitle": "Dart Packages",
"@aboutLicensesDartPackagesSectionTitle": {},
- "aboutLicensesShowAllButtonLabel": "Laat alle licenties zien",
+ "aboutLicensesShowAllButtonLabel": "Alle licenties tonen",
"@aboutLicensesShowAllButtonLabel": {},
"collectionPageTitle": "Verzameling",
"@collectionPageTitle": {},
@@ -615,7 +615,7 @@
"@drawerCollectionAnimated": {},
"drawerCollectionMotionPhotos": "Bewegende foto’s",
"@drawerCollectionMotionPhotos": {},
- "drawerCollectionPanoramas": "Panoramas",
+ "drawerCollectionPanoramas": "Panorama's",
"@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Raw foto’s",
"@drawerCollectionRaws": {},
@@ -637,7 +637,7 @@
"@sortBySize": {},
"sortByAlbumFileName": "Op album- en bestandsnaam",
"@sortByAlbumFileName": {},
- "sortByRating": "Op rating",
+ "sortByRating": "Op waardering",
"@sortByRating": {},
"sortOrderNewestFirst": "Nieuwste eerst",
"@sortOrderNewestFirst": {},
@@ -667,7 +667,7 @@
"@albumMimeTypeMixed": {},
"albumPickPageTitleCopy": "Kopieer naar Album",
"@albumPickPageTitleCopy": {},
- "albumPickPageTitleExport": "Exporteer naar Album",
+ "albumPickPageTitleExport": "Exporteren naar Album",
"@albumPickPageTitleExport": {},
"albumPickPageTitleMove": "Verplaats naar Album",
"@albumPickPageTitleMove": {},
@@ -715,7 +715,7 @@
"@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Labels",
"@searchTagsSectionTitle": {},
- "searchRatingSectionTitle": "Beoordeling",
+ "searchRatingSectionTitle": "Waarderingen",
"@searchRatingSectionTitle": {},
"searchMetadataSectionTitle": "Metadata",
"@searchMetadataSectionTitle": {},
@@ -731,13 +731,13 @@
"@settingsSearchFieldLabel": {},
"settingsSearchEmpty": "Geen instellingen gevonden",
"@settingsSearchEmpty": {},
- "settingsActionExport": "Exporteer",
+ "settingsActionExport": "Exporteren",
"@settingsActionExport": {},
- "settingsActionExportDialogTitle": "Exporteer",
+ "settingsActionExportDialogTitle": "Exporteren",
"@settingsActionExportDialogTitle": {},
- "settingsActionImport": "Importeer",
+ "settingsActionImport": "Importeren",
"@settingsActionImport": {},
- "settingsActionImportDialogTitle": "Importeer",
+ "settingsActionImportDialogTitle": "Importeren",
"@settingsActionImportDialogTitle": {},
"appExportCovers": "Omslagen",
"@appExportCovers": {},
@@ -793,13 +793,13 @@
"@settingsThumbnailOverlayPageTitle": {},
"settingsThumbnailShowFavouriteIcon": "Favorieten icoon zichtbaar",
"@settingsThumbnailShowFavouriteIcon": {},
- "settingsThumbnailShowTagIcon": "Label icoon zichtbaar",
+ "settingsThumbnailShowTagIcon": "Label-pictogram tonen",
"@settingsThumbnailShowTagIcon": {},
"settingsThumbnailShowLocationIcon": "Locatie icoon zichtbaar",
"@settingsThumbnailShowLocationIcon": {},
"settingsThumbnailShowMotionPhotoIcon": "Bewegende foto icoon zichtbaar",
"@settingsThumbnailShowMotionPhotoIcon": {},
- "settingsThumbnailShowRating": "Rating zichtbaar",
+ "settingsThumbnailShowRating": "Waardering tonen",
"@settingsThumbnailShowRating": {},
"settingsThumbnailShowRawIcon": "RAW icoon zichtbaar",
"@settingsThumbnailShowRawIcon": {},
@@ -865,7 +865,7 @@
"@settingsViewerSlideshowPageTitle": {},
"settingsSlideshowRepeat": "Herhalen",
"@settingsSlideshowRepeat": {},
- "settingsSlideshowShuffle": "Shuffle",
+ "settingsSlideshowShuffle": "Willekeurige volgorde",
"@settingsSlideshowShuffle": {},
"settingsSlideshowFillScreen": "Volledig scherm",
"@settingsSlideshowFillScreen": {},
@@ -951,13 +951,13 @@
"@settingsHiddenItemsPageTitle": {},
"settingsHiddenItemsTabFilters": "Verborgen Filters",
"@settingsHiddenItemsTabFilters": {},
- "settingsHiddenFiltersBanner": "Foto’s en video’s die overeenkomen met verborgen filters, worden niet weergegeven in uw verzameling.",
+ "settingsHiddenFiltersBanner": "Foto’s en video’s die overeenkomen met verborgen filters, worden niet weergegeven in je verzameling.",
"@settingsHiddenFiltersBanner": {},
"settingsHiddenFiltersEmpty": "Geen verborgen filters",
"@settingsHiddenFiltersEmpty": {},
"settingsHiddenItemsTabPaths": "Verborgen paden",
"@settingsHiddenItemsTabPaths": {},
- "settingsHiddenPathsBanner": "Foto’s en video’s in deze mappen, of een van hun submappen, verschijnen niet in uw verzameling.",
+ "settingsHiddenPathsBanner": "Foto’s en video’s in deze mappen, of een van hun submappen, verschijnen niet in je verzameling.",
"@settingsHiddenPathsBanner": {},
"addPathTooltip": "Pad toevoegen",
"@addPathTooltip": {},
@@ -965,7 +965,7 @@
"@settingsStorageAccessTile": {},
"settingsStorageAccessPageTitle": "Toegang tot opslag",
"@settingsStorageAccessPageTitle": {},
- "settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. U kunt hier directory’s bekijken waartoe u eerder toegang heeft verleend.",
+ "settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. Je kunt hier directory’s bekijken waartoe je eerder toegang hebt verleend.",
"@settingsStorageAccessBanner": {},
"settingsStorageAccessEmpty": "Geen toegang verleend",
"@settingsStorageAccessEmpty": {},
@@ -1029,7 +1029,7 @@
"@statsTopTagsSectionTitle": {},
"statsTopAlbumsSectionTitle": "Top Albums",
"@statsTopAlbumsSectionTitle": {},
- "viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
+ "viewerOpenPanoramaButtonLabel": "PANORAMA OPENEN",
"@viewerOpenPanoramaButtonLabel": {},
"viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN",
"@viewerSetWallpaperButtonLabel": {},
@@ -1089,7 +1089,7 @@
"@viewerInfoOpenLinkText": {},
"viewerInfoViewXmlLinkText": "Bekijk XML",
"@viewerInfoViewXmlLinkText": {},
- "viewerInfoSearchFieldLabel": "Doorzoek metadata",
+ "viewerInfoSearchFieldLabel": "Metadata doorzoeken",
"@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "Geen overeenkomstige zoeksleutels",
"@viewerInfoSearchEmpty": {},
@@ -1105,7 +1105,7 @@
"@viewerInfoSearchSuggestionRights": {},
"wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm",
"@wallpaperUseScrollEffect": {},
- "tagEditorPageTitle": "Wijzig Labels",
+ "tagEditorPageTitle": "Labels bewerken",
"@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Nieuw label",
"@tagEditorPageNewTagFieldLabel": {},
@@ -1155,17 +1155,17 @@
"@lengthUnitPercent": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
- "filterAspectRatioLandscapeLabel": "Landschap",
+ "filterAspectRatioLandscapeLabel": "Liggend",
"@filterAspectRatioLandscapeLabel": {},
- "chipActionCreateVault": "Creëer kluis",
+ "chipActionCreateVault": "Kluis aanmaken",
"@chipActionCreateVault": {},
"entryInfoActionRemoveLocation": "Verwijder locatie",
"@entryInfoActionRemoveLocation": {},
- "chipActionConfigureVault": "Configureer kluis",
+ "chipActionConfigureVault": "Kluis configureren",
"@chipActionConfigureVault": {},
- "filterNoAddressLabel": "Geen adres",
+ "filterNoAddressLabel": "Zonder adres",
"@filterNoAddressLabel": {},
- "filterAspectRatioPortraitLabel": "Portret",
+ "filterAspectRatioPortraitLabel": "Staand",
"@filterAspectRatioPortraitLabel": {},
"widgetDisplayedItemRandom": "Willekeurige",
"@widgetDisplayedItemRandom": {},
@@ -1175,7 +1175,7 @@
"@keepScreenOnVideoPlayback": {},
"settingsVideoEnablePip": "Beeld-in-beeld",
"@settingsVideoEnablePip": {},
- "filterTaggedLabel": "Getagd",
+ "filterTaggedLabel": "Met label",
"@filterTaggedLabel": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
@@ -1191,9 +1191,9 @@
"@stopTooltip": {},
"chipActionLock": "Vergrendel",
"@chipActionLock": {},
- "chipActionShowCountryStates": "Status laten xien",
+ "chipActionShowCountryStates": "Status tonen",
"@chipActionShowCountryStates": {},
- "chipActionGoToPlacePage": "Laat zien in plaatsen",
+ "chipActionGoToPlacePage": "In Plaatsen tonen",
"@chipActionGoToPlacePage": {},
"subtitlePositionTop": "Boven",
"@subtitlePositionTop": {},
@@ -1227,7 +1227,7 @@
"@aboutDataUsageMisc": {},
"settingsModificationWarningDialogMessage": "Andere instellingen zullen worden aangepast.",
"@settingsModificationWarningDialogMessage": {},
- "vaultDialogLockModeWhenScreenOff": "Vergrendel als scherm uitgaat",
+ "vaultDialogLockModeWhenScreenOff": "Vergrendelen wanneer het scherm wordt uitgeschakeld",
"@vaultDialogLockModeWhenScreenOff": {},
"aboutDataUsageData": "Data",
"@aboutDataUsageData": {},
@@ -1269,8 +1269,122 @@
"@maxBrightnessNever": {},
"videoResumptionModeAlways": "Altijd",
"@videoResumptionModeAlways": {},
- "exportEntryDialogWriteMetadata": "Schrijf metadata",
+ "exportEntryDialogWriteMetadata": "Metadata schrijven",
"@exportEntryDialogWriteMetadata": {},
"chipActionShowCollection": "Tonen in Collectie",
- "@chipActionShowCollection": {}
+ "@chipActionShowCollection": {},
+ "entryActionCast": "Casten",
+ "@entryActionCast": {},
+ "videoRepeatActionSetStart": "Start instellen",
+ "@videoRepeatActionSetStart": {},
+ "videoRepeatActionSetEnd": "Einde instellen",
+ "@videoRepeatActionSetEnd": {},
+ "viewerActionUnlock": "Weergave ontgrendelen",
+ "@viewerActionUnlock": {},
+ "filterLocatedLabel": "Met Plaats",
+ "@filterLocatedLabel": {},
+ "overlayHistogramNone": "Geen",
+ "@overlayHistogramNone": {},
+ "authenticateToUnlockVault": "Verifieer om de kluis te ontgrendelen",
+ "@authenticateToUnlockVault": {},
+ "vaultBinUsageDialogMessage": "Sommige kluizen gebruiken de prullenbak.",
+ "@vaultBinUsageDialogMessage": {},
+ "settingsDisablingBinWarningDialogMessage": "Items in de Prullenbak worden voor altijd verwijderd.",
+ "@settingsDisablingBinWarningDialogMessage": {},
+ "statsTopStatesSectionTitle": "Top Staten",
+ "@statsTopStatesSectionTitle": {},
+ "editorTransformRotate": "Roteren",
+ "@editorTransformRotate": {},
+ "editorActionTransform": "Transformeren",
+ "@editorActionTransform": {},
+ "stateEmpty": "Zonder Staten",
+ "@stateEmpty": {},
+ "settingsViewerShowRatingTags": "Waardering & labels tonen",
+ "@settingsViewerShowRatingTags": {},
+ "drawerPlacePage": "Plaatsen",
+ "@drawerPlacePage": {},
+ "newVaultWarningDialogMessage": "Items in kluizen zijn alleen beschikbaar voor deze app en niet voor andere.\n\nAls je deze app verwijdert of deze app-gegevens wist, verlies je al deze items.",
+ "@newVaultWarningDialogMessage": {},
+ "vaultDialogLockTypeLabel": "Vergrendelingstype",
+ "@vaultDialogLockTypeLabel": {},
+ "tagEditorDiscardDialogMessage": "Wijzigingen ongedaan maken?",
+ "@tagEditorDiscardDialogMessage": {},
+ "renameProcessorHash": "Controlenummer",
+ "@renameProcessorHash": {},
+ "castDialogTitle": "Cast-apparaten",
+ "@castDialogTitle": {},
+ "aboutDataUsageSectionTitle": "Gegevensgebruik",
+ "@aboutDataUsageSectionTitle": {},
+ "statePageTitle": "Staten",
+ "@statePageTitle": {},
+ "searchStatesSectionTitle": "Staten",
+ "@searchStatesSectionTitle": {},
+ "settingsVideoPlaybackTile": "Afspelen",
+ "@settingsVideoPlaybackTile": {},
+ "settingsVideoResumptionModeTile": "Afspelen hervatten",
+ "@settingsVideoResumptionModeTile": {},
+ "settingsVideoResumptionModeDialogTitle": "Afspelen hervatten",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "settingsVideoBackgroundMode": "Achtergrond-modus",
+ "@settingsVideoBackgroundMode": {},
+ "configureVaultDialogTitle": "Kluis configureren",
+ "@configureVaultDialogTitle": {},
+ "settingsWidgetDisplayedItem": "Getoond item",
+ "@settingsWidgetDisplayedItem": {},
+ "albumTierVaults": "Kluizen",
+ "@albumTierVaults": {},
+ "aboutDataUsageClearCache": "Cache wissen",
+ "@aboutDataUsageClearCache": {},
+ "placePageTitle": "Plaatsen",
+ "@placePageTitle": {},
+ "placeEmpty": "Zonder plaatsen",
+ "@placeEmpty": {},
+ "settingsCollectionBurstPatternsNone": "Geen",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsVideoPlaybackPageTitle": "Afspelen",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoBackgroundModeDialogTitle": "Achtergrond-modus",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsTile": "Burst-patronen",
+ "@settingsCollectionBurstPatternsTile": {},
+ "settingsAccessibilityShowPinchGestureAlternatives": "Alternatieven voor multi-touch-gebaren weergeven",
+ "@settingsAccessibilityShowPinchGestureAlternatives": {},
+ "settingsDisplayUseTvInterface": "Android TV-interface",
+ "@settingsDisplayUseTvInterface": {},
+ "settingsForceWesternArabicNumeralsTile": "Arabische cijfers forceren",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "explorerPageTitle": "Bestanden",
+ "@explorerPageTitle": {},
+ "columnCount": "{count, plural, =1{{count} kolom} other{{count} kolommen}}",
+ "@columnCount": {
+ "placeholders": {
+ "count": {
+ "format": "decimalPattern"
+ }
+ }
+ },
+ "widgetTapUpdateWidget": "Widget bijwerken",
+ "@widgetTapUpdateWidget": {},
+ "authenticateToConfigureVault": "Verifieer om de kluis te configureren",
+ "@authenticateToConfigureVault": {},
+ "settingsConfirmationVaultDataLoss": "Waarschuwing voor verlies van kluisgegevens weergeven",
+ "@settingsConfirmationVaultDataLoss": {},
+ "newVaultDialogTitle": "Nieuwe kluis",
+ "@newVaultDialogTitle": {},
+ "chipActionGoToExplorerPage": "In Bestanden tonen",
+ "@chipActionGoToExplorerPage": {},
+ "cropAspectRatioFree": "Vrij",
+ "@cropAspectRatioFree": {},
+ "videoActionABRepeat": "A-B herhalen",
+ "@videoActionABRepeat": {},
+ "viewerActionLock": "Weergave vergrendelen",
+ "@viewerActionLock": {},
+ "collectionActionSetHome": "Als startpagina instellen",
+ "@collectionActionSetHome": {},
+ "setHomeCustom": "Aangepast",
+ "@setHomeCustom": {},
+ "explorerActionSelectStorageVolume": "Selecteer opslag",
+ "@explorerActionSelectStorageVolume": {},
+ "selectStorageVolumeDialogTitle": "Selecteer opslag",
+ "@selectStorageVolumeDialogTitle": {}
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index 0e0c23180..4dab574a9 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR",
"@settingsThumbnailShowHdrIcon": {},
- "setHomeCustomCollection": "Własna kolekcja",
- "@setHomeCustomCollection": {},
"collectionActionSetHome": "Ustaw jako stronę główną",
"@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Ustaw początek",
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 30d164592..f3a4922ca 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Definir como início",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Coleção personalizada",
- "@setHomeCustomCollection": {},
"videoActionABRepeat": "Repetição A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Definir fim",
@@ -1372,5 +1370,13 @@
"videoRepeatActionSetStart": "Definir início",
"@videoRepeatActionSetStart": {},
"chipActionShowCollection": "Mostrar na Coleção",
- "@chipActionShowCollection": {}
+ "@chipActionShowCollection": {},
+ "renameProcessorHash": "Hash",
+ "@renameProcessorHash": {},
+ "settingsForceWesternArabicNumeralsTile": "Forçar numerais arábicos",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "chipActionGoToExplorerPage": "Mostrar no Explorador",
+ "@chipActionGoToExplorerPage": {},
+ "explorerPageTitle": "Explorador",
+ "@explorerPageTitle": {}
}
diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb
index f18518f55..80cb63323 100644
--- a/lib/l10n/app_ro.arb
+++ b/lib/l10n/app_ro.arb
@@ -1491,8 +1491,6 @@
"@collectionActionSetHome": {},
"aboutDataUsageClearCache": "Golește memoria cache",
"@aboutDataUsageClearCache": {},
- "setHomeCustomCollection": "Colecție personalizată",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR",
"@settingsThumbnailShowHdrIcon": {},
"settingsViewerShowHistogram": "Afișare histogramă",
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 612ecd260..13d019e99 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1359,8 +1359,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Показать значок HDR",
"@settingsThumbnailShowHdrIcon": {},
- "setHomeCustomCollection": "Собственная коллекция",
- "@setHomeCustomCollection": {},
"collectionActionSetHome": "Установить как главную",
"@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Установить начало",
@@ -1380,5 +1378,7 @@
"chipActionGoToExplorerPage": "Показать в проводнике",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Проводник",
- "@explorerPageTitle": {}
+ "@explorerPageTitle": {},
+ "explorerActionSelectStorageVolume": "Выбрать хранилище",
+ "@explorerActionSelectStorageVolume": {}
}
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index 639751980..28b9a8512 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"collectionActionSetHome": "Nastaviť ako doma",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Kolekcia na mieru",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR",
"@settingsThumbnailShowHdrIcon": {},
"chipActionShowCollection": "Zobraziť v kolekcií",
diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb
index 00bbff35e..8c29b282d 100644
--- a/lib/l10n/app_tr.arb
+++ b/lib/l10n/app_tr.arb
@@ -1321,8 +1321,6 @@
"@passwordDialogConfirm": {},
"collectionActionSetHome": "Ana ekran olarak ayarla",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "Kişisel koleksiyon",
- "@setHomeCustomCollection": {},
"statsTopStatesSectionTitle": "Baş Eyaletler",
"@statsTopStatesSectionTitle": {},
"pinDialogEnter": "PIN girin",
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index e38b5da66..cdfae5627 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Показати іконку HDR",
"@settingsThumbnailShowHdrIcon": {},
- "setHomeCustomCollection": "Власна колекція",
- "@setHomeCustomCollection": {},
"collectionActionSetHome": "Встановити як головну",
"@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Змінити початок",
@@ -1538,5 +1536,11 @@
"chipActionGoToExplorerPage": "Показати в провіднику",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Провідник",
- "@explorerPageTitle": {}
+ "@explorerPageTitle": {},
+ "setHomeCustom": "Власне",
+ "@setHomeCustom": {},
+ "explorerActionSelectStorageVolume": "Обрати сховище",
+ "@explorerActionSelectStorageVolume": {},
+ "selectStorageVolumeDialogTitle": "Оберіть сховище",
+ "@selectStorageVolumeDialogTitle": {}
}
diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb
index 6b072d378..d3626446f 100644
--- a/lib/l10n/app_vi.arb
+++ b/lib/l10n/app_vi.arb
@@ -1515,8 +1515,6 @@
"@entryActionCast": {},
"castDialogTitle": "Thiết bị truyền",
"@castDialogTitle": {},
- "setHomeCustomCollection": "Bộ sưu tập tùy chỉnh",
- "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Đặt làm nhà",
@@ -1534,5 +1532,13 @@
"settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập",
"@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Hiển thị trong Bộ sưu tập",
- "@chipActionShowCollection": {}
+ "@chipActionShowCollection": {},
+ "selectStorageVolumeDialogTitle": "Chọn dung lượng",
+ "@selectStorageVolumeDialogTitle": {},
+ "explorerActionSelectStorageVolume": "Chọn dung lượng",
+ "@explorerActionSelectStorageVolume": {},
+ "chipActionGoToExplorerPage": "Hiển thị ở Explorer",
+ "@chipActionGoToExplorerPage": {},
+ "explorerPageTitle": "Explorer",
+ "@explorerPageTitle": {}
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index b8dbdfcb9..43039c451 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -519,7 +519,7 @@
"@aboutTranslatorsSectionTitle": {},
"aboutLicensesSectionTitle": "开源许可协议",
"@aboutLicensesSectionTitle": {},
- "aboutLicensesBanner": "本应用使用以下开源软件包和库",
+ "aboutLicensesBanner": "本应用使用以下开源软件包和库。",
"@aboutLicensesBanner": {},
"aboutLicensesShowAllButtonLabel": "显示所有许可协议",
"@aboutLicensesShowAllButtonLabel": {},
@@ -1161,9 +1161,9 @@
"@settingsSubtitleThemeTextPositionTile": {},
"settingsSubtitleThemeTextPositionDialogTitle": "文本位置",
"@settingsSubtitleThemeTextPositionDialogTitle": {},
- "aboutLicensesDartPackagesSectionTitle": "Dart Packages",
+ "aboutLicensesDartPackagesSectionTitle": "Dart 软件包",
"@aboutLicensesDartPackagesSectionTitle": {},
- "aboutLicensesFlutterPackagesSectionTitle": "Flutter Packages",
+ "aboutLicensesFlutterPackagesSectionTitle": "Flutter 软件包",
"@aboutLicensesFlutterPackagesSectionTitle": {},
"keepScreenOnVideoPlayback": "视频播放期间",
"@keepScreenOnVideoPlayback": {},
@@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "设置为首页",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "自定义媒体集",
- "@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "设置起点",
"@videoRepeatActionSetStart": {},
"stopTooltip": "停止",
diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb
index 676617960..490b5e76a 100644
--- a/lib/l10n/app_zh_Hant.arb
+++ b/lib/l10n/app_zh_Hant.arb
@@ -1511,8 +1511,6 @@
"@overlayHistogramLuminance": {},
"collectionActionSetHome": "設為首頁",
"@collectionActionSetHome": {},
- "setHomeCustomCollection": "自訂收藏品",
- "@setHomeCustomCollection": {},
"aboutDataUsageClearCache": "清除快取",
"@aboutDataUsageClearCache": {},
"settingsViewerShowHistogram": "顯示直方圖",
@@ -1534,5 +1532,9 @@
"settingsForceWesternArabicNumeralsTile": "強制使用阿拉伯數字",
"@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "在收藏品中顯示",
- "@chipActionShowCollection": {}
+ "@chipActionShowCollection": {},
+ "explorerPageTitle": "檔案總管",
+ "@explorerPageTitle": {},
+ "chipActionGoToExplorerPage": "在檔案總管裡顯示",
+ "@chipActionGoToExplorerPage": {}
}
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
index 5c4204abc..925f81886 100644
--- a/lib/model/app/contributors.dart
+++ b/lib/model/app/contributors.dart
@@ -93,6 +93,8 @@ class Contributors {
Contributor('Maxi', 'maxitendo01@proton.me'),
Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'),
Contributor('elfriob', 'elfriob@ya.ru'),
+ Contributor('Stephan Paternotte', 'stephan@paternottes.net'),
+ Contributor('Tung Anh', 'buihuutunganh2007@gmail.com'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
@@ -102,6 +104,7 @@ class Contributors {
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
+ // Contributor('Sartaj', 'ssaarrttaajj111@gmail.com'), // Hindi
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)
diff --git a/lib/model/settings/modules/navigation.dart b/lib/model/settings/modules/navigation.dart
index c11c97968..a2c6969a5 100644
--- a/lib/model/settings/modules/navigation.dart
+++ b/lib/model/settings/modules/navigation.dart
@@ -14,11 +14,19 @@ mixin NavigationSettings on SettingsAccess {
HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
- set homePage(HomePageSetting newValue) => set(SettingKeys.homePageKey, newValue.toString());
-
Set get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
- set homeCustomCollection(Set newValue) => set(SettingKeys.homeCustomCollectionKey, newValue.map((filter) => filter.toJson()).toList());
+ String? get homeCustomExplorerPath => getString(SettingKeys.homeCustomExplorerPathKey);
+
+ void setHome(
+ HomePageSetting homePage, {
+ Set customCollection = const {},
+ String? customExplorerPath,
+ }) {
+ set(SettingKeys.homePageKey, homePage.toString());
+ set(SettingKeys.homeCustomCollectionKey, customCollection.map((filter) => filter.toJson()).toList());
+ set(SettingKeys.homeCustomExplorerPathKey, customExplorerPath);
+ }
bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar;
diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart
index e5a046b03..67302c862 100644
--- a/lib/model/settings/settings.dart
+++ b/lib/model/settings/settings.dart
@@ -440,6 +440,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
case SettingKeys.maxBrightnessKey:
case SettingKeys.keepScreenOnKey:
case SettingKeys.homePageKey:
+ case SettingKeys.homeCustomExplorerPathKey:
case SettingKeys.collectionGroupFactorKey:
case SettingKeys.collectionSortFactorKey:
case SettingKeys.thumbnailLocationIconKey:
diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart
index a198cc66e..6f21d047d 100644
--- a/lib/model/source/collection_source.dart
+++ b/lib/model/source/collection_source.dart
@@ -93,6 +93,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
_rawEntries.forEach((v) => v.dispose());
}
+ set safeMode(bool enabled);
+
final EventBus _eventBus = EventBus();
@override
diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart
index d616dbe98..b8e808001 100644
--- a/lib/model/source/media_store_source.dart
+++ b/lib/model/source/media_store_source.dart
@@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource {
final Set _changedUris = {};
int? _lastGeneration;
SourceInitializationState _initState = SourceInitializationState.none;
+ bool _safeMode = false;
+
+ @override
+ set safeMode(bool enabled) => _safeMode = enabled;
@override
SourceInitializationState get initState => _initState;
@@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
analysisController: analysisController,
directory: directory,
loadTopEntriesFirst: loadTopEntriesFirst,
- canAnalyze: canAnalyze,
+ canAnalyze: canAnalyze && !_safeMode,
));
}
@@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource {
pendingNewEntries.clear();
}
- mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
+ mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
(entry) {
// when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc.
diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart
index 6efd63410..da6042143 100644
--- a/lib/services/analysis_service.dart
+++ b/lib/services/analysis_service.dart
@@ -127,21 +127,16 @@ class Analyzer with WidgetsBindingObserver {
Future start(dynamic args) async {
List? entryIds;
var force = false;
- var progressTotal = 0, progressOffset = 0;
if (args is Map) {
entryIds = (args['entryIds'] as List?)?.cast();
force = args['force'] ?? false;
- progressTotal = args['progressTotal'];
- progressOffset = args['progressOffset'];
}
- await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
+ await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries');
_controller?.dispose();
_controller = AnalysisController(
canStartService: false,
entryIds: entryIds,
force: force,
- progressTotal: progressTotal,
- progressOffset: progressOffset,
);
settings.systemLocalesFallback = await deviceService.getLocales();
diff --git a/lib/services/app_service.dart b/lib/services/app_service.dart
index 25295e367..774dacedc 100644
--- a/lib/services/app_service.dart
+++ b/lib/services/app_service.dart
@@ -30,7 +30,7 @@ abstract class AppService {
Future shareSingle(String uri, String mimeType);
- Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? uri});
+ Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? explorerPath, String? uri});
}
class PlatformAppService implements AppService {
@@ -203,7 +203,7 @@ class PlatformAppService implements AppService {
// app shortcuts
@override
- Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? uri}) async {
+ Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? explorerPath, String? uri}) async {
Uint8List? iconBytes;
if (coverEntry != null) {
final size = coverEntry.isVideo ? 0.0 : 256.0;
@@ -222,6 +222,7 @@ class PlatformAppService implements AppService {
'label': label,
'iconBytes': iconBytes,
'filters': filters?.map((filter) => filter.toJson()).toList(),
+ 'explorerPath': explorerPath,
'uri': uri,
});
} on PlatformException catch (e, stack) {
diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart
index 72a7296f7..f60414ee7 100644
--- a/lib/services/media/media_store_service.dart
+++ b/lib/services/media/media_store_service.dart
@@ -15,7 +15,7 @@ abstract class MediaStoreService {
Future getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs
- Stream getEntries(Map knownEntries, {String? directory});
+ Stream getEntries(bool safe, Map knownEntries, {String? directory});
// returns media URI
Future scanFile(String path, String mimeType);
@@ -75,12 +75,13 @@ class PlatformMediaStoreService implements MediaStoreService {
}
@override
- Stream getEntries(Map knownEntries, {String? directory}) {
+ Stream getEntries(bool safe, Map knownEntries, {String? directory}) {
try {
return _stream
.receiveBroadcastStream({
'knownEntries': knownEntries,
'directory': directory,
+ 'safe': safe,
})
.where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map));
diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart
index e3c620ce0..0152bd1aa 100644
--- a/lib/utils/android_file_utils.dart
+++ b/lib/utils/android_file_utils.dart
@@ -106,8 +106,8 @@ class AndroidFileUtils {
if (isScreenshotsPath(dirPath)) return AlbumType.screenshots;
if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures;
- final dir = pContext.split(dirPath).last;
- if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
+ final dir = pContext.split(dirPath).lastOrNull;
+ if (dir != null && dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
return AlbumType.regular;
}
diff --git a/lib/view/src/actions/explorer.dart b/lib/view/src/actions/explorer.dart
new file mode 100644
index 000000000..38fb845c3
--- /dev/null
+++ b/lib/view/src/actions/explorer.dart
@@ -0,0 +1,23 @@
+import 'package:aves/theme/icons.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
+import 'package:aves_model/aves_model.dart';
+import 'package:flutter/material.dart';
+
+extension ExtraExplorerActionView on ExplorerAction {
+ String getText(BuildContext context) {
+ final l10n = context.l10n;
+ return switch (this) {
+ ExplorerAction.addShortcut => l10n.collectionActionAddShortcut,
+ ExplorerAction.setHome => l10n.collectionActionSetHome,
+ };
+ }
+
+ Widget getIcon() => Icon(_getIconData());
+
+ IconData _getIconData() {
+ return switch (this) {
+ ExplorerAction.addShortcut => AIcons.addShortcut,
+ ExplorerAction.setHome => AIcons.home,
+ };
+ }
+}
diff --git a/lib/widgets/about/about_tv_page.dart b/lib/widgets/about/about_tv_page.dart
index e8f0d8c68..4e01d8e43 100644
--- a/lib/widgets/about/about_tv_page.dart
+++ b/lib/widgets/about/about_tv_page.dart
@@ -21,7 +21,7 @@ class AboutTvPage extends StatelessWidget {
Widget build(BuildContext context) {
return AvesScaffold(
body: AvesPopScope(
- handlers: const [TvNavigationPopHandler.pop],
+ handlers: [tvNavigationPopHandler],
child: Row(
children: [
TvRail(
diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart
index 6ee414f49..f279c2984 100644
--- a/lib/widgets/aves_app.dart
+++ b/lib/widgets/aves_app.dart
@@ -174,7 +174,8 @@ class _AvesAppState extends State with WidgetsBindingObserver {
// Flutter has various page transition implementations for Android:
// - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
- // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
+ // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
+ // - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back
static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
static final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
static ScreenBrightness? _screenBrightness;
diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart
index 5a6812e99..777a4f262 100644
--- a/lib/widgets/collection/collection_page.dart
+++ b/lib/widgets/collection/collection_page.dart
@@ -55,7 +55,6 @@ class _CollectionPageState extends State {
final List _subscriptions = [];
late CollectionLens _collection;
final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast();
- final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
@@ -80,7 +79,6 @@ class _CollectionPageState extends State {
..forEach((sub) => sub.cancel())
..clear();
_collection.dispose();
- _doubleBackPopHandler.dispose();
super.dispose();
}
@@ -98,16 +96,12 @@ class _CollectionPageState extends State {
builder: (context) {
return AvesPopScope(
handlers: [
- (context) {
- final selection = context.read>();
- if (selection.isSelecting) {
- selection.browse();
- return false;
- }
- return true;
- },
- TvNavigationPopHandler.pop,
- _doubleBackPopHandler.pop,
+ APopHandler(
+ canPop: (context) => context.select, bool>((v) => !v.isSelecting),
+ onPopBlocked: (context) => context.read>().browse(),
+ ),
+ tvNavigationPopHandler,
+ doubleBackPopHandler,
],
child: GestureAreaProtectorStack(
child: DirectionalSafeArea(
diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart
index db0504ad1..11edabc8c 100644
--- a/lib/widgets/collection/entry_set_action_delegate.dart
+++ b/lib/widgets/collection/entry_set_action_delegate.dart
@@ -753,8 +753,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
}
void _setHome(BuildContext context) async {
- settings.homeCustomCollection = context.read().filters;
- settings.homePage = HomePageSetting.collection;
+ settings.setHome(HomePageSetting.collection, customCollection: context.read().filters);
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}
diff --git a/lib/widgets/common/behaviour/pop/double_back.dart b/lib/widgets/common/behaviour/pop/double_back.dart
index 224e264bb..b5a2b3729 100644
--- a/lib/widgets/common/behaviour/pop/double_back.dart
+++ b/lib/widgets/common/behaviour/pop/double_back.dart
@@ -1,48 +1,49 @@
import 'dart:async';
import 'package:aves/model/settings/settings.dart';
+import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
+import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:overlay_support/overlay_support.dart';
+import 'package:provider/provider.dart';
-class DoubleBackPopHandler {
+final DoubleBackPopHandler doubleBackPopHandler = DoubleBackPopHandler._private();
+
+class DoubleBackPopHandler extends PopHandler {
bool _backOnce = false;
Timer? _backTimer;
- DoubleBackPopHandler() {
- if (kFlutterMemoryAllocationsEnabled) {
- FlutterMemoryAllocations.instance.dispatchObjectCreated(
- library: 'aves',
- className: '$DoubleBackPopHandler',
- object: this,
- );
- }
+ DoubleBackPopHandler._private();
+
+ @override
+ bool canPop(BuildContext context) {
+ if (context.select((s) => !s.mustBackTwiceToExit)) return true;
+ if (Navigator.canPop(context)) return true;
+ return false;
}
- void dispose() {
- if (kFlutterMemoryAllocationsEnabled) {
- FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
- }
- _stopBackTimer();
- }
-
- bool pop(BuildContext context) {
- if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) {
+ @override
+ void onPopBlocked(BuildContext context) {
+ if (_backOnce) {
+ if (Navigator.canPop(context)) {
+ Navigator.maybeOf(context)?.pop();
+ } else {
+ // exit
+ reportService.log('Exit by pop');
+ PopExitNotification().dispatch(context);
+ SystemNavigator.pop();
+ }
+ } else {
_backOnce = true;
- _stopBackTimer();
+ _backTimer?.cancel();
_backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false);
toast(
context.l10n.doubleBackExitMessage,
duration: ADurations.doubleBackTimerDelay,
);
- return false;
}
- return true;
- }
-
- void _stopBackTimer() {
- _backTimer?.cancel();
}
}
diff --git a/lib/widgets/common/behaviour/pop/scope.dart b/lib/widgets/common/behaviour/pop/scope.dart
index c2f7bfb20..f75ca980e 100644
--- a/lib/widgets/common/behaviour/pop/scope.dart
+++ b/lib/widgets/common/behaviour/pop/scope.dart
@@ -1,11 +1,9 @@
-import 'package:aves/services/common/services.dart';
-import 'package:flutter/services.dart';
+import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
-// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random
-// so this widget combines multiple handlers with a guaranteed order
+// this widget combines multiple pop handlers with a guaranteed order
class AvesPopScope extends StatelessWidget {
- final List handlers;
+ final List handlers;
final Widget child;
const AvesPopScope({
@@ -16,21 +14,12 @@ class AvesPopScope extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final blocker = handlers.firstWhereOrNull((v) => !v.canPop(context));
return PopScope(
- canPop: false,
+ canPop: blocker == null,
onPopInvoked: (didPop) {
- if (didPop) return;
-
- final shouldPop = handlers.fold(true, (prev, v) => prev ? v(context) : false);
- if (shouldPop) {
- if (Navigator.canPop(context)) {
- Navigator.maybeOf(context)?.pop();
- } else {
- // exit
- reportService.log('Exit by pop');
- PopExitNotification().dispatch(context);
- SystemNavigator.pop();
- }
+ if (!didPop) {
+ blocker?.onPopBlocked(context);
}
},
child: child,
@@ -38,5 +27,28 @@ class AvesPopScope extends StatelessWidget {
}
}
+abstract class PopHandler {
+ bool canPop(BuildContext context);
+
+ void onPopBlocked(BuildContext context);
+}
+
+class APopHandler implements PopHandler {
+ final bool Function(BuildContext context) _canPop;
+ final void Function(BuildContext context) _onPopBlocked;
+
+ APopHandler({
+ required bool Function(BuildContext context) canPop,
+ required void Function(BuildContext context) onPopBlocked,
+ }) : _canPop = canPop,
+ _onPopBlocked = onPopBlocked;
+
+ @override
+ bool canPop(BuildContext context) => _canPop(context);
+
+ @override
+ void onPopBlocked(BuildContext context) => _onPopBlocked(context);
+}
+
@immutable
class PopExitNotification extends Notification {}
diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart
index b55953eb8..b895aa917 100644
--- a/lib/widgets/common/behaviour/pop/tv_navigation.dart
+++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart
@@ -3,6 +3,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/collection/collection_page.dart';
+import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
@@ -11,18 +12,25 @@ import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
-class TvNavigationPopHandler {
- static bool pop(BuildContext context) {
- if (!settings.useTvLayout || _isHome(context)) {
- return true;
- }
+final TvNavigationPopHandler tvNavigationPopHandler = TvNavigationPopHandler._private();
+// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
+class TvNavigationPopHandler implements PopHandler {
+ TvNavigationPopHandler._private();
+
+ @override
+ bool canPop(BuildContext context) {
+ if (context.select((s) => !s.useTvLayout)) return true;
+ if (_isHome(context)) return true;
+ return false;
+ }
+
+ @override
+ void onPopBlocked(BuildContext context) {
Navigator.maybeOf(context)?.pushAndRemoveUntil(
_getHomeRoute(),
(route) => false,
);
- return false;
}
static bool _isHome(BuildContext context) {
diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart
index 65bbddce9..6bbf50f8f 100644
--- a/lib/widgets/common/search/page.dart
+++ b/lib/widgets/common/search/page.dart
@@ -1,4 +1,3 @@
-
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/debouncer.dart';
@@ -31,7 +30,6 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State {
final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay);
final FocusNode _searchFieldFocusNode = FocusNode();
- final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
@@ -55,7 +53,6 @@ class _SearchPageState extends State {
_unregisterWidget(widget);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
_searchFieldFocusNode.dispose();
- _doubleBackPopHandler.dispose();
widget.delegate.dispose();
super.dispose();
}
@@ -151,8 +148,8 @@ class _SearchPageState extends State {
),
body: AvesPopScope(
handlers: [
- TvNavigationPopHandler.pop,
- _doubleBackPopHandler.pop,
+ tvNavigationPopHandler,
+ doubleBackPopHandler,
],
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart
index 9b8296419..79e7259a1 100644
--- a/lib/widgets/debug/app_debug_page.dart
+++ b/lib/widgets/debug/app_debug_page.dart
@@ -67,7 +67,7 @@ class AppDebugPage extends StatelessWidget {
],
),
body: AvesPopScope(
- handlers: const [TvNavigationPopHandler.pop],
+ handlers: [tvNavigationPopHandler],
child: SafeArea(
child: ListView(
padding: const EdgeInsets.all(8),
diff --git a/lib/widgets/dialogs/select_storage_dialog.dart b/lib/widgets/dialogs/select_storage_dialog.dart
new file mode 100644
index 000000000..fa667c19e
--- /dev/null
+++ b/lib/widgets/dialogs/select_storage_dialog.dart
@@ -0,0 +1,75 @@
+import 'package:aves/utils/android_file_utils.dart';
+import 'package:aves/view/view.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
+import 'package:aves/widgets/dialogs/aves_dialog.dart';
+import 'package:aves_model/aves_model.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+
+class SelectStorageDialog extends StatefulWidget {
+ static const routeName = '/dialog/select_storage';
+
+ final StorageVolume? initialVolume;
+
+ const SelectStorageDialog({super.key, this.initialVolume});
+
+ @override
+ State createState() => _SelectStorageDialogState();
+}
+
+class _SelectStorageDialogState extends State {
+ late Set _allVolumes;
+ late StorageVolume? _primaryVolume, _selectedVolume;
+
+ @override
+ void initState() {
+ super.initState();
+ _allVolumes = androidFileUtils.storageVolumes;
+ _primaryVolume = _allVolumes.firstWhereOrNull((volume) => volume.isPrimary) ?? _allVolumes.firstOrNull;
+ _selectedVolume = widget.initialVolume ?? _primaryVolume;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final byPrimary = groupBy(_allVolumes, (volume) => volume.isPrimary);
+ int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCaseNatural(a.path, b.path);
+ final primaryVolumes = (byPrimary[true] ?? [])..sort(compare);
+ final otherVolumes = (byPrimary[false] ?? [])..sort(compare);
+
+ return AvesDialog(
+ title: context.l10n.selectStorageVolumeDialogTitle,
+ scrollableContent: [
+ ...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)),
+ ...otherVolumes.map((volume) => _buildVolumeTile(context, volume)),
+ ],
+ actions: [
+ const CancelButton(),
+ TextButton(
+ onPressed: () => Navigator.maybeOf(context)?.pop(_selectedVolume),
+ child: Text(context.l10n.applyButtonLabel),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildVolumeTile(BuildContext context, StorageVolume volume) => RadioListTile(
+ value: volume,
+ groupValue: _selectedVolume,
+ onChanged: (volume) {
+ _selectedVolume = volume!;
+ setState(() {});
+ },
+ title: Text(
+ volume.getDescription(context),
+ softWrap: false,
+ overflow: TextOverflow.fade,
+ maxLines: 1,
+ ),
+ subtitle: Text(
+ volume.path,
+ softWrap: false,
+ overflow: TextOverflow.fade,
+ maxLines: 1,
+ ),
+ );
+}
diff --git a/lib/widgets/dialogs/selection_dialogs/common.dart b/lib/widgets/dialogs/selection_dialogs/common.dart
index 3c80ec6a9..7fa8b4708 100644
--- a/lib/widgets/dialogs/selection_dialogs/common.dart
+++ b/lib/widgets/dialogs/selection_dialogs/common.dart
@@ -20,4 +20,4 @@ Future showSelectionDialog({
}
}
-typedef TextBuilder = String Function(T value);
+typedef TextBuilder = String? Function(T value);
diff --git a/lib/widgets/explorer/app_bar.dart b/lib/widgets/explorer/app_bar.dart
index 061b04435..c5a996ee9 100644
--- a/lib/widgets/explorer/app_bar.dart
+++ b/lib/widgets/explorer/app_bar.dart
@@ -7,6 +7,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/android_file_utils.dart';
+import 'package:aves/view/src/actions/explorer.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
@@ -15,9 +16,12 @@ import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/search/route.dart';
+import 'package:aves/widgets/dialogs/select_storage_dialog.dart';
+import 'package:aves/widgets/explorer/explorer_action_delegate.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
import 'package:aves_model/aves_model.dart';
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
@@ -108,32 +112,75 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse
onPressed: () => _goToSearch(context),
tooltip: MaterialLocalizations.of(context).searchFieldLabel,
),
- if (_volumes.length > 1)
- FontSizeIconTheme(
- child: PopupMenuButton(
- itemBuilder: (context) {
- return _volumes.map((v) {
- final selected = widget.directoryNotifier.value.volumePath == v.path;
- final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
- return PopupMenuItem(
- value: v,
- enabled: !selected,
- child: MenuRow(
- text: v.getDescription(context),
- icon: Icon(icon),
- ),
- );
- }).toList();
- },
- onSelected: (volume) async {
- // wait for the popup menu to hide before proceeding with the action
- await Future.delayed(animations.popUpAnimationDelay * timeDilation);
- widget.goTo(volume.path);
- },
- popUpAnimationStyle: animations.popUpAnimationStyle,
- ),
- ),
- ];
+ if (_volumes.length > 1) _buildVolumeSelector(context),
+ PopupMenuButton(
+ itemBuilder: (context) {
+ return [
+ ExplorerAction.addShortcut,
+ ExplorerAction.setHome,
+ ].map((v) {
+ return PopupMenuItem(
+ value: v,
+ child: MenuRow(text: v.getText(context), icon: v.getIcon()),
+ );
+ }).toList();
+ },
+ onSelected: (action) async {
+ // wait for the popup menu to hide before proceeding with the action
+ await Future.delayed(animations.popUpAnimationDelay * timeDilation);
+ final directory = widget.directoryNotifier.value;
+ ExplorerActionDelegate(directory: directory).onActionSelected(context, action);
+ },
+ popUpAnimationStyle: animations.popUpAnimationStyle,
+ ),
+ ].map((v) => FontSizeIconTheme(child: v)).toList();
+ }
+
+ Widget _buildVolumeSelector(BuildContext context) {
+ if (_volumes.length == 2) {
+ return ValueListenableBuilder(
+ valueListenable: widget.directoryNotifier,
+ builder: (context, directory, child) {
+ final currentVolume = directory.volumePath;
+ final otherVolume = _volumes.firstWhere((volume) => volume.path != currentVolume);
+ final icon = otherVolume.isRemovable ? AIcons.storageCard : AIcons.storageMain;
+ return IconButton(
+ icon: Icon(icon),
+ onPressed: () => widget.goTo(otherVolume.path),
+ tooltip: otherVolume.getDescription(context),
+ );
+ },
+ );
+ } else {
+ return IconButton(
+ icon: const Icon(AIcons.storageCard),
+ onPressed: () async {
+ _volumes.map((v) {
+ final selected = widget.directoryNotifier.value.volumePath == v.path;
+ final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
+ return PopupMenuItem(
+ value: v,
+ enabled: !selected,
+ child: MenuRow(
+ text: v.getDescription(context),
+ icon: Icon(icon),
+ ),
+ );
+ }).toList();
+ final volumePath = widget.directoryNotifier.value.volumePath;
+ final initialVolume = _volumes.firstWhereOrNull((v) => v.path == volumePath);
+ final volume = await showDialog(
+ context: context,
+ builder: (context) => SelectStorageDialog(initialVolume: initialVolume),
+ routeSettings: const RouteSettings(name: SelectStorageDialog.routeName),
+ );
+ if (volume != null) {
+ widget.goTo(volume.path);
+ }
+ },
+ tooltip: context.l10n.explorerActionSelectStorageVolume,
+ );
+ }
}
double get appBarContentHeight {
diff --git a/lib/widgets/explorer/explorer_action_delegate.dart b/lib/widgets/explorer/explorer_action_delegate.dart
new file mode 100644
index 000000000..82d938d52
--- /dev/null
+++ b/lib/widgets/explorer/explorer_action_delegate.dart
@@ -0,0 +1,85 @@
+import 'package:aves/app_mode.dart';
+import 'package:aves/model/device.dart';
+import 'package:aves/model/entry/entry.dart';
+import 'package:aves/model/filters/path.dart';
+import 'package:aves/model/settings/settings.dart';
+import 'package:aves/model/source/collection_lens.dart';
+import 'package:aves/model/source/collection_source.dart';
+import 'package:aves/services/common/services.dart';
+import 'package:aves/widgets/common/action_mixins/feedback.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
+import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
+import 'package:aves_model/aves_model.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class ExplorerActionDelegate with FeedbackMixin {
+ final VolumeRelativeDirectory directory;
+
+ ExplorerActionDelegate({required this.directory});
+
+ bool isVisible(
+ ExplorerAction action, {
+ required AppMode appMode,
+ }) {
+ final isMain = appMode == AppMode.main;
+ final useTvLayout = settings.useTvLayout;
+ switch (action) {
+ case ExplorerAction.addShortcut:
+ return isMain && device.canPinShortcut;
+ case ExplorerAction.setHome:
+ return isMain && !useTvLayout;
+ }
+ }
+
+ bool canApply(ExplorerAction action) {
+ switch (action) {
+ case ExplorerAction.addShortcut:
+ case ExplorerAction.setHome:
+ return true;
+ }
+ }
+
+ void onActionSelected(BuildContext context, ExplorerAction action) {
+ reportService.log('$action');
+ switch (action) {
+ case ExplorerAction.addShortcut:
+ _addShortcut(context);
+ case ExplorerAction.setHome:
+ _setHome(context);
+ }
+ }
+
+ Future _addShortcut(BuildContext context) async {
+ final path = directory.dirPath;
+ final filter = PathFilter(path);
+ final defaultName = filter.getLabel(context);
+ final collection = CollectionLens(
+ source: context.read(),
+ filters: {filter},
+ );
+
+ final result = await showDialog<(AvesEntry?, String)>(
+ context: context,
+ builder: (context) => AddShortcutDialog(
+ defaultName: defaultName,
+ collection: collection,
+ ),
+ routeSettings: const RouteSettings(name: AddShortcutDialog.routeName),
+ );
+ if (result == null) return;
+
+ final (coverEntry, name) = result;
+ if (name.isEmpty) return;
+
+ await appService.pinToHomeScreen(name, coverEntry, explorerPath: path);
+ if (!device.showPinShortcutFeedback) {
+ showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
+ }
+ }
+
+ void _setHome(BuildContext context) async {
+ settings.setHome(HomePageSetting.explorer, customExplorerPath: directory.dirPath);
+ showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
+ }
+}
diff --git a/lib/widgets/explorer/explorer_page.dart b/lib/widgets/explorer/explorer_page.dart
index 6119e7098..a414a8d95 100644
--- a/lib/widgets/explorer/explorer_page.dart
+++ b/lib/widgets/explorer/explorer_page.dart
@@ -43,7 +43,6 @@ class _ExplorerPageState extends State {
final List _subscriptions = [];
final ValueNotifier _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: ''));
final ValueNotifier> _contents = ValueNotifier([]);
- final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
Set get _volumes => androidFileUtils.storageVolumes;
@@ -78,99 +77,95 @@ class _ExplorerPageState extends State {
..clear();
_directory.dispose();
_contents.dispose();
- _doubleBackPopHandler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
- return AvesPopScope(
- handlers: [
- (context) {
- if (_directory.value.relativeDir.isNotEmpty) {
- final parent = pContext.dirname(_currentDirectoryPath);
- _goTo(parent);
- return false;
- }
- return true;
- },
- TvNavigationPopHandler.pop,
- _doubleBackPopHandler.pop,
- ],
- child: AvesScaffold(
- drawer: const AppDrawer(),
- body: GestureAreaProtectorStack(
- child: Column(
- children: [
- Expanded(
- child: ValueListenableBuilder>(
- valueListenable: _contents,
- builder: (context, contents, child) {
- final durations = context.watch();
- return CustomScrollView(
- // workaround to prevent scrolling the app bar away
- // when there is no content and we use `SliverFillRemaining`
- physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null,
- slivers: [
- ExplorerAppBar(
- key: const Key('appbar'),
- directoryNotifier: _directory,
- goTo: _goTo,
- ),
- AnimationLimiter(
- // animation limiter should not be above the app bar
- // so that the crumb line can automatically scroll
- key: ValueKey(_currentDirectoryPath),
- child: SliverList.builder(
- itemBuilder: (context, index) {
- return AnimationConfiguration.staggeredList(
- position: index,
- duration: durations.staggeredAnimation,
- delay: durations.staggeredAnimationDelay * timeDilation,
- child: SlideAnimation(
- verticalOffset: 50.0,
- child: FadeInAnimation(
- child: _buildContentLine(context, contents[index]),
- ),
- ),
- );
- },
- itemCount: contents.length,
- ),
- ),
- contents.isEmpty
- ? SliverFillRemaining(
- child: _buildEmptyContent(),
- )
- : const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
- ],
- );
- },
- ),
- ),
- const Divider(height: 0),
- SafeArea(
- top: false,
- bottom: true,
- child: Padding(
- padding: const EdgeInsets.all(8),
- child: ValueListenableBuilder(
- valueListenable: _directory,
- builder: (context, directory, child) {
- return AvesFilterChip(
+ return ValueListenableBuilder(
+ valueListenable: _directory,
+ builder: (context, directory, child) {
+ final atRoot = directory.relativeDir.isEmpty;
+ return AvesPopScope(
+ handlers: [
+ APopHandler(
+ canPop: (context) => atRoot,
+ onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)),
+ ),
+ tvNavigationPopHandler,
+ doubleBackPopHandler,
+ ],
+ child: AvesScaffold(
+ drawer: const AppDrawer(),
+ body: GestureAreaProtectorStack(
+ child: Column(
+ children: [
+ Expanded(
+ child: ValueListenableBuilder>(
+ valueListenable: _contents,
+ builder: (context, contents, child) {
+ final durations = context.watch();
+ return CustomScrollView(
+ // workaround to prevent scrolling the app bar away
+ // when there is no content and we use `SliverFillRemaining`
+ physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null,
+ slivers: [
+ ExplorerAppBar(
+ key: const Key('appbar'),
+ directoryNotifier: _directory,
+ goTo: _goTo,
+ ),
+ AnimationLimiter(
+ // animation limiter should not be above the app bar
+ // so that the crumb line can automatically scroll
+ key: ValueKey(_currentDirectoryPath),
+ child: SliverList.builder(
+ itemBuilder: (context, index) {
+ return AnimationConfiguration.staggeredList(
+ position: index,
+ duration: durations.staggeredAnimation,
+ delay: durations.staggeredAnimationDelay * timeDilation,
+ child: SlideAnimation(
+ verticalOffset: 50.0,
+ child: FadeInAnimation(
+ child: _buildContentLine(context, contents[index]),
+ ),
+ ),
+ );
+ },
+ itemCount: contents.length,
+ ),
+ ),
+ contents.isEmpty
+ ? SliverFillRemaining(
+ child: _buildEmptyContent(),
+ )
+ : const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
+ ],
+ );
+ },
+ ),
+ ),
+ const Divider(height: 0),
+ SafeArea(
+ top: false,
+ bottom: true,
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: AvesFilterChip(
filter: PathFilter(_currentDirectoryPath),
maxWidth: double.infinity,
onTap: (filter) => _goToCollectionPage(context, filter),
onLongPress: null,
- );
- },
+ ),
+ ),
),
- ),
+ ],
),
- ],
+ ),
),
- ),
- ),
+ );
+ },
);
}
diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart
index 1d69ff71f..9cc9fed3b 100644
--- a/lib/widgets/filter_grids/common/filter_grid_page.dart
+++ b/lib/widgets/filter_grids/common/filter_grid_page.dart
@@ -191,12 +191,10 @@ class _FilterGrid extends StatefulWidget {
class _FilterGridState extends State<_FilterGrid> {
TileExtentController? _tileExtentController;
- final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void dispose() {
_tileExtentController?.dispose();
- _doubleBackPopHandler.dispose();
super.dispose();
}
@@ -212,16 +210,12 @@ class _FilterGridState extends State<_FilterGrid>
);
return AvesPopScope(
handlers: [
- (context) {
- final selection = context.read>>();
- if (selection.isSelecting) {
- selection.browse();
- return false;
- }
- return true;
- },
- TvNavigationPopHandler.pop,
- _doubleBackPopHandler.pop,
+ APopHandler(
+ canPop: (context) => context.select>, bool>((v) => !v.isSelecting),
+ onPopBlocked: (context) => context.read>>().browse(),
+ ),
+ tvNavigationPopHandler,
+ doubleBackPopHandler,
],
child: TileExtentControllerProvider(
controller: _tileExtentController!,
diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart
index 64fc2ae08..5ec822745 100644
--- a/lib/widgets/home_page.dart
+++ b/lib/widgets/home_page.dart
@@ -61,11 +61,13 @@ class _HomePageState extends State {
int? _widgetId;
String? _initialRouteName, _initialSearchQuery;
Set? _initialFilters;
+ String? _initialExplorerPath;
List? _secureUris;
static const allowedShortcutRoutes = [
- CollectionPage.routeName,
AlbumListPage.routeName,
+ CollectionPage.routeName,
+ ExplorerPage.routeName,
SearchPage.routeName,
];
@@ -92,6 +94,7 @@ class _HomePageState extends State {
final safeMode = intentData[IntentDataKeys.safeMode] ?? false;
final intentAction = intentData[IntentDataKeys.action];
_initialFilters = null;
+ _initialExplorerPath = null;
_secureUris = null;
await androidFileUtils.init();
@@ -186,6 +189,7 @@ class _HomePageState extends State {
final extraFilters = intentData[IntentDataKeys.filters];
_initialFilters = extraFilters != null ? (extraFilters as List).cast().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
}
+ _initialExplorerPath = intentData[IntentDataKeys.explorerPath];
}
context.read>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
@@ -199,10 +203,10 @@ class _HomePageState extends State {
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read();
+ source.safeMode = safeMode;
if (source.initState != SourceInitializationState.full) {
await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
- canAnalyze: !safeMode,
);
}
case AppMode.screenSaver:
@@ -351,7 +355,8 @@ class _HomePageState extends State {
case TagListPage.routeName:
return buildRoute((context) => const TagListPage());
case ExplorerPage.routeName:
- return buildRoute((context) => const ExplorerPage());
+ final path = _initialExplorerPath ?? settings.homeCustomExplorerPath;
+ return buildRoute((context) => ExplorerPage(path: path));
case HomeWidgetSettingsPage.routeName:
return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!));
case ScreenSaverPage.routeName:
diff --git a/lib/widgets/intent.dart b/lib/widgets/intent.dart
index e0a97587e..da8f1dca5 100644
--- a/lib/widgets/intent.dart
+++ b/lib/widgets/intent.dart
@@ -15,6 +15,7 @@ class IntentDataKeys {
static const action = 'action';
static const allowMultiple = 'allowMultiple';
static const brightness = 'brightness';
+ static const explorerPath = 'explorerPath';
static const filters = 'filters';
static const mimeType = 'mimeType';
static const page = 'page';
diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart
index a23493848..c2b323957 100644
--- a/lib/widgets/settings/navigation/navigation.dart
+++ b/lib/widgets/settings/navigation/navigation.dart
@@ -2,8 +2,10 @@ import 'dart:async';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
+import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
+import 'package:aves/theme/text.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tile_leading.dart';
@@ -43,24 +45,44 @@ class NavigationSection extends SettingsSection {
class _HomeOption {
final HomePageSetting page;
final Set customCollection;
+ final String? customExplorerPath;
const _HomeOption(
this.page, {
this.customCollection = const {},
+ this.customExplorerPath,
});
String getName(BuildContext context) {
- if (page == HomePageSetting.collection && customCollection.isNotEmpty) {
- return context.l10n.setHomeCustomCollection;
+ final pageName = page.getName(context);
+ switch (page) {
+ case HomePageSetting.collection:
+ return customCollection.isNotEmpty ? context.l10n.setHomeCustom : pageName;
+ case HomePageSetting.explorer:
+ return customExplorerPath != null ? context.l10n.setHomeCustom : pageName;
+ default:
+ return pageName;
+ }
+ }
+
+ String? getDetails(BuildContext context) {
+ switch (page) {
+ case HomePageSetting.collection:
+ final filters = customCollection;
+ return filters.isNotEmpty ? [context.l10n.collectionPageTitle, filters.map((v) => v.getLabel(context)).join(', ')].join(AText.separator) : null;
+ case HomePageSetting.explorer:
+ final path = customExplorerPath;
+ return path != null ? [context.l10n.explorerPageTitle, pContext.basename(path)].join(AText.separator) : null;
+ default:
+ return null;
}
- return page.getName(context);
}
@override
- bool operator ==(Object other) => identical(this, other) || other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection);
+ bool operator ==(Object other) => identical(this, other) || (other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection) && customExplorerPath == other.customExplorerPath);
@override
- int get hashCode => page.hashCode ^ customCollection.hashCode;
+ int get hashCode => page.hashCode ^ customCollection.hashCode ^ customExplorerPath.hashCode;
}
class SettingsTileNavigationHomePage extends SettingsTile {
@@ -75,15 +97,18 @@ class SettingsTileNavigationHomePage extends SettingsTile {
const _HomeOption(HomePageSetting.tags),
const _HomeOption(HomePageSetting.explorer),
if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection),
+ if (settings.homeCustomExplorerPath != null) _HomeOption(HomePageSetting.explorer, customExplorerPath: settings.homeCustomExplorerPath),
],
getName: (context, v) => v.getName(context),
- selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection),
- onSelection: (v) {
- settings.homePage = v.page;
- settings.homeCustomCollection = v.customCollection;
- },
+ selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection, customExplorerPath: s.homeCustomExplorerPath),
+ onSelection: (v) => settings.setHome(
+ v.page,
+ customCollection: v.customCollection,
+ customExplorerPath: v.customExplorerPath,
+ ),
tileTitle: title(context),
dialogTitle: context.l10n.settingsHomeDialogTitle,
+ optionSubtitleBuilder: (v) => v.getDetails(context),
);
}
diff --git a/lib/widgets/settings/settings_tv_page.dart b/lib/widgets/settings/settings_tv_page.dart
index 2f74d3d52..753d5042c 100644
--- a/lib/widgets/settings/settings_tv_page.dart
+++ b/lib/widgets/settings/settings_tv_page.dart
@@ -16,7 +16,7 @@ class SettingsTvPage extends StatelessWidget {
Widget build(BuildContext context) {
return AvesScaffold(
body: AvesPopScope(
- handlers: const [TvNavigationPopHandler.pop],
+ handlers: [tvNavigationPopHandler],
child: Row(
children: [
TvRail(
diff --git a/plugins/aves_model/lib/aves_model.dart b/plugins/aves_model/lib/aves_model.dart
index bef278c0c..e406bb5c4 100644
--- a/plugins/aves_model/lib/aves_model.dart
+++ b/plugins/aves_model/lib/aves_model.dart
@@ -1,6 +1,7 @@
library aves_model;
export 'src/actions/chip.dart';
+export 'src/actions/explorer.dart';
export 'src/actions/chip_set.dart';
export 'src/actions/entry.dart';
export 'src/actions/entry_set.dart';
diff --git a/plugins/aves_model/lib/src/actions/explorer.dart b/plugins/aves_model/lib/src/actions/explorer.dart
new file mode 100644
index 000000000..f71619c75
--- /dev/null
+++ b/plugins/aves_model/lib/src/actions/explorer.dart
@@ -0,0 +1,4 @@
+enum ExplorerAction {
+ addShortcut,
+ setHome,
+}
diff --git a/plugins/aves_model/lib/src/settings/keys.dart b/plugins/aves_model/lib/src/settings/keys.dart
index 4517d18d9..a0ca10939 100644
--- a/plugins/aves_model/lib/src/settings/keys.dart
+++ b/plugins/aves_model/lib/src/settings/keys.dart
@@ -43,6 +43,7 @@ class SettingKeys {
static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page';
static const homeCustomCollectionKey = 'home_custom_collection';
+ static const homeCustomExplorerPathKey = 'home_custom_explorer_path';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmCreateVaultKey = 'confirm_create_vault';
static const confirmDeleteForeverKey = 'confirm_delete_forever';
diff --git a/plugins/aves_screen_state/android/build.gradle b/plugins/aves_screen_state/android/build.gradle
index 2a7c98004..79767327b 100644
--- a/plugins/aves_screen_state/android/build.gradle
+++ b/plugins/aves_screen_state/android/build.gradle
@@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript {
ext {
kotlin_version = '1.9.24'
- agp_version = '8.5.0'
+ agp_version = '8.5.1'
}
repositories {
diff --git a/pubspec.yaml b/pubspec.yaml
index 69244d036..602f621c4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
-version: 1.11.5+124
+version: 1.11.6+125
publish_to: none
environment:
diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart
index d3037b39b..81e8e08a9 100644
--- a/test/fake/media_store_service.dart
+++ b/test/fake/media_store_service.dart
@@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
}
@override
- Stream getEntries(Map knownEntries, {String? directory}) => Stream.fromIterable(entries);
+ Stream getEntries(bool safe, Map knownEntries, {String? directory}) => Stream.fromIterable(entries);
static var _lastId = 1;
diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart
index 2013db5a3..48bdb8e91 100644
--- a/test_driver/driver_screenshots.dart
+++ b/test_driver/driver_screenshots.dart
@@ -30,8 +30,7 @@ Future configureAndLaunch() async {
..enableBlurEffect = true
// navigation
..keepScreenOn = KeepScreenOn.always
- ..homePage = HomePageSetting.collection
- ..homeCustomCollection = {}
+ ..setHome(HomePageSetting.collection)
..enableBottomNavigationBar = true
..drawerTypeBookmarks = [null, FavouriteFilter.instance]
// collection
diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart
index 0d8382fc6..ac303883f 100644
--- a/test_driver/driver_shaders.dart
+++ b/test_driver/driver_shaders.dart
@@ -26,8 +26,7 @@ Future configureAndLaunch() async {
..enableBlurEffect = true
// navigation
..keepScreenOn = KeepScreenOn.always
- ..homePage = HomePageSetting.collection
- ..homeCustomCollection = {}
+ ..setHome(HomePageSetting.collection)
..enableBottomNavigationBar = true
// collection
..collectionSectionFactor = EntryGroupFactor.album
diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US
index d8b37d15e..3ab2a9554 100644
--- a/whatsnew/whatsnew-en-US
+++ b/whatsnew/whatsnew-en-US
@@ -1,4 +1,4 @@
-In v1.11.5:
+In v1.11.6:
- explore your collection with the... explorer
- convert your motion photos to stills in bulk
Full changelog available on GitHub
\ No newline at end of file