adding first pass at a image gallery component with folder fetching

This commit is contained in:
Adam Brown 2022-09-28 19:51:24 +01:00
parent 846cf66fa1
commit debfc5e5f0
4 changed files with 200 additions and 19 deletions

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.dapk.st">
package="app.dapk.st">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name="app.dapk.st.SmallTalkApplication"
@ -17,13 +19,13 @@
android:exported="true"
android:targetActivity="app.dapk.st.home.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
android:resource="@xml/shortcuts"/>
</activity-alias>

View File

@ -13,4 +13,5 @@ dependencies {
implementation project(':domains:store')
implementation project(":core")
implementation project(":design-library")
implementation Dependencies.mavenCentral.coil
}

View File

@ -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<Folder>())
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<List<Folder>>) {
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)
}
}
}
}
}
}

View File

@ -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<Folder> {
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<String, Folder>()
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++
}
}