Skip to content

Commit

Permalink
Separate UI-specific state from view model
Browse files Browse the repository at this point in the history
  • Loading branch information
MHShetty committed Nov 29, 2024
1 parent f3ec28f commit 39d04a5
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ suspend fun SnackbarHostState.showOrReplaceSnackbar(
duration: SnackbarDuration =
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
) {
currentSnackbarData?.dismiss()
dismissSnackBarIfVisible()
showSnackbar(
message = message,
actionLabel = actionLabel,
withDismissAction = withDismissAction,
duration = duration
)
}

fun SnackbarHostState.dismissSnackBarIfVisible() {
currentSnackbarData?.dismiss()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package app.grapheneos.camera.ui.composable.component

import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import app.grapheneos.camera.ktx.dismissSnackBarIfVisible
import app.grapheneos.camera.ktx.showOrReplaceSnackbar
import app.grapheneos.camera.ui.composable.model.NoDataSnackBarMessage
import app.grapheneos.camera.ui.composable.model.SnackBarMessage

@Composable
fun SnackBarMessageHandler(
snackBarHostState: SnackbarHostState,
snackBarMessage: SnackBarMessage
) {
LaunchedEffect(snackBarMessage) {
if (snackBarMessage == NoDataSnackBarMessage) {
snackBarHostState.dismissSnackBarIfVisible()
} else {
snackBarHostState.showOrReplaceSnackbar(snackBarMessage.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.grapheneos.camera.ui.composable.model

import java.util.UUID

data class SnackBarMessage(
val message: String,
val id: UUID = UUID.randomUUID()
)

val NoDataSnackBarMessage = SnackBarMessage("")
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember

import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -43,6 +45,7 @@ import app.grapheneos.camera.CapturedItem
import app.grapheneos.camera.R
import app.grapheneos.camera.ktx.header
import app.grapheneos.camera.ktx.requestDeviceUnlock
import app.grapheneos.camera.ui.composable.component.SnackBarMessageHandler
import app.grapheneos.camera.ui.composable.component.dialog.MultipleFileDeletionDialog
import app.grapheneos.camera.ui.composable.component.mediapreview.SQUARE_MEDIA_PREVIEW_SIZE
import app.grapheneos.camera.ui.composable.component.mediapreview.SquareMediaPreview
Expand All @@ -62,6 +65,15 @@ fun ExtendedGalleryScreen(
ExtendedGalleryViewModel(context)
}

val snackBarHostState = remember {
SnackbarHostState()
}

SnackBarMessageHandler(
snackBarHostState = snackBarHostState,
snackBarMessage = viewModel.snackBarMessage,
)

BackHandler {
if (viewModel.selectMode) {
viewModel.exitSelectionMode()
Expand All @@ -83,7 +95,7 @@ fun ExtendedGalleryScreen(

Scaffold (
snackbarHost = {
SnackbarHost(viewModel.snackBarHostState)
SnackbarHost(snackBarHostState)
},

topBar = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState

import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
Expand All @@ -24,6 +25,7 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text

import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -63,6 +65,7 @@ import me.saket.telephoto.zoomable.zoomable

import app.grapheneos.camera.ITEM_TYPE_IMAGE
import app.grapheneos.camera.R
import app.grapheneos.camera.ui.composable.component.SnackBarMessageHandler
import app.grapheneos.camera.ui.composable.component.tooltip.QuickTooltip
import app.grapheneos.camera.ui.composable.component.tooltip.QuickTooltipVerticalDirection

Expand Down Expand Up @@ -90,10 +93,23 @@ fun GalleryScreen(

val coroutineScope = rememberCoroutineScope()

val snackBarHostState = remember {
SnackbarHostState()
}

val viewModel = viewModel {
GalleryViewModel(context)
}

val pagerState = rememberPagerState {
viewModel.capturedItems.size
}

SnackBarMessageHandler(
snackBarHostState = snackBarHostState,
snackBarMessage = viewModel.snackBarMessage
)

val backgroundColor by animateColorAsState(
label = "background_color_animation",
targetValue = if (viewModel.inFocusMode) Color.Black else AppColor.BackgroundColor,
Expand All @@ -111,7 +127,7 @@ fun GalleryScreen(

val focusIndex = viewModel.capturedItems.indexOf(focusItem)
if (focusIndex != -1) {
viewModel.pagerState.scrollToPage(focusIndex)
pagerState.scrollToPage(focusIndex)
}
}

Expand All @@ -121,8 +137,9 @@ fun GalleryScreen(
}

// Update the current focus item when the user slides between pages
LaunchedEffect(viewModel.pagerState.currentPage) {
val page = viewModel.pagerState.currentPage
LaunchedEffect(pagerState.currentPage) {
val page = pagerState.currentPage
viewModel.currentPage = page
if (page < viewModel.capturedItems.size) {
viewModel.focusItem = viewModel.capturedItems[page]
}
Expand Down Expand Up @@ -169,7 +186,7 @@ fun GalleryScreen(
Scaffold(
containerColor = backgroundColor,

snackbarHost = { SnackbarHost(hostState = viewModel.snackbarHostState) },
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },

floatingActionButton = {
AnimatedVisibility(
Expand Down Expand Up @@ -239,7 +256,7 @@ fun GalleryScreen(
} else {
if (viewModel.hasCapturedItems) {
HorizontalPager(
state = viewModel.pagerState,
state = pagerState,
userScrollEnabled = !viewModel.isZoomedIn,
beyondViewportPageCount = 1,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.viewModelScope

import app.grapheneos.camera.CapturedItem
import app.grapheneos.camera.R
import app.grapheneos.camera.ktx.isDeviceLocked
import app.grapheneos.camera.ktx.showOrReplaceSnackbar
import app.grapheneos.camera.ui.composable.model.NoDataSnackBarMessage
import app.grapheneos.camera.ui.composable.model.SnackBarMessage
import app.grapheneos.camera.util.getMimeTypeForItems
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
Expand Down Expand Up @@ -58,14 +58,21 @@ class ExtendedGalleryViewModel(context: Context) : ViewModel() {

var isDeletionDialogVisible by mutableStateOf(false)

val snackBarHostState = SnackbarHostState()
var snackBarMessage by mutableStateOf<SnackBarMessage>(NoDataSnackBarMessage)
private set

fun showSnackBar(message: String) {
snackBarMessage = SnackBarMessage(message)
}

fun hideSnackBar() {
snackBarMessage = NoDataSnackBarMessage
}

fun showDeletionDialog(context: Context) {
viewModelScope.launch {
if (selectedItems.isEmpty()) {
snackBarHostState.showOrReplaceSnackbar(
context.getString(R.string.select_an_item_request)
)
showSnackBar(context.getString(R.string.select_an_item_request))
return@launch
}
isDeletionDialogVisible = true
Expand Down Expand Up @@ -165,16 +172,12 @@ class ExtendedGalleryViewModel(context: Context) : ViewModel() {
fun shareSelectedItems(context: Context) {
viewModelScope.launch {
if (context.isDeviceLocked()) {
snackBarHostState.showOrReplaceSnackbar(
context.getString(R.string.sharing_not_allowed)
)
showSnackBar(context.getString(R.string.sharing_not_allowed))
return@launch
}

if (selectedItems.isEmpty()) {
snackBarHostState.showOrReplaceSnackbar(
context.getString(R.string.select_an_item_request)
)
showSnackBar(context.getString(R.string.select_an_item_request))
return@launch
}

Expand Down Expand Up @@ -218,13 +221,7 @@ class ExtendedGalleryViewModel(context: Context) : ViewModel() {
}

if (failedDeletions != 0) {
snackBarHostState.showOrReplaceSnackbar(
context.getString(
R.string.failed_multiple_deletion_message,
failedDeletions,
selectedItemsSize
)
)
showSnackBar(context.getString(R.string.failed_multiple_deletion_message, failedDeletions, selectedItemsSize))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import android.content.Intent

import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -21,8 +19,9 @@ import androidx.lifecycle.viewModelScope
import app.grapheneos.camera.CapturedItem
import app.grapheneos.camera.R
import app.grapheneos.camera.ktx.isDeviceLocked
import app.grapheneos.camera.ktx.showOrReplaceSnackbar
import app.grapheneos.camera.ui.composable.model.MediaItemDetails
import app.grapheneos.camera.ui.composable.model.NoDataSnackBarMessage
import app.grapheneos.camera.ui.composable.model.SnackBarMessage
import kotlinx.coroutines.launch

private const val TAG = "GalleryViewModel"
Expand All @@ -39,8 +38,10 @@ class GalleryViewModel(context: Context) : ViewModel() {

var deletionItem by mutableStateOf<CapturedItem?>(null)

var currentPage = 0

private val currentItem: CapturedItem
get() = capturedItems[pagerState.currentPage]
get() = capturedItems[currentPage]

private val capturedItemsViewModel = CapturedItemsRepository.get(context)

Expand All @@ -54,18 +55,21 @@ class GalleryViewModel(context: Context) : ViewModel() {
capturedItemsViewModel.isLoading
}

val snackbarHostState = SnackbarHostState()
var snackBarMessage by mutableStateOf<SnackBarMessage>(NoDataSnackBarMessage)
private set

val pagerState = PagerState(
pageCount = {
capturedItems.size
}
)
fun showSnackBar(message: String) {
snackBarMessage = SnackBarMessage(message)
}

fun hideSnackBar() {
snackBarMessage = NoDataSnackBarMessage
}

fun displayMediaInfo(context: Context, item: CapturedItem = currentItem) {
viewModelScope.launch {
if (!hasCapturedItems) {
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.unable_to_obtain_file_details)
)
return@launch
Expand All @@ -76,7 +80,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
} catch (e: Exception) {
Log.i(TAG, "Unable to obtain file details for MediaInfoDialog")
e.printStackTrace()
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.unable_to_obtain_file_details)
)
}
Expand Down Expand Up @@ -112,12 +116,12 @@ class GalleryViewModel(context: Context) : ViewModel() {
.show()
onLastItemDeletion()
} else {
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.deleted_successfully)
)
}
} else {
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.deleting_unexpected_error)
)
}
Expand All @@ -138,7 +142,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
) {
viewModelScope.launch {
if (context.isDeviceLocked()) {
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.edit_not_allowed)
)
return@launch
Expand Down Expand Up @@ -166,7 +170,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
try {
context.startActivity(editIntent)
} catch (ignored: ActivityNotFoundException) {
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.no_editor_app_error)
)
}
Expand All @@ -180,7 +184,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
) {
viewModelScope.launch {
if (context.isDeviceLocked()) {
snackbarHostState.showOrReplaceSnackbar(
showSnackBar(
context.getString(R.string.sharing_not_allowed)
)
return@launch
Expand Down

0 comments on commit 39d04a5

Please sign in to comment.