From debfc5e5f0730aac6bb7c34741ec1cee700bb36e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 28 Sep 2022 19:51:24 +0100 Subject: [PATCH] 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 | 134 ++++++++++++++++-- .../home/gallery/FetchMediaFoldersUseCase.kt | 72 ++++++++++ 4 files changed, 200 insertions(+), 19 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 6e493a2..b478f91 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 3265a1f..0422dfa 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 609f52d..5170101 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() - } - }.launchIn(lifecycleScope) - setContent { - if (homeViewModel.hasVersionChanged()) { - BetaUpgradeDialog() - } else { - Surface(Modifier.fillMaxSize()) { - HomeScreen(homeViewModel) + val state = mutableStateOf(emptyList()) + + lifecycleScope.launch { + when (ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + PermissionResult.Denied -> { + } + + PermissionResult.Granted -> { + state.value = FetchMediaFoldersUseCase(contentResolver).fetchFolders() + } + + PermissionResult.ShowRational -> { + } } + } + + setContent { + 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 0000000..4333753 --- /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