diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b4ec2e3dc..8b6aa1d53 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -21,7 +21,7 @@ android {
applicationId = "com.dot.gallery"
minSdk = 30
targetSdk = 35
- versionCode = 30024
+ versionCode = 30033
versionName = "3.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -132,7 +132,6 @@ dependencies {
// Compose
implementation(libs.compose.activity)
- implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
@@ -144,6 +143,9 @@ dependencies {
// Compose - Material3
implementation(libs.compose.material3)
implementation(libs.compose.material3.window.size)
+ implementation(libs.androidx.adaptive)
+ implementation(libs.androidx.adaptive.layout)
+ implementation(libs.androidx.adaptive.navigation)
// Compose - Accompanists
implementation(libs.accompanist.permissions)
@@ -162,6 +164,8 @@ dependencies {
// Dagger - Hilt
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.dagger.hilt)
+ implementation(libs.androidx.hilt.common)
+ implementation(libs.androidx.hilt.work)
ksp(libs.dagger.hilt.compiler)
ksp(libs.androidx.hilt.compiler)
@@ -221,10 +225,16 @@ dependencies {
// Composables - Core
implementation(libs.core)
+ // Worker
+ implementation(libs.androidx.work.runtime.ktx)
+
+ // Composable - Scrollbar
+ implementation(libs.lazycolumnscrollbar)
+
+
// Tests
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
- androidTestImplementation(platform(libs.compose.bom))
debugImplementation(libs.compose.ui.tooling)
debugRuntimeOnly(libs.compose.ui.test.manifest)
}
diff --git a/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/4.json b/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/4.json
index 741cc9a74..57b91fbe9 100644
--- a/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/4.json
+++ b/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/4.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 4,
- "identityHash": "b923fda23747db68de4db23fb2360ff8",
+ "identityHash": "d11e7f0b804e55f9526cc63d181876a1",
"entities": [
{
"tableName": "pinned_table",
@@ -69,12 +69,142 @@
},
"indices": [],
"foreignKeys": []
+ },
+ {
+ "tableName": "media",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `label` TEXT NOT NULL, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, `relativePath` TEXT NOT NULL, `albumID` INTEGER NOT NULL, `albumLabel` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `expiryTimestamp` INTEGER, `takenTimestamp` INTEGER, `fullDate` TEXT NOT NULL, `mimeType` TEXT NOT NULL, `favorite` INTEGER NOT NULL, `trashed` INTEGER NOT NULL, `size` INTEGER NOT NULL, `duration` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "label",
+ "columnName": "label",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "relativePath",
+ "columnName": "relativePath",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "albumID",
+ "columnName": "albumID",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "albumLabel",
+ "columnName": "albumLabel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expiryTimestamp",
+ "columnName": "expiryTimestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "takenTimestamp",
+ "columnName": "takenTimestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fullDate",
+ "columnName": "fullDate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favorite",
+ "columnName": "favorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trashed",
+ "columnName": "trashed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "media_version",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`version` TEXT NOT NULL, PRIMARY KEY(`version`))",
+ "fields": [
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "version"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b923fda23747db68de4db23fb2360ff8')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd11e7f0b804e55f9526cc63d181876a1')"
]
}
}
\ No newline at end of file
diff --git a/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/5.json b/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/5.json
new file mode 100644
index 000000000..3c4cb0059
--- /dev/null
+++ b/app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/5.json
@@ -0,0 +1,258 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 5,
+ "identityHash": "65c73e58b535f1a24397f4143bdf1dde",
+ "entities": [
+ {
+ "tableName": "pinned_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "blacklist",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `label` TEXT NOT NULL, `wildcard` TEXT, `location` INTEGER NOT NULL DEFAULT 0, `matchedAlbums` TEXT NOT NULL DEFAULT '[]', PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "label",
+ "columnName": "label",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "wildcard",
+ "columnName": "wildcard",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "matchedAlbums",
+ "columnName": "matchedAlbums",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'[]'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "media",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `label` TEXT NOT NULL, `uri` TEXT NOT NULL, `path` TEXT NOT NULL, `relativePath` TEXT NOT NULL, `albumID` INTEGER NOT NULL, `albumLabel` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `expiryTimestamp` INTEGER, `takenTimestamp` INTEGER, `fullDate` TEXT NOT NULL, `mimeType` TEXT NOT NULL, `favorite` INTEGER NOT NULL, `trashed` INTEGER NOT NULL, `size` INTEGER NOT NULL, `duration` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "label",
+ "columnName": "label",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "relativePath",
+ "columnName": "relativePath",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "albumID",
+ "columnName": "albumID",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "albumLabel",
+ "columnName": "albumLabel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expiryTimestamp",
+ "columnName": "expiryTimestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "takenTimestamp",
+ "columnName": "takenTimestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fullDate",
+ "columnName": "fullDate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "favorite",
+ "columnName": "favorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trashed",
+ "columnName": "trashed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "media_version",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`version` TEXT NOT NULL, PRIMARY KEY(`version`))",
+ "fields": [
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "version"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "timeline_settings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupTimelineByMonth` INTEGER NOT NULL DEFAULT 0, `groupTimelineInAlbums` INTEGER NOT NULL DEFAULT 0, `timelineMediaOrder` TEXT NOT NULL DEFAULT '{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}', `albumMediaOrder` TEXT NOT NULL DEFAULT '{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}')",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupTimelineByMonth",
+ "columnName": "groupTimelineByMonth",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "groupTimelineInAlbums",
+ "columnName": "groupTimelineInAlbums",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "timelineMediaOrder",
+ "columnName": "timelineMediaOrder",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}'"
+ },
+ {
+ "fieldPath": "albumMediaOrder",
+ "columnName": "albumMediaOrder",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '65c73e58b535f1a24397f4143bdf1dde')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7a3846560..d8a241b07 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -173,6 +173,11 @@
android:resource="@xml/filepaths" />
+
+
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt b/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt
index 17ff90ee9..6d2c8d188 100644
--- a/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt
+++ b/app/src/main/kotlin/com/dot/gallery/GalleryApp.kt
@@ -6,13 +6,14 @@
package com.dot.gallery
import android.app.Application
+import androidx.hilt.work.HiltWorkerFactory
+import androidx.work.Configuration
import com.dot.gallery.core.decoder.supportHeifDecoder
import com.dot.gallery.core.decoder.supportJxlDecoder
import com.github.panpf.sketch.PlatformContext
import com.github.panpf.sketch.SingletonSketch
import com.github.panpf.sketch.Sketch
import com.github.panpf.sketch.cache.DiskCache
-import com.github.panpf.sketch.cache.MemoryCache
import com.github.panpf.sketch.decode.supportAnimatedGif
import com.github.panpf.sketch.decode.supportAnimatedHeif
import com.github.panpf.sketch.decode.supportAnimatedWebp
@@ -24,15 +25,17 @@ import com.github.panpf.sketch.request.supportSaveCellularTraffic
import com.github.panpf.sketch.util.appCacheDirectory
import dagger.hilt.android.HiltAndroidApp
import okio.FileSystem
+import javax.inject.Inject
@HiltAndroidApp
-class GalleryApp : Application(), SingletonSketch.Factory {
+class GalleryApp : Application(), SingletonSketch.Factory, Configuration.Provider {
override fun createSketch(context: PlatformContext): Sketch = Sketch.Builder(this).apply {
httpStack(KtorStack())
components {
supportSaveCellularTraffic()
supportPauseLoadWhenScrolling()
+ //supportThumbnailDecoder()
supportSvg()
supportVideoFrame()
supportAnimatedGif()
@@ -47,7 +50,14 @@ class GalleryApp : Application(), SingletonSketch.Factory {
resultCache(diskCache)
downloadCache(diskCache)
- memoryCache(MemoryCache.Builder(context).maxSizePercent(0.75).build())
}.build()
+ @Inject
+ lateinit var workerFactory: HiltWorkerFactory
+
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .build()
+
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/Constants.kt b/app/src/main/kotlin/com/dot/gallery/core/Constants.kt
index a7c0620f5..65aa2e0d9 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/Constants.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/Constants.kt
@@ -107,6 +107,8 @@ object Constants {
}
val cellsList = listOf(
+ GridCells.Fixed(9),
+ GridCells.Fixed(8),
GridCells.Fixed(7),
GridCells.Fixed(6),
GridCells.Fixed(5),
diff --git a/app/src/main/kotlin/com/dot/gallery/core/DatabaseUpdaterWorker.kt b/app/src/main/kotlin/com/dot/gallery/core/DatabaseUpdaterWorker.kt
new file mode 100644
index 000000000..fa0988250
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/DatabaseUpdaterWorker.kt
@@ -0,0 +1,63 @@
+package com.dot.gallery.core
+
+import android.content.Context
+import androidx.hilt.work.HiltWorker
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import com.dot.gallery.feature_node.data.data_source.InternalDatabase
+import com.dot.gallery.feature_node.domain.model.MediaVersion
+import com.dot.gallery.feature_node.domain.repository.MediaRepository
+import com.dot.gallery.feature_node.presentation.util.isMediaUpToDate
+import com.dot.gallery.feature_node.presentation.util.mediaStoreVersion
+import com.dot.gallery.feature_node.presentation.util.printDebug
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.single
+import kotlinx.coroutines.withContext
+
+fun WorkManager.updateDatabase() {
+ val uniqueWork = OneTimeWorkRequestBuilder()
+ .setConstraints(
+ Constraints.Builder()
+ .setRequiresStorageNotLow(true)
+ .build()
+ )
+ .build()
+
+ enqueueUniqueWork("DatabaseUpdaterWorker", ExistingWorkPolicy.KEEP, uniqueWork)
+}
+
+@HiltWorker
+class DatabaseUpdaterWorker @AssistedInject constructor(
+ private val database: InternalDatabase,
+ private val repository: MediaRepository,
+ @Assisted private val appContext: Context,
+ @Assisted workerParams: WorkerParameters
+) :
+ CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ if (database.isMediaUpToDate(appContext)) {
+ printDebug("Database is up to date")
+ return Result.success()
+ }
+ withContext(Dispatchers.IO) {
+ val mediaVersion = appContext.mediaStoreVersion
+ printDebug("Database is not up to date. Updating to version $mediaVersion")
+ database.getMediaDao().setMediaVersion(MediaVersion(mediaVersion))
+ val media = repository.getMedia().map { it.data ?: emptyList() }.single()
+ database.getMediaDao().updateMedia(media)
+ delay(5000)
+ }
+
+ return Result.success()
+ }
+}
+
diff --git a/app/src/main/kotlin/com/dot/gallery/core/MediaObserver.kt b/app/src/main/kotlin/com/dot/gallery/core/MediaObserver.kt
index 3de2144e9..4c5ce2610 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/MediaObserver.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/MediaObserver.kt
@@ -6,51 +6,14 @@
package com.dot.gallery.core
import android.content.Context
-import android.database.ContentObserver
-import android.net.Uri
import android.os.FileObserver
-import com.dot.gallery.feature_node.data.data_source.InternalDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-private var observerJob: Job? = null
-/**
- * Register an observer class that gets callbacks when data identified by a given content URI
- * changes.
- */
-fun Context.contentFlowObserver(uris: Array) = callbackFlow {
- val observer = object : ContentObserver(null) {
- override fun onChange(selfChange: Boolean) {
- observerJob?.cancel()
- observerJob = launch(Dispatchers.IO) {
- send(false)
- }
- }
- }
- for (uri in uris)
- contentResolver.registerContentObserver(uri, true, observer)
- // trigger first.
- observerJob = launch(Dispatchers.IO) {
- send(true)
- }
- awaitClose {
- contentResolver.unregisterContentObserver(observer)
- }
-}.conflate().onEach { if (!it) delay(1000) }
-
-fun Context.contentFlowWithDatabase(uris: Array, database: InternalDatabase) = merge(
- contentFlowObserver(uris),
- database.getBlacklistDao().getBlacklistedAlbums()
-)
-
-
private var observerFileJob: Job? = null
fun Context.fileFlowObserver() = callbackFlow {
val observer = object : FileObserver(filesDir, CREATE or DELETE or MODIFY or MOVED_FROM or MOVED_TO) {
@@ -68,4 +31,4 @@ fun Context.fileFlowObserver() = callbackFlow {
awaitClose {
observer.stopWatching()
}
-}
\ No newline at end of file
+}.conflate()
diff --git a/app/src/main/kotlin/com/dot/gallery/core/MediaState.kt b/app/src/main/kotlin/com/dot/gallery/core/MediaState.kt
deleted file mode 100644
index 496fe0cf9..000000000
--- a/app/src/main/kotlin/com/dot/gallery/core/MediaState.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.core
-
-import android.os.Parcelable
-import androidx.compose.runtime.Immutable
-import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.model.EncryptedMedia
-import com.dot.gallery.feature_node.domain.model.EncryptedMediaItem
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.model.MediaItem
-import kotlinx.parcelize.Parcelize
-
-@Immutable
-@Parcelize
-data class MediaState(
- val media: List = emptyList(),
- val mappedMedia: List = emptyList(),
- val mappedMediaWithMonthly: List = emptyList(),
- val dateHeader: String = "",
- val error: String = "",
- val isLoading: Boolean = true
-) : Parcelable
-
-
-@Immutable
-@Parcelize
-data class AlbumState(
- val albums: List = emptyList(),
- val error: String = ""
-) : Parcelable
-
-@Immutable
-@Parcelize
-data class EncryptedMediaState(
- val media: List = emptyList(),
- val mappedMedia: List = emptyList(),
- val mappedMediaWithMonthly: List = emptyList(),
- val dateHeader: String = "",
- val error: String = "",
- val isLoading: Boolean = true
-) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/Settings.kt b/app/src/main/kotlin/com/dot/gallery/core/Settings.kt
index f0341d8b6..314f1fa32 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/Settings.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/Settings.kt
@@ -5,11 +5,16 @@
package com.dot.gallery.core
+import android.app.Activity
import android.content.Context
import android.os.Build
import android.os.Parcelable
import android.provider.MediaStore
import androidx.annotation.RequiresApi
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
@@ -23,6 +28,8 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
+import com.dot.gallery.core.Constants.albumCellsList
+import com.dot.gallery.core.Constants.cellsList
import com.dot.gallery.core.Settings.PREFERENCE_NAME
import com.dot.gallery.core.presentation.components.FilterKind
import com.dot.gallery.core.util.rememberPreference
@@ -47,12 +54,16 @@ object Settings {
data class LastSort(
val orderType: OrderType,
val kind: FilterKind
- ): Parcelable
+ ) : Parcelable
@Composable
fun rememberLastSort() =
- rememberPreference(key = LAST_SORT, defaultValue = LastSort(OrderType.Descending, FilterKind.DATE))
+ rememberPreference(
+ key = LAST_SORT,
+ defaultValue = LastSort(OrderType.Descending, FilterKind.DATE)
+ )
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun rememberAlbumGridSize(): MutableState {
val scope = rememberCoroutineScope()
@@ -60,8 +71,21 @@ object Settings {
val prefs = remember(context) {
context.getSharedPreferences("ui_settings", Context.MODE_PRIVATE)
}
+
+ val windowSizeClass = if (context is Activity) calculateWindowSizeClass(context) else null
+ val defaultValue = remember(windowSizeClass) {
+ albumCellsList.indexOf(
+ GridCells.Fixed(
+ when (windowSizeClass?.widthSizeClass) {
+ WindowWidthSizeClass.Expanded -> 5
+ else -> 2
+ }
+ )
+ )
+ }
+
var storedSize = remember(prefs) {
- prefs.getInt("album_grid_size", 5)
+ prefs.getInt("album_grid_size", defaultValue)
}
return remember(storedSize) {
@@ -131,6 +155,7 @@ object Settings {
fun rememberForcedLastScreen() =
rememberPreference(key = FORCED_LAST_SCREEN, defaultValue = false)
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun rememberGridSize(): MutableState {
val scope = rememberCoroutineScope()
@@ -138,8 +163,20 @@ object Settings {
val prefs = remember(context) {
context.getSharedPreferences("ui_settings", Context.MODE_PRIVATE)
}
+ val windowSizeClass = if (context is Activity) calculateWindowSizeClass(context) else null
+ val defaultValue = remember(windowSizeClass) {
+ cellsList.indexOf(
+ GridCells.Fixed(
+ when (windowSizeClass?.widthSizeClass) {
+ WindowWidthSizeClass.Expanded -> 6
+ else -> 4
+ }
+ )
+ )
+ }
+
var storedSize = remember(prefs) {
- prefs.getInt("media_grid_size", 3)
+ prefs.getInt("media_grid_size", defaultValue)
}
return remember(storedSize) {
diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt
index 03a51d27c..d0cc43c86 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchHeifDecoder.kt
@@ -1,9 +1,5 @@
package com.dot.gallery.core.decoder
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.graphics.PostProcessor
-import android.os.Build
import com.github.panpf.sketch.ComponentRegistry
import com.github.panpf.sketch.asSketchImage
import com.github.panpf.sketch.decode.DecodeResult
@@ -13,23 +9,12 @@ import com.github.panpf.sketch.decode.internal.calculateSampleSize
import com.github.panpf.sketch.decode.internal.createInSampledTransformed
import com.github.panpf.sketch.decode.internal.isSmallerSizeMode
import com.github.panpf.sketch.fetch.FetchResult
-import com.github.panpf.sketch.request.animatedTransformation
-import com.github.panpf.sketch.request.bitmapConfig
-import com.github.panpf.sketch.request.colorSpace
import com.github.panpf.sketch.request.internal.RequestContext
-import com.github.panpf.sketch.source.AssetDataSource
-import com.github.panpf.sketch.source.ByteArrayDataSource
-import com.github.panpf.sketch.source.ContentDataSource
import com.github.panpf.sketch.source.DataSource
-import com.github.panpf.sketch.source.ResourceDataSource
-import com.github.panpf.sketch.transform.AnimatedTransformation
-import com.github.panpf.sketch.transform.flag
import com.github.panpf.sketch.util.Size
import com.radzivon.bartoshyk.avif.coder.HeifCoder
import com.radzivon.bartoshyk.avif.coder.PreferredColorConfig
-import com.radzivon.bartoshyk.avif.coder.ScaleMode
import okio.buffer
-import java.nio.ByteBuffer
fun ComponentRegistry.Builder.supportHeifDecoder(): ComponentRegistry.Builder = apply {
addDecoder(SketchHeifDecoder.Factory())
@@ -38,18 +23,17 @@ fun ComponentRegistry.Builder.supportHeifDecoder(): ComponentRegistry.Builder =
class SketchHeifDecoder(
private val requestContext: RequestContext,
private val dataSource: DataSource,
+ private val mimeType: String
) : Decoder {
- private val coder = HeifCoder(requestContext.request.context)
-
class Factory : Decoder.Factory {
override val key: String
get() = "HeifDecoder"
override fun create(requestContext: RequestContext, fetchResult: FetchResult): Decoder? {
- return if (fetchResult.mimeType in AVAILABLE_MIME_TYPES) {
- SketchHeifDecoder(requestContext, fetchResult.dataSource)
+ return if (HEIF_MIMETYPES.any { fetchResult.mimeType?.contains(it) == true }) {
+ SketchHeifDecoder(requestContext, fetchResult.dataSource, fetchResult.mimeType!!)
} else {
null
}
@@ -67,7 +51,7 @@ class SketchHeifDecoder(
override fun toString(): String = key
companion object {
- private val AVAILABLE_MIME_TYPES = listOf(
+ val HEIF_MIMETYPES = listOf(
"image/heif",
"image/heic",
"image/heif-sequence",
@@ -79,128 +63,60 @@ class SketchHeifDecoder(
}
override suspend fun decode(): Result = runCatching {
- val request = requestContext.request
+ val coder = HeifCoder(requestContext.request.context)
dataSource.openSource().use { src ->
val sourceData = src.buffer().readByteArray()
- val source = when (dataSource) {
- is AssetDataSource -> {
- ImageDecoder.createSource(request.context.assets, dataSource.fileName)
- }
-
- is ResourceDataSource -> {
- ImageDecoder.createSource(dataSource.resources, dataSource.resId)
- }
-
- is ContentDataSource -> {
- ImageDecoder.createSource(
- request.context.contentResolver,
- dataSource.contentUri
- )
- }
-
- is ByteArrayDataSource -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- ImageDecoder.createSource(dataSource.data)
- } else {
- ImageDecoder.createSource(ByteBuffer.wrap(dataSource.data))
- }
- }
-
- else -> {
- dataSource.getFileOrNull()
- ?.let { ImageDecoder.createSource(it.toFile()) }
- ?: throw Exception("Unsupported DataSource: ${dataSource::class}")
- }
- }
- val bitmapConfig = requestContext.request.bitmapConfig?.getConfig(null)
+ val request = requestContext.request
- var mPreferredColorConfig: PreferredColorConfig = when (bitmapConfig) {
- Bitmap.Config.ALPHA_8 -> PreferredColorConfig.RGBA_8888
- Bitmap.Config.ARGB_8888 -> PreferredColorConfig.RGBA_8888
- else -> PreferredColorConfig.DEFAULT
- }
- if (bitmapConfig == Bitmap.Config.RGBA_F16) {
- mPreferredColorConfig = PreferredColorConfig.RGBA_F16
- } else if (bitmapConfig == Bitmap.Config.HARDWARE) {
- mPreferredColorConfig = PreferredColorConfig.HARDWARE
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && bitmapConfig == Bitmap.Config.RGBA_1010102) {
- mPreferredColorConfig = PreferredColorConfig.RGBA_1010102
- }
+ val imageInfo: ImageInfo
- var imageInfo: ImageInfo? = null
- var inSampleSize = 1
- var imageDecoder: ImageDecoder? = null
- try {
- ImageDecoder.decodeDrawable(source) { decoder, info, _ ->
- imageDecoder = decoder
- imageInfo = ImageInfo(
- width = info.size.width,
- height = info.size.height,
- mimeType = info.mimeType,
- )
- val size = requestContext.size!!
- val precision = request.precisionDecider.get(
- imageSize = Size(info.size.width, info.size.height),
- targetSize = size,
- )
- inSampleSize = calculateSampleSize(
- imageSize = Size(info.size.width, info.size.height),
- targetSize = size,
- smallerSizeMode = precision.isSmallerSizeMode()
- )
- decoder.setTargetSampleSize(inSampleSize)
-
- request.colorSpace?.let {
- decoder.setTargetColorSpace(it)
- }
-
- // Set the animated transformation to be applied on each frame.
- decoder.postProcessor = request.animatedTransformation?.asPostProcessor()
- }
- } finally {
- imageDecoder?.close()
- }
- val transformeds: List? =
- if (inSampleSize != 1) listOf(createInSampledTransformed(inSampleSize)) else null
-
- if (requestContext.size == Size.Origin) {
- val originalImage =
- coder.decode(
- sourceData,
- preferredColorConfig = mPreferredColorConfig
- )
- return@runCatching DecodeResult(
- image = originalImage.asSketchImage(),
- imageInfo = imageInfo!!,
- dataFrom = dataSource.dataFrom,
- resize = requestContext.computeResize(requestContext.size!!),
- transformeds = transformeds,
- extras = null
- )
- }
+ val size: Size
- val resize = requestContext.computeResize(imageInfo!!.size)
- val originalImage =
+ val decodedImage = if (requestContext.size == Size.Origin) {
+ val originalImageBitmap = coder.decode(sourceData)
+ size = Size(originalImageBitmap.width, originalImageBitmap.height)
+ imageInfo = ImageInfo(
+ width = size.width,
+ height = size.height,
+ mimeType = mimeType,
+ )
+ originalImageBitmap
+ } else {
+ val resize = requestContext.computeResize(requestContext.size!!)
+ size = resize.size
+ imageInfo = ImageInfo(
+ width = size.width,
+ height = size.height,
+ mimeType = mimeType,
+ )
coder.decodeSampled(
sourceData,
- resize.size.width,
- resize.size.height,
- preferredColorConfig = mPreferredColorConfig,
- scaleMode = ScaleMode.FIT,
+ size.width,
+ size.height,
+ preferredColorConfig = PreferredColorConfig.RGBA_8888
)
+ }
+
+ val precision = request.precisionDecider.get(
+ imageSize = Size(imageInfo.size.width, imageInfo.size.height),
+ targetSize = size,
+ )
+ val inSampleSize = calculateSampleSize(
+ imageSize = Size(imageInfo.size.width, imageInfo.size.height),
+ targetSize = size,
+ smallerSizeMode = precision.isSmallerSizeMode()
+ )
DecodeResult(
- image = originalImage.asSketchImage(),
- imageInfo = imageInfo!!,
+ image = decodedImage.asSketchImage(),
+ imageInfo = imageInfo,
dataFrom = dataSource.dataFrom,
- resize = resize,
- transformeds = transformeds,
+ resize = requestContext.computeResize(if (size == Size.Origin) requestContext.size!! else size),
+ transformeds = if (inSampleSize != 1) listOf(createInSampledTransformed(inSampleSize)) else null,
extras = null
)
}
}
- private fun AnimatedTransformation.asPostProcessor() =
- PostProcessor { canvas -> transform(canvas).flag }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt
index 779c5a79c..a06960067 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/SketchJxlDecoder.kt
@@ -1,10 +1,6 @@
package com.dot.gallery.core.decoder
-import android.graphics.ImageDecoder
-import android.graphics.PostProcessor
-import android.os.Build
import com.awxkee.jxlcoder.JxlCoder
-import com.awxkee.jxlcoder.JxlResizeFilter
import com.github.panpf.sketch.ComponentRegistry
import com.github.panpf.sketch.asSketchImage
import com.github.panpf.sketch.decode.DecodeResult
@@ -14,19 +10,10 @@ import com.github.panpf.sketch.decode.internal.calculateSampleSize
import com.github.panpf.sketch.decode.internal.createInSampledTransformed
import com.github.panpf.sketch.decode.internal.isSmallerSizeMode
import com.github.panpf.sketch.fetch.FetchResult
-import com.github.panpf.sketch.request.animatedTransformation
-import com.github.panpf.sketch.request.colorSpace
import com.github.panpf.sketch.request.internal.RequestContext
-import com.github.panpf.sketch.source.AssetDataSource
-import com.github.panpf.sketch.source.ByteArrayDataSource
-import com.github.panpf.sketch.source.ContentDataSource
import com.github.panpf.sketch.source.DataSource
-import com.github.panpf.sketch.source.ResourceDataSource
-import com.github.panpf.sketch.transform.AnimatedTransformation
-import com.github.panpf.sketch.transform.flag
import com.github.panpf.sketch.util.Size
import okio.buffer
-import java.nio.ByteBuffer
fun ComponentRegistry.Builder.supportJxlDecoder(): ComponentRegistry.Builder = apply {
addDecoder(SketchJxlDecoder.Factory())
@@ -43,7 +30,7 @@ class SketchJxlDecoder(
get() = "JxlDecoder"
override fun create(requestContext: RequestContext, fetchResult: FetchResult): Decoder? {
- return if (fetchResult.mimeType in AVAILABLE_MIME_TYPES) {
+ return if (fetchResult.mimeType?.contains(JXL_MIMETYPE) == true) {
SketchJxlDecoder(requestContext, fetchResult.dataSource)
} else {
null
@@ -62,117 +49,57 @@ class SketchJxlDecoder(
override fun toString(): String = key
companion object {
- private val AVAILABLE_MIME_TYPES = listOf(
- "image/jxl"
- )
+ const val JXL_MIMETYPE = "image/jxl"
}
+
}
override suspend fun decode(): Result = kotlin.runCatching {
val request = requestContext.request
val sourceData = dataSource.openSource().buffer().readByteArray()
- val source = when (dataSource) {
- is AssetDataSource -> {
- ImageDecoder.createSource(request.context.assets, dataSource.fileName)
- }
-
- is ResourceDataSource -> {
- ImageDecoder.createSource(dataSource.resources, dataSource.resId)
- }
-
- is ContentDataSource -> {
- ImageDecoder.createSource(request.context.contentResolver, dataSource.contentUri)
- }
-
- is ByteArrayDataSource -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- ImageDecoder.createSource(dataSource.data)
- } else {
- ImageDecoder.createSource(ByteBuffer.wrap(dataSource.data))
- }
- }
-
- else -> {
- dataSource.getFileOrNull()
- ?.let { ImageDecoder.createSource(it.toFile()) }
- ?: throw Exception("Unsupported DataSource: ${dataSource::class}")
- }
- }
-
-
- var imageInfo: ImageInfo? = null
- var inSampleSize = 1
- var imageDecoder: ImageDecoder? = null
- try {
- ImageDecoder.decodeDrawable(source) { decoder, info, _ ->
- imageDecoder = decoder
- imageInfo = ImageInfo(
- width = info.size.width,
- height = info.size.height,
- mimeType = info.mimeType,
- )
- val size = requestContext.size!!
- val precision = request.precisionDecider.get(
- imageSize = Size(info.size.width, info.size.height),
- targetSize = size,
- )
- inSampleSize = calculateSampleSize(
- imageSize = Size(info.size.width, info.size.height),
- targetSize = size,
- smallerSizeMode = precision.isSmallerSizeMode()
- )
- decoder.setTargetSampleSize(inSampleSize)
-
- request.colorSpace?.let {
- decoder.setTargetColorSpace(it)
- }
-
- // Set the animated transformation to be applied on each frame.
- decoder.postProcessor = request.animatedTransformation?.asPostProcessor()
- }
- } finally {
- imageDecoder?.close()
- }
- val transformeds: List? =
- if (inSampleSize != 1) listOf(createInSampledTransformed(inSampleSize)) else null
+ val originalImageBitmap = JxlCoder.decode(sourceData)
+ var imageInfo = ImageInfo(
+ width = originalImageBitmap.width,
+ height = originalImageBitmap.height,
+ mimeType = "image/jxl",
+ )
- if (requestContext.size == Size.Origin) {
- val originalImage =
- JxlCoder.decode(
- sourceData
- )
- return@runCatching DecodeResult(
- image = originalImage.asSketchImage(),
- imageInfo = imageInfo!!,
- dataFrom = dataSource.dataFrom,
- resize = requestContext.computeResize(requestContext.size!!),
- transformeds = transformeds,
- extras = null
+ val resize = requestContext.computeResize(imageInfo.size)
+ val decodedImage = if (requestContext.size == Size.Origin) {
+ originalImageBitmap
+ } else {
+ imageInfo = ImageInfo(
+ width = resize.size.width,
+ height = resize.size.height,
+ mimeType = "image/jxl",
)
- }
-
- val resize = requestContext.computeResize(imageInfo!!.size)
- val originalImage =
JxlCoder.decodeSampled(
sourceData,
resize.size.width,
- resize.size.height,
- scaleMode = com.awxkee.jxlcoder.ScaleMode.FIT,
- jxlResizeFilter = JxlResizeFilter.BILINEAR
+ resize.size.height
)
+ }
+ val size = requestContext.size!!
+ val precision = request.precisionDecider.get(
+ imageSize = Size(imageInfo.size.width, imageInfo.size.height),
+ targetSize = size,
+ )
+ val inSampleSize = calculateSampleSize(
+ imageSize = Size(imageInfo.size.width, imageInfo.size.height),
+ targetSize = size,
+ smallerSizeMode = precision.isSmallerSizeMode()
+ )
+ val transformeds: List? =
+ if (inSampleSize != 1) listOf(createInSampledTransformed(inSampleSize)) else null
DecodeResult(
- image = originalImage.asSketchImage(),
- imageInfo = imageInfo!!,
+ image = decodedImage.asSketchImage(),
+ imageInfo = imageInfo,
dataFrom = dataSource.dataFrom,
- resize = resize,
+ resize = requestContext.computeResize(if (size == Size.Origin) requestContext.size!! else size),
transformeds = transformeds,
extras = null
)
-
}
-
- private fun AnimatedTransformation.asPostProcessor() =
- PostProcessor { canvas -> transform(canvas).flag }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt b/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt
index 3861168ed..c30e6ab09 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/decoder/ThumbnailDecoder.kt
@@ -1,7 +1,7 @@
-/*package com.dot.gallery.core.decoder
+package com.dot.gallery.core.decoder
-import coil3.decode.DecodeUtils
-import coil3.svg.isSvg
+import com.dot.gallery.core.decoder.SketchHeifDecoder.Factory.Companion.HEIF_MIMETYPES
+import com.dot.gallery.core.decoder.SketchJxlDecoder.Factory.Companion.JXL_MIMETYPE
import com.github.panpf.sketch.ComponentRegistry
import com.github.panpf.sketch.asSketchImage
import com.github.panpf.sketch.decode.DecodeResult
@@ -16,7 +16,6 @@ import com.github.panpf.sketch.source.ContentDataSource
import com.github.panpf.sketch.source.DataSource
import com.github.panpf.sketch.util.MimeTypeMap
import com.github.panpf.sketch.util.Size
-import okio.buffer
fun ComponentRegistry.Builder.supportThumbnailDecoder(): ComponentRegistry.Builder = apply {
@@ -39,6 +38,7 @@ class ThumbnailDecoder(
mimeType != null &&
mimeType.isVideoOrImage &&
!isSvg(fetchResult) &&
+ !isSpecialFormat(fetchResult) &&
fetchResult.dataSource is ContentDataSource
)
ThumbnailDecoder(requestContext, fetchResult.dataSource)
@@ -59,13 +59,13 @@ class ThumbnailDecoder(
private val String.isVideoOrImage get() = startsWith("video/") || startsWith("image/")
private fun isSvg(result: FetchResult) =
- result.mimeType == MIME_TYPE_SVG || DecodeUtils.isSvg(
- result.dataSource.openSource().buffer()
- )
+ result.mimeType?.contains(MIME_TYPE_SVG) == true
+ private fun isSpecialFormat(result: FetchResult) =
+ HEIF_MIMETYPES.any { result.mimeType?.contains(it) == true } || result.mimeType?.contains(JXL_MIMETYPE) == true
companion object {
- private const val MIME_TYPE_SVG = "image/svg+xml"
+ private const val MIME_TYPE_SVG = "image/svg"
}
}
@@ -106,4 +106,3 @@ class ThumbnailDecoder(
}
}
-*/
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/AppBar.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/AppBar.kt
index e9a2257aa..012010f87 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/AppBar.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/AppBar.kt
@@ -5,6 +5,7 @@
package com.dot.gallery.core.presentation.components
+import android.app.Activity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
@@ -41,17 +42,22 @@ import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.NavigationRailItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
-import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -67,6 +73,8 @@ import com.dot.gallery.core.Settings.Misc.rememberOldNavbar
import com.dot.gallery.feature_node.presentation.util.NavigationItem
import com.dot.gallery.feature_node.presentation.util.Screen
import com.dot.gallery.ui.core.icons.Albums
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
fun rememberNavigationItems(): List {
@@ -94,16 +102,18 @@ fun rememberNavigationItems(): List {
}
}
+@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Stable
@Composable
fun AppBarContainer(
- windowSizeClass: WindowSizeClass,
navController: NavController,
bottomBarState: Boolean,
paddingValues: PaddingValues,
isScrolling: Boolean,
content: @Composable () -> Unit,
) {
+ val context = LocalContext.current
+ val windowSizeClass = calculateWindowSizeClass(context as Activity)
val backStackEntry by navController.currentBackStackEntryAsState()
val bottomNavItems = rememberNavigationItems()
val useNavRail by remember(windowSizeClass) {
@@ -141,9 +151,16 @@ fun AppBarContainer(
content()
}
val hideNavBarSetting by rememberAutoHideNavBar()
- val showClassicNavbar by remember(useNavRail, isScrolling, bottomBarState, hideNavBarSetting) {
+ var showClassicNavbar by remember {
mutableStateOf(!useNavRail && bottomBarState && (!isScrolling || !hideNavBarSetting))
}
+ LaunchedEffect(useNavRail, isScrolling, bottomBarState, hideNavBarSetting) {
+ snapshotFlow {
+ !useNavRail && bottomBarState && (!isScrolling || !hideNavBarSetting)
+ }.distinctUntilChanged().collectLatest {
+ showClassicNavbar = it
+ }
+ }
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomCenter),
visible = showClassicNavbar,
@@ -167,9 +184,16 @@ fun AppBarContainer(
Box(modifier = Modifier.fillMaxSize()) {
content()
val hideNavBarSetting by rememberAutoHideNavBar()
- val showNavbar by remember(bottomBarState, isScrolling, hideNavBarSetting) {
+ var showNavbar by remember(bottomBarState, isScrolling, hideNavBarSetting) {
mutableStateOf(bottomBarState && (!isScrolling || !hideNavBarSetting))
}
+ LaunchedEffect(bottomBarState, isScrolling, hideNavBarSetting) {
+ snapshotFlow {
+ bottomBarState && (!isScrolling || !hideNavBarSetting)
+ }.distinctUntilChanged().collectLatest {
+ showNavbar = it
+ }
+ }
AnimatedVisibility(
modifier = Modifier
.align(Alignment.BottomEnd)
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyAlbum.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyAlbum.kt
new file mode 100644
index 000000000..f94ca92ff
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyAlbum.kt
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: 2023 IacobIacob01
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.core.presentation.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.dot.gallery.R
+
+@Composable
+fun EmptyAlbum(
+ modifier: Modifier = Modifier,
+ title: String = stringResource(R.string.no_media_title),
+) = LoadingAlbum(
+ modifier = modifier,
+ shouldShimmer = false,
+ bottomContent = {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(32.dp),
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center
+ )
+ }
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyMedia.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyMedia.kt
index 259511175..de17d31d6 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyMedia.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyMedia.kt
@@ -5,7 +5,6 @@
package com.dot.gallery.core.presentation.components
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
@@ -20,15 +19,13 @@ import com.dot.gallery.R
@Composable
fun EmptyMedia(
modifier: Modifier = Modifier,
- paddingValues: PaddingValues = PaddingValues(16.dp),
title: String = stringResource(R.string.no_media_title),
) = LoadingMedia(
modifier = modifier,
- paddingValues = paddingValues,
shouldShimmer = false,
- topContent = {
+ bottomContent = {
Text(
- modifier = Modifier.fillMaxWidth().padding(16.dp).padding(top = paddingValues.calculateTopPadding() / 2),
+ modifier = Modifier.fillMaxWidth().padding(32.dp),
text = title,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingAlbum.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingAlbum.kt
new file mode 100644
index 000000000..0df319096
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingAlbum.kt
@@ -0,0 +1,92 @@
+/*
+ * SPDX-FileCopyrightText: 2023 IacobIacob01
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.core.presentation.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.dot.gallery.core.Constants.albumCellsList
+import com.dot.gallery.core.Settings.Album.rememberAlbumGridSize
+import com.dot.gallery.ui.theme.Dimens
+import com.valentinilk.shimmer.shimmer
+
+@Composable
+fun LoadingAlbum(
+ modifier: Modifier = Modifier,
+ shouldShimmer: Boolean = true,
+ bottomContent: @Composable (() -> Unit)? = null,
+) {
+ val gridSize by rememberAlbumGridSize()
+ val grid = remember(gridSize) { albumCellsList.size - gridSize }
+ val shape = remember { RoundedCornerShape(16.dp) }
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ .padding(top = 48.dp)
+ .then(if (shouldShimmer) Modifier.shimmer() else Modifier),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ repeat(2) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ repeat(grid) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Album())
+ .background(
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ shape = shape
+ )
+ )
+ }
+ }
+ }
+ if (shouldShimmer) {
+ repeat(4) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ repeat(grid) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Album())
+ .background(
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ shape = shape
+ )
+ )
+ }
+ }
+ }
+ }
+
+ if (bottomContent != null) {
+ bottomContent()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingMedia.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingMedia.kt
index e25bca479..62b6f4104 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingMedia.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/LoadingMedia.kt
@@ -8,7 +8,8 @@ package com.dot.gallery.core.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
@@ -16,13 +17,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.dot.gallery.core.Constants.cellsList
@@ -33,28 +33,73 @@ import com.valentinilk.shimmer.shimmer
@Composable
fun LoadingMedia(
modifier: Modifier = Modifier,
- paddingValues: PaddingValues,
shouldShimmer: Boolean = true,
- topContent: @Composable (() -> Unit)? = null,
+ bottomContent: @Composable (() -> Unit)? = null,
) {
- val gridState = rememberLazyGridState()
val gridSize by rememberGridSize()
- LazyVerticalGrid(
- state = gridState,
+ val grid = remember(gridSize) { cellsList.size - gridSize }
+ Column(
modifier = modifier
.fillMaxSize()
.then(if (shouldShimmer) Modifier.shimmer() else Modifier),
- columns = cellsList[gridSize],
- contentPadding = paddingValues,
- horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(1.dp),
) {
- if (topContent != null) {
- item(span = { GridItemSpan(maxLineSpan) }) {
- topContent()
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 24.dp, vertical = 24.dp),
+ ) {
+ Spacer(
+ modifier = Modifier
+ .height(24.dp)
+ .fillMaxWidth(0.45f)
+ .background(
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
+ shape = RoundedCornerShape(100)
+ )
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(1.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ repeat(grid) {
+ Spacer(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Photo())
+ .background(
+ color = MaterialTheme.colorScheme.surfaceVariant
+ )
+ )
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(1.dp)
+ ) {
+ repeat(grid / 2) {
+ Spacer(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Photo())
+ .background(
+ color = MaterialTheme.colorScheme.surfaceVariant
+ )
+ )
+ }
+ repeat(grid / 2) {
+ Spacer(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Photo())
+ )
}
}
- item(span = { GridItemSpan(maxLineSpan) }) {
+ if (shouldShimmer) {
Box(
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 24.dp)
@@ -69,44 +114,52 @@ fun LoadingMedia(
)
)
}
- }
- items(count = 10) {
- Box(
- modifier = Modifier
- .aspectRatio(1f)
- .size(Dimens.Photo())
- .background(
- color = MaterialTheme.colorScheme.surfaceVariant
- )
- )
- }
- if (shouldShimmer) {
- item(span = { GridItemSpan(maxLineSpan) }) {
- Box(
- modifier = Modifier
- .padding(horizontal = 24.dp, vertical = 24.dp)
+ repeat(10) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(1.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
+ repeat(grid) {
+ Spacer(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Photo())
+ .background(
+ color = MaterialTheme.colorScheme.surfaceVariant
+ )
+ )
+ }
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(1.dp)
+ ) {
+ repeat(grid / 2) {
Spacer(
modifier = Modifier
- .height(24.dp)
- .fillMaxWidth(0.35f)
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Photo())
.background(
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
- shape = RoundedCornerShape(100)
+ color = MaterialTheme.colorScheme.surfaceVariant
)
)
}
+ repeat(grid / 2) {
+ Spacer(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .size(Dimens.Photo())
+ )
+ }
}
- items(count = 25) {
- Box(
- modifier = Modifier
- .aspectRatio(1f)
- .size(Dimens.Photo())
- .background(
- color = MaterialTheme.colorScheme.surfaceVariant
- )
- )
- }
+ }
+ if (bottomContent != null) {
+ bottomContent()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/StickyHeader.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/MediaItemHeader.kt
similarity index 98%
rename from app/src/main/kotlin/com/dot/gallery/core/presentation/components/StickyHeader.kt
rename to app/src/main/kotlin/com/dot/gallery/core/presentation/components/MediaItemHeader.kt
index f3b8ba90b..82681647b 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/StickyHeader.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/MediaItemHeader.kt
@@ -28,7 +28,7 @@ import com.dot.gallery.core.Constants.Animation.exitAnimation
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun StickyHeader(
+fun MediaItemHeader(
modifier: Modifier = Modifier,
date: String,
showAsBig: Boolean = false,
@@ -55,7 +55,7 @@ fun StickyHeader(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = date,
+ text = remember { date },
style = if (showAsBig) bigTextStyle else smallTextStyle,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.then(
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationBarSpacer.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationBarSpacer.kt
new file mode 100644
index 000000000..3ed32a833
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationBarSpacer.kt
@@ -0,0 +1,17 @@
+package com.dot.gallery.core.presentation.components
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.dot.gallery.feature_node.presentation.util.getNavigationBarHeight
+
+@Composable
+fun NavigationBarSpacer() {
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(getNavigationBarHeight())
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationComp.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationComp.kt
index 4ed237710..f5e339581 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationComp.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationComp.kt
@@ -6,7 +6,6 @@
package com.dot.gallery.core.presentation.components
import android.os.Build
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -39,7 +38,6 @@ import com.dot.gallery.core.Constants.Target.TARGET_TRASH
import com.dot.gallery.core.Settings.Album.rememberHideTimelineOnAlbum
import com.dot.gallery.core.Settings.Misc.rememberLastScreen
import com.dot.gallery.core.Settings.Misc.rememberTimelineGroupByMonth
-import com.dot.gallery.core.presentation.components.util.ObserveCustomMediaState
import com.dot.gallery.core.presentation.components.util.OnLifecycleEvent
import com.dot.gallery.core.presentation.components.util.permissionGranted
import com.dot.gallery.feature_node.presentation.albums.AlbumsScreen
@@ -57,8 +55,7 @@ import com.dot.gallery.feature_node.presentation.timeline.TimelineScreen
import com.dot.gallery.feature_node.presentation.trashed.TrashedGridScreen
import com.dot.gallery.feature_node.presentation.util.Screen
import com.dot.gallery.feature_node.presentation.vault.VaultScreen
-import com.dot.gallery.feature_node.presentation.vault.VaultViewModel
-import com.dot.gallery.feature_node.presentation.vault.encryptedmediaview.EncryptedMediaViewScreen
+import kotlinx.coroutines.Dispatchers
@Stable
@NonRestartableComposable
@@ -83,12 +80,14 @@ fun NavigationComp(
val groupTimelineByMonth by rememberTimelineGroupByMonth()
val context = LocalContext.current
- var permissionState = remember { context.permissionGranted(Constants.PERMISSIONS) }
+ var permissionState = rememberSaveable { context.permissionGranted(Constants.PERMISSIONS) }
var lastStartScreen by rememberLastScreen()
- val startDest = remember(permissionState, lastStartScreen) {
- if (permissionState) {
- lastStartScreen
- } else Screen.SetupScreen()
+ val startDest by rememberSaveable(permissionState, lastStartScreen) {
+ mutableStateOf(
+ if (permissionState) {
+ lastStartScreen
+ } else Screen.SetupScreen()
+ )
}
val currentDest = remember(navController.currentDestination) {
navController.currentDestination?.route ?: lastStartScreen
@@ -119,22 +118,21 @@ fun NavigationComp(
if (it != Screen.VaultScreen()) {
shouldSkipAuth.value = false
}
- if (it.contains(Screen.EncryptedMediaViewScreen())) {
- shouldSkipAuth.value = true
- }
systemBarFollowThemeState.value = !it.contains(Screen.MediaViewScreen.route)
}
}
// Preloaded viewModels
- val albumsViewModel = hiltViewModel().apply {
- attachToLifecycle()
- }
+ val albumsViewModel = hiltViewModel()
+ val albumsState =
+ albumsViewModel.albumsFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
+
+ val timelineViewModel = hiltViewModel()
+ timelineViewModel.CollectDatabaseUpdates()
+
+ val timelineState =
+ timelineViewModel.mediaFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
- val timelineViewModel = hiltViewModel().apply {
- attachToLifecycle()
- }
- val vaults by timelineViewModel.vaults.collectAsStateWithLifecycle()
LaunchedEffect(groupTimelineByMonth) {
timelineViewModel.groupByMonth = groupTimelineByMonth
}
@@ -162,11 +160,10 @@ fun NavigationComp(
route = Screen.TimelineScreen()
) {
TimelineScreen(
- vm = timelineViewModel,
paddingValues = paddingValues,
handler = timelineViewModel.handler,
- mediaState = timelineViewModel.mediaState,
- albumState = albumsViewModel.albumsState,
+ mediaState = timelineState,
+ albumsState = albumsState,
selectionState = timelineViewModel.multiSelectState,
selectedMedia = timelineViewModel.selectedPhotoState,
toggleSelection = timelineViewModel::toggleSelection,
@@ -180,19 +177,19 @@ fun NavigationComp(
composable(
route = Screen.TrashedScreen()
) {
- val viewModel = hiltViewModel()
- .apply { target = TARGET_TRASH }
- .apply { groupByMonth = groupTimelineByMonth }
- viewModel.attachToLifecycle()
+ val vm = hiltViewModel().apply {
+ target = TARGET_TRASH
+ }
+ val trashedMediaState =
+ vm.mediaFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
TrashedGridScreen(
- vm = viewModel,
paddingValues = paddingValues,
- mediaState = viewModel.mediaState,
- albumState = albumsViewModel.albumsState,
- selectionState = viewModel.multiSelectState,
- selectedMedia = viewModel.selectedPhotoState,
- handler = viewModel.handler,
- toggleSelection = viewModel::toggleSelection,
+ handler = vm.handler,
+ mediaState = trashedMediaState,
+ albumsState = albumsState,
+ selectionState = vm.multiSelectState,
+ selectedMedia = vm.selectedPhotoState,
+ toggleSelection = vm::toggleSelection,
navigate = navPipe::navigate,
navigateUp = navPipe::navigateUp,
toggleNavbar = navPipe::toggleNavbar
@@ -201,17 +198,20 @@ fun NavigationComp(
composable(
route = Screen.FavoriteScreen()
) {
- timelineViewModel.ObserveCustomMediaState(MediaViewModel::getFavoriteMedia)
+ val vm = hiltViewModel().apply {
+ target = TARGET_FAVORITES
+ }
+ val favoritesMediaState =
+ vm.mediaFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
FavoriteScreen(
- vm = timelineViewModel,
paddingValues = paddingValues,
- mediaState = timelineViewModel.customMediaState,
- albumState = albumsViewModel.albumsState,
- handler = timelineViewModel.handler,
- selectionState = timelineViewModel.multiSelectState,
- selectedMedia = timelineViewModel.selectedPhotoState,
- toggleFavorite = timelineViewModel::toggleFavorite,
- toggleSelection = timelineViewModel::toggleCustomSelection,
+ handler = vm.handler,
+ mediaState = favoritesMediaState,
+ albumsState = albumsState,
+ selectionState = vm.multiSelectState,
+ selectedMedia = vm.selectedPhotoState,
+ toggleFavorite = vm::toggleFavorite,
+ toggleSelection = vm::toggleSelection,
navigate = navPipe::navigate,
navigateUp = navPipe::navigateUp,
toggleNavbar = navPipe::toggleNavbar
@@ -221,13 +221,17 @@ fun NavigationComp(
route = Screen.AlbumsScreen()
) {
AlbumsScreen(
- mediaViewModel = timelineViewModel,
navigate = navPipe::navigate,
toggleNavbar = navPipe::toggleNavbar,
+ mediaState = timelineState,
+ albumsState = albumsState,
paddingValues = paddingValues,
- viewModel = albumsViewModel,
isScrolling = isScrolling,
- searchBarActive = searchBarActive
+ searchBarActive = searchBarActive,
+ onAlbumClick = albumsViewModel.onAlbumClick(navPipe::navigate),
+ onAlbumLongClick = albumsViewModel.onAlbumLongClick,
+ filterOptions = albumsViewModel.rememberFilters(),
+ onMoveAlbumToTrash = albumsViewModel::moveAlbumToTrash
)
}
composable(
@@ -250,24 +254,24 @@ fun NavigationComp(
val argumentAlbumId = remember(backStackEntry) {
backStackEntry.arguments?.getLong("albumId") ?: -1
}
- timelineViewModel.ObserveCustomMediaState {
- getMediaFromAlbum(argumentAlbumId)
+ val vm = hiltViewModel().apply {
+ albumId = argumentAlbumId
}
val hideTimeline by rememberHideTimelineOnAlbum()
+ val mediaState = vm.mediaFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
TimelineScreen(
- vm = timelineViewModel,
paddingValues = paddingValues,
albumId = argumentAlbumId,
albumName = argumentAlbumName,
- handler = timelineViewModel.handler,
- mediaState = timelineViewModel.customMediaState,
- albumState = albumsViewModel.albumsState,
- selectionState = timelineViewModel.multiSelectState,
- selectedMedia = timelineViewModel.selectedPhotoState,
+ handler = vm.handler,
+ mediaState = mediaState,
+ albumsState = albumsState,
+ selectionState = vm.multiSelectState,
+ selectedMedia = vm.selectedPhotoState,
allowNavBar = false,
allowHeaders = !hideTimeline,
enableStickyHeaders = !hideTimeline,
- toggleSelection = timelineViewModel::toggleCustomSelection,
+ toggleSelection = vm::toggleSelection,
navigate = navPipe::navigate,
navigateUp = navPipe::navigateUp,
toggleNavbar = navPipe::toggleNavbar,
@@ -279,11 +283,11 @@ fun NavigationComp(
arguments = listOf(
navArgument(name = "mediaId") {
type = NavType.LongType
- defaultValue = -1
+ defaultValue = -1L
},
navArgument(name = "albumId") {
type = NavType.LongType
- defaultValue = -1
+ defaultValue = -1L
}
)
) { backStackEntry ->
@@ -293,15 +297,25 @@ fun NavigationComp(
val albumId: Long = remember(backStackEntry) {
backStackEntry.arguments?.getLong("albumId") ?: -1L
}
- AnimatedVisibility(albumId != -1L) {
- timelineViewModel.ObserveCustomMediaState {
- getMediaFromAlbum(albumId)
+ val entryName = remember(backStackEntry) {
+ if (albumId == -1L) {
+ Screen.TimelineScreen.route
+ } else {
+ Screen.AlbumViewScreen.route
}
}
- val mediaState by remember(albumId) {
- if (albumId != -1L) timelineViewModel.customMediaState else timelineViewModel.mediaState
- }.collectAsStateWithLifecycle()
- val albumsState by albumsViewModel.albumsState.collectAsStateWithLifecycle()
+
+ val parentEntry = remember(backStackEntry) {
+ navController.getBackStackEntry(entryName)
+ }
+ val vm = hiltViewModel(parentEntry).apply {
+ this.albumId = albumId
+ }
+ val mediaState = if (entryName == Screen.AlbumViewScreen()) {
+ vm.mediaFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
+ } else timelineState
+
+ val vaultState = timelineViewModel.vaultsFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
MediaViewScreen(
navigateUp = navPipe::navigateUp,
toggleRotate = toggleRotate,
@@ -309,9 +323,9 @@ fun NavigationComp(
mediaId = mediaId,
mediaState = mediaState,
albumsState = albumsState,
- handler = timelineViewModel.handler,
- addMedia = timelineViewModel::addMedia,
- vaults = vaults
+ handler = vm.handler,
+ addMedia = vm::addMedia,
+ vaultState = vaultState
)
}
composable(
@@ -343,20 +357,13 @@ fun NavigationComp(
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(entryName)
}
- val viewModel = if (target == TARGET_FAVORITES) {
- timelineViewModel.apply {
- ObserveCustomMediaState(MediaViewModel::getFavoriteMedia)
- }
- } else {
- hiltViewModel(parentEntry).apply {
- attachToLifecycle()
- }
+ val viewModel = hiltViewModel(parentEntry).apply {
+ this.target = target
}
- val mediaState by remember(target) {
- if (target == TARGET_FAVORITES) viewModel.customMediaState else viewModel.mediaState
- }.collectAsStateWithLifecycle()
- val albumsState by albumsViewModel.albumsState.collectAsStateWithLifecycle()
+ val mediaState =
+ viewModel.mediaFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
+ val vaultState = timelineViewModel.vaultsFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
MediaViewScreen(
navigateUp = navPipe::navigateUp,
toggleRotate = toggleRotate,
@@ -367,7 +374,48 @@ fun NavigationComp(
albumsState = albumsState,
handler = viewModel.handler,
addMedia = viewModel::addMedia,
- vaults = vaults
+ vaultState = vaultState
+ )
+ }
+ composable(
+ route = Screen.MediaViewScreen.idAndQuery(),
+ arguments = listOf(
+ navArgument(name = "mediaId") {
+ type = NavType.LongType
+ defaultValue = -1
+ },
+ navArgument(name = "query") {
+ type = NavType.BoolType
+ defaultValue = true
+ }
+ )
+ ) { backStackEntry ->
+ val mediaId: Long = remember(backStackEntry) {
+ backStackEntry.arguments?.getLong("mediaId") ?: -1
+ }
+ val query: Boolean = remember(backStackEntry) {
+ backStackEntry.arguments?.getBoolean("query") ?: true
+ }
+ val parentEntry = remember(backStackEntry) {
+ navController.getBackStackEntry(Screen.TimelineScreen.route)
+ }
+ val viewModel = hiltViewModel(parentEntry)
+ val mediaState = remember(query) {
+ if (query) viewModel.searchMediaState else viewModel.mediaFlow
+ }.collectAsStateWithLifecycle(context = Dispatchers.IO)
+
+ val vaultState = timelineViewModel.vaultsFlow.collectAsStateWithLifecycle(context = Dispatchers.IO)
+
+ MediaViewScreen(
+ navigateUp = navPipe::navigateUp,
+ toggleRotate = toggleRotate,
+ paddingValues = paddingValues,
+ mediaId = mediaId,
+ mediaState = mediaState,
+ albumsState = albumsState,
+ handler = viewModel.handler,
+ addMedia = viewModel::addMedia,
+ vaultState = vaultState
)
}
composable(
@@ -381,7 +429,6 @@ fun NavigationComp(
composable(
route = Screen.IgnoredScreen()
) {
- val albumsState by albumsViewModel.unfilteredAlbums.collectAsStateWithLifecycle()
IgnoredScreen(
navigateUp = navPipe::navigateUp,
startSetup = { navPipe.navigate(Screen.IgnoredSetupScreen()) },
@@ -392,7 +439,6 @@ fun NavigationComp(
composable(
route = Screen.IgnoredSetupScreen()
) {
- val albumsState by albumsViewModel.unfilteredAlbums.collectAsStateWithLifecycle()
IgnoredSetup(
onCancel = navPipe::navigateUp,
albumState = albumsState
@@ -403,39 +449,11 @@ fun NavigationComp(
composable(
route = Screen.VaultScreen()
) {
- val vm = hiltViewModel()
-
VaultScreen(
- shouldSkipAuth = shouldSkipAuth,
- navigateUp = navPipe::navigateUp,
- navigate = navPipe::navigate,
- vm = vm
- )
- }
- composable(
- route = Screen.EncryptedMediaViewScreen.id(),
- arguments = listOf(
- navArgument("mediaId") {
- type = NavType.LongType
- }
- )
- ) { backStackEntry ->
- val parentEntry = remember(backStackEntry) {
- navController.getBackStackEntry(Screen.VaultScreen())
- }
- val mediaId = remember(backStackEntry) {
- backStackEntry.arguments?.getLong("mediaId")
- }
- val vm = hiltViewModel(parentEntry)
- EncryptedMediaViewScreen(
- navigateUp = navPipe::navigateUp,
- toggleRotate = toggleRotate,
paddingValues = paddingValues,
- mediaId = mediaId ?: -1,
- mediaState = vm.mediaState,
- vault = vm.currentVault,
- restoreMedia = vm::restoreMedia,
- deleteMedia = vm::deleteMedia
+ toggleRotate = toggleRotate,
+ shouldSkipAuth = shouldSkipAuth,
+ navigateUp = navPipe::navigateUp
)
}
}
@@ -445,7 +463,6 @@ fun NavigationComp(
) {
LibraryScreen(
navigate = navPipe::navigate,
- mediaViewModel = timelineViewModel,
toggleNavbar = navPipe::toggleNavbar,
paddingValues = paddingValues,
isScrolling = isScrolling,
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SelectionSheet.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SelectionSheet.kt
index c9c9db1b7..6e5e77fae 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SelectionSheet.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SelectionSheet.kt
@@ -6,8 +6,6 @@
package com.dot.gallery.core.presentation.components
import android.app.Activity
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
@@ -47,8 +45,10 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -65,14 +65,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
-import com.dot.gallery.core.Constants.Target.TARGET_FAVORITES
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.feature_node.domain.model.Media
import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.exif.CopyMediaSheet
import com.dot.gallery.feature_node.presentation.exif.MoveMediaSheet
import com.dot.gallery.feature_node.presentation.trashed.components.TrashDialog
import com.dot.gallery.feature_node.presentation.trashed.components.TrashDialogAction
+import com.dot.gallery.feature_node.presentation.util.rememberActivityResult
import com.dot.gallery.feature_node.presentation.util.rememberAppBottomSheetState
import com.dot.gallery.feature_node.presentation.util.shareMedia
import com.dot.gallery.ui.theme.Shapes
@@ -85,7 +85,7 @@ fun SelectionSheet(
target: String?,
selectedMedia: SnapshotStateList,
selectionState: MutableState,
- albumsState: AlbumState,
+ albumsState: State,
handler: MediaHandleUseCase
) {
fun clearSelection() {
@@ -99,24 +99,25 @@ fun SelectionSheet(
val trashSheetState = rememberAppBottomSheetState()
val moveSheetState = rememberAppBottomSheetState()
val copySheetState = rememberAppBottomSheetState()
- val result = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.StartIntentSenderForResult(),
- onResult = {
- if (it.resultCode == Activity.RESULT_OK) {
- clearSelection()
- if (trashSheetState.isVisible) {
- scope.launch {
- trashSheetState.hide()
- shouldMoveToTrash = true
- }
+ val result = rememberActivityResult(
+ onResultOk = {
+ clearSelection()
+ if (trashSheetState.isVisible) {
+ scope.launch {
+ trashSheetState.hide()
+ shouldMoveToTrash = true
}
}
}
)
val windowSizeClass = calculateWindowSizeClass(LocalContext.current as Activity)
- val tabletMode = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact
- val sizeModifier = if (!tabletMode) Modifier.fillMaxWidth()
- else Modifier.wrapContentWidth()
+ val tabletMode = remember(windowSizeClass) {
+ windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact
+ }
+ val sizeModifier = remember(tabletMode) {
+ if (!tabletMode) Modifier.fillMaxWidth()
+ else Modifier.wrapContentWidth()
+ }
AnimatedVisibility(
modifier = modifier,
visible = selectionState.value,
@@ -145,7 +146,7 @@ fun SelectionSheet(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
IconButton(
- onClick = { clearSelection() },
+ onClick = ::clearSelection,
modifier = Modifier.size(24.dp),
) {
Image(
@@ -173,30 +174,24 @@ fun SelectionSheet(
) {
// Share Component
SelectionBarColumn(
- selectedMedia = selectedMedia,
imageVector = Icons.Outlined.Share,
tabletMode = tabletMode,
title = stringResource(R.string.share)
) {
- context.shareMedia(it)
+ context.shareMedia(selectedMedia)
}
- val favoriteTitle =
- if (target == TARGET_FAVORITES) stringResource(id = R.string.remove_selected)
- else stringResource(id = R.string.favorite)
// Favorite Component
SelectionBarColumn(
- selectedMedia = selectedMedia,
imageVector = Icons.Outlined.FavoriteBorder,
tabletMode = tabletMode,
- title = favoriteTitle
+ title = stringResource(R.string.favorite)
) {
scope.launch {
- handler.toggleFavorite(result = result, it)
+ handler.toggleFavorite(result = result, selectedMedia)
}
}
// Copy Component
SelectionBarColumn(
- selectedMedia = selectedMedia,
imageVector = Icons.Outlined.CopyAll,
tabletMode = tabletMode,
title = stringResource(R.string.copy)
@@ -207,7 +202,6 @@ fun SelectionSheet(
}
// Move Component
SelectionBarColumn(
- selectedMedia = selectedMedia,
imageVector = Icons.AutoMirrored.Outlined.DriveFileMove,
tabletMode = tabletMode,
title = stringResource(R.string.move)
@@ -218,7 +212,6 @@ fun SelectionSheet(
}
// Trash Component
SelectionBarColumn(
- selectedMedia = selectedMedia,
imageVector = Icons.Outlined.DeleteOutline,
tabletMode = tabletMode,
title = stringResource(id = R.string.trash),
@@ -242,7 +235,7 @@ fun SelectionSheet(
MoveMediaSheet(
sheetState = moveSheetState,
mediaList = selectedMedia,
- albumState = albumsState,
+ albumState = albumsState.value,
handler = handler,
onFinish = ::clearSelection
)
@@ -250,7 +243,7 @@ fun SelectionSheet(
CopyMediaSheet(
sheetState = copySheetState,
mediaList = selectedMedia,
- albumsState = albumsState,
+ albumsState = albumsState.value,
handler = handler,
onFinish = ::clearSelection
)
@@ -258,7 +251,9 @@ fun SelectionSheet(
TrashDialog(
appBottomSheetState = trashSheetState,
data = selectedMedia,
- action = if (shouldMoveToTrash) TrashDialogAction.TRASH else TrashDialogAction.DELETE,
+ action = remember(shouldMoveToTrash) {
+ if (shouldMoveToTrash) TrashDialogAction.TRASH else TrashDialogAction.DELETE
+ },
) {
if (shouldMoveToTrash) {
handler.trashMedia(result, it, true)
@@ -271,12 +266,11 @@ fun SelectionSheet(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RowScope.SelectionBarColumn(
- selectedMedia: SnapshotStateList,
imageVector: ImageVector,
title: String,
tabletMode: Boolean,
- onItemLongClick: ((List) -> Unit)? = null,
- onItemClick: (List) -> Unit,
+ onItemLongClick: (() -> Unit)? = null,
+ onItemClick: () -> Unit,
) {
val tintColor = MaterialTheme.colorScheme.onSurface
Column(
@@ -288,12 +282,8 @@ private fun RowScope.SelectionBarColumn(
else Modifier.weight(1f)
)
.combinedClickable(
- onClick = {
- onItemClick.invoke(selectedMedia)
- },
- onLongClick = {
- onItemLongClick?.invoke(selectedMedia)
- }
+ onClick = onItemClick,
+ onLongClick = onItemLongClick
)
.padding(top = 12.dp, bottom = 16.dp),
verticalArrangement = Arrangement.Center,
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SetupWizard.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SetupWizard.kt
index 1e7e1c737..2554cdf6a 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SetupWizard.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/SetupWizard.kt
@@ -27,6 +27,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.dot.gallery.ui.theme.GalleryTheme
@@ -36,6 +37,7 @@ fun SetupWizard(
icon: ImageVector,
title: String,
subtitle: String,
+ contentPadding: Dp = 32.dp,
content: @Composable () -> Unit,
bottomBar: @Composable () -> Unit
) {
@@ -96,6 +98,8 @@ fun SetupWizard(
modifier = Modifier
.padding(horizontal = 32.dp)
.padding(bottom = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(contentPadding),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
content()
}
@@ -109,6 +113,7 @@ fun SetupWizard(
painter: Painter,
title: String,
subtitle: String,
+ contentPadding: Dp = 32.dp,
content: @Composable () -> Unit,
bottomBar: @Composable () -> Unit
) {
@@ -169,6 +174,8 @@ fun SetupWizard(
modifier = Modifier
.padding(horizontal = 32.dp)
.padding(bottom = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(contentPadding),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
content()
}
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/BatteryExt.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/BatteryExt.kt
index 1195efe25..0594705b3 100644
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/BatteryExt.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/BatteryExt.kt
@@ -8,6 +8,7 @@ import android.os.PowerManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -62,6 +63,7 @@ class BatteryStatusReceiver(private val onReceive: () -> Unit) : BroadcastReceiv
}
+@Stable
data class BatteryStatus(
val isPowerSavingMode: Boolean
)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/observeCustomMediaState.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/observeCustomMediaState.kt
deleted file mode 100644
index 260e4be4b..000000000
--- a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/observeCustomMediaState.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.dot.gallery.core.presentation.components.util
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.dot.gallery.feature_node.presentation.common.MediaViewModel
-
-@Composable
-fun MediaViewModel.ObserveCustomMediaState(onChange: MediaViewModel.() -> Unit) {
- val state by mediaState.collectAsStateWithLifecycle()
- LaunchedEffect(state) {
- onChange()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/swipe.kt b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/swipe.kt
new file mode 100644
index 000000000..f29f31930
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/swipe.kt
@@ -0,0 +1,69 @@
+package com.dot.gallery.core.presentation.components.util
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntOffset
+import com.dot.gallery.feature_node.presentation.util.rememberFeedbackManager
+import kotlin.math.roundToInt
+
+@Composable
+fun Modifier.swipe(
+ enabled: Boolean = true,
+ onSwipeDown: () -> Unit,
+ onSwipeUp: (() -> Unit)?
+): Modifier {
+ var delta by remember { mutableFloatStateOf(0f) }
+ var isDragging by remember { mutableStateOf(false) }
+ val feedbackManager = rememberFeedbackManager()
+ var isVibrating by remember { mutableStateOf(false) }
+ val draggableState = rememberDraggableState {
+ delta += if (onSwipeUp != null) it else if (it > 0) it else 0f
+ delta = delta.coerceIn(-185f, 400f)
+ if (!isVibrating && (delta == 400f || (onSwipeUp != null && delta == -185f))) {
+ feedbackManager.vibrate()
+ isVibrating = true
+ }
+ }
+ val animatedDelta by animateFloatAsState(
+ label = "animatedDelta",
+ targetValue = if (isDragging) delta else 0f,
+ animationSpec = tween(
+ durationMillis = 200
+ )
+ )
+ return this then Modifier
+ .draggable(
+ enabled = enabled,
+ state = draggableState,
+ orientation = Orientation.Vertical,
+ onDragStarted = {
+ isVibrating = false
+ isDragging = true
+ },
+ onDragStopped = {
+ isVibrating = false
+ isDragging = false
+ if (delta == 400f) {
+ onSwipeDown()
+ }
+ if (onSwipeUp != null && delta == -185f) {
+ onSwipeUp()
+ }
+ delta = 0f
+ }
+ )
+ .offset {
+ IntOffset(0, if (isDragging) delta.roundToInt() else animatedDelta.roundToInt())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/util/Column.kt b/app/src/main/kotlin/com/dot/gallery/core/util/Column.kt
new file mode 100644
index 000000000..966d6baa8
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/Column.kt
@@ -0,0 +1,34 @@
+package com.dot.gallery.core.util
+
+typealias Column = String
+
+sealed interface Node {
+ fun build(): String = when (this) {
+ is Eq -> "${lhs.build()} = ${rhs.build()}"
+ is Or -> "(${lhs.build()}) OR (${rhs.build()})"
+ is And -> "(${lhs.build()}) AND (${rhs.build()})"
+ is Literal<*> -> "$`val`"
+ }
+}
+
+private class Eq(val lhs: Node, val rhs: Node) : Node
+private class Or(val lhs: Node, val rhs: Node) : Node
+private class And(val lhs: Node, val rhs: Node) : Node
+private class Literal(val `val`: T) : Node
+
+class Query(val root: Node) {
+ fun build() = root.build()
+
+ companion object {
+ const val ARG = "?"
+ }
+}
+
+infix fun Query.or(other: Query) = Query(Or(this.root, other.root))
+infix fun Query.and(other: Query) = Query(And(this.root, other.root))
+infix fun Query.eq(other: Query) = Query(Eq(this.root, other.root))
+infix fun Column.eq(other: T) = Query(Literal(this)) eq Query(Literal(other))
+
+fun Iterable.join(
+ func: Query.(other: Query) -> Query,
+) = reduceOrNull(func)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/util/MediaStoreBuckets.kt b/app/src/main/kotlin/com/dot/gallery/core/util/MediaStoreBuckets.kt
new file mode 100644
index 000000000..b385a2d54
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/MediaStoreBuckets.kt
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.dot.gallery.core.util
+
+enum class MediaStoreBuckets {
+ /**
+ * Favorites album.
+ */
+ MEDIA_STORE_BUCKET_FAVORITES,
+
+ /**
+ * Trash album.
+ */
+ MEDIA_STORE_BUCKET_TRASH,
+
+ /**
+ * Timeline, contains all medias.
+ */
+ MEDIA_STORE_BUCKET_TIMELINE,
+
+ /**
+ * Reserved bucket ID for placeholders, throw an exception if this value is used.
+ */
+ MEDIA_STORE_BUCKET_PLACEHOLDER,
+
+ /**
+ * Timeline, contains only photos.
+ */
+ MEDIA_STORE_BUCKET_PHOTOS,
+
+ /**
+ * Timeline, contains only videos.
+ */
+ MEDIA_STORE_BUCKET_VIDEOS;
+
+ val id = (-0x0000DEAD - ((ordinal + 1) shl 16)).toLong()
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/util/PickerUtils.kt b/app/src/main/kotlin/com/dot/gallery/core/util/PickerUtils.kt
new file mode 100644
index 000000000..611e24281
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/PickerUtils.kt
@@ -0,0 +1,58 @@
+package com.dot.gallery.core.util
+
+import android.content.Intent
+import android.provider.MediaStore
+import com.dot.gallery.feature_node.domain.model.MediaType
+
+object PickerUtils {
+ private const val MIME_TYPE_IMAGE_ANY = "image/*"
+ private const val MIME_TYPE_VIDEO_ANY = "video/*"
+ private const val MIME_TYPE_ANY = "*/*"
+
+ /**
+ * Get a fixed up MIME type from an [Intent].
+ * @param intent An [Intent]
+ * @return A simpler MIME type, null if not supported
+ */
+ fun translateMimeType(intent: Intent?) = when (intent?.action) {
+ Intent.ACTION_SET_WALLPAPER -> MIME_TYPE_IMAGE_ANY
+ else -> (intent?.type ?: MIME_TYPE_ANY).let {
+ when (it) {
+ MediaStore.Images.Media.CONTENT_TYPE -> MIME_TYPE_IMAGE_ANY
+ MediaStore.Video.Media.CONTENT_TYPE -> MIME_TYPE_VIDEO_ANY
+ else -> when {
+ it == MIME_TYPE_ANY
+ || it.startsWith("image/")
+ || it.startsWith("video/") -> it
+
+ else -> null
+ }
+ }
+ }
+ }
+
+ /**
+ * Get a [MediaType] only if the provided MIME type is a generic one, else return null.
+ * @param mimeType A MIME type
+ * @return [MediaType] if the MIME type is generic, else null
+ * (assume MIME type represent either a specific file format or any)
+ */
+ fun mediaTypeFromGenericMimeType(mimeType: String?) = when (mimeType) {
+ MIME_TYPE_IMAGE_ANY -> MediaType.IMAGE
+ MIME_TYPE_VIDEO_ANY -> MediaType.VIDEO
+ else -> null
+ }
+
+ /**
+ * Given a MIME type, check if it specifies both a content type and a sub type.
+ * @param mimeType A MIME type
+ * @return true if it specifies both a file category and a specific type
+ */
+ fun isMimeTypeNotGeneric(mimeType: String?) = mimeType?.let {
+ it !in listOf(
+ MIME_TYPE_IMAGE_ANY,
+ MIME_TYPE_VIDEO_ANY,
+ MIME_TYPE_ANY,
+ )
+ } ?: false
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/MediaStoreCursor.kt b/app/src/main/kotlin/com/dot/gallery/core/util/ext/ContentResolverExt.kt
similarity index 65%
rename from app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/MediaStoreCursor.kt
rename to app/src/main/kotlin/com/dot/gallery/core/util/ext/ContentResolverExt.kt
index 209abfd00..493d63a3a 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/MediaStoreCursor.kt
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/ext/ContentResolverExt.kt
@@ -1,57 +1,86 @@
/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
-package com.dot.gallery.feature_node.data.data_types
+package com.dot.gallery.core.util.ext
import android.content.ContentResolver
-import android.content.ContentUris
import android.content.ContentValues
-import android.database.Cursor
-import android.database.MergeCursor
+import android.database.ContentObserver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
-import android.os.Build
+import android.os.Bundle
+import android.os.CancellationSignal
import android.os.Environment
import android.os.FileUtils
+import android.os.Handler
+import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.dot.gallery.core.Constants
-import com.dot.gallery.feature_node.data.data_source.Query
import com.dot.gallery.feature_node.domain.model.ExifAttributes
import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.presentation.util.getDate
+import com.dot.gallery.feature_node.domain.model.isVideo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.IOException
+fun ContentResolver.queryFlow(
+ uri: Uri,
+ projection: Array? = null,
+ queryArgs: Bundle? = Bundle(),
+) = callbackFlow {
+ // Each query will have its own cancellationSignal.
+ // Before running any new query the old cancellationSignal must be cancelled
+ // to ensure the currently running query gets interrupted so that we don't
+ // send data across the channel if we know we received a newer set of data.
+ var cancellationSignal = CancellationSignal()
+ // ContentObserver.onChange can be called concurrently so make sure
+ // access to the cancellationSignal is synchronized.
+ val mutex = Mutex()
+
+ val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
+ override fun onChange(selfChange: Boolean) {
+ launch(Dispatchers.IO) {
+ mutex.withLock {
+ cancellationSignal.cancel()
+ cancellationSignal = CancellationSignal()
+ }
+ runCatching {
+ trySend(query(uri, projection, queryArgs, cancellationSignal))
+ }
+ }
+ }
+ }
+
+ registerContentObserver(uri, true, observer)
-suspend fun ContentResolver.query(
- mediaQuery: Query
-): Cursor {
- return withContext(Dispatchers.IO) {
- return@withContext MergeCursor(
- arrayOf(
- query(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- mediaQuery.projection,
- mediaQuery.bundle,
- null
- ),
- query(
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
- mediaQuery.projection,
- mediaQuery.bundle,
- null
- )
+ // The first set of values must always be generated and cannot (shouldn't) be cancelled.
+ launch(Dispatchers.IO) {
+ runCatching {
+ trySend(
+ query(uri, projection, queryArgs, null)
)
- )
+ }
}
-}
+
+ awaitClose {
+ // Stop receiving content changes.
+ unregisterContentObserver(observer)
+ // Cancel any possibly running query.
+ cancellationSignal.cancel()
+ }
+}.conflate()
suspend fun ContentResolver.copyMedia(
from: Media,
@@ -104,7 +133,6 @@ suspend fun ContentResolver.copyMedia(
}
}
-
fun ContentResolver.overrideImage(
uri: Uri,
bitmap: Bitmap,
@@ -223,72 +251,3 @@ suspend fun ContentResolver.updateMediaExif(
false
}
}
-
-
-@Throws(Exception::class)
-fun Cursor.getMediaFromCursor(): Media {
- val id: Long =
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
- val path: String =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.DATA))
- val relativePath: String =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))
- val title: String =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
- val albumID: Long =
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID))
- val albumLabel: String? = try {
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME))
- } catch (_: Exception) {
- Build.MODEL
- }
- val takenTimestamp: Long? = try {
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN))
- } catch (_: Exception) {
- null
- }
- val modifiedTimestamp: Long =
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED))
- val duration: String? = try {
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION))
- } catch (_: Exception) {
- null
- }
- val size: Long =
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE))
- val mimeType: String =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE))
- val isFavorite: Int =
- getInt(getColumnIndexOrThrow(MediaStore.MediaColumns.IS_FAVORITE))
- val isTrashed: Int =
- getInt(getColumnIndexOrThrow(MediaStore.MediaColumns.IS_TRASHED))
- val expiryTimestamp: Long? = try {
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_EXPIRES))
- } catch (_: Exception) {
- null
- }
- val contentUri = if (mimeType.contains("image"))
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- else
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- val uri = ContentUris.withAppendedId(contentUri, id)
- val formattedDate = modifiedTimestamp.getDate(Constants.FULL_DATE_FORMAT)
- return Media(
- id = id,
- label = title,
- uri = uri,
- path = path,
- relativePath = relativePath,
- albumID = albumID,
- albumLabel = albumLabel ?: Build.MODEL,
- timestamp = modifiedTimestamp,
- takenTimestamp = takenTimestamp,
- expiryTimestamp = expiryTimestamp,
- fullDate = formattedDate,
- duration = duration,
- favorite = isFavorite,
- trashed = isTrashed,
- size = size,
- mimeType = mimeType
- )
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/util/ext/CursorExt.kt b/app/src/main/kotlin/com/dot/gallery/core/util/ext/CursorExt.kt
new file mode 100644
index 000000000..39a2fb352
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/ext/CursorExt.kt
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.core.util.ext
+
+import android.database.Cursor
+import androidx.core.database.getLongOrNull
+import androidx.core.database.getStringOrNull
+import com.dot.gallery.core.Resource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+fun Cursor?.mapEachRow(
+ projection: Array,
+ mapping: (Cursor, Array) -> T,
+) = this?.use { cursor ->
+ if (!cursor.moveToFirst()) {
+ return@use emptyList()
+ }
+
+ val indexCache = projection.map { column ->
+ cursor.getColumnIndexOrThrow(column)
+ }.toTypedArray()
+
+ val data = mutableListOf()
+ do {
+ data.add(mapping(cursor, indexCache))
+ } while (cursor.moveToNext())
+
+ data.toList()
+} ?: emptyList()
+
+fun Cursor?.tryGetString(columnIndex: Int, fallback: String? = null): String? {
+ return this?.getStringOrNull(columnIndex) ?: fallback
+}
+
+fun Cursor?.tryGetLong(columnIndex: Int, fallback: Long? = null): Long? {
+ return this?.getLongOrNull(columnIndex) ?: fallback
+}
+
+fun Flow>.mapAsResource(errorOnEmpty: Boolean = false, errorMessage: String = "No data found") = map {
+ if (errorOnEmpty && it.isEmpty()) {
+ Resource.Error(errorMessage)
+ } else {
+ Resource.Success(it)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/util/ext/ExifInterfaceExt.kt b/app/src/main/kotlin/com/dot/gallery/core/util/ext/ExifInterfaceExt.kt
new file mode 100644
index 000000000..122b7c543
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/ext/ExifInterfaceExt.kt
@@ -0,0 +1,79 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.core.util.ext
+
+import android.util.Size
+import androidx.exifinterface.media.ExifInterface
+
+private const val DEFAULT_VALUE_INT = -1
+private const val DEFAULT_VALUE_DOUBLE = -1.0
+
+fun ExifInterface.getAttributeInt(tag: String) =
+ getAttributeInt(tag, DEFAULT_VALUE_INT).takeIf {
+ it != DEFAULT_VALUE_INT
+ }
+
+fun ExifInterface.getAttributeDouble(tag: String): Double? =
+ getAttributeDouble(tag, DEFAULT_VALUE_DOUBLE).takeIf {
+ it != DEFAULT_VALUE_DOUBLE
+ }
+
+val ExifInterface.artist
+ get() = getAttribute(ExifInterface.TAG_ARTIST)
+
+val ExifInterface.apertureValue
+ get() = getAttributeDouble(ExifInterface.TAG_APERTURE_VALUE)
+
+val ExifInterface.copyright
+ get() = getAttribute(ExifInterface.TAG_COPYRIGHT)
+
+val ExifInterface.exposureTime
+ get() = getAttributeDouble(ExifInterface.TAG_EXPOSURE_TIME)
+
+val ExifInterface.isoSpeed
+ get() = getAttributeInt(ExifInterface.TAG_ISO_SPEED)
+
+val ExifInterface.focalLength
+ get() = getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH)
+
+val ExifInterface.make
+ get() = getAttribute(ExifInterface.TAG_MAKE)
+
+val ExifInterface.model
+ get() = getAttribute(ExifInterface.TAG_MODEL)
+
+val ExifInterface.pixelXDimension
+ get() = getAttributeInt(ExifInterface.TAG_PIXEL_X_DIMENSION)
+
+val ExifInterface.pixelYDimension
+ get() = getAttributeInt(ExifInterface.TAG_PIXEL_Y_DIMENSION)
+
+val ExifInterface.size
+ get() = pixelXDimension?.let { x -> pixelYDimension?.let { y -> Size(x, y) } }
+
+val ExifInterface.software
+ get() = getAttribute(ExifInterface.TAG_SOFTWARE)
+
+var ExifInterface.userComment
+ get() = getAttribute(ExifInterface.TAG_USER_COMMENT)
+ set(value) {
+ setAttribute(ExifInterface.TAG_USER_COMMENT, value)
+ }
+
+val ExifInterface.isSupportedFormatForSavingAttributes: Boolean
+ get() {
+ val mimeType = ExifInterface::class.java.getDeclaredField("mMimeType").apply {
+ isAccessible = true
+ }.get(this) as Int
+
+ val isSupportedFormatForSavingAttributes = ExifInterface::class.java.getDeclaredMethod(
+ "isSupportedFormatForSavingAttributes", Int::class.java
+ ).apply {
+ isAccessible = true
+ }
+
+ return isSupportedFormatForSavingAttributes.invoke(null, mimeType) as Boolean
+ }
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/core/util/ext/FlowExt.kt b/app/src/main/kotlin/com/dot/gallery/core/util/ext/FlowExt.kt
new file mode 100644
index 000000000..a0176af1f
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/core/util/ext/FlowExt.kt
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.core.util.ext
+
+import android.database.Cursor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+fun Flow.mapEachRow(
+ projection: Array,
+ mapping: (Cursor, Array) -> T,
+) = map { it.mapEachRow(projection, mapping) }
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/BlacklistDao.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/BlacklistDao.kt
index 119ccf2aa..e18a30c8e 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/BlacklistDao.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/BlacklistDao.kt
@@ -13,9 +13,6 @@ interface BlacklistDao {
@Query("SELECT * FROM blacklist")
fun getBlacklistedAlbums(): Flow>
- @Query("SELECT * FROM blacklist")
- fun getBlacklistedAlbumsSync(): List
-
@Upsert
suspend fun addBlacklistedAlbum(ignoredAlbum: IgnoredAlbum)
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/InternalDatabase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/InternalDatabase.kt
index 38a68b45e..4de3d2c3a 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/InternalDatabase.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/InternalDatabase.kt
@@ -10,17 +10,21 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
+import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.MediaVersion
import com.dot.gallery.feature_node.domain.model.PinnedAlbum
+import com.dot.gallery.feature_node.domain.model.TimelineSettings
import com.dot.gallery.feature_node.domain.util.Converters
@Database(
- entities = [PinnedAlbum::class, IgnoredAlbum::class],
- version = 4,
+ entities = [PinnedAlbum::class, IgnoredAlbum::class, Media::class, MediaVersion::class, TimelineSettings::class],
+ version = 5,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
- AutoMigration(from = 3, to = 4)
+ AutoMigration(from = 3, to = 4),
+ AutoMigration(from = 4, to = 5),
]
)
@TypeConverters(Converters::class)
@@ -30,6 +34,8 @@ abstract class InternalDatabase: RoomDatabase() {
abstract fun getBlacklistDao(): BlacklistDao
+ abstract fun getMediaDao(): MediaDao
+
companion object {
const val NAME = "internal_db"
}
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/KeychainHolder.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/KeychainHolder.kt
index 670f75b15..50aa5e897 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/KeychainHolder.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/KeychainHolder.kt
@@ -32,23 +32,41 @@ class KeychainHolder @Inject constructor(
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
- fun writeVaultInfo(vault: Vault, onSuccess: () -> Unit = {}) {
- val vaultFolder = File(filesDir, vault.uuid.toString())
- if (!vaultFolder.exists()) {
- vaultFolder.mkdir()
+ fun writeVaultInfo(vault: Vault, onSuccess: () -> Unit = {}, onFailed: (reason: String) -> Unit = {}) {
+ try {
+ val vaultFolder = File(filesDir, vault.uuid.toString())
+ if (!vaultFolder.exists()) {
+ vaultFolder.mkdirs()
+ }
+
+ File(vaultFolder, VAULT_INFO_FILE_NAME).apply {
+ if (exists()) delete()
+ encrypt(vault)
+ onSuccess()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ onFailed(e.message.toString())
}
+ }
- File(vaultFolder, VAULT_INFO_FILE_NAME).apply {
- if (exists()) delete()
- encrypt(vault)
+ fun deleteVault(vault: Vault, onSuccess: () -> Unit, onFailed: (reason: String) -> Unit) {
+ try {
+ val vaultFolder = vaultFolder(vault)
+ if (vaultFolder.exists()) {
+ vaultFolder.deleteRecursively()
+ }
+ onSuccess()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ onFailed(e.message.toString())
}
- onSuccess()
}
fun checkVaultFolder(vault: Vault) {
val mainFolder = File(filesDir, vault.uuid.toString())
if (!mainFolder.exists()) {
- mainFolder.mkdir()
+ mainFolder.mkdirs()
writeVaultInfo(vault)
}
}
@@ -63,7 +81,6 @@ class KeychainHolder @Inject constructor(
fromByteArray(it.readBytes())
}
-
@Throws(GeneralSecurityException::class, IOException::class, FileNotFoundException::class, UserNotAuthenticatedException::class)
fun File.encrypt(data: T) {
EncryptedFile.Builder(
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MediaDao.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MediaDao.kt
new file mode 100644
index 000000000..e09ccc978
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MediaDao.kt
@@ -0,0 +1,67 @@
+package com.dot.gallery.feature_node.data.data_source
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Upsert
+import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.MediaVersion
+import com.dot.gallery.feature_node.domain.model.TimelineSettings
+import com.dot.gallery.feature_node.presentation.picker.AllowedMedia
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface MediaDao {
+
+ /** Media */
+ @Query("SELECT * FROM media ORDER BY timestamp DESC")
+ suspend fun getMedia(): List
+
+ @Query("SELECT * FROM media WHERE mimeType LIKE :allowedMedia ORDER BY timestamp DESC")
+ suspend fun getMediaByType(allowedMedia: AllowedMedia): List
+
+ @Query("SELECT * FROM media WHERE favorite = 1 ORDER BY timestamp DESC")
+ suspend fun getFavorites(): List
+
+ @Query("SELECT * FROM media WHERE id = :id LIMIT 1")
+ suspend fun getMediaById(id: Long): Media
+
+ @Query("SELECT * FROM media WHERE albumID = :albumId ORDER BY timestamp DESC")
+ suspend fun getMediaByAlbumId(albumId: Long): List
+
+ @Query("SELECT * FROM media WHERE albumID = :albumId AND mimeType LIKE :allowedMedia ORDER BY timestamp DESC")
+ suspend fun getMediaByAlbumIdAndType(albumId: Long, allowedMedia: AllowedMedia): List
+
+ @Upsert(entity = Media::class)
+ suspend fun addMediaList(mediaList: List)
+
+ @Transaction
+ suspend fun updateMedia(mediaList: List) {
+ // Upsert the items in mediaList
+ addMediaList(mediaList)
+
+ // Get the IDs of the media items in mediaList
+ val mediaIds = mediaList.map { it.id }
+
+ // Delete items from the database that are not in mediaList
+ deleteMediaNotInList(mediaIds)
+ }
+
+ @Query("DELETE FROM media WHERE id NOT IN (:mediaIds)")
+ suspend fun deleteMediaNotInList(mediaIds: List)
+
+ /** MediaVersion */
+ @Upsert(entity = MediaVersion::class)
+ suspend fun setMediaVersion(version: MediaVersion)
+
+ @Query("SELECT EXISTS(SELECT * FROM media_version WHERE version = :version) LIMIT 1")
+ suspend fun isMediaVersionUpToDate(version: String): Boolean
+
+ /** Timeline Settings */
+ @Query("SELECT * FROM timeline_settings LIMIT 1")
+ fun getTimelineSettings(): Flow
+
+ @Upsert(entity = TimelineSettings::class)
+ suspend fun setTimelineSettings(settings: TimelineSettings)
+
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MediaQuery.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MediaQuery.kt
deleted file mode 100644
index 96cde8be7..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MediaQuery.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.data.data_source
-
-import android.content.ContentResolver
-import android.os.Bundle
-import android.provider.MediaStore
-
-sealed class Query(
- var projection: Array,
- var bundle: Bundle? = null
-) {
- class MediaQuery : Query(
- projection = arrayOf(
- MediaStore.MediaColumns._ID,
- MediaStore.MediaColumns.DATA,
- MediaStore.MediaColumns.RELATIVE_PATH,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.BUCKET_ID,
- MediaStore.MediaColumns.DATE_MODIFIED,
- MediaStore.MediaColumns.DATE_TAKEN,
- MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
- MediaStore.MediaColumns.DURATION,
- MediaStore.MediaColumns.SIZE,
- MediaStore.MediaColumns.MIME_TYPE,
- MediaStore.MediaColumns.IS_FAVORITE,
- MediaStore.MediaColumns.IS_TRASHED
- ),
- )
-
- class TrashQuery : Query(
- projection = arrayOf(
- MediaStore.MediaColumns._ID,
- MediaStore.MediaColumns.DATA,
- MediaStore.MediaColumns.RELATIVE_PATH,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.BUCKET_ID,
- MediaStore.MediaColumns.DATE_MODIFIED,
- MediaStore.MediaColumns.DATE_EXPIRES,
- MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
- MediaStore.MediaColumns.DURATION,
- MediaStore.MediaColumns.SIZE,
- MediaStore.MediaColumns.MIME_TYPE,
- MediaStore.MediaColumns.IS_FAVORITE,
- MediaStore.MediaColumns.IS_TRASHED
- ),
- bundle = Bundle().apply {
- putStringArray(
- ContentResolver.QUERY_ARG_SORT_COLUMNS,
- arrayOf(MediaStore.MediaColumns.DATE_EXPIRES)
- )
- putInt(
- ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
- ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
- )
- putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
- }
- )
-
- class PhotoQuery: Query(
- projection = arrayOf(
- MediaStore.MediaColumns._ID,
- MediaStore.MediaColumns.DATA,
- MediaStore.MediaColumns.RELATIVE_PATH,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.BUCKET_ID,
- MediaStore.MediaColumns.DATE_MODIFIED,
- MediaStore.MediaColumns.DATE_TAKEN,
- MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
- MediaStore.MediaColumns.DURATION,
- MediaStore.MediaColumns.SIZE,
- MediaStore.MediaColumns.MIME_TYPE,
- MediaStore.MediaColumns.IS_FAVORITE,
- MediaStore.MediaColumns.IS_TRASHED
- ),
- bundle = defaultBundle.apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.MIME_TYPE + " like ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf("image%")
- )
- }
- )
-
- class VideoQuery: Query(
- projection = arrayOf(
- MediaStore.MediaColumns._ID,
- MediaStore.MediaColumns.DATA,
- MediaStore.MediaColumns.RELATIVE_PATH,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.BUCKET_ID,
- MediaStore.MediaColumns.DATE_MODIFIED,
- MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
- MediaStore.MediaColumns.DURATION,
- MediaStore.MediaColumns.SIZE,
- MediaStore.MediaColumns.MIME_TYPE,
- MediaStore.MediaColumns.ORIENTATION,
- MediaStore.MediaColumns.IS_FAVORITE,
- MediaStore.MediaColumns.IS_TRASHED
- ),
- bundle = defaultBundle.apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.MIME_TYPE + " LIKE ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf("video%")
- )
- }
- )
-
- class AlbumQuery : Query(
- projection = arrayOf(
- MediaStore.MediaColumns.BUCKET_ID,
- MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.DATA,
- MediaStore.MediaColumns.RELATIVE_PATH,
- MediaStore.MediaColumns._ID,
- MediaStore.MediaColumns.SIZE,
- MediaStore.MediaColumns.MIME_TYPE,
- MediaStore.MediaColumns.DATE_MODIFIED,
- MediaStore.MediaColumns.DATE_TAKEN
- )
- )
-
- fun copy(
- projection: Array = this.projection,
- bundle: Bundle? = this.bundle,
- ): Query {
- this.projection = projection
- this.bundle = bundle
- return this
- }
-
- companion object {
- val defaultBundle = Bundle().apply {
- putStringArray(
- ContentResolver.QUERY_ARG_SORT_COLUMNS,
- arrayOf(MediaStore.MediaColumns.DATE_MODIFIED)
- )
- putInt(
- ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
- ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
- )
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/MediaQuery.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/MediaQuery.kt
new file mode 100644
index 000000000..01921e9b1
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/MediaQuery.kt
@@ -0,0 +1,46 @@
+package com.dot.gallery.feature_node.data.data_source.mediastore
+
+import android.net.Uri
+import android.provider.MediaStore
+import com.dot.gallery.core.util.eq
+import com.dot.gallery.core.util.or
+
+object MediaQuery {
+ val MediaStoreFileUri: Uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
+ val MediaProjection = arrayOf(
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.RELATIVE_PATH,
+ MediaStore.Files.FileColumns.DISPLAY_NAME,
+ MediaStore.Files.FileColumns.BUCKET_ID,
+ MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
+ MediaStore.Files.FileColumns.DATE_TAKEN,
+ MediaStore.Files.FileColumns.DATE_MODIFIED,
+ MediaStore.Files.FileColumns.DURATION,
+ MediaStore.Files.FileColumns.SIZE,
+ MediaStore.Files.FileColumns.MIME_TYPE,
+ MediaStore.Files.FileColumns.IS_FAVORITE,
+ MediaStore.Files.FileColumns.IS_TRASHED,
+ MediaStore.Files.FileColumns.DATE_EXPIRES
+ )
+ val AlbumsProjection = arrayOf(
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.RELATIVE_PATH,
+ MediaStore.Files.FileColumns.DISPLAY_NAME,
+ MediaStore.Files.FileColumns.BUCKET_ID,
+ MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
+ MediaStore.Files.FileColumns.DATE_TAKEN,
+ MediaStore.Files.FileColumns.DATE_MODIFIED,
+ MediaStore.Files.FileColumns.SIZE,
+ MediaStore.Files.FileColumns.MIME_TYPE,
+ )
+
+ object Selection {
+ val image =
+ MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
+ val video =
+ MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
+ val imageOrVideo = image or video
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/AlbumsFlow.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/AlbumsFlow.kt
new file mode 100644
index 000000000..a5fa7e18a
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/AlbumsFlow.kt
@@ -0,0 +1,134 @@
+package com.dot.gallery.feature_node.data.data_source.mediastore.quries
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.Context
+import android.database.Cursor
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.core.os.bundleOf
+import com.dot.gallery.feature_node.data.data_source.mediastore.MediaQuery
+import com.dot.gallery.core.util.PickerUtils
+import com.dot.gallery.core.util.Query
+import com.dot.gallery.core.util.and
+import com.dot.gallery.core.util.eq
+import com.dot.gallery.core.util.join
+import com.dot.gallery.core.util.ext.queryFlow
+import com.dot.gallery.core.util.ext.tryGetString
+import com.dot.gallery.feature_node.domain.model.Album
+import com.dot.gallery.feature_node.domain.model.MediaType
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Albums flow
+ *
+ * This class is responsible for fetching albums from the media store
+ *
+ * @property context
+ * @property mimeType
+ */
+class AlbumsFlow(
+ private val context: Context,
+ private val mimeType: String? = null,
+) : QueryFlow() {
+ override fun flowCursor(): Flow {
+ val uri = MediaQuery.MediaStoreFileUri
+ val projection = MediaQuery.AlbumsProjection
+ val imageOrVideo = PickerUtils.mediaTypeFromGenericMimeType(mimeType)?.let {
+ when (it) {
+ MediaType.IMAGE -> MediaQuery.Selection.image
+ MediaType.VIDEO -> MediaQuery.Selection.video
+ }
+ } ?: MediaQuery.Selection.imageOrVideo
+ val rawMimeType = mimeType?.takeIf { PickerUtils.isMimeTypeNotGeneric(it) }
+ val mimeTypeQuery = rawMimeType?.let {
+ MediaStore.Files.FileColumns.MIME_TYPE eq Query.ARG
+ }
+
+ // Join all the non-null queries
+ val selection = listOfNotNull(
+ mimeTypeQuery,
+ imageOrVideo,
+ ).join(Query::and)
+
+ val selectionArgs = listOfNotNull(
+ rawMimeType,
+ ).toTypedArray()
+
+ val sortOrder = MediaStore.Files.FileColumns.DATE_MODIFIED + " DESC"
+
+ val queryArgs = Bundle().apply {
+ putAll(
+ bundleOf(
+ ContentResolver.QUERY_ARG_SQL_SELECTION to selection?.build(),
+ ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs,
+ ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
+ )
+ )
+ }
+
+ return context.contentResolver.queryFlow(
+ uri,
+ projection,
+ queryArgs,
+ )
+ }
+
+ override fun flowData() = flowCursor().map {
+ mutableMapOf().apply {
+ it?.use {
+ val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
+ val albumIdIndex = it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
+ val labelIndex = it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
+ val thumbnailPathIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATA)
+ val thumbnailRelativePathIndex =
+ it.getColumnIndex(MediaStore.Files.FileColumns.RELATIVE_PATH)
+ val thumbnailDateIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATE_MODIFIED)
+ val sizeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.SIZE)
+ val mimeTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)
+
+ if (!it.moveToFirst()) {
+ return@use
+ }
+
+ while (!it.isAfterLast) {
+ val bucketId = it.getInt(albumIdIndex)
+
+ this[bucketId]?.also { album ->
+ album.count += 1
+ album.size += it.getLong(sizeIndex)
+ } ?: run {
+ val albumId = it.getLong(albumIdIndex)
+ val id = it.getLong(idIndex)
+ val label = it.tryGetString(labelIndex, Build.MODEL)
+ val thumbnailPath = it.getString(thumbnailPathIndex)
+ val thumbnailRelativePath = it.getString(thumbnailRelativePathIndex)
+ val thumbnailDate = it.getLong(thumbnailDateIndex)
+ val size = it.getLong(sizeIndex)
+ val mimeType = it.getString(mimeTypeIndex)
+ val contentUri = if (mimeType.contains("image"))
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ else
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+
+ this[bucketId] = Album(
+ id = albumId,
+ label = label ?: Build.MODEL,
+ uri = ContentUris.withAppendedId(contentUri, id),
+ pathToThumbnail = thumbnailPath,
+ relativePath = thumbnailRelativePath,
+ timestamp = thumbnailDate
+ ).apply {
+ this.count += 1
+ this.size += size
+ }
+ }
+
+ it.moveToNext()
+ }
+ }
+ }.values.toList()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/MediaFlow.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/MediaFlow.kt
new file mode 100644
index 000000000..2d632b8cb
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/MediaFlow.kt
@@ -0,0 +1,171 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.feature_node.data.data_source.mediastore.quries
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.database.Cursor
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.core.os.bundleOf
+import com.dot.gallery.core.Constants
+import com.dot.gallery.feature_node.data.data_source.mediastore.MediaQuery
+import com.dot.gallery.core.util.PickerUtils
+import com.dot.gallery.core.util.Query
+import com.dot.gallery.core.util.and
+import com.dot.gallery.core.util.eq
+import com.dot.gallery.core.util.join
+import com.dot.gallery.core.util.MediaStoreBuckets
+import com.dot.gallery.core.util.ext.mapEachRow
+import com.dot.gallery.core.util.ext.queryFlow
+import com.dot.gallery.core.util.ext.tryGetLong
+import com.dot.gallery.core.util.ext.tryGetString
+import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.MediaType
+import com.dot.gallery.feature_node.presentation.util.getDate
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Media flow
+ *
+ * This class is responsible for fetching media from the media store
+ *
+ * @property contentResolver
+ * @property buckedId
+ * @property mimeType
+ */
+class MediaFlow(
+ private val contentResolver: ContentResolver,
+ private val buckedId: Long,
+ private val mimeType: String? = null
+) : QueryFlow() {
+ init {
+ assert(buckedId != MediaStoreBuckets.MEDIA_STORE_BUCKET_PLACEHOLDER.id) {
+ "MEDIA_STORE_BUCKET_PLACEHOLDER found"
+ }
+ }
+
+ override fun flowCursor(): Flow {
+ val uri = MediaQuery.MediaStoreFileUri
+ val projection = MediaQuery.MediaProjection
+ val imageOrVideo = PickerUtils.mediaTypeFromGenericMimeType(mimeType)?.let {
+ when (it) {
+ MediaType.IMAGE -> MediaQuery.Selection.image
+ MediaType.VIDEO -> MediaQuery.Selection.video
+ }
+ } ?: when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_PHOTOS.id -> MediaQuery.Selection.image
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_VIDEOS.id -> MediaQuery.Selection.video
+ else -> MediaQuery.Selection.imageOrVideo
+ }
+ val albumFilter = when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> MediaStore.Files.FileColumns.IS_FAVORITE eq 1
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
+ MediaStore.Files.FileColumns.IS_TRASHED eq 1
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TIMELINE.id,
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_PHOTOS.id,
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_VIDEOS.id -> null
+
+ else -> MediaStore.Files.FileColumns.BUCKET_ID eq Query.ARG
+ }
+ val rawMimeType = mimeType?.takeIf { PickerUtils.isMimeTypeNotGeneric(it) }
+ val mimeTypeQuery = rawMimeType?.let {
+ MediaStore.Files.FileColumns.MIME_TYPE eq Query.ARG
+ }
+
+ // Join all the non-null queries
+ val selection = listOfNotNull(
+ imageOrVideo,
+ albumFilter,
+ mimeTypeQuery,
+ ).join(Query::and)
+
+ val selectionArgs = listOfNotNull(
+ buckedId.takeIf {
+ MediaStoreBuckets.entries.toTypedArray().none { bucket -> it == bucket.id }
+ }?.toString(),
+ rawMimeType,
+ ).toTypedArray()
+
+ val sortOrder = when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
+ "${MediaStore.Files.FileColumns.DATE_EXPIRES} DESC"
+
+ else -> "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
+ }
+
+ val queryArgs = Bundle().apply {
+ putAll(
+ bundleOf(
+ ContentResolver.QUERY_ARG_SQL_SELECTION to selection?.build(),
+ ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs,
+ ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
+ )
+ )
+
+ // Exclude trashed media unless we want data for the trashed album
+ putInt(
+ MediaStore.QUERY_ARG_MATCH_TRASHED, when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> MediaStore.MATCH_ONLY
+
+ else -> MediaStore.MATCH_EXCLUDE
+ }
+ )
+ }
+
+ return contentResolver.queryFlow(
+ uri,
+ projection,
+ queryArgs,
+ )
+ }
+
+ override fun flowData() = flowCursor().mapEachRow(MediaQuery.MediaProjection) { it, indexCache ->
+ var i = 0
+
+ val id = it.getLong(indexCache[i++])
+ val path = it.getString(indexCache[i++])
+ val relativePath = it.getString(indexCache[i++])
+ val title = it.getString(indexCache[i++])
+ val albumID = it.getLong(indexCache[i++])
+ val albumLabel = it.tryGetString(indexCache[i++], Build.MODEL)
+ val takenTimestamp = it.tryGetLong(indexCache[i++])
+ val modifiedTimestamp = it.getLong(indexCache[i++])
+ val duration = it.tryGetString(indexCache[i++])
+ val size = it.getLong(indexCache[i++])
+ val mimeType = it.getString(indexCache[i++])
+ val isFavorite = it.getInt(indexCache[i++])
+ val isTrashed = it.getInt(indexCache[i++])
+ val expiryTimestamp = it.tryGetLong(indexCache[i])
+ val contentUri = if (mimeType.contains("image"))
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ else
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ val uri = ContentUris.withAppendedId(contentUri, id)
+ val formattedDate = modifiedTimestamp.getDate(Constants.FULL_DATE_FORMAT)
+ Media(
+ id = id,
+ label = title,
+ uri = uri,
+ path = path,
+ relativePath = relativePath,
+ albumID = albumID,
+ albumLabel = albumLabel ?: Build.MODEL,
+ timestamp = modifiedTimestamp,
+ takenTimestamp = takenTimestamp,
+ expiryTimestamp = expiryTimestamp,
+ fullDate = formattedDate,
+ duration = duration,
+ favorite = isFavorite,
+ trashed = isTrashed,
+ size = size,
+ mimeType = mimeType
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/MediaUriFlow.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/MediaUriFlow.kt
new file mode 100644
index 000000000..109114717
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/MediaUriFlow.kt
@@ -0,0 +1,185 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.dot.gallery.feature_node.data.data_source.mediastore.quries
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.core.os.bundleOf
+import com.dot.gallery.core.Constants
+import com.dot.gallery.feature_node.data.data_source.mediastore.MediaQuery
+import com.dot.gallery.core.util.PickerUtils
+import com.dot.gallery.core.util.Query
+import com.dot.gallery.core.util.and
+import com.dot.gallery.core.util.eq
+import com.dot.gallery.core.util.join
+import com.dot.gallery.core.util.MediaStoreBuckets
+import com.dot.gallery.core.util.ext.mapEachRow
+import com.dot.gallery.core.util.ext.queryFlow
+import com.dot.gallery.core.util.ext.tryGetLong
+import com.dot.gallery.core.util.ext.tryGetString
+import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.MediaType
+import com.dot.gallery.feature_node.domain.model.isTrashed
+import com.dot.gallery.feature_node.presentation.util.getDate
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Media uri flow
+ *
+ * This class is responsible for fetching media from the media store based on the provided uris
+ *
+ * @property contentResolver
+ * @property mimeType
+ * @property uris
+ * @property reviewMode
+ */
+class MediaUriFlow(
+ private val contentResolver: ContentResolver,
+ private val mimeType: String? = null,
+ private val uris: List,
+ private val reviewMode: Boolean = false
+) : QueryFlow() {
+
+ private var buckedId: Long = MediaStoreBuckets.MEDIA_STORE_BUCKET_TIMELINE.id
+
+ override fun flowCursor(): Flow {
+ val uri = MediaQuery.MediaStoreFileUri
+ val projection = MediaQuery.MediaProjection
+ val imageOrVideo = PickerUtils.mediaTypeFromGenericMimeType(mimeType)?.let {
+ when (it) {
+ MediaType.IMAGE -> MediaQuery.Selection.image
+ MediaType.VIDEO -> MediaQuery.Selection.video
+ }
+ } ?: MediaQuery.Selection.imageOrVideo
+ val albumFilter = when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> MediaStore.Files.FileColumns.IS_FAVORITE eq 1
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
+ MediaStore.Files.FileColumns.IS_TRASHED eq 1
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TIMELINE.id,
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_PHOTOS.id,
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_VIDEOS.id -> null
+
+ else -> MediaStore.Files.FileColumns.BUCKET_ID eq Query.ARG
+ }
+ val rawMimeType = mimeType?.takeIf { PickerUtils.isMimeTypeNotGeneric(it) }
+ val mimeTypeQuery = rawMimeType?.let {
+ MediaStore.Files.FileColumns.MIME_TYPE eq Query.ARG
+ }
+
+ // Join all the non-null queries
+ val selection = listOfNotNull(
+ imageOrVideo,
+ albumFilter,
+ mimeTypeQuery,
+ ).join(Query::and)
+
+ val selectionArgs = listOfNotNull(
+ buckedId.takeIf {
+ MediaStoreBuckets.entries.toTypedArray().none { bucket -> it == bucket.id }
+ }?.toString(),
+ rawMimeType,
+ ).toTypedArray()
+
+ val sortOrder = when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
+ "${MediaStore.Files.FileColumns.DATE_EXPIRES} DESC"
+
+ else -> "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
+ }
+
+ val queryArgs = Bundle().apply {
+ putAll(
+ bundleOf(
+ ContentResolver.QUERY_ARG_SQL_SELECTION to selection?.build(),
+ ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs,
+ ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
+ )
+ )
+
+ // Exclude trashed media unless we want data for the trashed album
+ putInt(
+ MediaStore.QUERY_ARG_MATCH_TRASHED, when (buckedId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> MediaStore.MATCH_ONLY
+
+ else -> MediaStore.MATCH_EXCLUDE
+ }
+ )
+ }
+
+ return contentResolver.queryFlow(
+ uri,
+ projection,
+ queryArgs,
+ )
+ }
+
+ override fun flowData() =
+ flowCursor().mapEachRow(MediaQuery.MediaProjection) { it, indexCache ->
+ var i = 0
+
+ val id = it.getLong(indexCache[i++])
+ val path = it.getString(indexCache[i++])
+ val relativePath = it.getString(indexCache[i++])
+ val title = it.getString(indexCache[i++])
+ val albumID = it.getLong(indexCache[i++])
+ val albumLabel = it.tryGetString(indexCache[i++], Build.MODEL)
+ val takenTimestamp = it.tryGetLong(indexCache[i++])
+ val modifiedTimestamp = it.getLong(indexCache[i++])
+ val duration = it.tryGetString(indexCache[i++])
+ val size = it.getLong(indexCache[i++])
+ val mimeType = it.getString(indexCache[i++])
+ val isFavorite = it.getInt(indexCache[i++])
+ val isTrashed = it.getInt(indexCache[i++])
+ val expiryTimestamp = it.tryGetLong(indexCache[i])
+ val contentUri = if (mimeType.contains("image"))
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ else
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ val uri = ContentUris.withAppendedId(contentUri, id)
+ val formattedDate = modifiedTimestamp.getDate(Constants.FULL_DATE_FORMAT)
+ Media(
+ id = id,
+ label = title,
+ uri = uri,
+ path = path,
+ relativePath = relativePath,
+ albumID = albumID,
+ albumLabel = albumLabel ?: Build.MODEL,
+ timestamp = modifiedTimestamp,
+ takenTimestamp = takenTimestamp,
+ expiryTimestamp = expiryTimestamp,
+ fullDate = formattedDate,
+ duration = duration,
+ favorite = isFavorite,
+ trashed = isTrashed,
+ size = size,
+ mimeType = mimeType
+ )
+ }.let { flow ->
+ val ids = uris.map { ContentUris.parseId(it) }
+ if (reviewMode) {
+ flow.map { mediaList ->
+ mediaList.filter { media ->
+ ids.contains(media.id) && !media.isTrashed
+ }
+ }
+ } else {
+ flow.map { mediaList ->
+ mediaList.filter { media ->
+ ids.contains(media.id)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/QueryFlow.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/QueryFlow.kt
new file mode 100644
index 000000000..7acc09ed9
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/quries/QueryFlow.kt
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.dot.gallery.feature_node.data.data_source.mediastore.quries
+
+import android.database.Cursor
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Query flow
+ *
+ * This class is responsible for fetching data with a cursor
+ *
+ * @param T
+ * @constructor Create empty Query flow
+ */
+abstract class QueryFlow {
+
+ /** A flow of the data specified by the query */
+ abstract fun flowData(): Flow>
+
+ /** A flow of the cursor specified by the query */
+ abstract fun flowCursor(): Flow
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetAlbums.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetAlbums.kt
deleted file mode 100644
index e0c802962..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetAlbums.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.data.data_types
-
-import android.content.ContentResolver
-import android.content.ContentUris
-import android.os.Build
-import android.os.Bundle
-import android.provider.MediaStore
-import com.dot.gallery.feature_node.data.data_source.Query
-import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-suspend fun ContentResolver.getAlbums(
- query: Query = Query.AlbumQuery(),
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)
-): List {
- return withContext(Dispatchers.IO) {
- val timeStart = System.currentTimeMillis()
- val albums = ArrayList()
- val bundle = query.bundle ?: Bundle()
- val albumQuery = query.copy(
- bundle = bundle.apply {
- putInt(
- ContentResolver.QUERY_ARG_SORT_DIRECTION,
- ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SORT_COLUMNS,
- arrayOf(MediaStore.MediaColumns.DATE_MODIFIED)
- )
- },
- )
- query(albumQuery).use {
- with(it) {
- while (moveToNext()) {
- try {
- val albumId = getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID))
- val id = getLong(getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
- val label: String? = try {
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_DISPLAY_NAME))
- } catch (e: Exception) {
- Build.MODEL
- }
- val thumbnailPath =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.DATA))
- val thumbnailRelativePath =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))
- val thumbnailDate =
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED))
- val size =
- getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE))
- val mimeType =
- getString(getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE))
- val contentUri = if (mimeType.contains("image"))
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- else
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- val album = Album(
- id = albumId,
- label = label ?: Build.MODEL,
- uri = ContentUris.withAppendedId(contentUri, id),
- pathToThumbnail = thumbnailPath,
- relativePath = thumbnailRelativePath,
- timestamp = thumbnailDate,
- size = size,
- count = 1
- )
- val currentAlbum = albums.find { albm -> albm.id == albumId }
- if (currentAlbum == null)
- albums.add(album)
- else {
- val i = albums.indexOf(currentAlbum)
- albums[i].count++
- albums[i].size += size
- if (albums[i].timestamp <= thumbnailDate) {
- album.count = albums[i].count
- albums[i] = album
- }
- }
-
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- }
- }
- return@withContext mediaOrder.sortAlbums(albums).also {
- println("Album parsing took: ${System.currentTimeMillis() - timeStart}ms")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetFavoriteMedia.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetFavoriteMedia.kt
deleted file mode 100644
index f711c3c4f..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetFavoriteMedia.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.data.data_types
-
-import android.content.ContentResolver
-import android.provider.MediaStore
-import com.dot.gallery.feature_node.data.data_source.Query.Companion.defaultBundle
-import com.dot.gallery.feature_node.data.data_source.Query.MediaQuery
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-suspend fun ContentResolver.getMediaFavorite(
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)
-): List {
- return withContext(Dispatchers.IO) {
- val mediaQuery = MediaQuery().copy(
- bundle = defaultBundle.apply {
- putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_ONLY)
- }
- )
- return@withContext mediaOrder.sortMedia(getMedia(mediaQuery))
- }
-}
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetMedia.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetMedia.kt
deleted file mode 100644
index 9de73de10..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetMedia.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.data.data_types
-
-import android.content.ContentResolver
-import com.dot.gallery.feature_node.data.data_source.Query
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-suspend fun ContentResolver.getMedia(
- mediaQuery: Query = Query.MediaQuery(),
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)
-): List {
- return withContext(Dispatchers.IO) {
- val timeStart = System.currentTimeMillis()
- val media = ArrayList()
- query(mediaQuery).use { cursor ->
- while (cursor.moveToNext()) {
- try {
- media.add(cursor.getMediaFromCursor())
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- }
- return@withContext mediaOrder.sortMedia(media).also {
- println("Media parsing took: ${System.currentTimeMillis() - timeStart}ms")
- }
- }
-}
-
-suspend fun ContentResolver.findMedia(mediaQuery: Query): Media? {
- return withContext(Dispatchers.IO) {
- val mediaList = getMedia(mediaQuery)
- return@withContext if (mediaList.isEmpty()) null else mediaList.first()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetMediaByUri.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetMediaByUri.kt
deleted file mode 100644
index a4e254ba5..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetMediaByUri.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.data.data_types
-
-import android.content.ContentResolver
-import android.content.Context
-import android.net.Uri
-import android.os.Bundle
-import android.provider.MediaStore
-import com.dot.gallery.feature_node.data.data_source.Query.MediaQuery
-import com.dot.gallery.feature_node.domain.model.Media
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-suspend fun Context.getMediaByUri(uri: Uri): Media? {
- return withContext(Dispatchers.IO) {
- var media: Media? = null
- val mediaQuery = MediaQuery().copy(
- bundle = Bundle().apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.DATA + "=?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(uri.toString())
- )
- }
- )
- with(contentResolver.query(mediaQuery)) {
- moveToFirst()
- while (!isAfterLast) {
- try {
- media = getMediaFromCursor()
- break
- } catch (e: Exception) {
- close()
- e.printStackTrace()
- }
- }
- moveToNext()
- close()
- }
- if (media == null) {
- media = Media.createFromUri(this@getMediaByUri, uri)
- }
-
- return@withContext media
- }
-}
-
-suspend fun Context.getMediaListByUris(list: List): List {
- return withContext(Dispatchers.IO) {
- val mediaList = ArrayList()
- val mediaQuery = MediaQuery().copy(
- bundle = Bundle().apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns._ID + "=?"
- )
- putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- list.map { it.toString().substringAfterLast("/") }.toTypedArray()
- )
- }
- )
- mediaList.addAll(contentResolver.getMedia(mediaQuery))
- if (mediaList.isEmpty()) {
- for (uri in list) {
- Media.createFromUri(this@getMediaListByUris, uri)?.let { mediaList.add(it) }
- }
- }
- mediaList
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetTrashedMedia.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetTrashedMedia.kt
deleted file mode 100644
index be5c2684a..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/data_types/GetTrashedMedia.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.data.data_types
-
-import android.content.ContentResolver
-import android.os.Bundle
-import android.provider.MediaStore
-import com.dot.gallery.feature_node.data.data_source.Query.TrashQuery
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-suspend fun ContentResolver.getMediaTrashed(): List {
- return withContext(Dispatchers.IO) {
- val mediaQuery = TrashQuery().copy(
- bundle = Bundle().apply {
- putStringArray(
- ContentResolver.QUERY_ARG_SORT_COLUMNS,
- arrayOf(MediaStore.MediaColumns.DATE_EXPIRES)
- )
- putInt(
- ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
- ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
- )
- putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
- }
- )
- return@withContext MediaOrder.Expiry().sortMedia(getMedia(mediaQuery))
- }
-}
-
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/data/repository/MediaRepositoryImpl.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/data/repository/MediaRepositoryImpl.kt
index 7ca16b8e1..7c07703f5 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/data/repository/MediaRepositoryImpl.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/data/repository/MediaRepositoryImpl.kt
@@ -12,102 +12,116 @@ import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
-import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.core.app.ActivityOptionsCompat
+import androidx.work.WorkManager
import com.dot.gallery.core.Resource
-import com.dot.gallery.core.contentFlowObserver
-import com.dot.gallery.core.contentFlowWithDatabase
import com.dot.gallery.core.fileFlowObserver
+import com.dot.gallery.core.updateDatabase
+import com.dot.gallery.core.util.MediaStoreBuckets
+import com.dot.gallery.core.util.ext.copyMedia
+import com.dot.gallery.core.util.ext.mapAsResource
+import com.dot.gallery.core.util.ext.overrideImage
+import com.dot.gallery.core.util.ext.saveImage
+import com.dot.gallery.core.util.ext.updateMedia
+import com.dot.gallery.core.util.ext.updateMediaExif
import com.dot.gallery.feature_node.data.data_source.InternalDatabase
import com.dot.gallery.feature_node.data.data_source.KeychainHolder
import com.dot.gallery.feature_node.data.data_source.KeychainHolder.Companion.VAULT_INFO_FILE_NAME
-import com.dot.gallery.feature_node.data.data_source.Query
-import com.dot.gallery.feature_node.data.data_types.copyMedia
-import com.dot.gallery.feature_node.data.data_types.findMedia
-import com.dot.gallery.feature_node.data.data_types.getAlbums
-import com.dot.gallery.feature_node.data.data_types.getMedia
-import com.dot.gallery.feature_node.data.data_types.getMediaByUri
-import com.dot.gallery.feature_node.data.data_types.getMediaFavorite
-import com.dot.gallery.feature_node.data.data_types.getMediaListByUris
-import com.dot.gallery.feature_node.data.data_types.getMediaTrashed
-import com.dot.gallery.feature_node.data.data_types.overrideImage
-import com.dot.gallery.feature_node.data.data_types.saveImage
-import com.dot.gallery.feature_node.data.data_types.updateMedia
-import com.dot.gallery.feature_node.data.data_types.updateMediaExif
+import com.dot.gallery.feature_node.data.data_source.mediastore.quries.AlbumsFlow
+import com.dot.gallery.feature_node.data.data_source.mediastore.quries.MediaFlow
+import com.dot.gallery.feature_node.data.data_source.mediastore.quries.MediaUriFlow
import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.domain.model.EncryptedMedia
import com.dot.gallery.feature_node.domain.model.ExifAttributes
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
import com.dot.gallery.feature_node.domain.model.Media
import com.dot.gallery.feature_node.domain.model.PinnedAlbum
+import com.dot.gallery.feature_node.domain.model.TimelineSettings
import com.dot.gallery.feature_node.domain.model.Vault
import com.dot.gallery.feature_node.domain.model.toEncryptedMedia
import com.dot.gallery.feature_node.domain.repository.MediaRepository
import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
import com.dot.gallery.feature_node.presentation.picker.AllowedMedia
import com.dot.gallery.feature_node.presentation.picker.AllowedMedia.BOTH
import com.dot.gallery.feature_node.presentation.picker.AllowedMedia.PHOTOS
import com.dot.gallery.feature_node.presentation.picker.AllowedMedia.VIDEOS
+import com.dot.gallery.feature_node.presentation.util.printError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.io.File
-import java.io.IOException
import java.io.Serializable
class MediaRepositoryImpl(
private val context: Context,
+ private val workManager: WorkManager,
private val database: InternalDatabase,
private val keychainHolder: KeychainHolder
) : MediaRepository {
+ private val contentResolver = context.contentResolver
+
+ override suspend fun updateInternalDatabase() {
+ workManager.updateDatabase()
+ }
+
/**
* TODO: Add media reordering
*/
override fun getMedia(): Flow>> =
- context.retrieveMedia(database) {
- it.getMedia(mediaOrder = DEFAULT_ORDER).removeBlacklisted()
- }
+ MediaFlow(
+ contentResolver = contentResolver,
+ buckedId = MediaStoreBuckets.MEDIA_STORE_BUCKET_TIMELINE.id
+ ).flowData().map {
+ Resource.Success(it)
+ }.flowOn(Dispatchers.IO)
override fun getMediaByType(allowedMedia: AllowedMedia): Flow>> =
- context.retrieveMedia(database) {
- val query = when (allowedMedia) {
- PHOTOS -> Query.PhotoQuery()
- VIDEOS -> Query.VideoQuery()
- BOTH -> Query.MediaQuery()
- }
- it.getMedia(mediaQuery = query, mediaOrder = DEFAULT_ORDER).removeBlacklisted()
- }
+ MediaFlow(
+ contentResolver = contentResolver,
+ buckedId = when (allowedMedia) {
+ PHOTOS -> MediaStoreBuckets.MEDIA_STORE_BUCKET_PHOTOS.id
+ VIDEOS -> MediaStoreBuckets.MEDIA_STORE_BUCKET_VIDEOS.id
+ BOTH -> MediaStoreBuckets.MEDIA_STORE_BUCKET_TIMELINE.id
+ },
+ mimeType = allowedMedia.toStringAny()
+ ).flowData().map {
+ Resource.Success(it)
+ }.flowOn(Dispatchers.IO)
override fun getFavorites(mediaOrder: MediaOrder): Flow>> =
- context.retrieveMedia(database) {
- it.getMediaFavorite(mediaOrder = mediaOrder).removeBlacklisted()
- }
+ MediaFlow(
+ contentResolver = contentResolver,
+ buckedId = MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id
+ ).flowData().map {
+ Resource.Success(it)
+ }.flowOn(Dispatchers.IO)
override fun getTrashed(): Flow>> =
- context.retrieveMedia(database) {
- it.getMediaTrashed().removeBlacklisted()
- }
-
- override fun getAlbums(
- mediaOrder: MediaOrder,
- ignoreBlacklisted: Boolean
- ): Flow>> =
- context.retrieveAlbums(database) { cr ->
- cr.getAlbums(mediaOrder = mediaOrder).toMutableList().apply {
- replaceAll { album ->
- album.copy(isPinned = database.getPinnedDao().albumIsPinned(album.id))
+ MediaFlow(
+ contentResolver = contentResolver,
+ buckedId = MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id
+ ).flowData().map { Resource.Success(it) }.flowOn(Dispatchers.IO)
+
+ override fun getAlbums(mediaOrder: MediaOrder): Flow>> =
+ AlbumsFlow(context).flowData().map {
+ withContext(Dispatchers.IO) {
+ val data = it.toMutableList().apply {
+ replaceAll { album ->
+ album.copy(isPinned = database.getPinnedDao().albumIsPinned(album.id))
+ }
}
- }.removeBlacklisted(ignoreBlacklisted)
- }
+
+ Resource.Success(mediaOrder.sortAlbums(data))
+ }
+ }.flowOn(Dispatchers.IO)
override suspend fun insertPinnedAlbum(pinnedAlbum: PinnedAlbum) =
database.getPinnedDao().insertPinnedAlbum(pinnedAlbum)
@@ -115,6 +129,9 @@ class MediaRepositoryImpl(
override suspend fun removePinnedAlbum(pinnedAlbum: PinnedAlbum) =
database.getPinnedDao().removePinnedAlbum(pinnedAlbum)
+ override fun getPinnedAlbums(): Flow> =
+ database.getPinnedDao().getPinnedAlbums()
+
override suspend fun addBlacklistedAlbum(ignoredAlbum: IgnoredAlbum) =
database.getBlacklistDao().addBlacklistedAlbum(ignoredAlbum)
@@ -124,144 +141,37 @@ class MediaRepositoryImpl(
override fun getBlacklistedAlbums(): Flow> =
database.getBlacklistDao().getBlacklistedAlbums()
- override suspend fun getMediaById(mediaId: Long): Media? {
- val query = Query.MediaQuery().copy(
- bundle = Bundle().apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns._ID + "= ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(mediaId.toString())
- )
- }
- )
- return context.contentResolver.findMedia(query)
- }
-
override fun getMediaByAlbumId(albumId: Long): Flow>> =
- context.retrieveMedia(database) {
- val query = Query.MediaQuery().copy(
- bundle = Bundle().apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.BUCKET_ID + "= ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(albumId.toString())
- )
- }
- )
- /** return@retrieveMedia */
- it.getMedia(query)
- }
+ MediaFlow(
+ contentResolver = contentResolver,
+ buckedId = albumId,
+ ).flowData().mapAsResource()
override fun getMediaByAlbumIdWithType(
albumId: Long,
allowedMedia: AllowedMedia
): Flow>> =
- context.retrieveMedia(database) {
- val query = Query.MediaQuery().copy(
- bundle = Bundle().apply {
- val mimeType = when (allowedMedia) {
- PHOTOS -> "image%"
- VIDEOS -> "video%"
- BOTH -> "%/%"
- }
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.BUCKET_ID + "= ? and " + MediaStore.MediaColumns.MIME_TYPE + " like ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(albumId.toString(), mimeType)
- )
- }
- )
- /** return@retrieveMedia */
- it.getMedia(query)
- }
+ MediaFlow(
+ contentResolver = contentResolver,
+ buckedId = albumId,
+ mimeType = allowedMedia.toStringAny()
+ ).flowData().mapAsResource()
override fun getAlbumsWithType(allowedMedia: AllowedMedia): Flow>> =
- context.retrieveAlbums(database) {
- val query = Query.AlbumQuery().copy(
- bundle = Bundle().apply {
- val mimeType = when (allowedMedia) {
- PHOTOS -> "image%"
- VIDEOS -> "video%"
- BOTH -> "%/%"
- }
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.MIME_TYPE + " like ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(mimeType)
- )
- }
- )
- it.getAlbums(query, mediaOrder = MediaOrder.Label(OrderType.Ascending))
- }
-
- override fun getMediaByUri(
- uriAsString: String,
- isSecure: Boolean
- ): Flow>> =
- context.retrieveMediaAsResource {
- val media = context.getMediaByUri(Uri.parse(uriAsString))
- /** return@retrieveMediaAsResource */
- if (media == null) {
- Resource.Error(message = "Media could not be opened")
- } else {
- val query = Query.MediaQuery().copy(
- bundle = Bundle().apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.BUCKET_ID + "= ?"
- )
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(media.albumID.toString())
- )
- }
- )
- Resource.Success(
- data = if (isSecure) listOf(media) else it.getMedia(query)
- .ifEmpty { listOf(media) })
- }
- }
+ AlbumsFlow(
+ context = context,
+ mimeType = allowedMedia.toStringAny()
+ ).flowData().mapAsResource()
override fun getMediaListByUris(
listOfUris: List,
reviewMode: Boolean
): Flow>> =
- context.retrieveMediaAsResource {
- var mediaList = context.getMediaListByUris(listOfUris)
- if (reviewMode) {
- val query = Query.MediaQuery().copy(
- bundle = Bundle().apply {
- putString(
- ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaStore.MediaColumns.BUCKET_ID + "= ?"
- )
- putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
- putStringArray(
- ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- arrayOf(mediaList.first().albumID.toString())
- )
- }
- )
- mediaList = it.getMedia(query).filter { media -> !media.isTrashed }
- }
- if (mediaList.isEmpty()) {
- Resource.Error(message = "Media could not be opened")
- } else {
- Resource.Success(data = mediaList)
- }
- }
+ MediaUriFlow(
+ contentResolver = contentResolver,
+ uris = listOfUris,
+ reviewMode = reviewMode
+ ).flowData().mapAsResource(errorOnEmpty = true, errorMessage = "Media could not be opened")
override suspend fun toggleFavorite(
result: ActivityResultLauncher,
@@ -269,7 +179,7 @@ class MediaRepositoryImpl(
favorite: Boolean
) {
val intentSender = MediaStore.createFavoriteRequest(
- context.contentResolver,
+ contentResolver,
mediaList.map { it.uri },
favorite
).intentSender
@@ -285,7 +195,7 @@ class MediaRepositoryImpl(
trash: Boolean
) {
val intentSender = MediaStore.createTrashRequest(
- context.contentResolver,
+ contentResolver,
mediaList.map { it.uri },
trash
).intentSender
@@ -301,7 +211,7 @@ class MediaRepositoryImpl(
) {
val intentSender =
MediaStore.createDeleteRequest(
- context.contentResolver,
+ contentResolver,
mediaList.map { it.uri }).intentSender
val senderRequest: IntentSenderRequest = IntentSenderRequest.Builder(intentSender)
.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION, 0)
@@ -312,7 +222,7 @@ class MediaRepositoryImpl(
override suspend fun copyMedia(
from: Media,
path: String
- ): Boolean = context.contentResolver.copyMedia(
+ ): Boolean = contentResolver.copyMedia(
from = from,
path = path
)
@@ -320,7 +230,7 @@ class MediaRepositoryImpl(
override suspend fun renameMedia(
media: Media,
newName: String
- ): Boolean = context.contentResolver.updateMedia(
+ ): Boolean = contentResolver.updateMedia(
media = media,
contentValues = displayName(newName)
)
@@ -328,7 +238,7 @@ class MediaRepositoryImpl(
override suspend fun moveMedia(
media: Media,
newPath: String
- ): Boolean = context.contentResolver.updateMedia(
+ ): Boolean = contentResolver.updateMedia(
media = media,
contentValues = relativePath(newPath)
)
@@ -336,7 +246,7 @@ class MediaRepositoryImpl(
override suspend fun updateMediaExif(
media: Media,
exifAttributes: ExifAttributes
- ): Boolean = context.contentResolver.updateMediaExif(
+ ): Boolean = contentResolver.updateMediaExif(
media = media,
exifAttributes = exifAttributes
)
@@ -347,7 +257,7 @@ class MediaRepositoryImpl(
mimeType: String,
relativePath: String,
displayName: String
- ) = context.contentResolver.saveImage(bitmap, format, mimeType, relativePath, displayName)
+ ) = contentResolver.saveImage(bitmap, format, mimeType, relativePath, displayName)
override fun overrideImage(
uri: Uri,
@@ -356,7 +266,7 @@ class MediaRepositoryImpl(
mimeType: String,
relativePath: String,
displayName: String
- ) = context.contentResolver.overrideImage(uri, bitmap, format, mimeType, relativePath, displayName)
+ ) = contentResolver.overrideImage(uri, bitmap, format, mimeType, relativePath, displayName)
override fun getVaults(): Flow>> =
context.retrieveInternalFiles {
@@ -379,40 +289,18 @@ class MediaRepositoryImpl(
vault: Vault,
onSuccess: () -> Unit,
onFailed: (reason: String) -> Unit
- ) {
- var collected = false
- getVaults().collectLatest {
- if (!collected) {
- collected = true
- val alreadyExists = it.data?.contains(vault) ?: false
- if (alreadyExists) {
- onFailed("Vault \"${vault.name}\" exists")
- } else {
- keychainHolder.writeVaultInfo(vault, onSuccess)
- }
- }
- }
- }
+ ) = withContext(Dispatchers.IO) { keychainHolder.writeVaultInfo(vault, onSuccess, onFailed) }
override suspend fun deleteVault(
vault: Vault,
onSuccess: () -> Unit,
onFailed: (reason: String) -> Unit
- ) {
- try {
- with(keychainHolder) { vaultFolder(vault).deleteRecursively() }
- } catch (e: IOException) {
- e.printStackTrace()
- onFailed("Failed to delete vault ${vault.name} (${vault.uuid})")
- return
- }
- onSuccess()
- }
+ ) = withContext(Dispatchers.IO) { keychainHolder.deleteVault(vault, onSuccess, onFailed) }
override fun getEncryptedMedia(vault: Vault): Flow>> =
context.retrieveInternalFiles {
with(keychainHolder) {
- vaultFolder(vault).listFiles()?.filter {
+ (vaultFolder(vault).listFiles()?.filter {
it.name.endsWith("enc")
}?.mapNotNull {
try {
@@ -421,7 +309,7 @@ class MediaRepositoryImpl(
} catch (e: Exception) {
null
}
- } ?: emptyList()
+ } ?: emptyList()).sortedByDescending { it.timestamp }
}
}
@@ -440,6 +328,7 @@ class MediaRepositoryImpl(
true
} catch (e: Exception) {
e.printStackTrace()
+ printError("Failed to add file: ${media.label}")
false
}
}
@@ -463,6 +352,7 @@ class MediaRepositoryImpl(
restored && deleted
} catch (e: Exception) {
e.printStackTrace()
+ printError("Failed to restore file: ${media.label}")
false
}
}
@@ -476,6 +366,7 @@ class MediaRepositoryImpl(
vault.mediaFile(media.id).delete()
} catch (e: Exception) {
e.printStackTrace()
+ printError("Failed to delete file: ${media.label}")
false
}
}
@@ -495,6 +386,7 @@ class MediaRepositoryImpl(
file.delete()
} catch (e: Exception) {
e.printStackTrace()
+ printError("Failed to delete file: ${file.name}")
failedFiles.add(file)
}
}
@@ -508,25 +400,12 @@ class MediaRepositoryImpl(
}
}
- private fun List.removeBlacklisted(): List = toMutableList().apply {
- val blacklistedAlbums = database.getBlacklistDao().getBlacklistedAlbumsSync()
- removeAll { media -> blacklistedAlbums.any { it.matchesMedia(media) } }
- }
-
- private fun MutableList.removeBlacklisted(ignoreBlacklisted: Boolean): List = apply {
- if (!ignoreBlacklisted) {
- val blacklistedAlbums = database.getBlacklistDao().getBlacklistedAlbumsSync()
- removeAll { album -> blacklistedAlbums.any { it.matchesAlbum(album) } }
- }
+ override fun getSettings(): Flow = database.getMediaDao().getTimelineSettings()
+ override suspend fun updateSettings(settings: TimelineSettings) {
+ database.getMediaDao().setTimelineSettings(settings)
}
companion object {
- private val DEFAULT_ORDER = MediaOrder.Date(OrderType.Descending)
- private val URIs = arrayOf(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- )
-
private fun displayName(newName: String) = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, newName)
}
@@ -535,33 +414,6 @@ class MediaRepositoryImpl(
put(MediaStore.MediaColumns.RELATIVE_PATH, newPath)
}
- private fun Context.retrieveMediaAsResource(dataBody: suspend (ContentResolver) -> Resource>) =
- contentFlowObserver(URIs).map {
- try {
- dataBody.invoke(contentResolver)
- } catch (e: Exception) {
- Resource.Error(message = e.localizedMessage ?: "An error occurred")
- }
- }.conflate()
-
- private fun Context.retrieveMedia(database: InternalDatabase, dataBody: suspend (ContentResolver) -> List) =
- contentFlowWithDatabase(URIs, database).map {
- try {
- Resource.Success(data = dataBody.invoke(contentResolver))
- } catch (e: Exception) {
- Resource.Error(message = e.localizedMessage ?: "An error occurred")
- }
- }.conflate()
-
- private fun Context.retrieveAlbums(database: InternalDatabase, dataBody: suspend (ContentResolver) -> List) =
- contentFlowWithDatabase(URIs, database).map {
- try {
- Resource.Success(data = dataBody.invoke(contentResolver))
- } catch (e: Exception) {
- Resource.Error(message = e.localizedMessage ?: "An error occurred")
- }
- }.conflate()
-
private fun Context.retrieveInternalFiles(dataBody: suspend (ContentResolver) -> List) =
fileFlowObserver().map {
try {
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Album.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Album.kt
index b1486343c..12fc28c6e 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Album.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Album.kt
@@ -24,7 +24,6 @@ data class Album(
val timestamp: Long,
var count: Long = 0,
var size: Long = 0,
- val selected: Boolean = false,
val isPinned: Boolean = false,
) : Parcelable {
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/AlbumState.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/AlbumState.kt
new file mode 100644
index 000000000..65edb1fb4
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/AlbumState.kt
@@ -0,0 +1,13 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+
+@Stable
+data class AlbumState(
+ val albums: List = emptyList(),
+ val albumsWithBlacklisted: List = emptyList(),
+ val albumsUnpinned: List = emptyList(),
+ val albumsPinned: List = emptyList(),
+ val error: String = "",
+ val isLoading: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/EncryptedMediaState.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/EncryptedMediaState.kt
new file mode 100644
index 000000000..dc612c381
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/EncryptedMediaState.kt
@@ -0,0 +1,14 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+
+@Stable
+data class EncryptedMediaState(
+ val media: List = emptyList(),
+ val mappedMedia: List = emptyList(),
+ val mappedMediaWithMonthly: List = emptyList(),
+ val headers: List = emptyList(),
+ val dateHeader: String = "",
+ val error: String = "",
+ val isLoading: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/InfoRow.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/InfoRow.kt
new file mode 100644
index 000000000..fc5f23e2a
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/InfoRow.kt
@@ -0,0 +1,15 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.vector.ImageVector
+
+@Stable
+data class InfoRow(
+ val label: String,
+ val content: String,
+ val icon: ImageVector,
+ val trailingIcon: ImageVector? = null,
+ val contentDescription: String? = null,
+ val onClick: (() -> Unit)? = null,
+ val onLongClick: (() -> Unit)? = null,
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LibraryIndicatorState.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LibraryIndicatorState.kt
new file mode 100644
index 000000000..04094d8bb
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LibraryIndicatorState.kt
@@ -0,0 +1,11 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+import kotlinx.serialization.Serializable
+
+@Stable
+@Serializable
+data class LibraryIndicatorState(
+ val trashCount: Int = 0,
+ val favoriteCount: Int = 0,
+)
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LocationData.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LocationData.kt
new file mode 100644
index 000000000..76b6ea151
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LocationData.kt
@@ -0,0 +1,59 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.dot.gallery.feature_node.presentation.util.ExifMetadata
+import com.dot.gallery.feature_node.presentation.util.formattedAddress
+import com.dot.gallery.feature_node.presentation.util.getLocation
+import com.dot.gallery.feature_node.presentation.util.rememberGeocoder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@Stable
+data class LocationData(
+ val latitude: Double,
+ val longitude: Double,
+ val location: String
+)
+
+@Composable
+fun rememberLocationData(
+ exifMetadata: ExifMetadata?,
+ media: Media
+): LocationData? {
+ val geocoder = rememberGeocoder()
+ var locationName by remember { mutableStateOf(exifMetadata?.formattedCords) }
+ LaunchedEffect(geocoder, exifMetadata) {
+ withContext(Dispatchers.IO) {
+ if (exifMetadata?.gpsLatLong != null) {
+ geocoder?.getLocation(
+ exifMetadata.gpsLatLong[0],
+ exifMetadata.gpsLatLong[1]
+ ) { address ->
+ address?.let {
+ val addressName = it.formattedAddress
+ if (addressName.isNotEmpty()) {
+ locationName = addressName
+ }
+ }
+ }
+ }
+ }
+ }
+ return remember(media, exifMetadata, locationName) {
+ exifMetadata?.let {
+ it.gpsLatLong?.let { latLong ->
+ LocationData(
+ latitude = latLong[0],
+ longitude = latLong[1],
+ location = locationName ?: "Unknown"
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Media.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Media.kt
index e1b11720f..4d0e79f89 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Media.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Media.kt
@@ -12,15 +12,16 @@ import android.os.Parcelable
import android.webkit.MimeTypeMap
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.room.Entity
import com.dot.gallery.core.Constants
import com.dot.gallery.feature_node.presentation.util.getDate
-import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.io.File
import kotlin.random.Random
@Immutable
@Parcelize
+@Entity(tableName = "media", primaryKeys = ["id"])
data class Media(
val id: Long = 0,
val label: String,
@@ -40,84 +41,11 @@ data class Media(
val duration: String? = null,
) : Parcelable {
- @IgnoredOnParcel
- @Stable
- val isVideo: Boolean = mimeType.startsWith("video/") && duration != null
-
- @IgnoredOnParcel
- @Stable
- val isImage: Boolean = mimeType.startsWith("image/")
-
- @IgnoredOnParcel
- @Stable
- val isTrashed: Boolean = trashed == 1
-
- @IgnoredOnParcel
- @Stable
- val isFavorite: Boolean = favorite == 1
-
@Stable
override fun toString(): String {
return "$id, $path, $fullDate, $mimeType, favorite=$favorite"
}
- /**
- * Used to determine if the Media object is not accessible
- * via MediaStore.
- * This happens when the user tries to open media from an app
- * using external sources (in our case, Gallery Media Viewer), but
- * the specific media is only available internally in that app
- * (Android/data(OR media)/com.package.name/)
- *
- * If it's readUriOnly then we know that we should expect a barebone
- * Media object with limited functionality (no favorites, trash, timestamp etc)
- */
- @IgnoredOnParcel
- @Stable
- val readUriOnly: Boolean = albumID == -99L && albumLabel == ""
-
- /**
- * Determine if the current media is a raw format
- *
- * Checks if [mimeType] starts with "image/x-" or "image/vnd."
- *
- * Most used formats:
- * - ARW: image/x-sony-arw
- * - CR2: image/x-canon-cr2
- * - CRW: image/x-canon-crw
- * - DCR: image/x-kodak-dcr
- * - DNG: image/x-adobe-dng
- * - ERF: image/x-epson-erf
- * - K25: image/x-kodak-k25
- * - KDC: image/x-kodak-kdc
- * - MRW: image/x-minolta-mrw
- * - NEF: image/x-nikon-nef
- * - ORF: image/x-olympus-orf
- * - PEF: image/x-pentax-pef
- * - RAF: image/x-fuji-raf
- * - RAW: image/x-panasonic-raw
- * - SR2: image/x-sony-sr2
- * - SRF: image/x-sony-srf
- * - X3F: image/x-sigma-x3f
- *
- * Other proprietary image types in the standard:
- * image/vnd.manufacturer.filename_extension for instance for NEF by Nikon and .mrv for Minolta:
- * - NEF: image/vnd.nikon.nef
- * - Minolta: image/vnd.minolta.mrw
- */
- @IgnoredOnParcel
- @Stable
- val isRaw: Boolean =
- mimeType.isNotBlank() && (mimeType.startsWith("image/x-") || mimeType.startsWith("image/vnd."))
-
- @IgnoredOnParcel
- @Stable
- val fileExtension: String = label.substringAfterLast(".").removePrefix(".")
-
- @IgnoredOnParcel
- @Stable
- val volume: String = path.substringBeforeLast("/").removeSuffix(relativePath.removeSuffix("/"))
-
companion object {
fun createFromUri(context: Context, uri: Uri): Media? {
if (uri.path == null) return null
@@ -171,3 +99,60 @@ data class Media(
}
}
}
+
+/**
+ * Determine if the current media is a raw format
+ *
+ * Checks if [Media.mimeType] starts with "image/x-" or "image/vnd."
+ *
+ * Most used formats:
+ * - ARW: image/x-sony-arw
+ * - CR2: image/x-canon-cr2
+ * - CRW: image/x-canon-crw
+ * - DCR: image/x-kodak-dcr
+ * - DNG: image/x-adobe-dng
+ * - ERF: image/x-epson-erf
+ * - K25: image/x-kodak-k25
+ * - KDC: image/x-kodak-kdc
+ * - MRW: image/x-minolta-mrw
+ * - NEF: image/x-nikon-nef
+ * - ORF: image/x-olympus-orf
+ * - PEF: image/x-pentax-pef
+ * - RAF: image/x-fuji-raf
+ * - RAW: image/x-panasonic-raw
+ * - SR2: image/x-sony-sr2
+ * - SRF: image/x-sony-srf
+ * - X3F: image/x-sigma-x3f
+ *
+ * Other proprietary image types in the standard:
+ * image/vnd.manufacturer.filename_extension for instance for NEF by Nikon and .mrv for Minolta:
+ * - NEF: image/vnd.nikon.nef
+ * - Minolta: image/vnd.minolta.mrw
+ */
+val Media.isRaw: Boolean get() =
+ mimeType.isNotBlank() && (mimeType.startsWith("image/x-") || mimeType.startsWith("image/vnd."))
+
+val Media.fileExtension: String get() = label.substringAfterLast(".").removePrefix(".")
+
+val Media.volume: String get() = path.substringBeforeLast("/").removeSuffix(relativePath.removeSuffix("/"))
+
+/**
+ * Used to determine if the Media object is not accessible
+ * via MediaStore.
+ * This happens when the user tries to open media from an app
+ * using external sources (in our case, Gallery Media Viewer), but
+ * the specific media is only available internally in that app
+ * (Android/data(OR media)/com.package.name/)
+ *
+ * If it's readUriOnly then we know that we should expect a barebone
+ * Media object with limited functionality (no favorites, trash, timestamp etc)
+ */
+val Media.readUriOnly: Boolean get() = albumID == -99L && albumLabel == ""
+
+val Media.isVideo: Boolean get() = mimeType.startsWith("video/") && duration != null
+
+val Media.isImage: Boolean get() = mimeType.startsWith("image/")
+
+val Media.isTrashed: Boolean get() = trashed == 1
+
+val Media.isFavorite: Boolean get() = favorite == 1
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaDateCaption.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaDateCaption.kt
new file mode 100644
index 000000000..9f26d4d6e
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaDateCaption.kt
@@ -0,0 +1,51 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.res.stringResource
+import com.dot.gallery.R
+import com.dot.gallery.core.Constants
+import com.dot.gallery.feature_node.presentation.util.ExifMetadata
+import com.dot.gallery.feature_node.presentation.util.getDate
+
+@Stable
+data class MediaDateCaption(
+ val date: String,
+ val deviceInfo: String? = null,
+ val description: String
+)
+
+@Composable
+fun rememberMediaDateCaption(
+ exifMetadata: ExifMetadata?,
+ media: Media
+): MediaDateCaption {
+ val deviceInfo = remember(exifMetadata) { exifMetadata?.lensDescription }
+ val defaultDesc = stringResource(R.string.image_add_description)
+ val description = remember(exifMetadata) { exifMetadata?.imageDescription ?: defaultDesc }
+ return remember(media) {
+ MediaDateCaption(
+ date = media.timestamp.getDate(Constants.EXIF_DATE_FORMAT),
+ deviceInfo = deviceInfo,
+ description = description
+ )
+ }
+}
+
+@Composable
+fun rememberMediaDateCaption(
+ exifMetadata: ExifMetadata?,
+ media: EncryptedMedia
+): MediaDateCaption {
+ val deviceInfo = remember(exifMetadata) { exifMetadata?.lensDescription }
+ val defaultDesc = stringResource(R.string.image_add_description)
+ val description = remember(exifMetadata) { exifMetadata?.imageDescription ?: defaultDesc }
+ return remember(media) {
+ MediaDateCaption(
+ date = media.timestamp.getDate(Constants.EXIF_DATE_FORMAT),
+ deviceInfo = deviceInfo,
+ description = description
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaItem.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaItem.kt
index 06d684bd5..4c7dc369d 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaItem.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaItem.kt
@@ -4,25 +4,20 @@
*/
package com.dot.gallery.feature_node.domain.model
-import android.os.Parcelable
-import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
-import kotlinx.parcelize.Parcelize
-@Parcelize
-@Immutable
-sealed class MediaItem : Parcelable {
- @Stable
+@Stable
+sealed class MediaItem {
abstract val key: String
- @Immutable
+ @Stable
data class Header(
override val key: String,
val text: String,
- val data: List
+ val data: Set
) : MediaItem()
- @Immutable
+ @Stable
data class MediaViewItem(
override val key: String,
val media: Media
@@ -30,20 +25,18 @@ sealed class MediaItem : Parcelable {
}
-@Parcelize
-@Immutable
-sealed class EncryptedMediaItem : Parcelable {
- @Stable
+@Stable
+sealed class EncryptedMediaItem {
abstract val key: String
- @Immutable
+ @Stable
data class Header(
override val key: String,
val text: String,
- val data: List
+ val data: Set
) : EncryptedMediaItem()
- @Immutable
+ @Stable
data class MediaViewItem(
override val key: String,
val media: EncryptedMedia
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaState.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaState.kt
new file mode 100644
index 000000000..f5cb4ab5f
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaState.kt
@@ -0,0 +1,14 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+
+@Stable
+data class MediaState(
+ val media: List = emptyList(),
+ val mappedMedia: List = emptyList(),
+ val mappedMediaWithMonthly: List = emptyList(),
+ val headers: List = emptyList(),
+ val dateHeader: String = "",
+ val error: String = "",
+ val isLoading: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaType.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaType.kt
new file mode 100644
index 000000000..cfb3970ad
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaType.kt
@@ -0,0 +1,48 @@
+package com.dot.gallery.feature_node.domain.model
+
+import android.net.Uri
+import android.provider.MediaStore
+
+enum class MediaType(
+ val externalContentUri: Uri,
+ val mediaStoreValue: Int,
+) {
+ IMAGE(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE,
+ ),
+ VIDEO(
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO,
+ );
+
+ companion object {
+ private val dashMimeTypes = listOf(
+ "application/dash+xml",
+ )
+
+ private val hlsMimeTypes = listOf(
+ "application/vnd.apple.mpegurl",
+ "application/x-mpegurl",
+ "audio/mpegurl",
+ "audio/x-mpegurl",
+ )
+
+ private val smoothStreamingMimeTypes = listOf(
+ "application/vnd.ms-sstr+xml",
+ )
+
+ fun fromMediaStoreValue(value: Int) = entries.first {
+ value == it.mediaStoreValue
+ }
+
+ fun fromMimeType(mimeType: String) = when {
+ mimeType.startsWith("image/") -> IMAGE
+ mimeType.startsWith("video/") -> VIDEO
+ mimeType in dashMimeTypes -> VIDEO
+ mimeType in hlsMimeTypes -> VIDEO
+ mimeType in smoothStreamingMimeTypes -> VIDEO
+ else -> null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaVersion.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaVersion.kt
new file mode 100644
index 000000000..64314ffc6
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaVersion.kt
@@ -0,0 +1,8 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.room.Entity
+
+@Entity(tableName = "media_version", primaryKeys = ["version"])
+data class MediaVersion(
+ val version: String
+)
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/TimelineSettings.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/TimelineSettings.kt
new file mode 100644
index 000000000..fde681e64
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/TimelineSettings.kt
@@ -0,0 +1,29 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.dot.gallery.feature_node.domain.util.MediaOrder
+import com.dot.gallery.feature_node.domain.util.OrderType
+
+/**
+ * Timeline settings
+ *
+ * This entity contains all settings of the app that
+ * affects the media display.
+ */
+@Stable
+@Entity(tableName = "timeline_settings")
+data class TimelineSettings(
+ @PrimaryKey(autoGenerate = true)
+ val id: Int = 0,
+ @ColumnInfo(defaultValue = "0")
+ val groupTimelineByMonth: Boolean = false,
+ @ColumnInfo(defaultValue = "0")
+ val groupTimelineInAlbums: Boolean = false,
+ @ColumnInfo(defaultValue = "{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}")
+ val timelineMediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending),
+ @ColumnInfo(defaultValue = "{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}")
+ val albumMediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending),
+)
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/VaultState.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/VaultState.kt
new file mode 100644
index 000000000..f2a2103bf
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/VaultState.kt
@@ -0,0 +1,19 @@
+package com.dot.gallery.feature_node.domain.model
+
+import androidx.compose.runtime.Stable
+import com.dot.gallery.feature_node.presentation.vault.VaultScreens
+
+@Stable
+data class VaultState(
+ val vaults: List = emptyList(),
+ val isLoading: Boolean = true
+) {
+
+ fun getStartScreen(): String {
+ return (if (isLoading) VaultScreens.LoadingScreen else if (vaults.isEmpty()) {
+ VaultScreens.VaultSetup
+ } else {
+ VaultScreens.VaultDisplay
+ }).invoke()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/repository/MediaRepository.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/repository/MediaRepository.kt
index d6b6c953a..08776055b 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/repository/MediaRepository.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/repository/MediaRepository.kt
@@ -11,11 +11,12 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import com.dot.gallery.core.Resource
import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
import com.dot.gallery.feature_node.domain.model.EncryptedMedia
import com.dot.gallery.feature_node.domain.model.ExifAttributes
+import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
import com.dot.gallery.feature_node.domain.model.Media
import com.dot.gallery.feature_node.domain.model.PinnedAlbum
+import com.dot.gallery.feature_node.domain.model.TimelineSettings
import com.dot.gallery.feature_node.domain.model.Vault
import com.dot.gallery.feature_node.domain.util.MediaOrder
import com.dot.gallery.feature_node.presentation.picker.AllowedMedia
@@ -24,6 +25,8 @@ import java.io.File
interface MediaRepository {
+ suspend fun updateInternalDatabase()
+
fun getMedia(): Flow>>
fun getMediaByType(allowedMedia: AllowedMedia): Flow>>
@@ -32,23 +35,20 @@ interface MediaRepository {
fun getTrashed(): Flow>>
- fun getAlbums(
- mediaOrder: MediaOrder,
- ignoreBlacklisted: Boolean = false
- ): Flow>>
+ fun getAlbums(mediaOrder: MediaOrder): Flow>>
suspend fun insertPinnedAlbum(pinnedAlbum: PinnedAlbum)
suspend fun removePinnedAlbum(pinnedAlbum: PinnedAlbum)
+ fun getPinnedAlbums(): Flow>
+
suspend fun addBlacklistedAlbum(ignoredAlbum: IgnoredAlbum)
suspend fun removeBlacklistedAlbum(ignoredAlbum: IgnoredAlbum)
fun getBlacklistedAlbums(): Flow>
- suspend fun getMediaById(mediaId: Long): Media?
-
fun getMediaByAlbumId(albumId: Long): Flow>>
fun getMediaByAlbumIdWithType(
@@ -58,8 +58,6 @@ interface MediaRepository {
fun getAlbumsWithType(allowedMedia: AllowedMedia): Flow>>
- fun getMediaByUri(uriAsString: String, isSecure: Boolean): Flow>>
-
fun getMediaListByUris(listOfUris: List, reviewMode: Boolean): Flow>>
suspend fun toggleFavorite(
@@ -144,4 +142,8 @@ interface MediaRepository {
onFailed: (failedFiles: List) -> Unit
): Boolean
+ fun getSettings(): Flow
+
+ suspend fun updateSettings(settings: TimelineSettings)
+
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/BlacklistUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/BlacklistUseCase.kt
deleted file mode 100644
index 691463773..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/BlacklistUseCase.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-
-class BlacklistUseCase(
- private val repository: MediaRepository
-) {
-
- suspend fun addToBlacklist(ignoredAlbum: IgnoredAlbum) =
- repository.addBlacklistedAlbum(ignoredAlbum)
-
- suspend fun removeFromBlacklist(ignoredAlbum: IgnoredAlbum) =
- repository.removeBlacklistedAlbum(ignoredAlbum)
-
- val blacklistedAlbums by lazy { repository.getBlacklistedAlbums() }
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/DeletePinnedAlbumUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/DeletePinnedAlbumUseCase.kt
deleted file mode 100644
index 5aa14754e..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/DeletePinnedAlbumUseCase.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.model.PinnedAlbum
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-
-class DeletePinnedAlbumUseCase(
- private val repository: MediaRepository
-) {
-
- suspend operator fun invoke(
- album: Album
- ) = repository.removePinnedAlbum(PinnedAlbum(album.id))
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetAlbumsUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetAlbumsUseCase.kt
deleted file mode 100644
index db287640e..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetAlbumsUseCase.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import kotlinx.coroutines.flow.Flow
-
-class GetAlbumsUseCase(
- private val repository: MediaRepository
-) {
-
- operator fun invoke(
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending),
- ignoreBlacklisted: Boolean = false
- ): Flow>> = repository.getAlbums(mediaOrder, ignoreBlacklisted)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetAlbumsWithTypeUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetAlbumsWithTypeUseCase.kt
deleted file mode 100644
index b2933f881..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetAlbumsWithTypeUseCase.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import com.dot.gallery.feature_node.presentation.picker.AllowedMedia
-import kotlinx.coroutines.flow.Flow
-
-class GetAlbumsWithTypeUseCase(
- private val repository: MediaRepository
-) {
-
- operator fun invoke(
- allowedMedia: AllowedMedia
- ): Flow>> = repository.getAlbumsWithType(allowedMedia)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByAlbumUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByAlbumUseCase.kt
deleted file mode 100644
index 41a441f94..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByAlbumUseCase.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-class GetMediaByAlbumUseCase(
- private val repository: MediaRepository
-) {
- operator fun invoke(
- albumId: Long,
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)
- ): Flow>> {
- return repository.getMediaByAlbumId(albumId).map {
- it.apply {
- data = data?.let { it1 -> mediaOrder.sortMedia(it1) }
- }
- }
- }
-
-}
-
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByAlbumWithTypeUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByAlbumWithTypeUseCase.kt
deleted file mode 100644
index 646513dfb..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByAlbumWithTypeUseCase.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import com.dot.gallery.feature_node.presentation.picker.AllowedMedia
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-class GetMediaByAlbumWithTypeUseCase(
- private val repository: MediaRepository
-) {
- operator fun invoke(
- albumId: Long,
- type: AllowedMedia,
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)
- ): Flow>> {
- return repository.getMediaByAlbumIdWithType(albumId, type).map {
- it.apply {
- data = data?.let { it1 -> mediaOrder.sortMedia(it1) }
- }
- }
- }
-
-}
-
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByTypeUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByTypeUseCase.kt
deleted file mode 100644
index ef1b77a2e..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByTypeUseCase.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import com.dot.gallery.feature_node.presentation.picker.AllowedMedia
-import kotlinx.coroutines.flow.Flow
-
-class GetMediaByTypeUseCase(
- private val repository: MediaRepository
-) {
- operator fun invoke(type: AllowedMedia): Flow>> = repository.getMediaByType(type)
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByUriUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByUriUseCase.kt
deleted file mode 100644
index b4baad4c9..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaByUriUseCase.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import kotlinx.coroutines.flow.Flow
-
-class GetMediaByUriUseCase(
- private val repository: MediaRepository
-) {
- operator fun invoke(
- uriAsString: String,
- isSecure: Boolean = false
- ): Flow>> {
- return repository.getMediaByUri(uriAsString, isSecure)
- }
-
-}
-
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaFavoriteUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaFavoriteUseCase.kt
deleted file mode 100644
index 1363721bc..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaFavoriteUseCase.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import com.dot.gallery.feature_node.domain.util.MediaOrder
-import com.dot.gallery.feature_node.domain.util.OrderType
-import kotlinx.coroutines.flow.Flow
-
-class GetMediaFavoriteUseCase(
- private val repository: MediaRepository
-) {
-
- operator fun invoke(
- mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)
- ): Flow>> = repository.getFavorites(mediaOrder)
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaListByUrisUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaListByUrisUseCase.kt
deleted file mode 100644
index d6c560bfa..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaListByUrisUseCase.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import android.net.Uri
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import kotlinx.coroutines.flow.Flow
-
-class GetMediaListByUrisUseCase(
- private val repository: MediaRepository
-) {
- operator fun invoke(
- listOfUris: List,
- reviewMode: Boolean
- ): Flow>> {
- return repository.getMediaListByUris(listOfUris, reviewMode)
- }
-
-}
-
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaTrashedUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaTrashedUseCase.kt
deleted file mode 100644
index d59ae82bb..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaTrashedUseCase.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import kotlinx.coroutines.flow.Flow
-
-class GetMediaTrashedUseCase(
- private val repository: MediaRepository
-) {
-
- operator fun invoke(): Flow>> = repository.getTrashed()
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaUseCase.kt
deleted file mode 100644
index 220b0a133..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/GetMediaUseCase.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.core.Resource
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import kotlinx.coroutines.flow.Flow
-
-class GetMediaUseCase(
- private val repository: MediaRepository
-) {
-
- operator fun invoke(): Flow>> = repository.getMedia()
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/InsertPinnedAlbumUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/InsertPinnedAlbumUseCase.kt
deleted file mode 100644
index 051f80696..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/InsertPinnedAlbumUseCase.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.model.PinnedAlbum
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-
-class InsertPinnedAlbumUseCase(
- private val repository: MediaRepository
-) {
-
- suspend operator fun invoke(
- album: Album
- ) = repository.insertPinnedAlbum(PinnedAlbum(album.id))
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaHandleUseCase.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaHandleUseCase.kt
index f972048ad..bea07b4b9 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaHandleUseCase.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaHandleUseCase.kt
@@ -16,7 +16,7 @@ import com.dot.gallery.feature_node.domain.model.Media
import com.dot.gallery.feature_node.domain.repository.MediaRepository
import com.dot.gallery.feature_node.presentation.util.mediaPair
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
class MediaHandleUseCase(
@@ -48,25 +48,22 @@ class MediaHandleUseCase(
result: ActivityResultLauncher,
mediaList: List,
trash: Boolean = true
- ) {
- withContext(Dispatchers.Default) {
- getTrashEnabled(context).collectLatest { isTrashEnabled ->
- /**
- * Trash media only if user enabled the Trash Can
- * Or if user wants to remove existing items from the trash
- * */
- if ((isTrashEnabled || !trash)) {
- val pair = mediaList.mediaPair()
- if (pair.first.isNotEmpty()) {
- repository.trashMedia(result, mediaList, trash)
- }
- if (pair.second.isNotEmpty()) {
- repository.deleteMedia(result, mediaList)
- }
- } else {
- repository.deleteMedia(result, mediaList)
- }
+ ) = withContext(Dispatchers.Default) {
+ val isTrashEnabled = getTrashEnabled(context).firstOrNull() ?: true
+ /**
+ * Trash media only if user enabled the Trash Can
+ * Or if user wants to remove existing items from the trash
+ * */
+ if ((isTrashEnabled || !trash)) {
+ val pair = mediaList.mediaPair()
+ if (pair.first.isNotEmpty()) {
+ repository.trashMedia(result, mediaList, trash)
+ }
+ if (pair.second.isNotEmpty()) {
+ repository.deleteMedia(result, mediaList)
}
+ } else {
+ repository.deleteMedia(result, mediaList)
}
}
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaUseCases.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaUseCases.kt
deleted file mode 100644
index ad69f415d..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/MediaUseCases.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 IacobIacob01
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.dot.gallery.feature_node.domain.use_case
-
-import android.content.Context
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-
-data class MediaUseCases(
- private val context: Context,
- private val repository: MediaRepository
-) {
- val getAlbumsUseCase = GetAlbumsUseCase(repository)
- val getAlbumsWithTypeUseCase = GetAlbumsWithTypeUseCase(repository)
- val getMediaUseCase = GetMediaUseCase(repository)
- val getMediaByAlbumUseCase = GetMediaByAlbumUseCase(repository)
- val getMediaByAlbumWithTypeUseCase = GetMediaByAlbumWithTypeUseCase(repository)
- val getMediaFavoriteUseCase = GetMediaFavoriteUseCase(repository)
- val getMediaTrashedUseCase = GetMediaTrashedUseCase(repository)
- val getMediaByTypeUseCase = GetMediaByTypeUseCase(repository)
- val getMediaByUriUseCase = GetMediaByUriUseCase(repository)
- val getMediaListByUrisUseCase = GetMediaListByUrisUseCase(repository)
- val mediaHandleUseCase = MediaHandleUseCase(repository, context)
- val insertPinnedAlbumUseCase = InsertPinnedAlbumUseCase(repository)
- val deletePinnedAlbumUseCase = DeletePinnedAlbumUseCase(repository)
- val blacklistUseCase = BlacklistUseCase(repository)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/VaultUseCases.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/VaultUseCases.kt
deleted file mode 100644
index ced383d98..000000000
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/use_case/VaultUseCases.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.dot.gallery.feature_node.domain.use_case
-
-import com.dot.gallery.feature_node.domain.model.EncryptedMedia
-import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.model.Vault
-import com.dot.gallery.feature_node.domain.repository.MediaRepository
-import kotlinx.coroutines.flow.map
-import java.io.File
-
-class VaultUseCases(
- private val repository: MediaRepository
-) {
-
- fun getVaults() = repository.getVaults().map { it.data ?: emptyList() }
-
- suspend fun createVault(
- vault: Vault,
- onSuccess: () -> Unit,
- onFailed: (reason: String) -> Unit
- ) = repository.createVault(vault, onSuccess, onFailed)
-
- suspend fun deleteVault(
- vault: Vault,
- onSuccess: () -> Unit,
- onFailed: (reason: String) -> Unit
- ) = repository.deleteVault(vault, onSuccess, onFailed)
-
- fun getEncryptedMedia(vault: Vault) =
- repository.getEncryptedMedia(vault)
-
- suspend fun addMedia(vault: Vault, media: Media) =
- repository.addMedia(vault, media)
-
- suspend fun restoreMedia(vault: Vault, media: EncryptedMedia) =
- repository.restoreMedia(vault, media)
-
- suspend fun deleteEncryptedMedia(vault: Vault, media: EncryptedMedia) =
- repository.deleteEncryptedMedia(vault, media)
-
- suspend fun deleteAllEncryptedMedia(
- vault: Vault,
- onSuccess: () -> Unit,
- onFailed: (reason: List) -> Unit
- ) = repository.deleteAllEncryptedMedia(vault, onSuccess, onFailed)
-
-}
-
-
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/Converters.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/Converters.kt
index 5df667ae1..ab3a39082 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/Converters.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/Converters.kt
@@ -1,13 +1,26 @@
package com.dot.gallery.feature_node.domain.util
+import android.net.Uri
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
object Converters {
@TypeConverter
- fun fromString(value: String?): List = Json.decodeFromString(value ?: "[]")
+ fun toString(value: String?): List = Json.decodeFromString(value ?: "[]")
@TypeConverter
fun fromList(list: List?): String = Json.encodeToString(list ?: emptyList())
+
+ @TypeConverter
+ fun toUri(value: String): Uri = Uri.parse(value)
+
+ @TypeConverter
+ fun fromUri(uri: Uri): String = uri.toString()
+
+ @TypeConverter
+ fun toMediaOrder(value: String): MediaOrder = Json.decodeFromString(value)
+
+ @TypeConverter
+ fun fromMediaOrder(mediaOrder: MediaOrder): String = Json.encodeToString(mediaOrder)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/MediaOrder.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/MediaOrder.kt
index ec29f5d74..d0cab2214 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/MediaOrder.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/MediaOrder.kt
@@ -7,28 +7,29 @@ package com.dot.gallery.feature_node.domain.util
import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.domain.model.Media
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
-sealed class MediaOrder(val orderType: OrderType) {
- class Label(orderType: OrderType) : MediaOrder(orderType)
- class Date(orderType: OrderType) : MediaOrder(orderType)
- class Expiry(orderType: OrderType = OrderType.Descending): MediaOrder(orderType)
+@Serializable
+sealed class MediaOrder(open val orderType: OrderType) {
+ @Serializable
+ data class Label(
+ @SerialName("orderType_label")
+ override val orderType: OrderType
+ ) : MediaOrder(orderType)
- fun flipOrder(): MediaOrder {
- return this.copy(
- orderType = when (orderType) {
- OrderType.Ascending -> OrderType.Descending
- OrderType.Descending -> OrderType.Ascending
- }
- )
- }
+ @Serializable
+ data class Date(
+ @SerialName("orderType_date")
+ override val orderType: OrderType
+ ) : MediaOrder(orderType)
- fun copy(orderType: OrderType): MediaOrder {
- return when (this) {
- is Date -> Date(orderType)
- is Label -> Label(orderType)
- is Expiry -> Expiry(orderType)
- }
- }
+
+ @Serializable
+ data class Expiry(
+ @SerialName("orderType_expiry")
+ override val orderType: OrderType = OrderType.Descending
+ ): MediaOrder(orderType)
fun sortMedia(media: List): List {
return when (orderType) {
@@ -69,4 +70,8 @@ sealed class MediaOrder(val orderType: OrderType) {
}
}
}
+
+ companion object {
+ val Default = Date(OrderType.Descending)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsScreen.kt
index 85774c577..901bc93c9 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsScreen.kt
@@ -5,6 +5,9 @@
package com.dot.gallery.feature_node.presentation.albums
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.IntentSenderRequest
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -24,45 +27,55 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.dokar.pinchzoomgrid.PinchZoomGridLayout
import com.dokar.pinchzoomgrid.rememberPinchZoomGridState
import com.dot.gallery.R
+import com.dot.gallery.feature_node.domain.model.AlbumState
+import com.dot.gallery.core.Constants.Animation.enterAnimation
+import com.dot.gallery.core.Constants.Animation.exitAnimation
import com.dot.gallery.core.Constants.albumCellsList
+import com.dot.gallery.feature_node.domain.model.MediaState
import com.dot.gallery.core.Settings.Album.rememberAlbumGridSize
import com.dot.gallery.core.Settings.Album.rememberLastSort
-import com.dot.gallery.core.presentation.components.EmptyMedia
+import com.dot.gallery.core.presentation.components.EmptyAlbum
import com.dot.gallery.core.presentation.components.Error
import com.dot.gallery.core.presentation.components.FilterButton
import com.dot.gallery.core.presentation.components.FilterKind
+import com.dot.gallery.core.presentation.components.FilterOption
+import com.dot.gallery.core.presentation.components.LoadingAlbum
+import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.domain.util.MediaOrder
import com.dot.gallery.feature_node.presentation.albums.components.AlbumComponent
import com.dot.gallery.feature_node.presentation.albums.components.CarouselPinnedAlbums
-import com.dot.gallery.feature_node.presentation.common.MediaViewModel
import com.dot.gallery.feature_node.presentation.search.MainSearchBar
import com.dot.gallery.feature_node.presentation.util.Screen
+import com.dot.gallery.feature_node.presentation.util.rememberActivityResult
@Composable
fun AlbumsScreen(
navigate: (route: String) -> Unit,
- mediaViewModel: MediaViewModel,
toggleNavbar: (Boolean) -> Unit,
+ mediaState: State,
+ albumsState: State,
paddingValues: PaddingValues,
- viewModel: AlbumsViewModel,
+ filterOptions: SnapshotStateList,
isScrolling: MutableState,
- searchBarActive: MutableState
+ searchBarActive: MutableState,
+ onAlbumClick: (Album) -> Unit,
+ onAlbumLongClick: (Album) -> Unit,
+ onMoveAlbumToTrash: (ActivityResultLauncher, Album) -> Unit
) {
- val mediaState by mediaViewModel.mediaState.collectAsStateWithLifecycle()
- val state by viewModel.unPinnedAlbumsState.collectAsStateWithLifecycle()
- val pinnedState by viewModel.pinnedAlbumState.collectAsStateWithLifecycle()
- val filterOptions = viewModel.rememberFilters()
var lastCellIndex by rememberAlbumGridSize()
val pinchState = rememberPinchZoomGridState(
@@ -85,10 +98,11 @@ fun AlbumsScreen(
)
}
+ var finalPaddingValues by remember(paddingValues) { mutableStateOf(paddingValues) }
+
Scaffold(
topBar = {
MainSearchBar(
- mediaViewModel = mediaViewModel,
bottomPadding = paddingValues.calculateBottomPadding(),
navigate = navigate,
toggleNavbar = toggleNavbar,
@@ -103,7 +117,13 @@ fun AlbumsScreen(
}
}
}
- ) {
+ ) { innerPaddingValues ->
+ LaunchedEffect(innerPaddingValues) {
+ finalPaddingValues = PaddingValues(
+ top = innerPaddingValues.calculateTopPadding(),
+ bottom = paddingValues.calculateBottomPadding() + 16.dp + 64.dp
+ )
+ }
PinchZoomGridLayout(state = pinchState) {
LaunchedEffect(gridState.isScrollInProgress) {
isScrolling.value = gridState.isScrollInProgress
@@ -114,17 +134,18 @@ fun AlbumsScreen(
.padding(horizontal = 8.dp)
.fillMaxSize(),
columns = gridCells,
- contentPadding = PaddingValues(
- top = it.calculateTopPadding(),
- bottom = paddingValues.calculateBottomPadding() + 16.dp + 64.dp
- ),
+ contentPadding = finalPaddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
- if (pinnedState.albums.isNotEmpty()) {
- item(
- span = { GridItemSpan(maxLineSpan) },
- key = "pinnedAlbums"
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "pinnedAlbums"
+ ) {
+ AnimatedVisibility(
+ visible = albumsState.value.albumsPinned.isNotEmpty(),
+ enter = enterAnimation,
+ exit = exitAnimation
) {
Column {
Text(
@@ -137,17 +158,21 @@ fun AlbumsScreen(
fontWeight = FontWeight.Medium
)
CarouselPinnedAlbums(
- albumList = pinnedState.albums,
- onAlbumClick = viewModel.onAlbumClick(navigate),
- onAlbumLongClick = viewModel.onAlbumLongClick
+ albumList = albumsState.value.albumsPinned,
+ onAlbumClick = onAlbumClick,
+ onAlbumLongClick = onAlbumLongClick
)
}
}
}
- if (state.albums.isNotEmpty()) {
- item(
- span = { GridItemSpan(maxLineSpan) },
- key = "filterButton"
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "filterButton"
+ ) {
+ AnimatedVisibility(
+ visible = albumsState.value.albumsUnpinned.isNotEmpty(),
+ enter = enterAnimation,
+ exit = exitAnimation
) {
FilterButton(
modifier = Modifier.pinchItem(key = "filterButton"),
@@ -156,21 +181,29 @@ fun AlbumsScreen(
}
}
items(
- items = state.albums,
+ items = albumsState.value.albumsUnpinned,
key = { item -> item.toString() }
) { item ->
+ val trashResult = rememberActivityResult()
AlbumComponent(
modifier = Modifier.pinchItem(key = item.toString()),
album = item,
- onItemClick = viewModel.onAlbumClick(navigate),
- onTogglePinClick = viewModel.onAlbumLongClick
+ onItemClick = onAlbumClick,
+ onTogglePinClick = onAlbumLongClick,
+ onMoveAlbumToTrash = {
+ onMoveAlbumToTrash(trashResult, it)
+ }
)
}
- if (state.albums.isNotEmpty()) {
- item(
- span = { GridItemSpan(maxLineSpan) },
- key = "albumDetails"
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "albumDetails"
+ ) {
+ AnimatedVisibility(
+ visible = mediaState.value.media.isNotEmpty() && albumsState.value.albums.isNotEmpty(),
+ enter = enterAnimation,
+ exit = exitAnimation
) {
Text(
modifier = Modifier
@@ -178,21 +211,52 @@ fun AlbumsScreen(
.pinchItem(key = "albumDetails")
.padding(horizontal = 8.dp)
.padding(vertical = 24.dp),
- text = stringResource(R.string.images_videos, mediaState.media.size),
+ text = stringResource(
+ R.string.images_videos,
+ mediaState.value.media.size
+ ),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center
)
}
}
+
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "emptyAlbums"
+ ) {
+ AnimatedVisibility(
+ visible = albumsState.value.albums.isEmpty() && albumsState.value.error.isEmpty(),
+ enter = enterAnimation,
+ exit = exitAnimation
+ ) {
+ EmptyAlbum()
+ }
+ }
+
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "loadingAlbums"
+ ) {
+ AnimatedVisibility(
+ visible = albumsState.value.isLoading,
+ enter = enterAnimation,
+ exit = exitAnimation
+ ) {
+ LoadingAlbum()
+ }
+ }
}
}
- /** Error State Handling Block **/
- if (state.error.isNotEmpty()) {
- Error(errorMessage = state.error)
- } else if (state.albums.isEmpty() && pinnedState.albums.isEmpty()) {
- EmptyMedia(modifier = Modifier.fillMaxSize())
- }
- /** ************ **/
}
+ /** Error State Handling Block **/
+ AnimatedVisibility(
+ visible = albumsState.value.error.isNotEmpty(),
+ enter = enterAnimation,
+ exit = exitAnimation
+ ) {
+ Error(errorMessage = albumsState.value.error)
+ }
+ /** ************ **/
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsViewModel.kt
index 4423b59eb..e76509da9 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsViewModel.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/AlbumsViewModel.kt
@@ -5,9 +5,9 @@
package com.dot.gallery.feature_node.presentation.albums
-import android.annotation.SuppressLint
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.IntentSenderRequest
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
@@ -15,39 +15,35 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Resource
import com.dot.gallery.core.Settings
import com.dot.gallery.core.presentation.components.FilterKind
import com.dot.gallery.core.presentation.components.FilterOption
import com.dot.gallery.feature_node.domain.model.Album
-import com.dot.gallery.feature_node.domain.use_case.MediaUseCases
+import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
+import com.dot.gallery.feature_node.domain.model.PinnedAlbum
+import com.dot.gallery.feature_node.domain.model.TimelineSettings
+import com.dot.gallery.feature_node.domain.repository.MediaRepository
+import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.domain.util.MediaOrder
import com.dot.gallery.feature_node.domain.util.OrderType
import com.dot.gallery.feature_node.presentation.util.Screen
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AlbumsViewModel @Inject constructor(
- private val mediaUseCases: MediaUseCases
+ private val repository: MediaRepository,
+ val handler: MediaHandleUseCase
) : ViewModel() {
- private val _albumsState = MutableStateFlow(AlbumState())
- val albumsState = _albumsState.asStateFlow()
- private val _unfilteredAlbums = MutableStateFlow(AlbumState())
- val unfilteredAlbums = _unfilteredAlbums.asStateFlow()
- private val _unPinnedAlbumsState = MutableStateFlow(AlbumState())
- val unPinnedAlbumsState = _unPinnedAlbumsState.asStateFlow()
- private val _pinnedAlbumState = MutableStateFlow(AlbumState())
- val pinnedAlbumState = _pinnedAlbumState.asStateFlow()
- val handler = mediaUseCases.mediaHandleUseCase
-
fun onAlbumClick(navigate: (String) -> Unit): (Album) -> Unit = { album ->
navigate(Screen.AlbumViewScreen.route + "?albumId=${album.id}&albumName=${album.label}")
}
@@ -56,11 +52,11 @@ class AlbumsViewModel @Inject constructor(
toggleAlbumPin(album, !album.isPinned)
}
- @SuppressLint("ComposableNaming")
- @Composable
- fun attachToLifecycle() {
- LaunchedEffect(Unit) {
- getAlbums()
+ fun moveAlbumToTrash(result: ActivityResultLauncher, album: Album) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val response = repository.getMediaByAlbumId(album.id).firstOrNull()
+ val data = response?.data ?: emptyList()
+ repository.trashMedia(result, data, true)
}
}
@@ -72,100 +68,73 @@ class AlbumsViewModel @Inject constructor(
FilterOption(
titleRes = R.string.filter_type_date,
filterKind = FilterKind.DATE,
- onClick = { updateOrder(it) }
+ onClick = { albumOrder = it }
),
FilterOption(
titleRes = R.string.filter_type_name,
filterKind = FilterKind.NAME,
- onClick = { updateOrder(it) }
+ onClick = { albumOrder = it }
)
)
}
}
- private fun updateOrder(mediaOrder: MediaOrder) {
- viewModelScope.launch(Dispatchers.IO) {
- val newState = unPinnedAlbumsState.value.copy(
- albums = mediaOrder.sortAlbums(unPinnedAlbumsState.value.albums)
- )
- if (unPinnedAlbumsState.value != newState) {
- _unPinnedAlbumsState.emit(newState)
- }
- }
- }
-
private fun toggleAlbumPin(album: Album, isPinned: Boolean = true) {
viewModelScope.launch(Dispatchers.IO) {
- val newAlbum = album.copy(isPinned = isPinned)
if (isPinned) {
- // Insert pinnedAlbumId to Database
- mediaUseCases.insertPinnedAlbumUseCase(newAlbum)
- // Remove original Album from unpinned List
- _unPinnedAlbumsState.emit(
- unPinnedAlbumsState.value.copy(
- albums = unPinnedAlbumsState.value.albums.minus(album)
- )
- )
- // Add 'pinned' version of the album object to the pinned List
- _pinnedAlbumState.emit(pinnedAlbumState.value.copy(
- albums = pinnedAlbumState.value.albums.toMutableList().apply { add(newAlbum) }
- ))
+ repository.insertPinnedAlbum(PinnedAlbum(album.id))
} else {
- // Delete pinnedAlbumId from Database
- mediaUseCases.deletePinnedAlbumUseCase(album)
- // Add 'un-pinned' version of the album object to the pinned List
- _unPinnedAlbumsState.emit(unPinnedAlbumsState.value.copy(
- albums = unPinnedAlbumsState.value.albums.toMutableList().apply { add(newAlbum) }
- ))
- // Remove original Album from pinned List
- _pinnedAlbumState.emit(
- pinnedAlbumState.value.copy(
- albums = pinnedAlbumState.value.albums.minus(album)
- )
- )
+ repository.removePinnedAlbum(PinnedAlbum(album.id))
}
}
}
- private fun getAlbums(mediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending)) {
- viewModelScope.launch(Dispatchers.IO) {
- mediaUseCases.getAlbumsUseCase(mediaOrder).collectLatest { result ->
- // Result data list
- val data = result.data ?: emptyList()
- val error =
- if (result is Resource.Error) result.message ?: "An error occurred" else ""
- val newAlbumState = AlbumState(error = error, albums = data)
- if (data == albumsState.value.albums) return@collectLatest
- val newUnPinnedAlbumState = AlbumState(error = error, albums = data.filter { !it.isPinned })
- val newPinnedState = AlbumState(
- error = error,
- albums = data.filter { it.isPinned }.sortedBy { it.label })
- if (unPinnedAlbumsState.value != newUnPinnedAlbumState) {
- _unPinnedAlbumsState.emit(newUnPinnedAlbumState)
- }
- if (pinnedAlbumState.value != newPinnedState) {
- _pinnedAlbumState.emit(newPinnedState)
- }
- if (albumsState.value != newAlbumState) {
- _albumsState.emit(newAlbumState)
- }
- if (unfilteredAlbums.value != newAlbumState) {
- _unfilteredAlbums.emit(newAlbumState)
+ private val settingsFlow = repository.getSettings()
+ .stateIn(viewModelScope, started = SharingStarted.Eagerly, TimelineSettings())
+
+ private val pinnedAlbums = repository.getPinnedAlbums()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ private val blacklistedAlbums = repository.getBlacklistedAlbums()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+
+ private var albumOrder: MediaOrder
+ get() = settingsFlow.value?.albumMediaOrder ?: MediaOrder.Date(OrderType.Descending)
+ set(value) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsFlow.value?.copy(albumMediaOrder = value)?.let {
+ repository.updateSettings(it)
}
}
}
- viewModelScope.launch(Dispatchers.IO) {
- mediaUseCases.getAlbumsUseCase(mediaOrder, ignoreBlacklisted = true).collectLatest { result ->
- val data = result.data ?: emptyList()
- val error =
- if (result is Resource.Error) result.message ?: "An error occurred" else ""
- val newAlbumState = AlbumState(error = error, albums = data)
- if (data == unfilteredAlbums.value.albums) return@collectLatest
- if (unfilteredAlbums.value != newAlbumState) {
- _unfilteredAlbums.emit(newAlbumState)
- }
- }
+
+ val albumsFlow = combine(
+ repository.getAlbums(mediaOrder = albumOrder),
+ pinnedAlbums,
+ blacklistedAlbums,
+ settingsFlow
+ ) { result, pinnedAlbums, blacklistedAlbums, settings ->
+ val newOrder = settings?.albumMediaOrder ?: albumOrder
+ val data = newOrder.sortAlbums(result.data ?: emptyList())
+ val cleanData = data.removeBlacklisted(blacklistedAlbums).mapPinned(pinnedAlbums)
+
+ AlbumState(
+ albums = cleanData,
+ albumsWithBlacklisted = data,
+ albumsUnpinned = cleanData.filter { !it.isPinned },
+ albumsPinned = cleanData.filter { it.isPinned }.sortedBy { it.label },
+ isLoading = false,
+ error = if (result is Resource.Error) result.message ?: "An error occurred" else ""
+ )
+ }.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), AlbumState())
+
+ private fun List.mapPinned(pinnedAlbums: List): List =
+ map { album -> album.copy(isPinned = pinnedAlbums.any { it.id == album.id }) }
+
+ private fun List.removeBlacklisted(blacklistedAlbums: List): List =
+ toMutableList().apply {
+ removeAll { album -> blacklistedAlbums.any { it.matchesAlbum(album) } }
}
- }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/components/AlbumComponent.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/components/AlbumComponent.kt
index c3fe97e0e..c74023163 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/components/AlbumComponent.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/albums/components/AlbumComponent.kt
@@ -44,16 +44,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import com.dot.gallery.R
-import com.dot.gallery.core.presentation.components.util.AutoResizeText
-import com.dot.gallery.core.presentation.components.util.FontSizeRange
import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.presentation.common.components.OptionItem
import com.dot.gallery.feature_node.presentation.common.components.OptionSheet
-import com.dot.gallery.feature_node.presentation.util.FeedbackManager.Companion.rememberFeedbackManager
import com.dot.gallery.feature_node.presentation.util.formatSize
import com.dot.gallery.feature_node.presentation.util.rememberAppBottomSheetState
+import com.dot.gallery.feature_node.presentation.util.rememberFeedbackManager
import com.dot.gallery.ui.theme.Shapes
import com.github.panpf.sketch.AsyncImage
import kotlinx.coroutines.launch
@@ -64,6 +61,7 @@ fun AlbumComponent(
album: Album,
isEnabled: Boolean = true,
onItemClick: (Album) -> Unit,
+ onMoveAlbumToTrash: ((Album) -> Unit)? = null,
onTogglePinClick: ((Album) -> Unit)? = null,
onToggleIgnoreClick: ((Album) -> Unit)? = null
) {
@@ -75,16 +73,31 @@ fun AlbumComponent(
.padding(horizontal = 8.dp),
) {
if (onTogglePinClick != null) {
+ val trashTitle = stringResource(R.string.move_album_to_trash)
val pinTitle = stringResource(R.string.pin)
val ignoredTitle = stringResource(id = R.string.add_to_ignored)
- val tertiaryContainer = MaterialTheme.colorScheme.tertiaryContainer
- val onTertiaryContainer = MaterialTheme.colorScheme.onTertiaryContainer
+ val secondaryContainer = MaterialTheme.colorScheme.secondaryContainer
+ val onSecondaryContainer = MaterialTheme.colorScheme.onSecondaryContainer
+ val primaryContainer = MaterialTheme.colorScheme.primaryContainer
+ val onPrimaryContainer = MaterialTheme.colorScheme.onPrimaryContainer
val optionList = remember {
mutableListOf(
+ OptionItem(
+ text = trashTitle,
+ containerColor = primaryContainer,
+ contentColor = onPrimaryContainer,
+ enabled = onMoveAlbumToTrash != null,
+ onClick = {
+ scope.launch {
+ appBottomSheetState.hide()
+ onMoveAlbumToTrash?.invoke(album)
+ }
+ }
+ ),
OptionItem(
text = pinTitle,
- containerColor = tertiaryContainer,
- contentColor = onTertiaryContainer,
+ containerColor = secondaryContainer,
+ contentColor = onSecondaryContainer,
onClick = {
scope.launch {
appBottomSheetState.hide()
@@ -143,7 +156,7 @@ fun AlbumComponent(
letterSpacing = MaterialTheme.typography.bodyMedium.letterSpacing
)
) {
- append(stringResource(R.string.s_items, album.count))
+ append(stringResource(R.string.s_items, album.count) + " (${formatSize(album.size)})")
}
},
textAlign = TextAlign.Center,
@@ -182,7 +195,7 @@ fun AlbumComponent(
)
}
}
- AutoResizeText(
+ Text(
modifier = Modifier
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
@@ -190,14 +203,10 @@ fun AlbumComponent(
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- fontSizeRange = FontSizeRange(
- min = 10.sp,
- max = 16.sp
- )
+ maxLines = 1
)
if (album.count > 0) {
- AutoResizeText(
+ Text(
modifier = Modifier
.padding(top = 2.dp, bottom = 16.dp)
.padding(horizontal = 16.dp),
@@ -209,10 +218,6 @@ fun AlbumComponent(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
- fontSizeRange = FontSizeRange(
- min = 6.sp,
- max = 12.sp
- )
)
}
@@ -241,7 +246,7 @@ fun AlbumImage(
.fillMaxSize()
.border(
width = 1.dp,
- color = MaterialTheme.colorScheme.outlineVariant,
+ color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
shape = RoundedCornerShape(cornerRadius)
)
.alpha(0.8f)
@@ -266,7 +271,7 @@ fun AlbumImage(
.fillMaxSize()
.border(
width = 1.dp,
- color = MaterialTheme.colorScheme.outlineVariant,
+ color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
shape = RoundedCornerShape(cornerRadius)
)
.clip(RoundedCornerShape(cornerRadius))
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/ChanneledViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/ChanneledViewModel.kt
index b32407119..afb7c6448 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/ChanneledViewModel.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/ChanneledViewModel.kt
@@ -45,6 +45,21 @@ class ChanneledViewModel @Inject constructor() : ViewModel() {
}
}
+ fun initWithNav(navController: NavController) =
+ eventChannel.receiveAsFlow().map {
+ when (it) {
+ is Event.NavigationRouteEvent ->
+ navController.navigate(it.route) {
+ launchSingleTop = true
+ restoreState = true
+ }
+ is Event.NavigationUpEvent ->
+ navController.navigateUp()
+
+ else -> {}
+ }
+ }
+
fun navigate(route: String) {
viewModelScope.launch(Dispatchers.IO) {
eventChannel.send(Event.NavigationRouteEvent(route))
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaScreen.kt
index 89133844f..fc62199a4 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaScreen.kt
@@ -8,7 +8,7 @@ package com.dot.gallery.feature_node.presentation.common
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
-import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
@@ -20,6 +20,7 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -29,20 +30,15 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.dokar.pinchzoomgrid.PinchZoomGridLayout
import com.dokar.pinchzoomgrid.rememberPinchZoomGridState
-import com.dot.gallery.core.AlbumState
-import com.dot.gallery.core.Constants.Animation.enterAnimation
-import com.dot.gallery.core.Constants.Animation.exitAnimation
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Constants.Target.TARGET_TRASH
import com.dot.gallery.core.Constants.cellsList
-import com.dot.gallery.core.MediaState
+import com.dot.gallery.feature_node.domain.model.MediaState
import com.dot.gallery.core.Settings.Misc.rememberGridSize
-import com.dot.gallery.core.presentation.components.Error
-import com.dot.gallery.core.presentation.components.LoadingMedia
+import com.dot.gallery.core.presentation.components.EmptyMedia
import com.dot.gallery.core.presentation.components.NavigationActions
import com.dot.gallery.core.presentation.components.NavigationButton
import com.dot.gallery.core.presentation.components.SelectionSheet
@@ -52,21 +48,19 @@ import com.dot.gallery.feature_node.presentation.common.components.MediaGridView
import com.dot.gallery.feature_node.presentation.common.components.TwoLinedDateToolbarTitle
import com.dot.gallery.feature_node.presentation.search.MainSearchBar
import com.dot.gallery.feature_node.presentation.util.Screen
-import kotlinx.coroutines.flow.StateFlow
@OptIn(
- ExperimentalMaterial3Api::class
+ ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class
)
@Composable
fun MediaScreen(
paddingValues: PaddingValues,
- albumId: Long = -1L,
- target: String? = null,
+ albumId: Long = remember { -1L },
+ target: String? = remember { null },
albumName: String,
- vm: MediaViewModel,
handler: MediaHandleUseCase,
- albumState: StateFlow,
- mediaState: StateFlow,
+ albumsState: State,
+ mediaState: State,
selectionState: MutableState,
selectedMedia: SnapshotStateList,
toggleSelection: (Int) -> Unit,
@@ -75,13 +69,13 @@ fun MediaScreen(
enableStickyHeaders: Boolean = true,
allowNavBar: Boolean = false,
navActionsContent: @Composable() (RowScope.(expandedDropDown: MutableState, result: ActivityResultLauncher) -> Unit),
- emptyContent: @Composable (PaddingValues) -> Unit,
- aboveGridContent: @Composable() (() -> Unit)? = null,
+ emptyContent: @Composable () -> Unit = { EmptyMedia() },
+ aboveGridContent: @Composable() (() -> Unit)? = remember { null },
navigate: (route: String) -> Unit,
navigateUp: () -> Unit,
toggleNavbar: (Boolean) -> Unit,
isScrolling: MutableState = remember { mutableStateOf(false) },
- searchBarActive: MutableState = mutableStateOf(false),
+ searchBarActive: MutableState = remember { mutableStateOf(false) },
onActivityResult: (result: ActivityResult) -> Unit,
) {
val showSearchBar = remember { albumId == -1L && target == null }
@@ -103,18 +97,12 @@ fun MediaScreen(
lastCellIndex = cellsList.indexOf(pinchState.currentCells)
}
- /** STATES BLOCK **/
- val state by mediaState.collectAsStateWithLifecycle()
- val albumsState by albumState.collectAsStateWithLifecycle()
- /** ************ **/
-
- /** Selection state handling **/
- LaunchedEffect(LocalConfiguration.current, selectionState.value) {
+ LaunchedEffect(selectionState.value) {
if (allowNavBar) {
toggleNavbar(!selectionState.value)
}
}
- /** ************ **/
+
Box {
Scaffold(
modifier = Modifier
@@ -129,7 +117,7 @@ fun MediaScreen(
title = {
TwoLinedDateToolbarTitle(
albumName = albumName,
- dateHeader = state.dateHeader
+ dateHeader = mediaState.value.dateHeader
)
},
navigationIcon = {
@@ -155,11 +143,12 @@ fun MediaScreen(
)
} else {
MainSearchBar(
- mediaViewModel = vm,
bottomPadding = paddingValues.calculateBottomPadding(),
navigate = navigate,
toggleNavbar = toggleNavbar,
- selectionState = if (selectedMedia.isNotEmpty()) selectionState else null,
+ selectionState = remember(selectedMedia) {
+ if (selectedMedia.isNotEmpty()) selectionState else null
+ },
isScrolling = isScrolling,
activeState = searchBarActive
) {
@@ -172,28 +161,20 @@ fun MediaScreen(
}
) { it ->
PinchZoomGridLayout(state = pinchState) {
- AnimatedVisibility(
- visible = state.isLoading,
- enter = enterAnimation,
- exit = exitAnimation
- ) {
- LoadingMedia(
- paddingValues = PaddingValues(
- top = it.calculateTopPadding(),
- bottom = paddingValues.calculateBottomPadding() + 16.dp + 64.dp
- )
- )
- }
MediaGridView(
- mediaState = state,
+ mediaState = mediaState,
allowSelection = true,
showSearchBar = showSearchBar,
- searchBarPaddingTop = paddingValues.calculateTopPadding(),
+ searchBarPaddingTop = remember(paddingValues) {
+ paddingValues.calculateTopPadding()
+ },
enableStickyHeaders = enableStickyHeaders,
- paddingValues = PaddingValues(
- top = it.calculateTopPadding(),
- bottom = paddingValues.calculateBottomPadding() + 16.dp + 64.dp
- ),
+ paddingValues = remember(paddingValues, it) {
+ PaddingValues(
+ top = it.calculateTopPadding(),
+ bottom = paddingValues.calculateBottomPadding() + 16.dp + 64.dp
+ )
+ },
canScroll = canScroll,
selectionState = selectionState,
selectedMedia = selectedMedia,
@@ -201,7 +182,8 @@ fun MediaScreen(
showMonthlyHeader = showMonthlyHeader,
toggleSelection = toggleSelection,
aboveGridContent = aboveGridContent,
- isScrolling = isScrolling
+ isScrolling = isScrolling,
+ emptyContent = emptyContent
) {
val albumRoute = "albumId=$albumId"
val targetRoute = "target=$target"
@@ -209,19 +191,6 @@ fun MediaScreen(
if (target != null) targetRoute else albumRoute
navigate(Screen.MediaViewScreen.route + "?mediaId=${it.id}&$param")
}
- /** Error State Handling Block **/
- val showError = remember(state) { state.error.isNotEmpty() }
- AnimatedVisibility(visible = showError) {
- Error(errorMessage = state.error)
- }
- val showEmpty = remember(state) { state.media.isEmpty() && !state.isLoading && !showError }
- AnimatedVisibility(visible = showEmpty) {
- emptyContent.invoke(PaddingValues(
- top = it.calculateTopPadding(),
- bottom = paddingValues.calculateBottomPadding() + 16.dp + 64.dp
- ))
- }
- /** ************ **/
}
}
if (target != TARGET_TRASH) {
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaViewModel.kt
index de6f90038..25cfb8aa3 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaViewModel.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/MediaViewModel.kt
@@ -5,7 +5,6 @@
package com.dot.gallery.feature_node.presentation.common
-import android.annotation.SuppressLint
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.compose.runtime.Composable
@@ -14,20 +13,29 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.dot.gallery.core.MediaState
import com.dot.gallery.core.Resource
import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.MediaState
+import com.dot.gallery.feature_node.domain.model.TimelineSettings
import com.dot.gallery.feature_node.domain.model.Vault
-import com.dot.gallery.feature_node.domain.use_case.MediaUseCases
-import com.dot.gallery.feature_node.domain.use_case.VaultUseCases
+import com.dot.gallery.feature_node.domain.model.VaultState
+import com.dot.gallery.feature_node.domain.repository.MediaRepository
+import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.util.collectMedia
+import com.dot.gallery.feature_node.presentation.util.mapMediaToItem
import com.dot.gallery.feature_node.presentation.util.mediaFlow
import com.dot.gallery.feature_node.presentation.util.update
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.xdrop.fuzzywuzzy.FuzzySearch
@@ -35,47 +43,96 @@ import javax.inject.Inject
@HiltViewModel
open class MediaViewModel @Inject constructor(
- private val mediaUseCases: MediaUseCases,
- private val vaultUseCases: VaultUseCases
+ private val repository: MediaRepository,
+ val handler: MediaHandleUseCase,
) : ViewModel() {
var lastQuery = mutableStateOf("")
- private set
val multiSelectState = mutableStateOf(false)
- private val _mediaState = MutableStateFlow(MediaState())
- val mediaState = _mediaState.asStateFlow()
- private val _customMediaState = MutableStateFlow(MediaState())
- val customMediaState = _customMediaState.asStateFlow()
private val _searchMediaState = MutableStateFlow(MediaState())
val searchMediaState = _searchMediaState.asStateFlow()
val selectedPhotoState = mutableStateListOf()
- val handler = mediaUseCases.mediaHandleUseCase
-
- private val _vaults = MutableStateFlow>(emptyList())
- val vaults = _vaults.asStateFlow()
var albumId: Long = -1L
var target: String? = null
- var groupByMonth: Boolean = false
+ var groupByMonth: Boolean
+ get() = settingsFlow.value?.groupTimelineByMonth ?: false
set(value) {
- field = value
- if (field != value) {
- getMedia(albumId, target)
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsFlow.value?.copy(groupTimelineByMonth = value)?.let {
+ repository.updateSettings(it)
+ }
}
}
- @SuppressLint("ComposableNaming")
+ private val settingsFlow = repository.getSettings()
+ .stateIn(
+ viewModelScope,
+ started = SharingStarted.Eagerly,
+ TimelineSettings()
+ )
+
+ private val blacklistedAlbums = repository.getBlacklistedAlbums()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ val mediaFlow by lazy {
+ combine(
+ repository.mediaFlow(albumId, target),
+ settingsFlow,
+ blacklistedAlbums,
+ ) { result, settings, blacklistedAlbums ->
+ if (result is Resource.Error) return@combine MediaState(
+ error = result.message ?: "",
+ isLoading = false
+ )
+ updateDatabase()
+ mapMediaToItem(
+ data = (result.data ?: emptyList()).toMutableList().apply {
+ removeAll { media -> blacklistedAlbums.any { it.matchesMedia(media) } }
+ },
+ error = result.message ?: "",
+ albumId = albumId,
+ groupByMonth = settings?.groupTimelineByMonth ?: false,
+ )
+ }.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), MediaState())
+ }
+
+ val vaultsFlow = repository.getVaults()
+ .map { it.data ?: emptyList() }
+ .map { VaultState(it, isLoading = false) }
+ .stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(), VaultState())
+
+ private sealed class Event {
+ data object UpdateDatabase : Event()
+ }
+
+ private val updater = Channel()
+
@Composable
- fun attachToLifecycle() {
+ fun CollectDatabaseUpdates() {
LaunchedEffect(Unit) {
- getMedia(albumId, target)
+ viewModelScope.launch(Dispatchers.IO) {
+ updater.receiveAsFlow().collectLatest {
+ when (it) {
+ is Event.UpdateDatabase -> {
+ repository.updateInternalDatabase()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateDatabase() {
+ viewModelScope.launch(Dispatchers.IO) {
+ updater.send(Event.UpdateDatabase)
}
}
fun addMedia(vault: Vault, media: Media) {
viewModelScope.launch(Dispatchers.IO) {
- vaultUseCases.addMedia(vault, media)
+ repository.addMedia(vault, media)
}
}
@@ -83,14 +140,6 @@ open class MediaViewModel @Inject constructor(
queryMedia("")
}
- private suspend fun List.parseQuery(query: String): List {
- return withContext(Dispatchers.IO) {
- if (query.isEmpty())
- return@withContext emptyList()
- val matches = FuzzySearch.extractSorted(query, this@parseQuery, { it.toString() }, 60)
- return@withContext matches.map { it.referent }.ifEmpty { emptyList() }
- }
- }
fun toggleFavorite(
result: ActivityResultLauncher,
@@ -102,22 +151,9 @@ open class MediaViewModel @Inject constructor(
}
}
- fun toggleCustomSelection(index: Int) {
- viewModelScope.launch(Dispatchers.IO) {
- val item = customMediaState.value.media[index]
- val selectedPhoto = selectedPhotoState.find { it.id == item.id }
- if (selectedPhoto != null) {
- selectedPhotoState.remove(selectedPhoto)
- } else {
- selectedPhotoState.add(item)
- }
- multiSelectState.update(selectedPhotoState.isNotEmpty())
- }
- }
-
fun toggleSelection(index: Int) {
viewModelScope.launch(Dispatchers.IO) {
- val item = mediaState.value.media[index]
+ val item = mediaFlow.value.media[index]
val selectedPhoto = selectedPhotoState.find { it.id == item.id }
if (selectedPhoto != null) {
selectedPhotoState.remove(selectedPhoto)
@@ -128,44 +164,6 @@ open class MediaViewModel @Inject constructor(
}
}
- fun getMediaFromAlbum(albumId: Long) {
- viewModelScope.launch(Dispatchers.IO) {
- val data = mediaState.value.media.filter { it.albumID == albumId }
- val error = mediaState.value.error
- if (error.isNotEmpty()) {
- return@launch _customMediaState.emit(MediaState(isLoading = false, error = error))
- }
- if (data.isEmpty()) {
- return@launch _customMediaState.emit(MediaState(isLoading = false))
- }
- _customMediaState.collectMedia(
- data = data,
- error = mediaState.value.error,
- albumId = albumId,
- groupByMonth = groupByMonth
- )
- }
- }
-
- fun getFavoriteMedia() {
- viewModelScope.launch(Dispatchers.IO) {
- val data = mediaState.value.media.filter { it.isFavorite }
- val error = mediaState.value.error
- if (error.isNotEmpty()) {
- return@launch _customMediaState.emit(MediaState(isLoading = false, error = error))
- }
- if (data.isEmpty()) {
- return@launch _customMediaState.emit(MediaState(isLoading = false))
- }
- _customMediaState.collectMedia(
- data = data,
- error = mediaState.value.error,
- albumId = -1L,
- groupByMonth = groupByMonth
- )
- }
- }
-
fun queryMedia(query: String) {
viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
@@ -176,9 +174,9 @@ open class MediaViewModel @Inject constructor(
return@launch
} else {
_searchMediaState.tryEmit(MediaState(isLoading = true))
- return@launch _searchMediaState.collectMedia(
- data = mediaState.value.media.parseQuery(query),
- error = mediaState.value.error,
+ _searchMediaState.collectMedia(
+ data = mediaFlow.value.media.parseQuery(query),
+ error = mediaFlow.value.error,
albumId = albumId,
groupByMonth = groupByMonth
)
@@ -186,32 +184,12 @@ open class MediaViewModel @Inject constructor(
}
}
- private fun getMedia(albumId: Long = -1L, target: String? = null) {
- viewModelScope.launch(Dispatchers.IO) {
- mediaUseCases.mediaFlow(albumId, target).collectLatest { result ->
- val data = result.data ?: emptyList()
- val error = if (result is Resource.Error) result.message
- ?: "An error occurred" else ""
- if (error.isNotEmpty()) {
- return@collectLatest _mediaState.emit(MediaState(isLoading = false, error = error))
- }
- if (data.isEmpty()) {
- return@collectLatest _mediaState.emit(MediaState(isLoading = false))
- }
- if (data == _mediaState.value.media) return@collectLatest
- return@collectLatest _mediaState.collectMedia(
- data = data,
- error = error,
- albumId = albumId,
- groupByMonth = groupByMonth,
- )
- }
- }
- viewModelScope.launch(Dispatchers.IO) {
- vaultUseCases.getVaults().collectLatest {
- _vaults.emit(it)
- }
+ private suspend fun List.parseQuery(query: String): List {
+ return withContext(Dispatchers.IO) {
+ if (query.isEmpty())
+ return@withContext emptyList()
+ val matches = FuzzySearch.extractSorted(query, this@parseQuery, { it.toString() }, 60)
+ return@withContext matches.map { it.referent }.ifEmpty { emptyList() }
}
}
-
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGrid.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGrid.kt
new file mode 100644
index 000000000..4a4877a03
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGrid.kt
@@ -0,0 +1,346 @@
+package com.dot.gallery.feature_node.presentation.common.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisallowComposableCalls
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.dokar.pinchzoomgrid.PinchZoomGridScope
+import com.dot.gallery.R
+import com.dot.gallery.core.Constants.Animation.enterAnimation
+import com.dot.gallery.core.Constants.Animation.exitAnimation
+import com.dot.gallery.feature_node.domain.model.MediaState
+import com.dot.gallery.core.presentation.components.Error
+import com.dot.gallery.core.presentation.components.LoadingMedia
+import com.dot.gallery.core.presentation.components.MediaItemHeader
+import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.MediaItem
+import com.dot.gallery.feature_node.domain.model.isBigHeaderKey
+import com.dot.gallery.feature_node.domain.model.isHeaderKey
+import com.dot.gallery.feature_node.domain.model.isImage
+import com.dot.gallery.feature_node.presentation.util.rememberFeedbackManager
+import com.dot.gallery.feature_node.presentation.util.update
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+@Composable
+fun PinchZoomGridScope.MediaGrid(
+ gridState: LazyGridState,
+ gridCells: GridCells,
+ mediaState: State,
+ mappedData: SnapshotStateList,
+ paddingValues: PaddingValues,
+ allowSelection: Boolean,
+ selectionState: MutableState,
+ selectedMedia: SnapshotStateList,
+ toggleSelection: @DisallowComposableCalls (Int) -> Unit,
+ canScroll: Boolean,
+ allowHeaders: Boolean,
+ aboveGridContent: @Composable (() -> Unit)?,
+ isScrolling: MutableState,
+ emptyContent: @Composable () -> Unit,
+ onMediaClick: @DisallowComposableCalls (media: Media) -> Unit
+) {
+ LaunchedEffect(gridState.isScrollInProgress) {
+ snapshotFlow {
+ gridState.isScrollInProgress
+ }.collectLatest {
+ isScrolling.value = it
+ }
+ }
+
+ val topContent: LazyGridScope.() -> Unit = remember(aboveGridContent) {
+ {
+ if (aboveGridContent != null) {
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "aboveGrid"
+ ) {
+ aboveGridContent.invoke()
+ }
+ }
+ }
+ }
+ val bottomContent: LazyGridScope.() -> Unit = remember {
+ {
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "loading"
+ ) {
+ AnimatedVisibility(
+ visible = mediaState.value.isLoading,
+ enter = enterAnimation,
+ exit = exitAnimation
+ ) {
+ LoadingMedia()
+ }
+ }
+
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "empty"
+ ) {
+ AnimatedVisibility(
+ visible = mediaState.value.media.isEmpty() && !mediaState.value.isLoading,
+ enter = enterAnimation,
+ exit = exitAnimation
+ ) {
+ emptyContent()
+ }
+ }
+ item(
+ span = { GridItemSpan(maxLineSpan) },
+ key = "error"
+ ) {
+ AnimatedVisibility(visible = mediaState.value.error.isNotEmpty()) {
+ Error(errorMessage = mediaState.value.error)
+ }
+ }
+ }
+ }
+
+ AnimatedVisibility(
+ visible = allowHeaders
+ ) {
+ MediaGridContentWithHeaders(
+ mediaState = mediaState,
+ mappedData = mappedData,
+ paddingValues = paddingValues,
+ allowSelection = allowSelection,
+ selectionState = selectionState,
+ selectedMedia = selectedMedia,
+ toggleSelection = toggleSelection,
+ canScroll = canScroll,
+ onMediaClick = onMediaClick,
+ topContent = topContent,
+ bottomContent = bottomContent
+ )
+ }
+
+ AnimatedVisibility(
+ visible = !allowHeaders
+ ) {
+ MediaGridContent(
+ mediaState = mediaState,
+ paddingValues = paddingValues,
+ allowSelection = allowSelection,
+ selectionState = selectionState,
+ selectedMedia = selectedMedia,
+ toggleSelection = toggleSelection,
+ canScroll = canScroll,
+ onMediaClick = onMediaClick,
+ topContent = topContent,
+ bottomContent = bottomContent
+ )
+ }
+
+}
+
+@Composable
+private fun PinchZoomGridScope.MediaGridContentWithHeaders(
+ mediaState: State,
+ mappedData: SnapshotStateList,
+ paddingValues: PaddingValues,
+ allowSelection: Boolean,
+ selectionState: MutableState,
+ selectedMedia: SnapshotStateList,
+ toggleSelection: @DisallowComposableCalls (Int) -> Unit,
+ canScroll: Boolean,
+ onMediaClick: @DisallowComposableCalls (media: Media) -> Unit,
+ topContent: LazyGridScope.() -> Unit,
+ bottomContent: LazyGridScope.() -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ val stringToday = stringResource(id = R.string.header_today)
+ val stringYesterday = stringResource(id = R.string.header_yesterday)
+ val feedbackManager = rememberFeedbackManager()
+ TimelineScroller(
+ modifier = Modifier
+ .padding(paddingValues)
+ .padding(top = 32.dp)
+ .padding(vertical = 32.dp),
+ mappedData = mappedData,
+ headers = remember(mediaState.value) {
+ mediaState.value.headers.toMutableStateList()
+ },
+ state = gridState,
+ ) {
+ LazyVerticalGrid(
+ state = gridState,
+ modifier = Modifier.fillMaxSize(),
+ columns = gridCells,
+ contentPadding = paddingValues,
+ userScrollEnabled = canScroll,
+ horizontalArrangement = Arrangement.spacedBy(1.dp),
+ verticalArrangement = Arrangement.spacedBy(1.dp)
+ ) {
+ topContent()
+
+ items(
+ items = mappedData,
+ key = { item -> item.key },
+ contentType = { item -> item.key.startsWith("media_") },
+ span = { item ->
+ GridItemSpan(if (item.key.isHeaderKey) maxLineSpan else 1)
+ }
+ ) { it ->
+ if (it is MediaItem.Header) {
+ val isChecked = rememberSaveable { mutableStateOf(false) }
+ if (allowSelection) {
+ LaunchedEffect(selectionState.value) {
+ // Uncheck if selectionState is set to false
+ isChecked.value = isChecked.value && selectionState.value
+ }
+ LaunchedEffect(selectedMedia.size) {
+ // Partial check of media items should not check the header
+ isChecked.value = selectedMedia.map { it.id }.containsAll(it.data)
+ }
+ }
+ MediaItemHeader(
+ modifier = Modifier
+ .animateItem(
+ fadeInSpec = null
+ )
+ .pinchItem(key = it.key),
+ date = remember {
+ it.text
+ .replace("Today", stringToday)
+ .replace("Yesterday", stringYesterday)
+ },
+ showAsBig = remember { it.key.isBigHeaderKey },
+ isCheckVisible = selectionState,
+ isChecked = isChecked
+ ) {
+ if (allowSelection) {
+ feedbackManager.vibrate()
+ scope.launch {
+ isChecked.value = !isChecked.value
+ if (isChecked.value) {
+ val toAdd = it.data.toMutableList().apply {
+ // Avoid media from being added twice to selection
+ removeIf {
+ selectedMedia.map { media -> media.id }.contains(it)
+ }
+ }
+ selectedMedia.addAll(mediaState.value.media.filter {
+ toAdd.contains(
+ it.id
+ )
+ })
+ } else selectedMedia.removeAll { media -> it.data.contains(media.id) }
+ selectionState.update(selectedMedia.isNotEmpty())
+ }
+ }
+ }
+ } else if (it is MediaItem.MediaViewItem) {
+ MediaImage(
+ modifier = Modifier
+ .animateItem(
+ fadeInSpec = null
+ )
+ .pinchItem(key = it.key),
+ media = it.media,
+ selectionState = selectionState,
+ selectedMedia = selectedMedia,
+ canClick = canScroll,
+ onItemClick = {
+ if (selectionState.value && allowSelection) {
+ feedbackManager.vibrate()
+ toggleSelection(mediaState.value.media.indexOf(it))
+ } else onMediaClick(it)
+ }
+ ) {
+ if (allowSelection) {
+ feedbackManager.vibrate()
+ toggleSelection(mediaState.value.media.indexOf(it))
+ }
+ }
+ }
+ }
+
+
+ bottomContent()
+ }
+ }
+}
+
+@Composable
+private fun PinchZoomGridScope.MediaGridContent(
+ mediaState: State,
+ paddingValues: PaddingValues,
+ allowSelection: Boolean,
+ selectionState: MutableState,
+ selectedMedia: SnapshotStateList,
+ toggleSelection: @DisallowComposableCalls (Int) -> Unit,
+ canScroll: Boolean,
+ onMediaClick: @DisallowComposableCalls (media: Media) -> Unit,
+ topContent: LazyGridScope.() -> Unit,
+ bottomContent: LazyGridScope.() -> Unit
+) {
+ val feedbackManager = rememberFeedbackManager()
+ LazyVerticalGrid(
+ state = gridState,
+ modifier = Modifier.fillMaxSize(),
+ columns = gridCells,
+ contentPadding = paddingValues,
+ userScrollEnabled = canScroll,
+ horizontalArrangement = Arrangement.spacedBy(1.dp),
+ verticalArrangement = Arrangement.spacedBy(1.dp)
+ ) {
+ topContent()
+
+ itemsIndexed(
+ items = mediaState.value.media,
+ key = { _, item -> item.toString() },
+ contentType = { _, item -> item.isImage }
+ ) { index, media ->
+ MediaImage(
+ modifier = Modifier
+ .animateItem(
+ fadeInSpec = null
+ )
+ .pinchItem(key = media.toString()),
+ media = media,
+ selectionState = selectionState,
+ selectedMedia = selectedMedia,
+ canClick = canScroll,
+ onItemClick = {
+ if (selectionState.value && allowSelection) {
+ feedbackManager.vibrate()
+ toggleSelection(index)
+ } else onMediaClick(it)
+ },
+ onItemLongClick = {
+ if (allowSelection) {
+ feedbackManager.vibrate()
+ toggleSelection(index)
+ }
+ }
+ )
+ }
+
+ bottomContent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGridView.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGridView.kt
index 4663f73ca..49d9c7bc9 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGridView.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaGridView.kt
@@ -9,89 +9,88 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import com.dokar.pinchzoomgrid.PinchZoomGridScope
-import com.dot.gallery.R
import com.dot.gallery.core.Constants.Animation.enterAnimation
import com.dot.gallery.core.Constants.Animation.exitAnimation
-import com.dot.gallery.core.MediaState
+import com.dot.gallery.feature_node.domain.model.MediaState
import com.dot.gallery.core.Settings.Misc.rememberAutoHideSearchBar
-import com.dot.gallery.core.presentation.components.StickyHeader
-import com.dot.gallery.core.presentation.components.util.StickyHeaderGrid
import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.model.MediaItem
-import com.dot.gallery.feature_node.domain.model.isBigHeaderKey
import com.dot.gallery.feature_node.domain.model.isHeaderKey
import com.dot.gallery.feature_node.domain.model.isIgnoredKey
-import com.dot.gallery.feature_node.presentation.util.FeedbackManager
-import com.dot.gallery.feature_node.presentation.util.update
-import kotlinx.coroutines.launch
+import com.dot.gallery.feature_node.presentation.util.roundDpToPx
+import com.dot.gallery.feature_node.presentation.util.roundSpToPx
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
-@Stable
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PinchZoomGridScope.MediaGridView(
- mediaState: MediaState,
+ mediaState: State,
paddingValues: PaddingValues = PaddingValues(0.dp),
searchBarPaddingTop: Dp = 0.dp,
- showSearchBar: Boolean = false,
- allowSelection: Boolean = false,
+ showSearchBar: Boolean = remember { false },
+ allowSelection: Boolean = remember { false },
selectionState: MutableState = remember { mutableStateOf(false) },
selectedMedia: SnapshotStateList = remember { mutableStateListOf() },
- toggleSelection: (Int) -> Unit = {},
+ toggleSelection: @DisallowComposableCalls (Int) -> Unit = {},
canScroll: Boolean = true,
- allowHeaders: Boolean = remember { true },
+ allowHeaders: Boolean = true,
enableStickyHeaders: Boolean = false,
showMonthlyHeader: Boolean = false,
aboveGridContent: @Composable (() -> Unit)? = null,
isScrolling: MutableState,
- onMediaClick: (media: Media) -> Unit = {}
+ emptyContent: @Composable () -> Unit,
+ onMediaClick: @DisallowComposableCalls (media: Media) -> Unit = {},
) {
- val stringToday = stringResource(id = R.string.header_today)
- val stringYesterday = stringResource(id = R.string.header_yesterday)
+ val mappedData by remember(mediaState.value, showMonthlyHeader) {
+ derivedStateOf {
+ (if (showMonthlyHeader) mediaState.value.mappedMediaWithMonthly
+ else mediaState.value.mappedMedia).toMutableStateList()
+ }
+ }
- val scope = rememberCoroutineScope()
- val mappedData = remember(showMonthlyHeader, mediaState) {
- if (showMonthlyHeader) mediaState.mappedMediaWithMonthly
- else mediaState.mappedMedia
+ LaunchedEffect(showMonthlyHeader, mediaState.value) {
+ snapshotFlow { mediaState.value }
+ .distinctUntilChanged()
+ .collectLatest {
+ mappedData.clear()
+ mappedData.addAll(
+ if (showMonthlyHeader) mediaState.value.mappedMediaWithMonthly
+ else mediaState.value.mappedMedia
+ )
+ }
}
- /** Selection state handling **/
BackHandler(
enabled = selectionState.value && allowSelection,
onBack = {
@@ -99,189 +98,41 @@ fun PinchZoomGridScope.MediaGridView(
selectedMedia.clear()
}
)
- /** ************ **/
-
- val feedbackManager = FeedbackManager.rememberFeedbackManager()
- @Composable
- fun mediaGrid() {
- LaunchedEffect(gridState.isScrollInProgress) {
- isScrolling.value = gridState.isScrollInProgress
- }
- Box {
- LazyVerticalGrid(
- state = gridState,
- modifier = Modifier.fillMaxSize(),
- columns = gridCells,
- contentPadding = paddingValues,
- userScrollEnabled = canScroll,
- horizontalArrangement = Arrangement.spacedBy(1.dp),
- verticalArrangement = Arrangement.spacedBy(1.dp)
- ) {
- if (aboveGridContent != null) {
- item(
- span = { GridItemSpan(maxLineSpan) },
- key = "aboveGrid"
- ) {
- aboveGridContent.invoke()
- }
+ /**
+ * Workaround for a small bug
+ * That shows the grid at the bottom after content is loaded
+ */
+ val lastLoadingState by remember { mutableStateOf(mediaState.value.isLoading) }
+ LaunchedEffect(gridState, mediaState.value) {
+ snapshotFlow { mediaState.value.isLoading }
+ .distinctUntilChanged()
+ .collectLatest { isLoading ->
+ if (!isLoading && lastLoadingState) {
+ gridState.scrollToItem(0)
}
-
- if (allowHeaders) {
- items(
- items = mappedData,
- key = { item -> item.key },
- contentType = { item -> item.key.startsWith("media_") },
- span = { item ->
- GridItemSpan(if (item.key.isHeaderKey) maxLineSpan else 1)
- }
- ) { it ->
- val item = remember {
- if (it is MediaItem.Header) it
- else it as MediaItem.MediaViewItem
- }
- if (item is MediaItem.Header) {
- val isChecked = rememberSaveable { mutableStateOf(false) }
- if (allowSelection) {
- LaunchedEffect(selectionState.value) {
- // Uncheck if selectionState is set to false
- isChecked.value = isChecked.value && selectionState.value
- }
- LaunchedEffect(selectedMedia.size) {
- // Partial check of media items should not check the header
- isChecked.value = selectedMedia.containsAll(item.data)
- }
- }
- StickyHeader(
- modifier = Modifier.pinchItem(key = it.key),
- date = remember {
- item.text
- .replace("Today", stringToday)
- .replace("Yesterday", stringYesterday)
- },
- showAsBig = remember { item.key.isBigHeaderKey },
- isCheckVisible = selectionState,
- isChecked = isChecked
- ) {
- if (allowSelection) {
- feedbackManager.vibrate()
- scope.launch {
- isChecked.value = !isChecked.value
- if (isChecked.value) {
- val toAdd = item.data.toMutableList().apply {
- // Avoid media from being added twice to selection
- removeIf { selectedMedia.contains(it) }
- }
- selectedMedia.addAll(toAdd)
- } else selectedMedia.removeAll(item.data)
- selectionState.update(selectedMedia.isNotEmpty())
- }
- }
- }
- } else {
- MediaImage(
- modifier = Modifier
- .animateItem()
- .pinchItem(key = it.key),
- media = (item as MediaItem.MediaViewItem).media,
- selectionState = selectionState,
- selectedMedia = selectedMedia,
- canClick = canScroll,
- onItemClick = {
- if (selectionState.value && allowSelection) {
- feedbackManager.vibrate()
- toggleSelection(mediaState.media.indexOf(it))
- } else onMediaClick(it)
- }
- ) {
- if (allowSelection) {
- feedbackManager.vibrate()
- toggleSelection(mediaState.media.indexOf(it))
- }
- }
- }
- }
- } else {
- itemsIndexed(
- items = mediaState.media,
- key = { _, item -> item.toString() },
- contentType = { _, item -> item.isImage }
- ) { index, media ->
- MediaImage(
- modifier = Modifier
- .animateItem()
- .pinchItem(key = media.toString()),
- media = media,
- selectionState = selectionState,
- selectedMedia = selectedMedia,
- canClick = canScroll,
- onItemClick = {
- if (selectionState.value && allowSelection) {
- feedbackManager.vibrate()
- toggleSelection(index)
- } else onMediaClick(it)
- },
- onItemLongClick = {
- if (allowSelection) {
- feedbackManager.vibrate()
- toggleSelection(index)
- }
- }
- )
- }
- }
- }
-
- if (allowHeaders) {
- TimelineScroller(
- gridState = gridState,
- mappedData = mappedData,
- paddingValues = paddingValues
- )
}
- }
}
- if (enableStickyHeaders) {
- /**
- * Remember last known header item
- */
- val stickyHeaderLastItem = remember { mutableStateOf(null) }
-
- val headers = remember(mappedData) {
- mappedData.filterIsInstance().filter { !it.key.isBigHeaderKey }
- }
-
- val stickyHeaderItem by remember(mappedData) {
- derivedStateOf {
- val firstItem = gridState.layoutInfo.visibleItemsInfo.firstOrNull()
- val firstHeaderIndex = gridState.layoutInfo.visibleItemsInfo.firstOrNull {
- it.key.isHeaderKey && !it.key.toString().contains("big")
- }?.index
-
- val item = firstHeaderIndex?.let(mappedData::getOrNull)
- stickyHeaderLastItem.apply {
- if (item != null && item is MediaItem.Header) {
- val newItem = item.text
- .replace("Today", stringToday)
- .replace("Yesterday", stringYesterday)
- val newIndex = (headers.indexOf(item) - 1).coerceAtLeast(0)
- val previousHeader = headers[newIndex].text
- .replace("Today", stringToday)
- .replace("Yesterday", stringYesterday)
- value = if (firstItem != null && !firstItem.key.isHeaderKey) {
- previousHeader
- } else {
- newItem
- }
- }
- }.value
- }
- }
+ AnimatedVisibility(
+ visible = enableStickyHeaders
+ ) {
+ val stickyHeaderItem by rememberStickyHeaderItem(
+ gridState = gridState,
+ headers = remember(mediaState.value) {
+ mediaState.value.headers.toMutableStateList()
+ },
+ mappedData = mappedData
+ )
val hideSearchBarSetting by rememberAutoHideSearchBar()
val searchBarPadding by animateDpAsState(
- targetValue = remember(isScrolling.value, showSearchBar, searchBarPaddingTop, hideSearchBarSetting) {
+ targetValue = remember(
+ isScrolling.value,
+ showSearchBar,
+ searchBarPaddingTop,
+ hideSearchBarSetting
+ ) {
if (showSearchBar && (!isScrolling.value || !hideSearchBarSetting)) {
SearchBarDefaults.InputFieldHeight + searchBarPaddingTop + 8.dp
} else if (showSearchBar && isScrolling.value) searchBarPaddingTop else 0.dp
@@ -290,25 +141,26 @@ fun PinchZoomGridScope.MediaGridView(
)
val density = LocalDensity.current
- val searchBarOffset = remember(density, showSearchBar, searchBarPadding) {
- with(density) {
- return@with if (showSearchBar)
- 28.sp.roundToPx() + searchBarPadding.roundToPx() else 0
- }
- }
- val headerMatcher: (LazyGridItemInfo) -> Boolean = remember {
- { item -> item.key.isHeaderKey || item.key.isIgnoredKey }
+ val searchBarHeightPx = WindowInsets.statusBars.getTop(density)
+ val searchBarPaddingPx by remember(density, searchBarPadding) {
+ derivedStateOf { with(density) { searchBarPadding.roundToPx() } }
}
- val headerOffset by rememberHeaderOffset(gridState, headerMatcher, searchBarOffset)
+
StickyHeaderGrid(
+ state = gridState,
modifier = Modifier.fillMaxSize(),
- showSearchBar = showSearchBar,
- headerOffset = headerOffset,
+ headerMatcher = { item -> item.key.isHeaderKey || item.key.isIgnoredKey },
+ searchBarOffset = { if (showSearchBar) 28.roundSpToPx(density) + searchBarPaddingPx else 0 },
+ toolbarOffset = { if (showSearchBar) 0 else 64.roundDpToPx(density) + searchBarHeightPx },
stickyHeader = {
- val show = remember(
- mediaState,
+ val show by remember(
+ mediaState.value,
stickyHeaderItem
- ) { mediaState.media.isNotEmpty() && stickyHeaderItem != null }
+ ) {
+ derivedStateOf {
+ mediaState.value.media.isNotEmpty() && stickyHeaderItem != null
+ }
+ }
AnimatedVisibility(
visible = show,
enter = enterAnimation,
@@ -339,8 +191,46 @@ fun PinchZoomGridScope.MediaGridView(
)
}
}
- ) { mediaGrid() }
- } else mediaGrid()
-
+ ) {
+ MediaGrid(
+ gridState = gridState,
+ gridCells = gridCells,
+ mediaState = mediaState,
+ mappedData = mappedData,
+ paddingValues = paddingValues,
+ allowSelection = allowSelection,
+ selectionState = selectionState,
+ selectedMedia = selectedMedia,
+ toggleSelection = toggleSelection,
+ canScroll = canScroll,
+ allowHeaders = allowHeaders,
+ aboveGridContent = aboveGridContent,
+ isScrolling = isScrolling,
+ emptyContent = emptyContent,
+ onMediaClick = onMediaClick
+ )
+ }
+ }
+ AnimatedVisibility(
+ visible = !enableStickyHeaders
+ ) {
+ MediaGrid(
+ gridState = gridState,
+ gridCells = gridCells,
+ mediaState = mediaState,
+ mappedData = mappedData,
+ paddingValues = paddingValues,
+ allowSelection = allowSelection,
+ selectionState = selectionState,
+ selectedMedia = selectedMedia,
+ toggleSelection = toggleSelection,
+ canScroll = canScroll,
+ allowHeaders = allowHeaders,
+ aboveGridContent = aboveGridContent,
+ isScrolling = isScrolling,
+ emptyContent = emptyContent,
+ onMediaClick = onMediaClick
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaImage.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaImage.kt
index c526933ac..34906ad01 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaImage.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MediaImage.kt
@@ -27,7 +27,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -44,12 +43,13 @@ import androidx.compose.ui.unit.dp
import com.dot.gallery.core.Constants.Animation
import com.dot.gallery.core.presentation.components.CheckBox
import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.isFavorite
+import com.dot.gallery.feature_node.domain.model.isVideo
import com.dot.gallery.feature_node.presentation.mediaview.components.video.VideoDurationHeader
import com.github.panpf.sketch.AsyncImage
import com.github.panpf.sketch.request.ComposableImageRequest
import com.github.panpf.sketch.resize.Scale
-@Stable
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MediaImage(
@@ -131,9 +131,7 @@ fun MediaImage(
}
AnimatedVisibility(
- visible = remember(media) {
- media.duration != null
- },
+ visible = remember(media) { media.isVideo },
enter = Animation.enterAnimation,
exit = Animation.exitAnimation,
modifier = Modifier.align(Alignment.TopEnd)
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/TimelineScroller.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/TimelineScroller.kt
index 27c023178..b4f639b93 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/TimelineScroller.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/TimelineScroller.kt
@@ -1,217 +1,143 @@
package com.dot.gallery.feature_node.presentation.common.components
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.LazyGridState
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.boundsInParent
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
import com.dot.gallery.R
+import com.dot.gallery.core.Constants.Animation.enterAnimation
+import com.dot.gallery.core.Constants.Animation.exitAnimation
import com.dot.gallery.feature_node.domain.model.MediaItem
-import com.dot.gallery.feature_node.presentation.util.FeedbackManager.Companion.rememberFeedbackManager
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.math.abs
+import com.dot.gallery.feature_node.presentation.util.getDate
+import my.nanihadesuka.compose.InternalLazyVerticalGridScrollbar
+import my.nanihadesuka.compose.ScrollbarLayoutSide
+import my.nanihadesuka.compose.ScrollbarSelectionActionable
+import my.nanihadesuka.compose.ScrollbarSelectionMode
+import my.nanihadesuka.compose.ScrollbarSettings
@Composable
-fun BoxScope.TimelineScroller(
- gridState: LazyGridState,
- mappedData: List,
- paddingValues: PaddingValues
-) {
- val scope = rememberCoroutineScope()
- val offsets = remember { mutableStateMapOf() }
- var selectedIndex by remember { mutableIntStateOf(0) }
- val feedbackManager = rememberFeedbackManager()
- fun scrollIfNeeded(offset: Float) {
- val index = offsets
- .mapValues { abs(it.value - offset) }
- .entries
- .minByOrNull { it.value }
- ?.key ?: return
- if (selectedIndex == index) return
- selectedIndex = index
- scope.launch {
- feedbackManager.vibrate()
- gridState.scrollToItem(selectedIndex, scrollOffset = -250)
- }
- }
-
- var scrollVisible by remember { mutableStateOf(false) }
- val scrollAlpha by animateFloatAsState(
- animationSpec = tween(durationMillis = 1000),
- targetValue = if (scrollVisible) 1f else 0f,
- label = "scrollAlpha"
- )
-
- val configuration = LocalConfiguration.current
- val height = remember(configuration) { configuration.screenHeightDp }
- val headerList =
- remember(mappedData) { mappedData.filterIsInstance() }
- val bottomPadding = remember { 32 /*dp*/ }
-
- /**
- * Distribute scroll items height proportionally to the amount of date headers
- **/
- val parentTopPadding = remember(paddingValues) { paddingValues.calculateTopPadding().value }
- val parentBottomPadding = remember { paddingValues.calculateBottomPadding().value }
- val heightSize =
- remember(height, parentTopPadding, parentBottomPadding, bottomPadding, headerList) {
- (height - parentTopPadding - parentBottomPadding - bottomPadding) / headerList.size
- }
-
- // W.I.P.
- /*val yearList = remember { mutableStateListOf() }
- LazyColumn(
- modifier = Modifier
- .align(Alignment.TopEnd)
- .padding(paddingValues)
- .padding(top = 32.dp, bottom = bottomPadding.dp)
- .padding(end = 48.dp)
- .fillMaxHeight()
- .wrapContentWidth(),
- verticalArrangement = Arrangement.Top,
- horizontalAlignment = Alignment.End
- ) {
- itemsIndexed(
- items = headerList,
- key = { _, item -> item.key }
- ) { index, item ->
- val year = getYear(item.text)
- if (!yearList.contains(year)) {
- yearList.add(year)
- val padding = heightSize * index
- Log.d(
- Constants.TAG,
- "Height size: $heightSize, Index: $index, Top padding for $year: $padding"
- )
- Text(
- modifier = Modifier
- .graphicsLayer {
- translationY = -padding
- }
- .background(
- color = MaterialTheme.colorScheme.secondaryContainer,
- shape = RoundedCornerShape(100)
- )
- .padding(vertical = 4.dp, horizontal = 5.dp),
- text = year,
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.onSecondaryContainer,
- )
- }
- }
- }*/
-
- Box(
- modifier = Modifier
- .padding(paddingValues)
- .padding(top = 32.dp, bottom = bottomPadding.dp)
- .graphicsLayer {
- translationY = offsets[selectedIndex] ?: 0f
- alpha = scrollAlpha
- }
- .size(48.dp)
- .background(
- color = MaterialTheme.colorScheme.tertiary,
- shape = RoundedCornerShape(
- topStartPercent = 100,
- bottomStartPercent = 100
- )
- )
- .align(Alignment.TopEnd)
- .padding(vertical = 2.5.dp),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_scroll_arrow),
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onTertiary
+fun rememberScrollbarSettings(
+ headers: SnapshotStateList,
+) : ScrollbarSettings {
+ val enabled by remember(headers) { derivedStateOf { headers.size > 3 } }
+ return remember(headers, enabled) {
+ ScrollbarSettings.Default.copy(
+ enabled = enabled,
+ side = ScrollbarLayoutSide.End,
+ selectionMode = ScrollbarSelectionMode.Thumb,
+ selectionActionable = ScrollbarSelectionActionable.WhenVisible,
+ scrollbarPadding = 0.dp,
+ thumbThickness = 24.dp,
+ thumbUnselectedColor = Color.Transparent,
+ thumbSelectedColor = Color.Transparent,
+ hideDisplacement = 0.dp
)
}
+}
- Column(
- modifier = Modifier
- .padding(paddingValues)
- .padding(top = 32.dp, bottom = bottomPadding.dp)
- .align(Alignment.TopEnd)
- .fillMaxHeight()
- .verticalScroll(state = rememberScrollState())
- .graphicsLayer {
- alpha = scrollAlpha
- }
- .pointerInput(Unit) {
- detectVerticalDragGestures(
- onDragStart = {
- scope.launch {
- scrollVisible = true
- }
- },
- onDragCancel = {
- scope.launch {
- delay(1500)
- scrollVisible = false
- }
- },
- onDragEnd = {
- scope.launch {
- delay(500)
- scrollVisible = false
- }
- },
- onVerticalDrag = { change, _ ->
- if (!scrollVisible) {
- scrollVisible = true
+@Composable
+fun TimelineScroller(
+ state: LazyGridState,
+ modifier: Modifier = Modifier,
+ mappedData: SnapshotStateList,
+ headers: SnapshotStateList,
+ settings: ScrollbarSettings = rememberScrollbarSettings(headers),
+ content: @Composable () -> Unit
+) {
+ if (!settings.enabled) content()
+ else Box {
+ content()
+ InternalLazyVerticalGridScrollbar(
+ state = state,
+ settings = settings,
+ modifier = modifier,
+ indicatorContent = { index, isSelected ->
+ val stringToday = stringResource(R.string.header_today)
+ val stringYesterday = stringResource(R.string.header_yesterday)
+ val currentDate by remember(mappedData, index) {
+ derivedStateOf {
+ mappedData.getOrNull((index + 1).coerceAtMost(mappedData.size - 1))?.let { item ->
+ when (item) {
+ is MediaItem.MediaViewItem -> item.media.timestamp.getDate(
+ stringToday = stringToday,
+ stringYesterday = stringYesterday
+ )
+ is MediaItem.Header -> item.text
+ }
}
- scrollIfNeeded(change.position.y)
}
+ }
+ val isScrolling by remember(state) { derivedStateOf { state.isScrollInProgress } }
+ val offset by animateDpAsState(
+ targetValue = if (isScrolling || isSelected) 24.dp else 72.dp, label = "thumbOffset"
)
- },
- verticalArrangement = Arrangement.Top
- ) {
- mappedData.forEachIndexed { i, header ->
- if (header is MediaItem.Header) {
- Spacer(
- modifier = Modifier
- .width(16.dp)
- .height(heightSize.dp)
- .onGloballyPositioned {
- offsets[i] = it.boundsInParent().center.y
- }
- )
+ Row(
+ modifier = Modifier.offset {
+ IntOffset(offset.roundToPx(), 0)
+ }.zIndex(5f),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = !currentDate.isNullOrEmpty() && isSelected,
+ enter = enterAnimation(250),
+ exit = exitAnimation(1000)
+ ) {
+ Text(
+ text = currentDate.toString(),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onPrimary,
+ modifier = Modifier
+ .background(
+ color = MaterialTheme.colorScheme.primary,
+ shape = RoundedCornerShape(100)
+ )
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ )
+ }
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .background(
+ color = MaterialTheme.colorScheme.tertiary,
+ shape = RoundedCornerShape(
+ topStartPercent = 100,
+ bottomStartPercent = 100
+ )
+ )
+ .padding(vertical = 2.5.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_scroll_arrow),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onTertiary
+ )
+ }
+ }
}
- }
+ )
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberHeaderOffset.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberHeaderOffset.kt
index 91d8ab396..9de964f01 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberHeaderOffset.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberHeaderOffset.kt
@@ -1,32 +1,134 @@
package com.dot.gallery.feature_node.presentation.common.components
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntOffsetAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
+import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.unit.IntOffset
@Composable
-fun rememberHeaderOffset(
- lazyGridState: LazyGridState,
- headerMatcher: (LazyGridItemInfo) -> Boolean,
- searchBarOffset: Int
-): State {
- return remember {
+inline fun StickyHeaderGrid(
+ state: LazyGridState,
+ modifier: Modifier,
+ crossinline headerMatcher: @DisallowComposableCalls (LazyGridItemInfo) -> Boolean,
+ crossinline searchBarOffset: @DisallowComposableCalls () -> Int,
+ crossinline toolbarOffset: @DisallowComposableCalls () -> Int,
+ stickyHeader: @Composable () -> Unit,
+ content: @Composable () -> Unit
+) {
+ StickyHeaderLayout(
+ lazyState = state,
+ modifier = modifier,
+ viewportStart = { state.layoutInfo.viewportStartOffset },
+ lazyItems = { state.layoutInfo.visibleItemsInfo },
+ lazyItemOffset = { offset.y },
+ lazyItemHeight = { size.height },
+ headerMatcher = headerMatcher,
+ stickyHeader = stickyHeader,
+ searchBarOffset = searchBarOffset,
+ toolbarOffset = toolbarOffset,
+ content = content,
+ )
+}
+
+@Composable
+inline fun StickyHeaderStaggeredGrid(
+ state: LazyStaggeredGridState,
+ modifier: Modifier,
+ crossinline headerMatcher: @DisallowComposableCalls (LazyStaggeredGridItemInfo) -> Boolean,
+ crossinline searchBarOffset: @DisallowComposableCalls () -> Int,
+ crossinline toolbarOffset: @DisallowComposableCalls () -> Int,
+ stickyHeader: @Composable () -> Unit,
+ content: @Composable () -> Unit
+) {
+ StickyHeaderLayout(
+ lazyState = state,
+ modifier = modifier,
+ viewportStart = { state.layoutInfo.viewportStartOffset },
+ lazyItems = { state.layoutInfo.visibleItemsInfo },
+ lazyItemOffset = { offset.y },
+ lazyItemHeight = { size.height },
+ headerMatcher = headerMatcher,
+ stickyHeader = stickyHeader,
+ searchBarOffset = searchBarOffset,
+ toolbarOffset = toolbarOffset,
+ content = content,
+ )
+}
+
+@Composable
+inline fun StickyHeaderLayout(
+ lazyState: LazyState,
+ modifier: Modifier = Modifier,
+ crossinline viewportStart: @DisallowComposableCalls (LazyState) -> Int,
+ crossinline lazyItems: @DisallowComposableCalls (LazyState) -> List,
+ crossinline lazyItemOffset: @DisallowComposableCalls LazyItem.() -> Int,
+ crossinline lazyItemHeight: @DisallowComposableCalls LazyItem.() -> Int,
+ crossinline searchBarOffset: @DisallowComposableCalls () -> Int,
+ crossinline toolbarOffset: @DisallowComposableCalls () -> Int,
+ crossinline headerMatcher: @DisallowComposableCalls LazyItem.() -> Boolean,
+ stickyHeader: @Composable () -> Unit,
+ content: @Composable () -> Unit
+) {
+ val headerOffset by remember(lazyState) {
derivedStateOf {
- val layoutInfo = lazyGridState.layoutInfo
- val startOffset = layoutInfo.viewportStartOffset
- val firstCompletelyVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull {
- it.offset.y >= startOffset
- }
- when (firstCompletelyVisibleItem?.let { headerMatcher(it) }) {
- true -> firstCompletelyVisibleItem.size
- .height
- .minus(firstCompletelyVisibleItem.offset.y)
- .let { difference -> if (difference < 0) 0 else -difference - searchBarOffset }
- else -> 0
+ val searchBarOffsetValue = searchBarOffset()
+ val startOffset = viewportStart(lazyState)
+ val visibleItems = lazyItems(lazyState)
+ val firstCompletelyVisibleItem = visibleItems.firstOrNull { lazyItem ->
+ lazyItemOffset(lazyItem) >= startOffset
+ } ?: return@derivedStateOf 0
+
+ when (headerMatcher(firstCompletelyVisibleItem)) {
+ false -> 0
+ true -> lazyItemHeight(firstCompletelyVisibleItem)
+ .minus(lazyItemOffset(firstCompletelyVisibleItem))
+ .let { difference -> if (difference < 0) 0 else -difference - searchBarOffsetValue}
}
}
}
+ val toolbarOffsetValue by remember(headerOffset, toolbarOffset()) {
+ derivedStateOf {
+ toolbarOffset()
+ }
+ }
+
+ val offsetAnimation by animateIntOffsetAsState(
+ remember(headerOffset, toolbarOffsetValue) {
+ IntOffset(
+ x = 0,
+ y = headerOffset + toolbarOffsetValue
+ )
+ }, label = "offsetAnimation"
+ )
+
+ val alphaAnimation by animateFloatAsState(
+ targetValue = remember(offsetAnimation) { if (offsetAnimation.y < -100) 0f else 1f },
+ label = "alphaAnimation",
+ animationSpec = tween(100, 10),
+ )
+
+ Box(modifier = modifier) {
+ content()
+ Box(
+ modifier = Modifier
+ .alpha(alphaAnimation)
+ .offset { offsetAnimation }
+ ) {
+ stickyHeader()
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberStickyHeaderItem.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberStickyHeaderItem.kt
new file mode 100644
index 000000000..8419f0b49
--- /dev/null
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/rememberStickyHeaderItem.kt
@@ -0,0 +1,58 @@
+package com.dot.gallery.feature_node.presentation.common.components
+
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.res.stringResource
+import com.dot.gallery.R
+import com.dot.gallery.feature_node.domain.model.MediaItem
+import com.dot.gallery.feature_node.domain.model.isHeaderKey
+
+@Composable
+fun rememberStickyHeaderItem(
+ gridState: LazyGridState,
+ headers: SnapshotStateList,
+ mappedData: SnapshotStateList
+): State {
+ val stringToday = stringResource(id = R.string.header_today)
+ val stringYesterday = stringResource(id = R.string.header_yesterday)
+
+ /**
+ * Remember last known header item
+ */
+ val stickyHeaderLastItem = remember { mutableStateOf(null) }
+
+ LaunchedEffect(gridState, headers, mappedData) {
+ snapshotFlow { gridState.layoutInfo.visibleItemsInfo }
+ .collect { visibleItems ->
+ val firstItem = visibleItems.firstOrNull()
+ val firstHeaderIndex = visibleItems.firstOrNull {
+ it.key.isHeaderKey && !it.key.toString().contains("big")
+ }?.index
+
+ val item = firstHeaderIndex?.let(mappedData::getOrNull)
+ stickyHeaderLastItem.value = if (item != null && item is MediaItem.Header) {
+ val newItem = item.text
+ .replace("Today", stringToday)
+ .replace("Yesterday", stringYesterday)
+ val newIndex = (headers.indexOf(item) - 1).coerceAtLeast(0)
+ val previousHeader = headers[newIndex].text
+ .replace("Today", stringToday)
+ .replace("Yesterday", stringYesterday)
+ if (firstItem != null && !firstItem.key.isHeaderKey) {
+ previousHeader
+ } else {
+ newItem
+ }
+ } else {
+ stickyHeaderLastItem.value
+ }
+ }
+ }
+ return stickyHeaderLastItem
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt
index 59f700bde..6842f7e62 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/EditViewModel.kt
@@ -18,7 +18,8 @@ import androidx.lifecycle.viewModelScope
import com.dot.gallery.feature_node.domain.model.ImageFilter
import com.dot.gallery.feature_node.domain.model.ImageModification
import com.dot.gallery.feature_node.domain.model.Media
-import com.dot.gallery.feature_node.domain.use_case.MediaUseCases
+import com.dot.gallery.feature_node.domain.repository.MediaRepository
+import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.edit.components.adjustments.Adjustment
import com.dot.gallery.feature_node.presentation.edit.components.adjustments.AdjustmentFilter
import com.dot.gallery.feature_node.presentation.util.flipHorizontally
@@ -40,7 +41,8 @@ import javax.inject.Inject
@HiltViewModel
class EditViewModel @Inject constructor(
- private val mediaUseCases: MediaUseCases,
+ private val repository: MediaRepository,
+ private val mediaUseCases: MediaHandleUseCase,
@ApplicationContext
private val applicationContext: Context
) : ViewModel() {
@@ -104,7 +106,7 @@ class EditViewModel @Inject constructor(
fun loadImage(uri: Uri) {
currentUri = uri
viewModelScope.launch(Dispatchers.IO) {
- mediaUseCases.getMediaListByUrisUseCase(listOf(uri), reviewMode = false)
+ repository.getMediaListByUris(listOf(uri), reviewMode = false)
.collectLatest {
val data = it.data ?: emptyList()
_image.emit(null)
@@ -179,7 +181,7 @@ class EditViewModel @Inject constructor(
) {
viewModelScope.launch(Dispatchers.IO) {
val done = if (asCopy) {
- mediaUseCases.mediaHandleUseCase.saveImage(
+ mediaUseCases.saveImage(
bitmap = image.asAndroidBitmap(),
format = bitmapFormat,
relativePath = mediaRef.value?.relativePath
@@ -189,7 +191,7 @@ class EditViewModel @Inject constructor(
mimeType = mediaRef.value?.mimeType ?: "image/png"
) != null
} else {
- mediaUseCases.mediaHandleUseCase.overrideImage(
+ mediaUseCases.overrideImage(
uri = mediaRef.value!!.uri,
bitmap = image.asAndroidBitmap(),
format = bitmapFormat,
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/CopyMediaSheet.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/CopyMediaSheet.kt
index 41b6efb41..52e249e75 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/CopyMediaSheet.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/CopyMediaSheet.kt
@@ -38,13 +38,14 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.SecureFlagPolicy
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Constants
import com.dot.gallery.core.Constants.albumCellsList
import com.dot.gallery.core.Settings.Album.rememberAlbumGridSize
import com.dot.gallery.core.presentation.components.DragHandle
import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.volume
import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.albums.components.AlbumComponent
import com.dot.gallery.feature_node.presentation.util.AppBottomSheetState
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MetadataEditSheet.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MetadataEditSheet.kt
index 02157d00d..22ed8f706 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MetadataEditSheet.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MetadataEditSheet.kt
@@ -1,8 +1,6 @@
package com.dot.gallery.feature_node.presentation.exif
import android.media.MediaScannerConnection
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -12,20 +10,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.DeleteSweep
-import androidx.compose.material.icons.outlined.GpsOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FilterChip
-import androidx.compose.material3.FilterChipDefaults
-import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
@@ -42,21 +31,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.dot.gallery.R
import com.dot.gallery.core.presentation.components.DragHandle
import com.dot.gallery.feature_node.domain.model.ExifAttributes
import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.isImage
+import com.dot.gallery.feature_node.domain.model.isVideo
import com.dot.gallery.feature_node.domain.model.rememberExifAttributes
import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.util.AppBottomSheetState
import com.dot.gallery.feature_node.presentation.util.rememberActivityResult
import com.dot.gallery.feature_node.presentation.util.rememberExifInterface
-import com.dot.gallery.feature_node.presentation.util.rememberExifMetadata
import com.dot.gallery.feature_node.presentation.util.toastError
import com.dot.gallery.feature_node.presentation.util.writeRequest
import com.dot.gallery.ui.theme.Shapes
@@ -119,104 +106,16 @@ fun MetadataEditSheet(
.verticalScroll(rememberScrollState())
) {
Text(
- text = buildAnnotatedString {
- withStyle(
- style = SpanStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontStyle = MaterialTheme.typography.titleLarge.fontStyle,
- fontSize = MaterialTheme.typography.titleLarge.fontSize,
- letterSpacing = MaterialTheme.typography.titleLarge.letterSpacing
- )
- ) {
- append(stringResource(R.string.edit_metadata))
- }
- append("\n")
- withStyle(
- style = SpanStyle(
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- fontStyle = MaterialTheme.typography.bodyMedium.fontStyle,
- fontSize = MaterialTheme.typography.bodyMedium.fontSize,
- letterSpacing = MaterialTheme.typography.bodyMedium.letterSpacing
- )
- ) {
- append(stringResource(R.string.beta))
- }
- },
+ text = if (media.isVideo) stringResource(R.string.update_file_name)
+ else stringResource(R.string.edit_metadata),
textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.padding(24.dp)
.fillMaxWidth()
)
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .horizontalScroll(state = rememberScrollState()),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Spacer(Modifier.width(8.dp))
- FilterChip(
- selected = shouldRemoveMetadata,
- onClick = {
- shouldRemoveLocation = false
- shouldRemoveMetadata = !shouldRemoveMetadata
- },
- label = {
- Text(text = stringResource(R.string.remove_metadata))
- },
- leadingIcon = {
- Icon(
- modifier = Modifier.size(18.dp),
- imageVector = Icons.Outlined.DeleteSweep,
- contentDescription = null
- )
- },
- shape = RoundedCornerShape(100),
- border = null,
- colors = FilterChipDefaults.filterChipColors(
- containerColor = MaterialTheme.colorScheme.tertiaryContainer,
- labelColor = MaterialTheme.colorScheme.onTertiaryContainer,
- iconColor = MaterialTheme.colorScheme.onTertiaryContainer,
- selectedContainerColor = MaterialTheme.colorScheme.tertiary,
- selectedLabelColor = MaterialTheme.colorScheme.onTertiary,
- selectedLeadingIconColor = MaterialTheme.colorScheme.onTertiary
- )
- )
- val exifMetadata = exifInterface?.let { rememberExifMetadata(media, it) }
- AnimatedVisibility(visible = exifMetadata?.gpsLatLong != null) {
- FilterChip(
- selected = shouldRemoveLocation,
- onClick = {
- shouldRemoveMetadata = false
- shouldRemoveLocation = !shouldRemoveLocation
- },
- label = {
- Text(text = stringResource(R.string.remove_location))
- },
- leadingIcon = {
- Icon(
- modifier = Modifier.size(18.dp),
- imageVector = Icons.Outlined.GpsOff,
- contentDescription = null
- )
- },
- enabled = exifMetadata?.gpsLatLong != null,
- shape = RoundedCornerShape(100),
- border = null,
- colors = FilterChipDefaults.filterChipColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- labelColor = MaterialTheme.colorScheme.onPrimaryContainer,
- iconColor = MaterialTheme.colorScheme.onPrimaryContainer,
- selectedContainerColor = MaterialTheme.colorScheme.primary,
- selectedLabelColor = MaterialTheme.colorScheme.onPrimary,
- selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimary
- )
- )
- }
- Spacer(Modifier.width(8.dp))
- }
-
TextField(
modifier = Modifier
.padding(horizontal = 16.dp)
@@ -238,26 +137,30 @@ fun MetadataEditSheet(
)
)
- TextField(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .fillMaxWidth()
- .height(112.dp),
- value = exifAttributes.imageDescription ?: "",
- onValueChange = { newValue ->
- exifAttributes = exifAttributes.copy(imageDescription = newValue)
- },
- label = {
- Text(text = stringResource(R.string.description))
- },
- shape = Shapes.large,
- colors = TextFieldDefaults.colors(
- unfocusedIndicatorColor = Color.Transparent,
- disabledIndicatorColor = Color.Transparent,
- errorIndicatorColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent
+ androidx.compose.animation.AnimatedVisibility(
+ visible = media.isImage
+ ) {
+ TextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ .height(112.dp),
+ value = exifAttributes.imageDescription ?: "",
+ onValueChange = { newValue ->
+ exifAttributes = exifAttributes.copy(imageDescription = newValue)
+ },
+ label = {
+ Text(text = stringResource(R.string.description))
+ },
+ shape = Shapes.large,
+ colors = TextFieldDefaults.colors(
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent,
+ errorIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ )
)
- )
+ }
Row(
modifier = Modifier.padding(24.dp),
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MoveMediaSheet.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MoveMediaSheet.kt
index 4026debda..fc3046f78 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MoveMediaSheet.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/exif/MoveMediaSheet.kt
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
@@ -38,13 +37,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Constants
import com.dot.gallery.core.Constants.albumCellsList
import com.dot.gallery.core.Settings.Album.rememberAlbumGridSize
import com.dot.gallery.core.presentation.components.DragHandle
import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.domain.model.Media
+import com.dot.gallery.feature_node.domain.model.volume
import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.albums.components.AlbumComponent
import com.dot.gallery.feature_node.presentation.util.AppBottomSheetState
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/FavoriteScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/FavoriteScreen.kt
index 05a68e13b..f9cb19c60 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/FavoriteScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/FavoriteScreen.kt
@@ -9,32 +9,28 @@ import android.app.Activity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Constants.Target.TARGET_FAVORITES
-import com.dot.gallery.core.MediaState
+import com.dot.gallery.feature_node.domain.model.MediaState
import com.dot.gallery.feature_node.domain.model.Media
import com.dot.gallery.feature_node.domain.use_case.MediaHandleUseCase
import com.dot.gallery.feature_node.presentation.common.MediaScreen
-import com.dot.gallery.feature_node.presentation.common.MediaViewModel
import com.dot.gallery.feature_node.presentation.favorites.components.EmptyFavorites
import com.dot.gallery.feature_node.presentation.favorites.components.FavoriteNavActions
-import kotlinx.coroutines.flow.StateFlow
@Composable
fun FavoriteScreen(
paddingValues: PaddingValues,
albumName: String = stringResource(id = R.string.favorites),
- vm: MediaViewModel,
handler: MediaHandleUseCase,
- mediaState: StateFlow,
- albumState: StateFlow,
+ mediaState: State,
+ albumsState: State,
selectionState: MutableState,
selectedMedia: SnapshotStateList,
toggleFavorite: (ActivityResultLauncher, List, Boolean) -> Unit,
@@ -46,18 +42,17 @@ fun FavoriteScreen(
paddingValues = paddingValues,
target = TARGET_FAVORITES,
albumName = albumName,
- vm = vm,
handler = handler,
+ albumsState = albumsState,
mediaState = mediaState,
- albumState = albumState,
selectionState = selectionState,
selectedMedia = selectedMedia,
toggleSelection = toggleSelection,
navActionsContent = { _: MutableState,
result: ActivityResultLauncher ->
- FavoriteNavActions(toggleFavorite, mediaState, selectedMedia, selectionState, result)
+ FavoriteNavActions(toggleFavorite, mediaState.value, selectedMedia, selectionState, result)
},
- emptyContent = { EmptyFavorites(Modifier.fillMaxSize()) },
+ emptyContent = { EmptyFavorites() },
navigate = navigate,
navigateUp = navigateUp,
toggleNavbar = toggleNavbar
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/components/FavoriteNavActions.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/components/FavoriteNavActions.kt
index c92e87cd4..25b557f8c 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/components/FavoriteNavActions.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/components/FavoriteNavActions.kt
@@ -7,24 +7,24 @@ package com.dot.gallery.feature_node.presentation.favorites.components
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.res.stringResource
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.dot.gallery.R
-import com.dot.gallery.core.MediaState
+import com.dot.gallery.core.Constants.Animation.enterAnimation
+import com.dot.gallery.core.Constants.Animation.exitAnimation
+import com.dot.gallery.feature_node.domain.model.MediaState
import com.dot.gallery.feature_node.domain.model.Media
-import kotlinx.coroutines.flow.StateFlow
@Composable
fun FavoriteNavActions(
toggleFavorite: (ActivityResultLauncher, List, Boolean) -> Unit,
- mediaState: StateFlow,
+ mediaState: MediaState,
selectedMedia: SnapshotStateList,
selectionState: MutableState,
result: ActivityResultLauncher
@@ -32,11 +32,14 @@ fun FavoriteNavActions(
val removeAllTitle = stringResource(R.string.remove_all)
val removeSelectedTitle = stringResource(R.string.remove_selected)
val title = if (selectionState.value) removeSelectedTitle else removeAllTitle
- val state by mediaState.collectAsStateWithLifecycle()
- if (state.media.isNotEmpty()) {
+ AnimatedVisibility(
+ visible = mediaState.media.isNotEmpty(),
+ enter = enterAnimation,
+ exit = exitAnimation
+ ) {
TextButton(
onClick = {
- toggleFavorite(result, selectedMedia.ifEmpty { state.media }, false)
+ toggleFavorite(result, selectedMedia.ifEmpty { mediaState.media }, false)
}
) {
Text(
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredScreen.kt
index 2428e14d1..d6059747f 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredScreen.kt
@@ -30,6 +30,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -45,7 +46,7 @@ import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Position
import com.dot.gallery.core.SettingsEntity
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
@@ -58,10 +59,9 @@ import com.dot.gallery.ui.core.Icons as GalleryIcons
fun IgnoredScreen(
navigateUp: () -> Unit,
startSetup: () -> Unit,
- albumsState: AlbumState,
+ albumsState: State,
) {
val vm = hiltViewModel()
- vm.attachToLifecycle()
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
val state by vm.blacklistState.collectAsStateWithLifecycle(IgnoredState())
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredViewModel.kt
index c283480a7..e71ec0111 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredViewModel.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredViewModel.kt
@@ -1,51 +1,28 @@
package com.dot.gallery.feature_node.presentation.ignored
-import android.annotation.SuppressLint
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
-import com.dot.gallery.feature_node.domain.use_case.MediaUseCases
-import com.dot.gallery.feature_node.presentation.util.RepeatOnResume
+import com.dot.gallery.feature_node.domain.repository.MediaRepository
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class IgnoredViewModel @Inject constructor(
- private val mediaUseCases: MediaUseCases
+ private val repository: MediaRepository
): ViewModel() {
- private val _ignoredState = MutableStateFlow(IgnoredState())
- val blacklistState = _ignoredState.asStateFlow()
-
- init {
- getIgnoredAlbums()
- }
-
- @SuppressLint("ComposableNaming")
- @Composable
- fun attachToLifecycle() {
- LaunchedEffect(Unit) {
- getIgnoredAlbums()
- }
- }
+ val blacklistState = repository.getBlacklistedAlbums()
+ .map { IgnoredState(it) }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), IgnoredState())
fun removeFromBlacklist(ignoredAlbum: IgnoredAlbum) {
viewModelScope.launch {
- mediaUseCases.blacklistUseCase.removeFromBlacklist(ignoredAlbum)
- }
- }
-
- private fun getIgnoredAlbums() {
- viewModelScope.launch {
- mediaUseCases.blacklistUseCase.blacklistedAlbums.collectLatest {
- _ignoredState.emit(IgnoredState(it))
- }
+ repository.removeBlacklistedAlbum(ignoredAlbum)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetup.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetup.kt
index 20290a46d..968c2bbf7 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetup.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetup.kt
@@ -3,6 +3,7 @@ package com.dot.gallery.feature_node.presentation.ignored.setup
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
@@ -10,7 +11,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import com.dot.gallery.core.AlbumState
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.core.Constants.Animation.navigateInAnimation
import com.dot.gallery.core.Constants.Animation.navigateUpAnimation
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
@@ -25,11 +26,9 @@ import java.util.UUID
@Composable
fun IgnoredSetup(
onCancel: () -> Unit,
- albumState: AlbumState,
+ albumState: State,
) {
- val vm = hiltViewModel().apply {
- attachToLifecycle()
- }
+ val vm = hiltViewModel()
val navController = rememberNavController()
val uiState by vm.uiState.collectAsStateWithLifecycle()
@@ -124,7 +123,7 @@ fun IgnoredSetup(
if ((uiState.type as IgnoredType.REGEX).regex.isEmpty()) return@LaunchedEffect
try {
val regex = (uiState.type as IgnoredType.REGEX).regex.toRegex()
- val matchedAlbums = albumState.albums.filter(regex::matchesAlbum)
+ val matchedAlbums = albumState.value.albumsWithBlacklisted.filter(regex::matchesAlbum)
vm.setMatchedAlbums(matchedAlbums)
} catch (e: Exception) {
e.printStackTrace()
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupViewModel.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupViewModel.kt
index 08159c79e..0f1188747 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupViewModel.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupViewModel.kt
@@ -1,8 +1,5 @@
package com.dot.gallery.feature_node.presentation.ignored.setup
-import android.annotation.SuppressLint
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -10,19 +7,20 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dot.gallery.feature_node.domain.model.Album
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
-import com.dot.gallery.feature_node.domain.use_case.MediaUseCases
+import com.dot.gallery.feature_node.domain.repository.MediaRepository
import com.dot.gallery.feature_node.presentation.ignored.IgnoredState
-import com.dot.gallery.feature_node.presentation.util.RepeatOnResume
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class IgnoredSetupViewModel @Inject constructor(
- private val mediaUseCases: MediaUseCases
+ private val repository: MediaRepository
): ViewModel() {
private val _uiState = MutableStateFlow(IgnoredSetupState())
@@ -30,20 +28,9 @@ class IgnoredSetupViewModel @Inject constructor(
var isLabelError by mutableStateOf(false)
- private val _ignoredState = MutableStateFlow(IgnoredState())
- val blacklistState = _ignoredState.asStateFlow()
-
- init {
- getIgnoredAlbums()
- }
-
- @SuppressLint("ComposableNaming")
- @Composable
- fun attachToLifecycle() {
- LaunchedEffect(Unit) {
- getIgnoredAlbums()
- }
- }
+ val blacklistState = repository.getBlacklistedAlbums()
+ .map { IgnoredState(it) }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), IgnoredState())
fun setLabel(label: String) {
_uiState.value = _uiState.value.copy(label = label)
@@ -68,15 +55,8 @@ class IgnoredSetupViewModel @Inject constructor(
fun addToIgnored(ignoredAlbum: IgnoredAlbum) {
viewModelScope.launch {
- mediaUseCases.blacklistUseCase.addToBlacklist(ignoredAlbum)
+ repository.addBlacklistedAlbum(ignoredAlbum)
}
}
- private fun getIgnoredAlbums() {
- viewModelScope.launch {
- mediaUseCases.blacklistUseCase.blacklistedAlbums.collectLatest {
- _ignoredState.emit(IgnoredState(it))
- }
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/SelectAlbumSheet.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/SelectAlbumSheet.kt
index 8db4e6e4b..d3a128a7f 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/SelectAlbumSheet.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/SelectAlbumSheet.kt
@@ -25,11 +25,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
import com.dot.gallery.core.Constants
import com.dot.gallery.core.Settings.Album.rememberAlbumGridSize
import com.dot.gallery.core.presentation.components.DragHandle
import com.dot.gallery.feature_node.domain.model.Album
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
import com.dot.gallery.feature_node.presentation.albums.components.AlbumComponent
import com.dot.gallery.feature_node.presentation.util.AppBottomSheetState
@@ -43,6 +43,7 @@ fun SelectAlbumSheet(
albumState: AlbumState,
onSelect: (Album) -> Unit
) {
+ val albumSize by rememberAlbumGridSize()
val scope = rememberCoroutineScope()
if (sheetState.isVisible) {
ModalBottomSheet(
@@ -71,7 +72,6 @@ fun SelectAlbumSheet(
.fillMaxWidth()
)
- val albumSize by rememberAlbumGridSize()
LazyVerticalGrid(
state = rememberLazyGridState(),
modifier = Modifier.padding(horizontal = 8.dp),
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupConfirmationScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupConfirmationScreen.kt
index 8bee761cb..8066654a7 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupConfirmationScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupConfirmationScreen.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
import com.dot.gallery.R
import com.dot.gallery.core.presentation.components.SetupWizard
import com.dot.gallery.feature_node.domain.model.Album
@@ -32,6 +33,7 @@ fun SetupConfirmationScreen(
title = stringResource(R.string.setup_confirmation_title),
subtitle = stringResource(R.string.setup_confirmation_subtitle),
icon = Icons.Outlined.Checklist,
+ contentPadding = 0.dp,
bottomBar = {
OutlinedButton(
onClick = onGoBack
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLabelScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLabelScreen.kt
index 7067c9330..79afb5964 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLabelScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLabelScreen.kt
@@ -41,6 +41,7 @@ fun SetupLabelScreen(
title = stringResource(R.string.setup_label_title),
subtitle = stringResource(R.string.setup_lavel_subtitle),
icon = Icons.Outlined.VisibilityOff,
+ contentPadding = 0.dp,
bottomBar = {
OutlinedButton(
onClick = onGoBack
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLocationScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLocationScreen.kt
index 4c8802094..f2face82b 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLocationScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupLocationScreen.kt
@@ -53,6 +53,7 @@ fun SetupLocationScreen(
title = stringResource(R.string.setup_location_title),
subtitle = stringResource(R.string.setup_location_subtitle),
icon = Icons.Outlined.Settings,
+ contentPadding = 0.dp,
bottomBar = {
OutlinedButton(
onClick = onGoBack
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeRegexScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeRegexScreen.kt
index 3a6b41a15..9f3377a4f 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeRegexScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeRegexScreen.kt
@@ -64,6 +64,7 @@ fun SetupTypeRegexScreen(
title = stringResource(R.string.setup_type_regex_title),
subtitle = stringResource(R.string.setup_type_regex_subtitle),
icon = Icons.RegularExpression,
+ contentPadding = 0.dp,
bottomBar = {
OutlinedButton(
onClick = onGoBack
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeSelectionScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeSelectionScreen.kt
index 120239cd1..bde5daaaf 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeSelectionScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/screens/SetupTypeSelectionScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -54,9 +55,9 @@ import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.star
import androidx.graphics.shapes.toPath
import com.dot.gallery.R
-import com.dot.gallery.core.AlbumState
import com.dot.gallery.core.presentation.components.SetupWizard
import com.dot.gallery.feature_node.domain.model.Album
+import com.dot.gallery.feature_node.domain.model.AlbumState
import com.dot.gallery.feature_node.domain.model.IgnoredAlbum
import com.dot.gallery.feature_node.presentation.ignored.setup.SelectAlbumSheet
import com.dot.gallery.feature_node.presentation.util.rememberAppBottomSheetState
@@ -69,7 +70,7 @@ fun SetupTypeSelectionScreen(
onNext: () -> Unit,
initialAlbum: Album?,
ignoredAlbums: List,
- albumsState: AlbumState,
+ albumsState: State,
onAlbumChanged: (Album?) -> Unit,
) {
var album by remember { mutableStateOf(initialAlbum) }
@@ -81,6 +82,7 @@ fun SetupTypeSelectionScreen(
title = stringResource(R.string.setup_type_selection_title),
subtitle = stringResource(R.string.setup_type_selection_subtitle),
icon = Icons.Outlined.PhotoAlbum,
+ contentPadding = 0.dp,
bottomBar = {
OutlinedButton(
onClick = onGoBack
@@ -205,7 +207,7 @@ fun SetupTypeSelectionScreen(
SelectAlbumSheet(
sheetState = pickAlbumState,
ignoredAlbums = ignoredAlbums,
- albumState = albumsState,
+ albumState = albumsState.value,
) { pickedAlbum ->
album = pickedAlbum
onAlbumChanged(pickedAlbum)
diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/library/LibraryScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/library/LibraryScreen.kt
index 948e40357..90f18bb04 100644
--- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/library/LibraryScreen.kt
+++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/library/LibraryScreen.kt
@@ -1,15 +1,13 @@
package com.dot.gallery.feature_node.presentation.library
-import android.os.Build
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.icons.Icons
@@ -17,8 +15,6 @@ import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.VisibilityOff
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
@@ -37,25 +33,28 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.dokar.pinchzoomgrid.PinchZoomGridLayout
import com.dokar.pinchzoomgrid.rememberPinchZoomGridState
import com.dot.gallery.R
import com.dot.gallery.core.Constants.albumCellsList
import com.dot.gallery.core.Settings.Album.rememberAlbumGridSize
-import com.dot.gallery.feature_node.presentation.common.MediaViewModel
+import com.dot.gallery.feature_node.presentation.library.components.LibrarySmallItem
import com.dot.gallery.feature_node.presentation.search.MainSearchBar
import com.dot.gallery.feature_node.presentation.util.Screen
import com.dot.gallery.ui.core.icons.Encrypted
+import com.dot.gallery.ui.core.Icons as GalleryIcons
@Composable
fun LibraryScreen(
navigate: (route: String) -> Unit,
- mediaViewModel: MediaViewModel,
toggleNavbar: (Boolean) -> Unit,
paddingValues: PaddingValues,
isScrolling: MutableState,
searchBarActive: MutableState