adding skeleton for image gallery folder viewing
This commit is contained in:
parent
debfc5e5f0
commit
6f89c71300
|
@ -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<Folder>())
|
||||
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 -> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 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,14 +120,119 @@ class MainActivity : DapkActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
data class ImageGalleryState(
|
||||
val page: SpiderPage<out ImageGalleryPage>,
|
||||
)
|
||||
|
||||
|
||||
sealed interface ImageGalleryPage {
|
||||
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
|
||||
data class Files(val content: Lce<List<Media>>) : ImageGalleryPage
|
||||
|
||||
object Routes {
|
||||
val folders = Route<Folders>("Folders")
|
||||
val files = Route<Files>("Files")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sealed interface ImageGalleryEvent
|
||||
|
||||
class ImageGalleryViewModel(
|
||||
private val foldersUseCase: FetchMediaFoldersUseCase,
|
||||
private val fetchMediaUseCase: FetchMediaUseCase,
|
||||
) : DapkViewModel<ImageGalleryState, ImageGalleryEvent>(
|
||||
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<ImageGalleryPage.Folders> { copy(content = Lce.Content(folders)) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun goTo(page: SpiderPage<out ImageGalleryPage>) {
|
||||
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<ImageGalleryPage.Files> {
|
||||
copy(content = Lce.Content(media))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private inline fun <reified S : ImageGalleryPage> updatePageState(crossinline block: S.() -> S) {
|
||||
val page = state.page
|
||||
val currentState = page.state
|
||||
require(currentState is S)
|
||||
updateState { copy(page = (page as SpiderPage<S>).copy(state = block(page.state))) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageGallery(state: State<List<Folder>>) {
|
||||
fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> Unit) {
|
||||
LifecycleEffect(onStart = {
|
||||
viewModel.start()
|
||||
})
|
||||
|
||||
val onNavigate: (SpiderPage<out ImageGalleryPage>?) -> 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 {
|
||||
Toolbar(title = "Send to Awesome Room", onNavigate = {})
|
||||
val columns = when {
|
||||
screenWidth > 600 -> 4
|
||||
else -> 2
|
||||
|
@ -129,8 +241,10 @@ fun ImageGallery(state: State<List<Folder>>) {
|
|||
columns = GridCells.Fixed(columns),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(state.value, key = { it.bucketId }) {
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(2.dp).onGloballyPositioned {
|
||||
items(content.value, key = { it.bucketId }) {
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(2.dp)
|
||||
.clickable { onClick(it) }
|
||||
.onGloballyPositioned {
|
||||
boxWidth = it.size
|
||||
}) {
|
||||
Image(
|
||||
|
@ -159,10 +273,62 @@ fun ImageGallery(state: State<List<Folder>>) {
|
|||
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 ImageGalleryMedia(state: ImageGalleryPage.Files) {
|
||||
var boxWidth by remember { mutableStateOf(IntSize.Zero) }
|
||||
val localDensity = LocalDensity.current
|
||||
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(),
|
||||
) {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -16,15 +16,14 @@ class FetchMediaFoldersUseCase(
|
|||
private val contentResolver: ContentResolver,
|
||||
) {
|
||||
|
||||
|
||||
suspend fun fetchFolders(): List<Folder> {
|
||||
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<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]))
|
||||
|
@ -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"
|
||||
|
||||
}
|
||||
|
||||
|
@ -70,3 +56,59 @@ 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<Media> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
|
||||
val media = mutableListOf<Media>()
|
||||
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"
|
||||
|
|
Loading…
Reference in New Issue