From 846cf66fa1c91c2e813289a5e4d38353df3f9376 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 28 Sep 2022 17:58:48 +0100 Subject: [PATCH 01/10] adding runtime permission granter --- .../kotlin/app/dapk/st/core/DapkActivity.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt index 29ee41b6..d297ca6d 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt @@ -1,15 +1,19 @@ package app.dapk.st.core +import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.design.components.ThemeConfig import app.dapk.st.navigator.navigator +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume import androidx.activity.compose.setContent as _setContent abstract class DapkActivity : ComponentActivity(), EffectScope { @@ -27,7 +31,6 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { super.onCreate(savedInstanceState) this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled()) - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } @@ -64,4 +67,32 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { } else super.onBackPressed() } + + protected suspend fun ensurePermission(permission: String): PermissionResult { + return when { + checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED -> PermissionResult.Granted + + shouldShowRequestPermissionRationale(permission) -> PermissionResult.ShowRational + + else -> { + val isGranted = suspendCancellableCoroutine { continuation -> + val callback: (result: Boolean) -> Unit = { result -> continuation.resume(result) } + val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission(), callback) + launcher.launch(permission) + continuation.invokeOnCancellation { launcher.unregister() } + } + + when (isGranted) { + true -> PermissionResult.Granted + false -> PermissionResult.Denied + } + } + } + } +} + +sealed interface PermissionResult { + object Granted : PermissionResult + object ShowRational : PermissionResult + object Denied : PermissionResult } From debfc5e5f0730aac6bb7c34741ec1cee700bb36e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 28 Sep 2022 19:51:24 +0100 Subject: [PATCH 02/10] adding first pass at a image gallery component with folder fetching --- app/src/main/AndroidManifest.xml | 12 +- features/home/build.gradle | 1 + .../kotlin/app/dapk/st/home/MainActivity.kt | 132 ++++++++++++++++-- .../home/gallery/FetchMediaFoldersUseCase.kt | 72 ++++++++++ 4 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6e493a2a..b478f916 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,10 @@ + package="app.dapk.st"> - + + + - - + + + android:resource="@xml/shortcuts"/> diff --git a/features/home/build.gradle b/features/home/build.gradle index 3265a1f0..0422dfad 100644 --- a/features/home/build.gradle +++ b/features/home/build.gradle @@ -13,4 +13,5 @@ dependencies { implementation project(':domains:store') implementation project(":core") implementation project(":design-library") + implementation Dependencies.mavenCentral.coil } \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index 609f52dd..5170101d 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -1,22 +1,49 @@ package app.dapk.st.home +import android.Manifest import android.os.Bundle -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.AlertDialog import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.lifecycleScope import app.dapk.st.core.DapkActivity +import app.dapk.st.core.PermissionResult import app.dapk.st.core.module import app.dapk.st.core.viewModel +import app.dapk.st.design.components.Toolbar import app.dapk.st.directory.DirectoryModule +import app.dapk.st.home.gallery.FetchMediaFoldersUseCase +import app.dapk.st.home.gallery.Folder import app.dapk.st.login.LoginModule import app.dapk.st.profile.ProfileModule -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest +import kotlinx.coroutines.launch class MainActivity : DapkActivity() { @@ -27,21 +54,46 @@ class MainActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - homeViewModel.events.onEach { - when (it) { - HomeEvent.Relaunch -> recreate() + + val state = mutableStateOf(emptyList()) + + lifecycleScope.launch { + when (ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + PermissionResult.Denied -> { + } + + PermissionResult.Granted -> { + state.value = FetchMediaFoldersUseCase(contentResolver).fetchFolders() + } + + PermissionResult.ShowRational -> { + + } } - }.launchIn(lifecycleScope) + + } setContent { - if (homeViewModel.hasVersionChanged()) { - BetaUpgradeDialog() - } else { - Surface(Modifier.fillMaxSize()) { - HomeScreen(homeViewModel) - } + Surface { + ImageGallery(state) } } + +// homeViewModel.events.onEach { +// when (it) { +// HomeEvent.Relaunch -> recreate() +// } +// }.launchIn(lifecycleScope) +// +// setContent { +// if (homeViewModel.hasVersionChanged()) { +// BetaUpgradeDialog() +// } else { +// Surface(Modifier.fillMaxSize()) { +// HomeScreen(homeViewModel) +// } +// } +// } } @Composable @@ -60,3 +112,57 @@ class MainActivity : DapkActivity() { ) } } + +@Composable +fun ImageGallery(state: State>) { + var boxWidth by remember { mutableStateOf(IntSize.Zero) } + val localDensity = LocalDensity.current + val screenWidth = LocalConfiguration.current.screenWidthDp + + Column { + Toolbar(title = "Send to Awesome Room", onNavigate = {}) + val columns = when { + screenWidth > 600 -> 4 + else -> 2 + } + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier.fillMaxSize(), + ) { + items(state.value, key = { it.bucketId }) { + Box(modifier = Modifier.fillMaxWidth().padding(2.dp).onGloballyPositioned { + boxWidth = it.size + }) { + Image( + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(it.thumbnail.toString()) + .build(), + ), + contentDescription = "123", + modifier = Modifier.fillMaxWidth().height(with(localDensity) { boxWidth.width.toDp() }), + contentScale = ContentScale.Crop + ) + + val gradient = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), + startY = boxWidth.width.toFloat() * 0.5f, + endY = boxWidth.width.toFloat() + ) + + Box(modifier = Modifier.matchParentSize().background(gradient)) + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(it.title, fontSize = 13.sp, color = Color.White) + Text(it.itemCount.toString(), fontSize = 11.sp, color = Color.White) + } + + } + } + } + } + +} diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt b/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt new file mode 100644 index 00000000..43337537 --- /dev/null +++ b/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt @@ -0,0 +1,72 @@ +package app.dapk.st.home.gallery + +import android.content.ContentResolver +import android.content.ContentUris +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.provider.MediaStore.Images +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +// https://github.com/signalapp/Signal-Android/blob/e22ddb8f96f8801f0abe622b5261abc6cb396d94/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java + +class FetchMediaFoldersUseCase( + private val contentResolver: ContentResolver, +) { + + + suspend fun fetchFolders(): List { + val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED) + val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?" + val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC" + + val folders = mutableMapOf() + val contentUri = Images.Media.EXTERNAL_CONTENT_URI + withContext(Dispatchers.IO) { + contentResolver.query(contentUri, projection, selection, arrayOf("%image/svg%"), sortBy).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0])) + val thumbnail = ContentUris.withAppendedId(contentUri, rowId) + val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1])) + val title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2])) ?: "" + val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3])) + + val folder = folders.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) } + folder.incrementItemCount() + +// val folder: FolderData = Util.getOrDefault(folders, bucketId, FolderData(thumbnail, localizeTitle(context, title), bucketId)) +// folder.incrementCount() +// folders.put(bucketId, folder) +// if (cameraBucketId == null && title == "Camera") { +// cameraBucketId = bucketId +// } +// if (timestamp > thumbnailTimestamp) { +// globalThumbnail = thumbnail +// thumbnailTimestamp = timestamp +// } + } + } + } + return folders.values.toList() + } + + private fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1" + +} + +data class Folder( + val bucketId: String, + val title: String, + val thumbnail: Uri, +) { + private var _itemCount: Long = 0L + val itemCount: Long + get() = _itemCount + + fun incrementItemCount() { + _itemCount++ + } + +} \ No newline at end of file From 6f89c7130036b21045ff0a5572124b9d6b2d2798 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 28 Sep 2022 22:37:53 +0100 Subject: [PATCH 03/10] adding skeleton for image gallery folder viewing --- .../kotlin/app/dapk/st/home/MainActivity.kt | 288 ++++++++++++++---- .../home/gallery/FetchMediaFoldersUseCase.kt | 86 ++++-- 2 files changed, 291 insertions(+), 83 deletions(-) diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index 5170101d..d6c6734e 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -1,9 +1,9 @@ package app.dapk.st.home -import android.Manifest import android.os.Bundle import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -13,7 +13,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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 @@ -30,19 +29,23 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.lifecycleScope -import app.dapk.st.core.DapkActivity -import app.dapk.st.core.PermissionResult -import app.dapk.st.core.module -import app.dapk.st.core.viewModel -import app.dapk.st.design.components.Toolbar +import androidx.lifecycle.viewModelScope +import app.dapk.st.core.* +import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.design.components.Route +import app.dapk.st.design.components.Spider +import app.dapk.st.design.components.SpiderPage import app.dapk.st.directory.DirectoryModule import app.dapk.st.home.gallery.FetchMediaFoldersUseCase +import app.dapk.st.home.gallery.FetchMediaUseCase import app.dapk.st.home.gallery.Folder +import app.dapk.st.home.gallery.Media import app.dapk.st.login.LoginModule import app.dapk.st.profile.ProfileModule +import app.dapk.st.viewmodel.DapkViewModel import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest +import kotlinx.coroutines.Job import kotlinx.coroutines.launch class MainActivity : DapkActivity() { @@ -55,27 +58,31 @@ class MainActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val state = mutableStateOf(emptyList()) - - lifecycleScope.launch { - when (ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - PermissionResult.Denied -> { - } - - PermissionResult.Granted -> { - state.value = FetchMediaFoldersUseCase(contentResolver).fetchFolders() - } - - PermissionResult.ShowRational -> { - - } - } + val viewModel = ImageGalleryViewModel( + FetchMediaFoldersUseCase(contentResolver), + FetchMediaUseCase(contentResolver), + ) - } +// lifecycleScope.launch { +// when (ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { +// PermissionResult.Denied -> { +// } +// +// PermissionResult.Granted -> { +// state.value = FetchMediaFoldersUseCase(contentResolver).fetchFolders() +// } +// +// PermissionResult.ShowRational -> { +// +// } +// } +// } setContent { Surface { - ImageGallery(state) + ImageGalleryScreen(viewModel) { + finish() + } } } @@ -113,56 +120,215 @@ class MainActivity : DapkActivity() { } } + +data class ImageGalleryState( + val page: SpiderPage, +) + + +sealed interface ImageGalleryPage { + data class Folders(val content: Lce>) : ImageGalleryPage + data class Files(val content: Lce>) : ImageGalleryPage + + object Routes { + val folders = Route("Folders") + val files = Route("Files") + } +} + + +sealed interface ImageGalleryEvent + +class ImageGalleryViewModel( + private val foldersUseCase: FetchMediaFoldersUseCase, + private val fetchMediaUseCase: FetchMediaUseCase, +) : DapkViewModel( + initialState = ImageGalleryState(page = SpiderPage(route = ImageGalleryPage.Routes.folders, "", null, ImageGalleryPage.Folders(Lce.Loading()))) +) { + + private var currentPageJob: Job? = null + + fun start() { + currentPageJob?.cancel() + currentPageJob = viewModelScope.launch { + val folders = foldersUseCase.fetchFolders() + updatePageState { copy(content = Lce.Content(folders)) } + } + + } + + fun goTo(page: SpiderPage) { + currentPageJob?.cancel() + updateState { copy(page = page) } + } + + fun selectFolder(folder: Folder) { + currentPageJob?.cancel() + + updateState { + copy( + page = SpiderPage( + route = ImageGalleryPage.Routes.files, + label = page.label, + parent = ImageGalleryPage.Routes.folders, + state = ImageGalleryPage.Files(Lce.Loading()) + ) + ) + } + + currentPageJob = viewModelScope.launch { + val media = fetchMediaUseCase.getMediaInBucket(folder.bucketId) + updatePageState { + copy(content = Lce.Content(media)) + } + } + } + + @Suppress("UNCHECKED_CAST") + private inline fun updatePageState(crossinline block: S.() -> S) { + val page = state.page + val currentState = page.state + require(currentState is S) + updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } + } + +} + +@Composable +fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> Unit) { + LifecycleEffect(onStart = { + viewModel.start() + }) + + val onNavigate: (SpiderPage?) -> Unit = { + when (it) { + null -> onTopLevelBack() + else -> viewModel.goTo(it) + } + } + + Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { + item(ImageGalleryPage.Routes.folders) { + ImageGalleryFolders(it) { folder -> + viewModel.selectFolder(folder) + } + } + item(ImageGalleryPage.Routes.files) { + ImageGalleryMedia(it) + } + } + +} + +@Composable +fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) { + var boxWidth by remember { mutableStateOf(IntSize.Zero) } + val localDensity = LocalDensity.current + val screenWidth = LocalConfiguration.current.screenWidthDp + + when (val content = state.content) { + is Lce.Loading -> { + CenteredLoading() + } + + is Lce.Content -> { + Column { + val columns = when { + screenWidth > 600 -> 4 + else -> 2 + } + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier.fillMaxSize(), + ) { + items(content.value, key = { it.bucketId }) { + Box(modifier = Modifier.fillMaxWidth().padding(2.dp) + .clickable { onClick(it) } + .onGloballyPositioned { + boxWidth = it.size + }) { + Image( + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(it.thumbnail.toString()) + .build(), + ), + contentDescription = "123", + modifier = Modifier.fillMaxWidth().height(with(localDensity) { boxWidth.width.toDp() }), + contentScale = ContentScale.Crop + ) + + val gradient = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), + startY = boxWidth.width.toFloat() * 0.5f, + endY = boxWidth.width.toFloat() + ) + + Box(modifier = Modifier.matchParentSize().background(gradient)) + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(it.title, fontSize = 13.sp, color = Color.White) + Text(it.itemCount.toString(), fontSize = 11.sp, color = Color.White) + } + } + } + } + } + } + + is Lce.Error -> TODO() + } +} + @Composable -fun ImageGallery(state: State>) { +fun ImageGalleryMedia(state: ImageGalleryPage.Files) { var boxWidth by remember { mutableStateOf(IntSize.Zero) } val localDensity = LocalDensity.current val screenWidth = LocalConfiguration.current.screenWidthDp Column { - Toolbar(title = "Send to Awesome Room", onNavigate = {}) val columns = when { screenWidth > 600 -> 4 else -> 2 } - LazyVerticalGrid( - columns = GridCells.Fixed(columns), - modifier = Modifier.fillMaxSize(), - ) { - items(state.value, key = { it.bucketId }) { - Box(modifier = Modifier.fillMaxWidth().padding(2.dp).onGloballyPositioned { - boxWidth = it.size - }) { - Image( - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(it.thumbnail.toString()) - .build(), - ), - contentDescription = "123", - modifier = Modifier.fillMaxWidth().height(with(localDensity) { boxWidth.width.toDp() }), - contentScale = ContentScale.Crop - ) - - val gradient = Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), - startY = boxWidth.width.toFloat() * 0.5f, - endY = boxWidth.width.toFloat() - ) - - Box(modifier = Modifier.matchParentSize().background(gradient)) - Row( - modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(it.title, fontSize = 13.sp, color = Color.White) - Text(it.itemCount.toString(), fontSize = 11.sp, color = Color.White) - } + when (val content = state.content) { + is Lce.Loading -> { + CenteredLoading() + } + is Lce.Content -> { + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier.fillMaxSize(), + ) { + items(content.value, key = { it.id }) { + Box(modifier = Modifier.fillMaxWidth().padding(2.dp).onGloballyPositioned { + boxWidth = it.size + }) { + Image( + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(it.uri.toString()) + .crossfade(true) + .build(), + ), + contentDescription = "123", + modifier = Modifier.fillMaxWidth().height(with(localDensity) { boxWidth.width.toDp() }), + contentScale = ContentScale.Crop + ) + } + } } } + + is Lce.Error -> TODO() } + } } + + diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt b/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt index 43337537..d4d0e66a 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt @@ -16,15 +16,14 @@ class FetchMediaFoldersUseCase( private val contentResolver: ContentResolver, ) { - suspend fun fetchFolders(): List { - val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED) - val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?" - val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC" + return withContext(Dispatchers.IO) { + val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED) + val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?" + val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC" - val folders = mutableMapOf() - val contentUri = Images.Media.EXTERNAL_CONTENT_URI - withContext(Dispatchers.IO) { + val folders = mutableMapOf() + val contentUri = Images.Media.EXTERNAL_CONTENT_URI contentResolver.query(contentUri, projection, selection, arrayOf("%image/svg%"), sortBy).use { cursor -> while (cursor != null && cursor.moveToNext()) { val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0])) @@ -32,27 +31,14 @@ class FetchMediaFoldersUseCase( val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1])) val title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2])) ?: "" val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3])) - val folder = folders.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) } folder.incrementItemCount() - -// val folder: FolderData = Util.getOrDefault(folders, bucketId, FolderData(thumbnail, localizeTitle(context, title), bucketId)) -// folder.incrementCount() -// folders.put(bucketId, folder) -// if (cameraBucketId == null && title == "Camera") { -// cameraBucketId = bucketId -// } -// if (timestamp > thumbnailTimestamp) { -// globalThumbnail = thumbnail -// thumbnailTimestamp = timestamp -// } } } + folders.values.toList() } - return folders.values.toList() } - private fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1" } @@ -69,4 +55,60 @@ data class Folder( _itemCount++ } -} \ No newline at end of file +} + + +class FetchMediaUseCase(private val contentResolver: ContentResolver) { + + private val projection = arrayOf( + Images.Media._ID, + Images.Media.MIME_TYPE, + Images.Media.DATE_MODIFIED, + Images.Media.ORIENTATION, + Images.Media.WIDTH, + Images.Media.HEIGHT, + Images.Media.SIZE + ) + + suspend fun getMediaInBucket(bucketId: String): List { + return withContext(Dispatchers.IO) { + + val media = mutableListOf() + val selection = Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + Images.Media.MIME_TYPE + " NOT LIKE ?" + val selectionArgs = arrayOf(bucketId, "%image/svg%") + val sortBy = Images.Media.DATE_MODIFIED + " DESC" + val contentUri = Images.Media.EXTERNAL_CONTENT_URI + contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0])) + val uri = ContentUris.withAppendedId(contentUri, rowId) + val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)) + val date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED)) + val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) + val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation))) + val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation))) + val size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)) + media.add(Media(rowId, uri, mimetype, width, height, size, date)) + } + } + media + } + } + + private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) Images.Media.WIDTH else Images.Media.HEIGHT + + private fun getHeightColumn(orientation: Int) = if (orientation == 0 || orientation == 180) Images.Media.HEIGHT else Images.Media.WIDTH + +} + +data class Media( + val id: Long, + val uri: Uri, + val mimeType: String, + val width: Int, + val height: Int, + val size: Long, + val dateModifiedEpochMillis: Long, +) + +private fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1" From 34e0415892ea25390c9148f0e694370a25ef6aae Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 13:11:23 +0100 Subject: [PATCH 04/10] fixing performance issue when loading large lists of images --- .../kotlin/app/dapk/st/home/MainActivity.kt | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index d6c6734e..6d150f7a 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -13,25 +13,19 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewModelScope import app.dapk.st.core.* import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.design.components.GenericError import app.dapk.st.design.components.Route import app.dapk.st.design.components.Spider import app.dapk.st.design.components.SpiderPage @@ -222,10 +216,12 @@ fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> U @Composable fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) { - var boxWidth by remember { mutableStateOf(IntSize.Zero) } - val localDensity = LocalDensity.current val screenWidth = LocalConfiguration.current.screenWidthDp + val gradient = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), + ) + when (val content = state.content) { is Lce.Loading -> { CenteredLoading() @@ -242,11 +238,8 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un modifier = Modifier.fillMaxSize(), ) { items(content.value, key = { it.bucketId }) { - Box(modifier = Modifier.fillMaxWidth().padding(2.dp) - .clickable { onClick(it) } - .onGloballyPositioned { - boxWidth = it.size - }) { + Box(modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f) + .clickable { onClick(it) }) { Image( painter = rememberAsyncImagePainter( model = ImageRequest.Builder(LocalContext.current) @@ -254,17 +247,11 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un .build(), ), contentDescription = "123", - modifier = Modifier.fillMaxWidth().height(with(localDensity) { boxWidth.width.toDp() }), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) - val gradient = Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), - startY = boxWidth.width.toFloat() * 0.5f, - endY = boxWidth.width.toFloat() - ) - - Box(modifier = Modifier.matchParentSize().background(gradient)) + Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.6f).background(gradient).align(Alignment.BottomStart)) Row( modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp), horizontalArrangement = Arrangement.SpaceBetween, @@ -279,14 +266,12 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un } } - is Lce.Error -> TODO() + is Lce.Error -> GenericError { } } } @Composable fun ImageGalleryMedia(state: ImageGalleryPage.Files) { - var boxWidth by remember { mutableStateOf(IntSize.Zero) } - val localDensity = LocalDensity.current val screenWidth = LocalConfiguration.current.screenWidthDp Column { @@ -299,15 +284,15 @@ fun ImageGalleryMedia(state: ImageGalleryPage.Files) { is Lce.Loading -> { CenteredLoading() } + is Lce.Content -> { LazyVerticalGrid( columns = GridCells.Fixed(columns), modifier = Modifier.fillMaxSize(), ) { + val modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f) items(content.value, key = { it.id }) { - Box(modifier = Modifier.fillMaxWidth().padding(2.dp).onGloballyPositioned { - boxWidth = it.size - }) { + Box(modifier = modifier) { Image( painter = rememberAsyncImagePainter( model = ImageRequest.Builder(LocalContext.current) @@ -316,7 +301,7 @@ fun ImageGalleryMedia(state: ImageGalleryPage.Files) { .build(), ), contentDescription = "123", - modifier = Modifier.fillMaxWidth().height(with(localDensity) { boxWidth.width.toDp() }), + modifier = Modifier.fillMaxWidth().fillMaxHeight(), contentScale = ContentScale.Crop ) } @@ -324,7 +309,7 @@ fun ImageGalleryMedia(state: ImageGalleryPage.Files) { } } - is Lce.Error -> TODO() + is Lce.Error -> GenericError { } } } From c61646bbd372ec340cf9d843f387c78d931ae3bb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 13:50:58 +0100 Subject: [PATCH 05/10] creating dedicated activity for image selection --- .../kotlin/app/dapk/st/core/DapkActivity.kt | 4 +- features/home/src/main/AndroidManifest.xml | 7 +- .../kotlin/app/dapk/st/home/MainActivity.kt | 273 ++---------------- .../st/home/gallery/ImageGalleryActivity.kt | 79 +++++ .../st/home/gallery/ImageGalleryScreen.kt | 160 ++++++++++ .../st/home/gallery/ImageGalleryViewModel.kt | 73 +++++ .../kotlin/app/dapk/st/navigator/Navigator.kt | 1 + 7 files changed, 344 insertions(+), 253 deletions(-) create mode 100644 features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt create mode 100644 features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt create mode 100644 features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt index d297ca6d..fbcf0ed0 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt @@ -61,11 +61,13 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { } } + @Suppress("OVERRIDE_DEPRECATION") override fun onBackPressed() { if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) { finishAfterTransition() - } else + } else { super.onBackPressed() + } } protected suspend fun ensurePermission(permission: String): PermissionResult { diff --git a/features/home/src/main/AndroidManifest.xml b/features/home/src/main/AndroidManifest.xml index 3e51f2ca..ccee2955 100644 --- a/features/home/src/main/AndroidManifest.xml +++ b/features/home/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ - - - + + + + \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index 6d150f7a..1ff20c11 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -1,46 +1,29 @@ package app.dapk.st.home import android.os.Bundle -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import android.widget.Toast +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.AlertDialog import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewModelScope -import app.dapk.st.core.* -import app.dapk.st.core.components.CenteredLoading -import app.dapk.st.design.components.GenericError +import androidx.lifecycle.lifecycleScope +import app.dapk.st.core.DapkActivity +import app.dapk.st.core.Lce +import app.dapk.st.core.module +import app.dapk.st.core.viewModel import app.dapk.st.design.components.Route -import app.dapk.st.design.components.Spider import app.dapk.st.design.components.SpiderPage import app.dapk.st.directory.DirectoryModule -import app.dapk.st.home.gallery.FetchMediaFoldersUseCase -import app.dapk.st.home.gallery.FetchMediaUseCase import app.dapk.st.home.gallery.Folder +import app.dapk.st.home.gallery.GetImageFromGallery import app.dapk.st.home.gallery.Media import app.dapk.st.login.LoginModule import app.dapk.st.profile.ProfileModule -import app.dapk.st.viewmodel.DapkViewModel -import coil.compose.rememberAsyncImagePainter -import coil.request.ImageRequest -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach class MainActivity : DapkActivity() { @@ -52,49 +35,26 @@ class MainActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val viewModel = ImageGalleryViewModel( - FetchMediaFoldersUseCase(contentResolver), - FetchMediaUseCase(contentResolver), - ) + homeViewModel.events.onEach { + when (it) { + HomeEvent.Relaunch -> recreate() + } + }.launchIn(lifecycleScope) + + registerForActivityResult(GetImageFromGallery()) { + Toast.makeText(this, it.toString(), Toast.LENGTH_SHORT).show() + }.launch(null) -// lifecycleScope.launch { -// when (ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { -// PermissionResult.Denied -> { -// } -// -// PermissionResult.Granted -> { -// state.value = FetchMediaFoldersUseCase(contentResolver).fetchFolders() -// } -// -// PermissionResult.ShowRational -> { -// -// } -// } -// } setContent { - Surface { - ImageGalleryScreen(viewModel) { - finish() + if (homeViewModel.hasVersionChanged()) { + BetaUpgradeDialog() + } else { + Surface(Modifier.fillMaxSize()) { + HomeScreen(homeViewModel) } } } - -// homeViewModel.events.onEach { -// when (it) { -// HomeEvent.Relaunch -> recreate() -// } -// }.launchIn(lifecycleScope) -// -// setContent { -// if (homeViewModel.hasVersionChanged()) { -// BetaUpgradeDialog() -// } else { -// Surface(Modifier.fillMaxSize()) { -// HomeScreen(homeViewModel) -// } -// } -// } } @Composable @@ -132,188 +92,3 @@ sealed interface ImageGalleryPage { sealed interface ImageGalleryEvent - -class ImageGalleryViewModel( - private val foldersUseCase: FetchMediaFoldersUseCase, - private val fetchMediaUseCase: FetchMediaUseCase, -) : DapkViewModel( - initialState = ImageGalleryState(page = SpiderPage(route = ImageGalleryPage.Routes.folders, "", null, ImageGalleryPage.Folders(Lce.Loading()))) -) { - - private var currentPageJob: Job? = null - - fun start() { - currentPageJob?.cancel() - currentPageJob = viewModelScope.launch { - val folders = foldersUseCase.fetchFolders() - updatePageState { copy(content = Lce.Content(folders)) } - } - - } - - fun goTo(page: SpiderPage) { - currentPageJob?.cancel() - updateState { copy(page = page) } - } - - fun selectFolder(folder: Folder) { - currentPageJob?.cancel() - - updateState { - copy( - page = SpiderPage( - route = ImageGalleryPage.Routes.files, - label = page.label, - parent = ImageGalleryPage.Routes.folders, - state = ImageGalleryPage.Files(Lce.Loading()) - ) - ) - } - - currentPageJob = viewModelScope.launch { - val media = fetchMediaUseCase.getMediaInBucket(folder.bucketId) - updatePageState { - copy(content = Lce.Content(media)) - } - } - } - - @Suppress("UNCHECKED_CAST") - private inline fun updatePageState(crossinline block: S.() -> S) { - val page = state.page - val currentState = page.state - require(currentState is S) - updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } - } - -} - -@Composable -fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> Unit) { - LifecycleEffect(onStart = { - viewModel.start() - }) - - val onNavigate: (SpiderPage?) -> Unit = { - when (it) { - null -> onTopLevelBack() - else -> viewModel.goTo(it) - } - } - - Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { - item(ImageGalleryPage.Routes.folders) { - ImageGalleryFolders(it) { folder -> - viewModel.selectFolder(folder) - } - } - item(ImageGalleryPage.Routes.files) { - ImageGalleryMedia(it) - } - } - -} - -@Composable -fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) { - val screenWidth = LocalConfiguration.current.screenWidthDp - - val gradient = Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), - ) - - when (val content = state.content) { - is Lce.Loading -> { - CenteredLoading() - } - - is Lce.Content -> { - Column { - val columns = when { - screenWidth > 600 -> 4 - else -> 2 - } - LazyVerticalGrid( - columns = GridCells.Fixed(columns), - modifier = Modifier.fillMaxSize(), - ) { - items(content.value, key = { it.bucketId }) { - Box(modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f) - .clickable { onClick(it) }) { - Image( - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(it.thumbnail.toString()) - .build(), - ), - contentDescription = "123", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - - Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.6f).background(gradient).align(Alignment.BottomStart)) - Row( - modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(it.title, fontSize = 13.sp, color = Color.White) - Text(it.itemCount.toString(), fontSize = 11.sp, color = Color.White) - } - } - } - } - } - } - - is Lce.Error -> GenericError { } - } -} - -@Composable -fun ImageGalleryMedia(state: ImageGalleryPage.Files) { - val screenWidth = LocalConfiguration.current.screenWidthDp - - Column { - val columns = when { - screenWidth > 600 -> 4 - else -> 2 - } - - when (val content = state.content) { - is Lce.Loading -> { - CenteredLoading() - } - - is Lce.Content -> { - LazyVerticalGrid( - columns = GridCells.Fixed(columns), - modifier = Modifier.fillMaxSize(), - ) { - val modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f) - items(content.value, key = { it.id }) { - Box(modifier = modifier) { - Image( - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(it.uri.toString()) - .crossfade(true) - .build(), - ), - contentDescription = "123", - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - contentScale = ContentScale.Crop - ) - } - } - } - } - - is Lce.Error -> GenericError { } - } - - } - -} - - diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt new file mode 100644 index 00000000..00275ea1 --- /dev/null +++ b/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt @@ -0,0 +1,79 @@ +package app.dapk.st.home.gallery + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContract +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.lifecycleScope +import app.dapk.st.core.DapkActivity +import app.dapk.st.core.Lce +import app.dapk.st.core.PermissionResult +import app.dapk.st.home.ImageGalleryScreen +import kotlinx.coroutines.launch + +class ImageGalleryActivity : DapkActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val viewModel = ImageGalleryViewModel( + FetchMediaFoldersUseCase(contentResolver), + FetchMediaUseCase(contentResolver), + ) + + val permissionState = mutableStateOf>(Lce.Loading()) + + lifecycleScope.launch { + permissionState.value = runCatching { ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE) }.fold( + onSuccess = { Lce.Content(it) }, + onFailure = { Lce.Error(it) } + ) + } + + setContent { + Surface { + PermissionGuard(permissionState) { + ImageGalleryScreen(viewModel, onTopLevelBack = { finish() }) { media -> + setResult(RESULT_OK, Intent().setData(media.uri)) + finish() + } + } + } + } + } +} + +@Composable +fun Activity.PermissionGuard(state: State>, onGranted: @Composable () -> Unit) { + when (val content = state.value) { + is Lce.Content -> when (content.value) { + PermissionResult.Granted -> onGranted() + PermissionResult.Denied -> finish() + PermissionResult.ShowRational -> finish() + } + + is Lce.Error -> finish() + is Lce.Loading -> { + // loading should be quick, let's avoid displaying anything + } + } + +} + +class GetImageFromGallery : ActivityResultContract() { + + override fun createIntent(context: Context, input: Void?): Intent { + return Intent(context, ImageGalleryActivity::class.java) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return intent.takeIf { resultCode == Activity.RESULT_OK }?.data + } +} \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt b/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt new file mode 100644 index 00000000..d9fcc2a6 --- /dev/null +++ b/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt @@ -0,0 +1,160 @@ +package app.dapk.st.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dapk.st.core.Lce +import app.dapk.st.core.LifecycleEffect +import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.design.components.GenericError +import app.dapk.st.design.components.Spider +import app.dapk.st.design.components.SpiderPage +import app.dapk.st.home.gallery.Folder +import app.dapk.st.home.gallery.ImageGalleryViewModel +import app.dapk.st.home.gallery.Media +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest + +@Composable +fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> Unit, onImageSelected: (Media) -> Unit) { + LifecycleEffect(onStart = { + viewModel.start() + }) + + val onNavigate: (SpiderPage?) -> Unit = { + when (it) { + null -> onTopLevelBack() + else -> viewModel.goTo(it) + } + } + + Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { + item(ImageGalleryPage.Routes.folders) { + ImageGalleryFolders(it) { folder -> + viewModel.selectFolder(folder) + } + } + item(ImageGalleryPage.Routes.files) { + ImageGalleryMedia(it, onImageSelected) + } + } + +} + + +@Composable +fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) { + val screenWidth = LocalConfiguration.current.screenWidthDp + + val gradient = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)), + ) + + when (val content = state.content) { + is Lce.Loading -> { + CenteredLoading() + } + + is Lce.Content -> { + Column { + val columns = when { + screenWidth > 600 -> 4 + else -> 2 + } + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier.fillMaxSize(), + ) { + items(content.value, key = { it.bucketId }) { + Box(modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f) + .clickable { onClick(it) }) { + Image( + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(it.thumbnail.toString()) + .build(), + ), + contentDescription = "123", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.6f).background(gradient).align(Alignment.BottomStart)) + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(it.title, fontSize = 13.sp, color = Color.White) + Text(it.itemCount.toString(), fontSize = 11.sp, color = Color.White) + } + } + } + } + } + } + + is Lce.Error -> GenericError { } + } +} + +@Composable +fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit) { + val screenWidth = LocalConfiguration.current.screenWidthDp + + Column { + val columns = when { + screenWidth > 600 -> 4 + else -> 2 + } + + when (val content = state.content) { + is Lce.Loading -> { + CenteredLoading() + } + + is Lce.Content -> { + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier.fillMaxSize(), + ) { + val modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f) + items(content.value, key = { it.id }) { + Box(modifier = modifier.clickable { onFileSelected(it) }) { + Image( + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(it.uri.toString()) + .crossfade(true) + .build(), + ), + contentDescription = "123", + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + contentScale = ContentScale.Crop + ) + } + } + } + } + + is Lce.Error -> GenericError { } + } + + } + +} diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt new file mode 100644 index 00000000..a8107b91 --- /dev/null +++ b/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt @@ -0,0 +1,73 @@ +package app.dapk.st.home.gallery + +import androidx.lifecycle.viewModelScope +import app.dapk.st.core.Lce +import app.dapk.st.design.components.SpiderPage +import app.dapk.st.home.ImageGalleryEvent +import app.dapk.st.home.ImageGalleryPage +import app.dapk.st.home.ImageGalleryState +import app.dapk.st.viewmodel.DapkViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class ImageGalleryViewModel( + private val foldersUseCase: FetchMediaFoldersUseCase, + private val fetchMediaUseCase: FetchMediaUseCase, +) : DapkViewModel( + initialState = ImageGalleryState( + page = SpiderPage( + route = ImageGalleryPage.Routes.folders, + label = "", + parent = null, + state = ImageGalleryPage.Folders(Lce.Loading()) + ) + ) +) { + + private var currentPageJob: Job? = null + + fun start() { + currentPageJob?.cancel() + currentPageJob = viewModelScope.launch { + val folders = foldersUseCase.fetchFolders() + updatePageState { copy(content = Lce.Content(folders)) } + } + + } + + fun goTo(page: SpiderPage) { + currentPageJob?.cancel() + updateState { copy(page = page) } + } + + fun selectFolder(folder: Folder) { + currentPageJob?.cancel() + + updateState { + copy( + page = SpiderPage( + route = ImageGalleryPage.Routes.files, + label = page.label, + parent = ImageGalleryPage.Routes.folders, + state = ImageGalleryPage.Files(Lce.Loading()) + ) + ) + } + + currentPageJob = viewModelScope.launch { + val media = fetchMediaUseCase.getMediaInBucket(folder.bucketId) + updatePageState { + copy(content = Lce.Content(media)) + } + } + } + + @Suppress("UNCHECKED_CAST") + private inline fun updatePageState(crossinline block: S.() -> S) { + val page = state.page + val currentState = page.state + require(currentState is S) + updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } + } + +} \ No newline at end of file diff --git a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt index 1e6efeae..4fb72815 100644 --- a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt +++ b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt @@ -1,6 +1,7 @@ package app.dapk.st.navigator import android.app.Activity +import android.app.Instrumentation.ActivityResult import android.app.PendingIntent import android.content.Context import android.content.Intent From 3f956d1903c053f9221b191088c36460692c9dbb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 14:32:04 +0100 Subject: [PATCH 06/10] moving the image gallery to the messenger module and attaching to a attachment button --- features/home/src/main/AndroidManifest.xml | 1 - .../kotlin/app/dapk/st/home/MainActivity.kt | 31 ---------------- .../messenger/src/main/AndroidManifest.xml | 1 + .../dapk/st/messenger/MessengerActivity.kt | 23 +++++++++--- .../app/dapk/st/messenger/MessengerScreen.kt | 36 ++++++++++++++----- .../app/dapk/st/messenger/MessengerState.kt | 4 ++- .../dapk/st/messenger/MessengerViewModel.kt | 7 ++++ .../gallery/FetchMediaFoldersUseCase.kt | 2 +- .../gallery/ImageGalleryActivity.kt | 3 +- .../messenger}/gallery/ImageGalleryScreen.kt | 5 +-- .../gallery/ImageGalleryViewModel.kt | 25 ++++++++++--- 11 files changed, 80 insertions(+), 58 deletions(-) rename features/{home/src/main/kotlin/app/dapk/st/home => messenger/src/main/kotlin/app/dapk/st/messenger}/gallery/FetchMediaFoldersUseCase.kt (99%) rename features/{home/src/main/kotlin/app/dapk/st/home => messenger/src/main/kotlin/app/dapk/st/messenger}/gallery/ImageGalleryActivity.kt (97%) rename features/{home/src/main/kotlin/app/dapk/st/home => messenger/src/main/kotlin/app/dapk/st/messenger}/gallery/ImageGalleryScreen.kt (97%) rename features/{home/src/main/kotlin/app/dapk/st/home => messenger/src/main/kotlin/app/dapk/st/messenger}/gallery/ImageGalleryViewModel.kt (80%) diff --git a/features/home/src/main/AndroidManifest.xml b/features/home/src/main/AndroidManifest.xml index ccee2955..e9b82476 100644 --- a/features/home/src/main/AndroidManifest.xml +++ b/features/home/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ - \ No newline at end of file diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index 1ff20c11..c308346e 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -1,7 +1,6 @@ package app.dapk.st.home import android.os.Bundle -import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.AlertDialog import androidx.compose.material3.Surface @@ -11,15 +10,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import app.dapk.st.core.DapkActivity -import app.dapk.st.core.Lce import app.dapk.st.core.module import app.dapk.st.core.viewModel -import app.dapk.st.design.components.Route -import app.dapk.st.design.components.SpiderPage import app.dapk.st.directory.DirectoryModule -import app.dapk.st.home.gallery.Folder -import app.dapk.st.home.gallery.GetImageFromGallery -import app.dapk.st.home.gallery.Media import app.dapk.st.login.LoginModule import app.dapk.st.profile.ProfileModule import kotlinx.coroutines.flow.launchIn @@ -41,11 +34,6 @@ class MainActivity : DapkActivity() { } }.launchIn(lifecycleScope) - registerForActivityResult(GetImageFromGallery()) { - Toast.makeText(this, it.toString(), Toast.LENGTH_SHORT).show() - }.launch(null) - - setContent { if (homeViewModel.hasVersionChanged()) { BetaUpgradeDialog() @@ -73,22 +61,3 @@ class MainActivity : DapkActivity() { ) } } - - -data class ImageGalleryState( - val page: SpiderPage, -) - - -sealed interface ImageGalleryPage { - data class Folders(val content: Lce>) : ImageGalleryPage - data class Files(val content: Lce>) : ImageGalleryPage - - object Routes { - val folders = Route("Folders") - val files = Route("Files") - } -} - - -sealed interface ImageGalleryEvent diff --git a/features/messenger/src/main/AndroidManifest.xml b/features/messenger/src/main/AndroidManifest.xml index 41b5625c..d81f7866 100644 --- a/features/messenger/src/main/AndroidManifest.xml +++ b/features/messenger/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ android:windowSoftInputMode="adjustResize"/> + diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt index a648deec..6849d35c 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt @@ -5,16 +5,16 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier -import app.dapk.st.core.DapkActivity +import app.dapk.st.core.* import app.dapk.st.core.extensions.unsafeLazy -import app.dapk.st.core.module -import app.dapk.st.core.viewModel import app.dapk.st.matrix.common.RoomId +import app.dapk.st.messenger.gallery.GetImageFromGallery import app.dapk.st.navigator.MessageAttachment import kotlinx.parcelize.Parcelize @@ -51,10 +51,25 @@ class MessengerActivity : DapkActivity() { super.onCreate(savedInstanceState) val payload = readPayload() val factory = module.decryptingFetcherFactory(RoomId(payload.roomId)) + + val galleryLauncher = registerForActivityResult(GetImageFromGallery()) { + it?.let { uri -> + viewModel.post( + MessengerAction.ComposerImageUpdate( + MessageAttachment( + AndroidUri(it.toString()), + MimeType.Image, + ) + ) + ) + } + } + + setContent { Surface(Modifier.fillMaxSize()) { CompositionLocalProvider(LocalDecyptingFetcherFactory provides factory) { - MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator) + MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator, galleryLauncher) } } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index a1e98f8a..7e9d8ae3 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -1,9 +1,11 @@ package app.dapk.st.messenger import android.content.res.Configuration +import androidx.activity.result.ActivityResultLauncher import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape @@ -11,9 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -47,10 +47,16 @@ import coil.request.ImageRequest import kotlinx.coroutines.launch @Composable -internal fun MessengerScreen(roomId: RoomId, attachments: List?, viewModel: MessengerViewModel, navigator: Navigator) { +internal fun MessengerScreen( + roomId: RoomId, + attachments: List?, + viewModel: MessengerViewModel, + navigator: Navigator, + galleryLauncher: ActivityResultLauncher<*> +) { val state = viewModel.state - viewModel.ObserveEvents() + viewModel.ObserveEvents(galleryLauncher) LifecycleEffect( onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) }, onStop = { viewModel.post(MessengerAction.OnMessengerGone) } @@ -74,6 +80,7 @@ internal fun MessengerScreen(roomId: RoomId, attachments: List) { StartObserving { this@ObserveEvents.events.launch { - // TODO() + when (it) { + MessengerEvent.SelectImageAttachment -> galleryLauncher.launch(null) + } } } } @@ -553,7 +562,7 @@ private fun RowScope.SendStatus(message: RoomEvent) { } @Composable -private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit) { +private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit) { Row( Modifier .fillMaxWidth() @@ -579,7 +588,16 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un onValueChange = { onTextChange(it) }, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true) + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true), + decorationBox = { + Box { + Icon( + modifier = Modifier.align(Alignment.CenterEnd).clickable { onAttach() }, + imageVector = Icons.Filled.Image, + contentDescription = "", + ) + } + } ) } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt index 17bcd11a..cf335e61 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt @@ -10,7 +10,9 @@ data class MessengerScreenState( val composerState: ComposerState, ) -sealed interface MessengerEvent +sealed interface MessengerEvent { + object SelectImageAttachment : MessengerEvent +} sealed interface ComposerState { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index 86930b05..aead2233 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -48,6 +48,7 @@ internal class MessengerViewModel( is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) } MessengerAction.ComposerSendText -> sendMessage() MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) } + is MessengerAction.ComposerImageUpdate -> updateState { copy(composerState = ComposerState.Attachments(listOf(action.newValue))) } } } @@ -100,6 +101,7 @@ internal class MessengerViewModel( } } } + is ComposerState.Attachments -> { val copy = composerState.copy() updateState { copy(composerState = ComposerState.Text("")) } @@ -123,6 +125,10 @@ internal class MessengerViewModel( } } + fun startAttachment() { + _events.tryEmit(MessengerEvent.SelectImageAttachment) + } + } private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events @@ -133,6 +139,7 @@ private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roo sealed interface MessengerAction { data class ComposerTextUpdate(val newValue: String) : MessengerAction + data class ComposerImageUpdate(val newValue: MessageAttachment) : MessengerAction object ComposerSendText : MessengerAction object ComposerClear : MessengerAction data class OnMessengerVisible(val roomId: RoomId, val attachments: List?) : MessengerAction diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt similarity index 99% rename from features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt rename to features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt index d4d0e66a..8d08c97e 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/gallery/FetchMediaFoldersUseCase.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt @@ -1,4 +1,4 @@ -package app.dapk.st.home.gallery +package app.dapk.st.messenger.gallery import android.content.ContentResolver import android.content.ContentUris diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt similarity index 97% rename from features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt rename to features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt index 00275ea1..53430dc8 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt @@ -1,4 +1,4 @@ -package app.dapk.st.home.gallery +package app.dapk.st.messenger.gallery import android.Manifest import android.app.Activity @@ -15,7 +15,6 @@ import androidx.lifecycle.lifecycleScope import app.dapk.st.core.DapkActivity import app.dapk.st.core.Lce import app.dapk.st.core.PermissionResult -import app.dapk.st.home.ImageGalleryScreen import kotlinx.coroutines.launch class ImageGalleryActivity : DapkActivity() { diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt similarity index 97% rename from features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt rename to features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt index d9fcc2a6..acb46c18 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt @@ -1,4 +1,4 @@ -package app.dapk.st.home +package app.dapk.st.messenger.gallery import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -24,9 +24,6 @@ import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.GenericError import app.dapk.st.design.components.Spider import app.dapk.st.design.components.SpiderPage -import app.dapk.st.home.gallery.Folder -import app.dapk.st.home.gallery.ImageGalleryViewModel -import app.dapk.st.home.gallery.Media import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest diff --git a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt similarity index 80% rename from features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt rename to features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt index a8107b91..1a6d3b1d 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/gallery/ImageGalleryViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt @@ -1,11 +1,9 @@ -package app.dapk.st.home.gallery +package app.dapk.st.messenger.gallery import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce +import app.dapk.st.design.components.Route import app.dapk.st.design.components.SpiderPage -import app.dapk.st.home.ImageGalleryEvent -import app.dapk.st.home.ImageGalleryPage -import app.dapk.st.home.ImageGalleryState import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -70,4 +68,21 @@ class ImageGalleryViewModel( updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } } -} \ No newline at end of file +} + +data class ImageGalleryState( + val page: SpiderPage, +) + + +sealed interface ImageGalleryPage { + data class Folders(val content: Lce>) : ImageGalleryPage + data class Files(val content: Lce>) : ImageGalleryPage + + object Routes { + val folders = Route("Folders") + val files = Route("Files") + } +} + +sealed interface ImageGalleryEvent From bd885823bd17ff5114c8a730d501dd62a69377c1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 15:12:53 +0100 Subject: [PATCH 07/10] removing unused import --- .../src/main/kotlin/app/dapk/st/navigator/Navigator.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt index 4fb72815..e52d34c5 100644 --- a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt +++ b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt @@ -1,7 +1,6 @@ package app.dapk.st.navigator import android.app.Activity -import android.app.Instrumentation.ActivityResult import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -82,7 +81,7 @@ data class MessageAttachment(val uri: AndroidUri, val type: MimeType) : Parcelab private companion object : Parceler { override fun create(parcel: Parcel): MessageAttachment { val uri = AndroidUri(parcel.readString()!!) - val type = when(parcel.readString()!!) { + val type = when (parcel.readString()!!) { "mimetype-image" -> MimeType.Image else -> throw IllegalStateException() } From 415ea4b15087b020a013f8cfdb66ac3f85f5c902 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 16:24:17 +0100 Subject: [PATCH 08/10] providing image gallery via DI graph --- .../app/dapk/st/SmallTalkApplication.kt | 2 + .../kotlin/app/dapk/st/graph/AppModule.kt | 5 ++ .../gallery/FetchMediaFoldersUseCase.kt | 69 ++----------------- .../st/messenger/gallery/FetchMediaUseCase.kt | 64 +++++++++++++++++ .../messenger/gallery/ImageGalleryActivity.kt | 15 ++-- .../messenger/gallery/ImageGalleryModule.kt | 17 +++++ .../messenger/gallery/MediaStoreExtensions.kt | 6 ++ 7 files changed, 104 insertions(+), 74 deletions(-) create mode 100644 features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt create mode 100644 features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt create mode 100644 features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaStoreExtensions.kt diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index d2e2f97a..1b284431 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -15,6 +15,7 @@ import app.dapk.st.graph.AppModule import app.dapk.st.home.HomeModule import app.dapk.st.login.LoginModule import app.dapk.st.messenger.MessengerModule +import app.dapk.st.messenger.gallery.ImageGalleryModule import app.dapk.st.notifications.NotificationsModule import app.dapk.st.profile.ProfileModule import app.dapk.st.push.PushModule @@ -81,6 +82,7 @@ class SmallTalkApplication : Application(), ModuleProvider { TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule CoreAndroidModule::class -> appModule.coreAndroidModule ShareEntryModule::class -> featureModules.shareEntryModule + ImageGalleryModule::class -> featureModules.imageGalleryModule else -> throw IllegalArgumentException("Unknown: $klass") } as T } diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 296ee1f9..f424c2b7 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -43,6 +43,7 @@ import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent import app.dapk.st.matrix.sync.internal.room.MessageDecrypter import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerModule +import app.dapk.st.messenger.gallery.ImageGalleryModule import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.MessageAttachment import app.dapk.st.notifications.MatrixPushHandler @@ -217,6 +218,10 @@ internal class FeatureModules internal constructor( ShareEntryModule(matrixModules.sync, matrixModules.room) } + val imageGalleryModule by unsafeLazy { + ImageGalleryModule(context.contentResolver, coroutineDispatchers) + } + val pushModule by unsafeLazy { domainModules.pushModule } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt index 8d08c97e..1f171022 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaFoldersUseCase.kt @@ -3,21 +3,17 @@ package app.dapk.st.messenger.gallery import android.content.ContentResolver import android.content.ContentUris import android.net.Uri -import android.os.Build -import android.provider.MediaStore import android.provider.MediaStore.Images -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - - -// https://github.com/signalapp/Signal-Android/blob/e22ddb8f96f8801f0abe622b5261abc6cb396d94/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java +import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.withIoContext class FetchMediaFoldersUseCase( private val contentResolver: ContentResolver, + private val dispatchers: CoroutineDispatchers, ) { suspend fun fetchFolders(): List { - return withContext(Dispatchers.IO) { + return dispatchers.withIoContext { val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED) val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?" val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC" @@ -39,7 +35,6 @@ class FetchMediaFoldersUseCase( } } - } data class Folder( @@ -56,59 +51,3 @@ data class Folder( } } - - -class FetchMediaUseCase(private val contentResolver: ContentResolver) { - - private val projection = arrayOf( - Images.Media._ID, - Images.Media.MIME_TYPE, - Images.Media.DATE_MODIFIED, - Images.Media.ORIENTATION, - Images.Media.WIDTH, - Images.Media.HEIGHT, - Images.Media.SIZE - ) - - suspend fun getMediaInBucket(bucketId: String): List { - return withContext(Dispatchers.IO) { - - val media = mutableListOf() - val selection = Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + Images.Media.MIME_TYPE + " NOT LIKE ?" - val selectionArgs = arrayOf(bucketId, "%image/svg%") - val sortBy = Images.Media.DATE_MODIFIED + " DESC" - val contentUri = Images.Media.EXTERNAL_CONTENT_URI - contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor -> - while (cursor != null && cursor.moveToNext()) { - val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0])) - val uri = ContentUris.withAppendedId(contentUri, rowId) - val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)) - val date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED)) - val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) - val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation))) - val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation))) - val size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)) - media.add(Media(rowId, uri, mimetype, width, height, size, date)) - } - } - media - } - } - - private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) Images.Media.WIDTH else Images.Media.HEIGHT - - private fun getHeightColumn(orientation: Int) = if (orientation == 0 || orientation == 180) Images.Media.HEIGHT else Images.Media.WIDTH - -} - -data class Media( - val id: Long, - val uri: Uri, - val mimeType: String, - val width: Int, - val height: Int, - val size: Long, - val dateModifiedEpochMillis: Long, -) - -private fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1" diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt new file mode 100644 index 00000000..7ea73f77 --- /dev/null +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/FetchMediaUseCase.kt @@ -0,0 +1,64 @@ +package app.dapk.st.messenger.gallery + +import android.content.ContentResolver +import android.content.ContentUris +import android.net.Uri +import android.provider.MediaStore +import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.withIoContext + +class FetchMediaUseCase(private val contentResolver: ContentResolver, private val dispatchers: CoroutineDispatchers) { + + private val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.MIME_TYPE, + MediaStore.Images.Media.DATE_MODIFIED, + MediaStore.Images.Media.ORIENTATION, + MediaStore.Images.Media.WIDTH, + MediaStore.Images.Media.HEIGHT, + MediaStore.Images.Media.SIZE + ) + + private val selection = MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?" + + suspend fun getMediaInBucket(bucketId: String): List { + + return dispatchers.withIoContext { + val media = mutableListOf() + val selectionArgs = arrayOf(bucketId, "%image/svg%") + val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC" + val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0])) + val uri = ContentUris.withAppendedId(contentUri, rowId) + val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)) + val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED)) + val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION)) + val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation))) + val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation))) + val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)) + media.add(Media(rowId, uri, mimetype, width, height, size, date)) + } + } + media + } + } + + private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) MediaStore.Images.Media.WIDTH else MediaStore.Images.Media.HEIGHT + + private fun getHeightColumn(orientation: Int) = + if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH + +} + +data class Media( + val id: Long, + val uri: Uri, + val mimeType: String, + val width: Int, + val height: Int, + val size: Long, + val dateModifiedEpochMillis: Long, +) + diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt index 53430dc8..0917df0c 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt @@ -12,21 +12,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.lifecycleScope -import app.dapk.st.core.DapkActivity -import app.dapk.st.core.Lce -import app.dapk.st.core.PermissionResult +import app.dapk.st.core.* +import app.dapk.st.core.extensions.unsafeLazy +import app.dapk.st.messenger.MessengerModule import kotlinx.coroutines.launch class ImageGalleryActivity : DapkActivity() { + private val module by unsafeLazy { module() } + private val viewModel by viewModel { module.imageGalleryViewModel() } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - val viewModel = ImageGalleryViewModel( - FetchMediaFoldersUseCase(contentResolver), - FetchMediaUseCase(contentResolver), - ) - val permissionState = mutableStateOf>(Lce.Loading()) lifecycleScope.launch { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt new file mode 100644 index 00000000..b3076b8f --- /dev/null +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt @@ -0,0 +1,17 @@ +package app.dapk.st.messenger.gallery + +import android.content.ContentResolver +import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.ProvidableModule + +class ImageGalleryModule( + private val contentResolver: ContentResolver, + private val dispatchers: CoroutineDispatchers, +) : ProvidableModule { + + fun imageGalleryViewModel() = ImageGalleryViewModel( + FetchMediaFoldersUseCase(contentResolver, dispatchers), + FetchMediaUseCase(contentResolver, dispatchers), + ) + +} \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaStoreExtensions.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaStoreExtensions.kt new file mode 100644 index 00000000..dd0679ff --- /dev/null +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/MediaStoreExtensions.kt @@ -0,0 +1,6 @@ +package app.dapk.st.messenger.gallery + +import android.os.Build +import android.provider.MediaStore + +fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) MediaStore.Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1" From 14d625765c6a1fef4b11b5c997f2f786d723cf8f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 16:25:33 +0100 Subject: [PATCH 09/10] avoiding try emit --- .../main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index aead2233..94a59f15 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -126,7 +126,9 @@ internal class MessengerViewModel( } fun startAttachment() { - _events.tryEmit(MessengerEvent.SelectImageAttachment) + viewModelScope.launch { + _events.emit(MessengerEvent.SelectImageAttachment) + } } } From ff69156330aaed477583090aa5bc26846cbd62e6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 29 Sep 2022 16:43:40 +0100 Subject: [PATCH 10/10] including room name in the image gallery toolbar --- .../app/dapk/st/messenger/MessengerScreen.kt | 17 +++++++++++---- .../messenger/gallery/ImageGalleryActivity.kt | 21 ++++++++++++++----- .../messenger/gallery/ImageGalleryModule.kt | 3 ++- .../gallery/ImageGalleryViewModel.kt | 3 ++- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 7e9d8ae3..6f226fac 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -13,7 +13,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Send import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -33,6 +36,7 @@ import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.design.components.* import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId @@ -40,6 +44,7 @@ import app.dapk.st.matrix.sync.MessageMeta import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent.Message import app.dapk.st.matrix.sync.RoomState +import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.Navigator import coil.compose.rememberAsyncImagePainter @@ -52,7 +57,7 @@ internal fun MessengerScreen( attachments: List?, viewModel: MessengerViewModel, navigator: Navigator, - galleryLauncher: ActivityResultLauncher<*> + galleryLauncher: ActivityResultLauncher ) { val state = viewModel.state @@ -96,11 +101,15 @@ internal fun MessengerScreen( } @Composable -private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLauncher<*>) { +private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLauncher) { StartObserving { this@ObserveEvents.events.launch { when (it) { - MessengerEvent.SelectImageAttachment -> galleryLauncher.launch(null) + MessengerEvent.SelectImageAttachment -> { + state.roomState.takeIfContent()?.let { + galleryLauncher.launch(ImageGalleryActivityPayload(it.roomState.roomOverview.roomName ?: "")) + } + } } } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt index 0917df0c..06faf139 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.Parcelable import androidx.activity.result.contract.ActivityResultContract import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -14,13 +15,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.lifecycleScope import app.dapk.st.core.* import app.dapk.st.core.extensions.unsafeLazy -import app.dapk.st.messenger.MessengerModule import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize class ImageGalleryActivity : DapkActivity() { private val module by unsafeLazy { module() } - private val viewModel by viewModel { module.imageGalleryViewModel() } + private val viewModel by viewModel { + val payload = intent.getParcelableExtra("key") as? ImageGalleryActivityPayload + module.imageGalleryViewModel(payload!!.roomName) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -63,13 +67,20 @@ fun Activity.PermissionGuard(state: State>, onGranted: @Co } -class GetImageFromGallery : ActivityResultContract() { +class GetImageFromGallery : ActivityResultContract() { - override fun createIntent(context: Context, input: Void?): Intent { + override fun createIntent(context: Context, input: ImageGalleryActivityPayload): Intent { return Intent(context, ImageGalleryActivity::class.java) + .putExtra("key", input) } override fun parseResult(resultCode: Int, intent: Intent?): Uri? { return intent.takeIf { resultCode == Activity.RESULT_OK }?.data } -} \ No newline at end of file +} + + +@Parcelize +data class ImageGalleryActivityPayload( + val roomName: String, +) : Parcelable \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt index b3076b8f..0e92bdbb 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryModule.kt @@ -9,9 +9,10 @@ class ImageGalleryModule( private val dispatchers: CoroutineDispatchers, ) : ProvidableModule { - fun imageGalleryViewModel() = ImageGalleryViewModel( + fun imageGalleryViewModel(roomName: String) = ImageGalleryViewModel( FetchMediaFoldersUseCase(contentResolver, dispatchers), FetchMediaUseCase(contentResolver, dispatchers), + roomName = roomName, ) } \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt index 1a6d3b1d..59cbbb4f 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt @@ -11,11 +11,12 @@ import kotlinx.coroutines.launch class ImageGalleryViewModel( private val foldersUseCase: FetchMediaFoldersUseCase, private val fetchMediaUseCase: FetchMediaUseCase, + roomName: String, ) : DapkViewModel( initialState = ImageGalleryState( page = SpiderPage( route = ImageGalleryPage.Routes.folders, - label = "", + label = "Send to $roomName", parent = null, state = ImageGalleryPage.Folders(Lce.Loading()) )