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() {
if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) {
finishAfterTransition()
} else
} else {
super.onBackPressed()
}
}
protected suspend fun ensurePermission(permission: String): PermissionResult {
return when {

View File

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

View File

@ -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<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
import android.app.Activity
import android.app.Instrumentation.ActivityResult
import android.app.PendingIntent
import android.content.Context
import android.content.Intent