creating dedicated activity for image selection

This commit is contained in:
Adam Brown 2022-09-29 13:50:58 +01:00
parent 34e0415892
commit c61646bbd3
7 changed files with 344 additions and 253 deletions

View File

@ -61,12 +61,14 @@ abstract class DapkActivity : ComponentActivity(), EffectScope {
} }
} }
@Suppress("OVERRIDE_DEPRECATION")
override fun onBackPressed() { override fun onBackPressed() {
if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) { if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) {
finishAfterTransition() finishAfterTransition()
} else } else {
super.onBackPressed() super.onBackPressed()
} }
}
protected suspend fun ensurePermission(permission: String): PermissionResult { protected suspend fun ensurePermission(permission: String): PermissionResult {
return when { return when {

View File

@ -3,6 +3,7 @@
<application> <application>
<activity android:name="app.dapk.st.home.MainActivity"/> <activity android:name="app.dapk.st.home.MainActivity"/>
<activity android:name="app.dapk.st.home.gallery.ImageGalleryActivity"/>
</application> </application>
</manifest> </manifest>

View File

@ -1,46 +1,29 @@
package app.dapk.st.home package app.dapk.st.home
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.Image import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize
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.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.lifecycle.lifecycleScope
import androidx.compose.ui.graphics.Color import app.dapk.st.core.DapkActivity
import androidx.compose.ui.layout.ContentScale import app.dapk.st.core.Lce
import androidx.compose.ui.platform.LocalConfiguration import app.dapk.st.core.module
import androidx.compose.ui.platform.LocalContext import app.dapk.st.core.viewModel
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.Route
import app.dapk.st.design.components.Spider
import app.dapk.st.design.components.SpiderPage import app.dapk.st.design.components.SpiderPage
import app.dapk.st.directory.DirectoryModule 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.Folder
import app.dapk.st.home.gallery.GetImageFromGallery
import app.dapk.st.home.gallery.Media import app.dapk.st.home.gallery.Media
import app.dapk.st.login.LoginModule import app.dapk.st.login.LoginModule
import app.dapk.st.profile.ProfileModule import app.dapk.st.profile.ProfileModule
import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.flow.launchIn
import coil.compose.rememberAsyncImagePainter import kotlinx.coroutines.flow.onEach
import coil.request.ImageRequest
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class MainActivity : DapkActivity() { class MainActivity : DapkActivity() {
@ -52,49 +35,26 @@ class MainActivity : DapkActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val viewModel = ImageGalleryViewModel( homeViewModel.events.onEach {
FetchMediaFoldersUseCase(contentResolver), when (it) {
FetchMediaUseCase(contentResolver), 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 { setContent {
Surface { if (homeViewModel.hasVersionChanged()) {
ImageGalleryScreen(viewModel) { BetaUpgradeDialog()
finish() } 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 @Composable
@ -132,188 +92,3 @@ sealed interface ImageGalleryPage {
sealed interface ImageGalleryEvent 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 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) {
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 { }
}
}
}

View File

@ -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<PermissionResult>>(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<Lce<PermissionResult>>, 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<Void?, Uri?>() {
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
}
}

View File

@ -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<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, 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 { }
}
}
}

View File

@ -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<ImageGalleryState, ImageGalleryEvent>(
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<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))) }
}
}

View File

@ -1,6 +1,7 @@
package app.dapk.st.navigator package app.dapk.st.navigator
import android.app.Activity import android.app.Activity
import android.app.Instrumentation.ActivityResult
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent