mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
update UI states and refactor code
Also, add repository as Single Source of Truth for performing operations on images
This commit is contained in:
parent
443e713955
commit
e611cbc86f
14 changed files with 262 additions and 49 deletions
|
|
@ -0,0 +1,29 @@
|
||||||
|
package fr.free.nrw.commons.customselector.data
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.domain.ImageRepository
|
||||||
|
import fr.free.nrw.commons.customselector.domain.model.Image
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ImageRepositoryImpl @Inject constructor(
|
||||||
|
private val mediaReader: MediaReader,
|
||||||
|
private val notForUploadStatusDao: NotForUploadStatusDao
|
||||||
|
): ImageRepository {
|
||||||
|
override suspend fun getImagesFromDevice(): Flow<Image> {
|
||||||
|
return mediaReader.getImages()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun markAsNotForUpload(imageSHA: String) {
|
||||||
|
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun unmarkAsNotForUpload(imageSHA: String) {
|
||||||
|
notForUploadStatusDao.deleteWithImageSHA1(imageSHA)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isNotForUpload(imageSHA: String): Boolean {
|
||||||
|
return notForUploadStatusDao.find(imageSHA) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package fr.free.nrw.commons.customselector.domain
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.customselector.domain.model.Image
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface ImageRepository {
|
||||||
|
|
||||||
|
suspend fun getImagesFromDevice(): Flow<Image>
|
||||||
|
|
||||||
|
suspend fun markAsNotForUpload(imageSHA: String)
|
||||||
|
|
||||||
|
suspend fun unmarkAsNotForUpload(imageSHA: String)
|
||||||
|
|
||||||
|
suspend fun isNotForUpload(imageSHA: String): Boolean
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package fr.free.nrw.commons.customselector.domain.use_case
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
||||||
|
import fr.free.nrw.commons.filepicker.PickedFiles
|
||||||
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import fr.free.nrw.commons.upload.FileProcessor
|
||||||
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okio.FileNotFoundException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ImageUseCase @Inject constructor(
|
||||||
|
private val fileUtilsWrapper: FileUtilsWrapper,
|
||||||
|
private val fileProcessor: FileProcessor,
|
||||||
|
private val mediaClient: MediaClient,
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the SHA1 hash of an image from its URI.
|
||||||
|
*
|
||||||
|
* @param uri The URI of the image.
|
||||||
|
* @return The SHA1 hash of the image, or an empty string if the image is not found.
|
||||||
|
*/
|
||||||
|
suspend fun getImageSHA1(uri: Uri): String = withContext(ioDispatcher) {
|
||||||
|
try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
fileUtilsWrapper.getSHA1(inputStream)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Timber.e(e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a modified SHA1 hash of an image after redacting sensitive EXIF tags.
|
||||||
|
*
|
||||||
|
* @param imageUri The URI of the image to process.
|
||||||
|
* @return The modified SHA1 hash of the image.
|
||||||
|
*/
|
||||||
|
suspend fun generateModifiedSHA1(imageUri: Uri): String = withContext(ioDispatcher) {
|
||||||
|
val uploadableFile = PickedFiles.pickedExistingPicture(context, imageUri)
|
||||||
|
val exifInterface: ExifInterface? = try {
|
||||||
|
ExifInterface(uploadableFile.file!!)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
|
||||||
|
|
||||||
|
val sha1 = fileUtilsWrapper.getSHA1(
|
||||||
|
fileUtilsWrapper.getFileInputStream(uploadableFile.filePath))
|
||||||
|
uploadableFile.file.delete()
|
||||||
|
sha1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a file with the given SHA1 hash exists on Wikimedia Commons.
|
||||||
|
*
|
||||||
|
* @param sha1 The SHA1 hash of the file to check.
|
||||||
|
* @return An ImageLoader.Result indicating the existence of the file on Commons.
|
||||||
|
*/
|
||||||
|
suspend fun checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||||
|
sha1: String
|
||||||
|
): ImageLoader.Result = withContext(ioDispatcher) {
|
||||||
|
return@withContext try {
|
||||||
|
if (mediaClient.checkFileExistsUsingSha(sha1).blockingGet()) {
|
||||||
|
ImageLoader.Result.TRUE
|
||||||
|
} else {
|
||||||
|
ImageLoader.Result.FALSE
|
||||||
|
}
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
Timber.e(e, "Network Connection Error")
|
||||||
|
ImageLoader.Result.ERROR
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
ImageLoader.Result.ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,12 +56,14 @@ import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorBottomBar
|
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorBottomBar
|
||||||
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorTopBar
|
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorTopBar
|
||||||
import fr.free.nrw.commons.customselector.ui.components.PartialStorageAccessDialog
|
import fr.free.nrw.commons.customselector.ui.components.PartialStorageAccessDialog
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.CustomSelectorUiState
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.ImageUiState
|
||||||
import fr.free.nrw.commons.ui.theme.CommonsTheme
|
import fr.free.nrw.commons.ui.theme.CommonsTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomSelectorScreen(
|
fun CustomSelectorScreen(
|
||||||
uiState: CustomSelectorState,
|
uiState: CustomSelectorUiState,
|
||||||
onEvent: (CustomSelectorEvent)-> Unit,
|
onEvent: (CustomSelectorEvent)-> Unit,
|
||||||
selectedImageIds: ()-> Set<Long>,
|
selectedImageIds: ()-> Set<Long>,
|
||||||
onViewImage: (id: Long)-> Unit,
|
onViewImage: (id: Long)-> Unit,
|
||||||
|
|
@ -112,7 +114,7 @@ fun CustomSelectorScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FoldersPane(
|
fun FoldersPane(
|
||||||
uiState: CustomSelectorState,
|
uiState: CustomSelectorUiState,
|
||||||
onFolderClick: (Folder)-> Unit,
|
onFolderClick: (Folder)-> Unit,
|
||||||
onUnselectAll: ()-> Unit,
|
onUnselectAll: ()-> Unit,
|
||||||
adaptiveInfo: WindowAdaptiveInfo,
|
adaptiveInfo: WindowAdaptiveInfo,
|
||||||
|
|
@ -270,7 +272,7 @@ private fun FolderItemPreview() {
|
||||||
private fun CustomSelectorScreenPreview() {
|
private fun CustomSelectorScreenPreview() {
|
||||||
CommonsTheme {
|
CommonsTheme {
|
||||||
CustomSelectorScreen(
|
CustomSelectorScreen(
|
||||||
uiState = CustomSelectorState(),
|
uiState = CustomSelectorUiState(),
|
||||||
onViewImage = { },
|
onViewImage = { },
|
||||||
onEvent = { },
|
onEvent = { },
|
||||||
selectedImageIds = { emptySet() },
|
selectedImageIds = { emptySet() },
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package fr.free.nrw.commons.customselector.ui.screens
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
|
||||||
|
|
||||||
data class CustomSelectorState(
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val folders: List<Folder> = emptyList(),
|
|
||||||
val filteredImages: List<Image> = emptyList(),
|
|
||||||
val selectedImageIds: Set<Long> = emptySet()
|
|
||||||
) {
|
|
||||||
val inSelectionMode: Boolean
|
|
||||||
get() = selectedImageIds.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +1,67 @@
|
||||||
package fr.free.nrw.commons.customselector.ui.screens
|
package fr.free.nrw.commons.customselector.ui.screens
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import fr.free.nrw.commons.customselector.data.MediaReader
|
import fr.free.nrw.commons.customselector.domain.ImageRepository
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.domain.model.Image
|
||||||
|
import fr.free.nrw.commons.customselector.domain.use_case.ImageUseCase
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.CustomSelectorUiState
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.ImageUiState
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.toImageUiState
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CustomSelectorViewModel(private val mediaReader: MediaReader): ViewModel() {
|
typealias imageId = Long
|
||||||
|
typealias imageSHA = String
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(CustomSelectorState())
|
class CustomSelectorViewModel @Inject constructor(
|
||||||
|
private val imageRepository: ImageRepository,
|
||||||
|
private val imageUseCase: ImageUseCase
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(CustomSelectorUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val cacheSHA1 = mutableMapOf<imageId, imageSHA>()
|
||||||
|
|
||||||
|
private val allImages = mutableListOf<ImageUiState>()
|
||||||
private val foldersMap = mutableMapOf<Long, MutableList<Image>>()
|
private val foldersMap = mutableMapOf<Long, MutableList<Image>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_uiState.update { it.copy(isLoading = true) }
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mediaReader.getImages().collect { image->
|
imageRepository.getImagesFromDevice().collect { image ->
|
||||||
val bucketId = image.bucketId
|
val bucketId = image.bucketId
|
||||||
|
|
||||||
|
allImages.add(image.toImageUiState())
|
||||||
foldersMap.getOrPut(bucketId) { mutableListOf() }.add(image)
|
foldersMap.getOrPut(bucketId) { mutableListOf() }.add(image)
|
||||||
}
|
}
|
||||||
val foldersList = foldersMap.map { (bucketId, images)->
|
val folders = foldersMap.map { (bucketId, images)->
|
||||||
val firstImage = images.first()
|
val firstImage = images.first()
|
||||||
Folder(
|
Folder(
|
||||||
bucketId = bucketId, bucketName = firstImage.bucketName,
|
bucketId = bucketId,
|
||||||
preview = firstImage.uri, itemsCount = images.size
|
bucketName = firstImage.bucketName,
|
||||||
|
preview = firstImage.uri,
|
||||||
|
itemsCount = images.size,
|
||||||
|
images = images
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(isLoading = false, folders = foldersList) }
|
_uiState.update { it.copy(isLoading = false, folders = folders) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEvent(e: CustomSelectorEvent) {
|
fun onEvent(e: CustomSelectorEvent) {
|
||||||
when(e) {
|
when(e) {
|
||||||
is CustomSelectorEvent.OnFolderClick-> {
|
is CustomSelectorEvent.OnFolderClick -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(filteredImages = foldersMap[e.bucketId]?.toList() ?: emptyList())
|
it.copy(
|
||||||
|
filteredImages = foldersMap[e.bucketId]?.map {
|
||||||
|
img -> img.toImageUiState()
|
||||||
|
} ?: emptyList()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +69,7 @@ class CustomSelectorViewModel(private val mediaReader: MediaReader): ViewModel()
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
val updatedSelectedIds = if (state.selectedImageIds.contains(e.imageId)) {
|
val updatedSelectedIds = if (state.selectedImageIds.contains(e.imageId)) {
|
||||||
state.selectedImageIds - e.imageId // Remove if already selected
|
state.selectedImageIds - e.imageId // Remove if already selected
|
||||||
} else{
|
} else {
|
||||||
state.selectedImageIds + e.imageId // Add if not selected
|
state.selectedImageIds + e.imageId // Add if not selected
|
||||||
}
|
}
|
||||||
state.copy(selectedImageIds = updatedSelectedIds)
|
state.copy(selectedImageIds = updatedSelectedIds)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.customselector.ui.screens
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import fr.free.nrw.commons.customselector.domain.model.Image
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -9,5 +10,6 @@ data class Folder(
|
||||||
val bucketId: Long,
|
val bucketId: Long,
|
||||||
val bucketName: String,
|
val bucketName: String,
|
||||||
val preview: Uri,
|
val preview: Uri,
|
||||||
|
val images: List<Image>,
|
||||||
val itemsCount: Int
|
val itemsCount: Int
|
||||||
): Parcelable
|
): Parcelable
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
@ -54,18 +55,20 @@ import androidx.compose.ui.unit.toIntRect
|
||||||
import androidx.window.core.layout.WindowWidthSizeClass
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.domain.model.Image
|
|
||||||
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorBottomBar
|
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorBottomBar
|
||||||
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorTopBar
|
import fr.free.nrw.commons.customselector.ui.components.CustomSelectorTopBar
|
||||||
import fr.free.nrw.commons.customselector.ui.components.PartialStorageAccessDialog
|
import fr.free.nrw.commons.customselector.ui.components.PartialStorageAccessDialog
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.CustomSelectorUiState
|
||||||
|
import fr.free.nrw.commons.customselector.ui.states.ImageUiState
|
||||||
import fr.free.nrw.commons.ui.theme.CommonsTheme
|
import fr.free.nrw.commons.ui.theme.CommonsTheme
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImagesPane(
|
fun ImagesPane(
|
||||||
uiState: CustomSelectorState,
|
uiState: CustomSelectorUiState,
|
||||||
selectedFolder: Folder,
|
selectedFolder: Folder,
|
||||||
selectedImages: () -> Set<Long>,
|
selectedImages: () -> Set<Long>,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
|
@ -74,7 +77,6 @@ fun ImagesPane(
|
||||||
adaptiveInfo: WindowAdaptiveInfo,
|
adaptiveInfo: WindowAdaptiveInfo,
|
||||||
hasPartialAccess: Boolean = false
|
hasPartialAccess: Boolean = false
|
||||||
) {
|
) {
|
||||||
// val inSelectionMode by remember { derivedStateOf { selectedImages().isNotEmpty() } }
|
|
||||||
val lazyGridState = rememberLazyGridState()
|
val lazyGridState = rememberLazyGridState()
|
||||||
var autoScrollSpeed by remember { mutableFloatStateOf(0f) }
|
var autoScrollSpeed by remember { mutableFloatStateOf(0f) }
|
||||||
val isCompatWidth by remember(adaptiveInfo.windowSizeClass) {
|
val isCompatWidth by remember(adaptiveInfo.windowSizeClass) {
|
||||||
|
|
@ -255,7 +257,7 @@ private fun ImageItemPreview() {
|
||||||
*/
|
*/
|
||||||
fun Modifier.imageGridDragHandler(
|
fun Modifier.imageGridDragHandler(
|
||||||
gridState: LazyGridState,
|
gridState: LazyGridState,
|
||||||
imageList: List<Image>,
|
imageList: List<ImageUiState>,
|
||||||
selectedImageIds: () -> Set<Long>,
|
selectedImageIds: () -> Set<Long>,
|
||||||
autoScrollThreshold: Float,
|
autoScrollThreshold: Float,
|
||||||
setSelectedImageIds: (Set<Long>) -> Unit,
|
setSelectedImageIds: (Set<Long>) -> Unit,
|
||||||
|
|
@ -337,7 +339,7 @@ fun Modifier.imageGridDragHandler(
|
||||||
* @param pointerKey The ending index of the range.
|
* @param pointerKey The ending index of the range.
|
||||||
* @return A set of image IDs within the specified range.
|
* @return A set of image IDs within the specified range.
|
||||||
*/
|
*/
|
||||||
fun List<Image>.getImageIdsInRange(initialKey: Int, pointerKey: Int): Set<Long> {
|
fun List<ImageUiState>.getImageIdsInRange(initialKey: Int, pointerKey: Int): Set<Long> {
|
||||||
val setOfKeys = mutableSetOf<Long>()
|
val setOfKeys = mutableSetOf<Long>()
|
||||||
if (initialKey < pointerKey) {
|
if (initialKey < pointerKey) {
|
||||||
(initialKey..pointerKey).forEach {
|
(initialKey..pointerKey).forEach {
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import fr.free.nrw.commons.customselector.domain.model.Image
|
import fr.free.nrw.commons.customselector.ui.states.ImageUiState
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ViewImageScreen(
|
fun ViewImageScreen(
|
||||||
currentImageIndex: Int,
|
currentImageIndex: Int,
|
||||||
imageList: List<Image>,
|
imageList: List<ImageUiState>,
|
||||||
) {
|
) {
|
||||||
var imageScale by remember { mutableFloatStateOf(1f) }
|
var imageScale by remember { mutableFloatStateOf(1f) }
|
||||||
var imageOffset by remember { mutableStateOf(Offset.Zero) }
|
var imageOffset by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
|
|
||||||
|
|
@ -27,21 +27,20 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.data.MediaReader
|
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.domain.model.Image
|
||||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
||||||
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper
|
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper
|
||||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.domain.model.Image
|
|
||||||
import fr.free.nrw.commons.customselector.ui.screens.CustomSelectorScreen
|
import fr.free.nrw.commons.customselector.ui.screens.CustomSelectorScreen
|
||||||
import fr.free.nrw.commons.customselector.ui.screens.ViewImageScreen
|
import fr.free.nrw.commons.customselector.ui.screens.ViewImageScreen
|
||||||
|
import fr.free.nrw.commons.customselector.utils.CustomSelectorViewModelFactory
|
||||||
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
||||||
|
|
@ -191,17 +190,11 @@ class CustomSelectorActivity :
|
||||||
// setContentView(view)
|
// setContentView(view)
|
||||||
|
|
||||||
prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE)
|
prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE)
|
||||||
viewModel =
|
|
||||||
ViewModelProvider(this, customSelectorViewModelFactory).get(
|
|
||||||
CustomSelectorViewModel::class.java,
|
|
||||||
)
|
|
||||||
|
|
||||||
val mediaReader = MediaReader(this)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val csViewModel = viewModel<fr.free.nrw.commons.customselector.ui.screens.CustomSelectorViewModel> {
|
val csViewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(
|
||||||
fr.free.nrw.commons.customselector.ui.screens.CustomSelectorViewModel(mediaReader)
|
fr.free.nrw.commons.customselector.ui.screens.CustomSelectorViewModel::class.java
|
||||||
}
|
)
|
||||||
|
|
||||||
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|
@ -265,7 +258,7 @@ class CustomSelectorActivity :
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
fetchData()
|
// fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package fr.free.nrw.commons.customselector.ui.states
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.customselector.ui.screens.Folder
|
||||||
|
import fr.free.nrw.commons.customselector.ui.screens.imageId
|
||||||
|
|
||||||
|
typealias isNotForUpload = Boolean
|
||||||
|
|
||||||
|
data class CustomSelectorUiState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val folders: List<Folder> = emptyList(),
|
||||||
|
val filteredImages: List<ImageUiState> = emptyList(),
|
||||||
|
val selectedImageIds: Set<Long> = emptySet(),
|
||||||
|
val imagesNotForUpload: Map<imageId, isNotForUpload> = emptyMap()
|
||||||
|
) {
|
||||||
|
val inSelectionMode: Boolean
|
||||||
|
get() = selectedImageIds.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package fr.free.nrw.commons.customselector.ui.states
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import fr.free.nrw.commons.customselector.domain.model.Image
|
||||||
|
|
||||||
|
data class ImageUiState(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val uri: Uri,
|
||||||
|
val bucketId: Long,
|
||||||
|
val isNotForUpload: Boolean = false,
|
||||||
|
val isUploaded: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Image.toImageUiState() = ImageUiState(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
uri = uri,
|
||||||
|
bucketId = bucketId
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.customselector.utils
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import fr.free.nrw.commons.customselector.domain.ImageRepository
|
||||||
|
import fr.free.nrw.commons.customselector.domain.use_case.ImageUseCase
|
||||||
|
import fr.free.nrw.commons.customselector.ui.screens.CustomSelectorViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CustomSelectorViewModelFactory @Inject constructor(
|
||||||
|
private val imageRepository: ImageRepository,
|
||||||
|
private val imageUseCase: ImageUseCase
|
||||||
|
): ViewModelProvider.Factory {
|
||||||
|
override fun <CustomSelectorViewModel : ViewModel> create(
|
||||||
|
modelClass: Class<CustomSelectorViewModel>
|
||||||
|
): CustomSelectorViewModel {
|
||||||
|
return CustomSelectorViewModel(
|
||||||
|
imageRepository, imageUseCase
|
||||||
|
) as CustomSelectorViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,12 @@ import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
|
import fr.free.nrw.commons.customselector.data.ImageRepositoryImpl;
|
||||||
|
import fr.free.nrw.commons.customselector.data.MediaReader;
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
|
||||||
|
import fr.free.nrw.commons.customselector.domain.ImageRepository;
|
||||||
|
import fr.free.nrw.commons.customselector.domain.use_case.ImageUseCase;
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
|
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.db.AppDatabase;
|
import fr.free.nrw.commons.db.AppDatabase;
|
||||||
|
|
@ -317,4 +321,13 @@ public class CommonsApplicationModule {
|
||||||
public ContentResolver providesContentResolver(Context context){
|
public ContentResolver providesContentResolver(Context context){
|
||||||
return context.getContentResolver();
|
return context.getContentResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public ImageRepository providesImageRepository(
|
||||||
|
MediaReader mediaReader,
|
||||||
|
NotForUploadStatusDao notForUploadStatusDao,
|
||||||
|
ImageUseCase imageUseCase
|
||||||
|
) {
|
||||||
|
return new ImageRepositoryImpl(mediaReader, notForUploadStatusDao);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue