Skip to content

Commit

Permalink
Simplify Gallery state object for inclusion of a user state
Browse files Browse the repository at this point in the history
  • Loading branch information
ashdavies committed Nov 1, 2023
1 parent 52fefb6 commit 2f10c2c
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.ashdavies.gallery

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import io.ashdavies.content.PlatformContext

private val DefaultState = GalleryScreen.State(
itemList = emptyList(),
showCapture = false,
isLoggedIn = false,
eventSink = { },
)

private val EmptyStorageManager = object : StorageManager {
override fun create(context: PlatformContext): File = TempFile
override fun delete(file: File): Boolean = false
}

private val TempFile = File.createTempFile(
/* prefix = */ "tmp",
/* suffix = */ null,
)

@Preview
@Composable
internal fun GalleryCaptureDefaultPreview() {
GalleryCapture(EmptyStorageManager) { }
}

@Preview
@Composable
internal fun GalleryBottomBarDefaultPreview() {
GalleryBottomBar(DefaultState, isSelecting = false)
}

@Preview
@Composable
internal fun GalleryBottomBarSelectingPreview() {
GalleryBottomBar(DefaultState, isSelecting = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ internal actual fun StorageManager(parent: File): StorageManager = object : Stor
return file
}

override fun list(): List<File> {
val files = parent.listFiles() ?: return emptyList()
return files.toList()
}

override fun delete(file: File): Boolean {
if (!file.exists()) throw IllegalArgumentException()
return file.delete()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package io.ashdavies.gallery

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
Expand Down Expand Up @@ -34,42 +33,19 @@ public object GalleryScreen : Parcelable, Screen {
data object Sync : Event
}

internal sealed interface State : CircuitUiState {
data class Empty(val eventSink: (Event) -> Unit) : State

data class Success(
val itemList: List<Item>,
val showCapture: Boolean,
val eventSink: (Event) -> Unit,
) : State {
constructor(
itemList: List<Image>,
isSelected: (Image) -> Boolean,
state: (Image) -> SyncState,
showCapture: Boolean,
eventSink: (Event) -> Unit,
) : this(
itemList = itemList.map { image ->
Item(
name = image.name,
file = File(image.path),
isSelected = isSelected(image),
state = state(image),
)
},
showCapture = showCapture,
eventSink = eventSink,
)

data class Item(
val name: String,
val file: File,
val isSelected: Boolean,
val state: SyncState,
)
}

data object Loading : State
internal data class State(
val itemList: List<Item>,
val showCapture: Boolean,
val isLoggedIn: Boolean,
val eventSink: (Event) -> Unit,
) : CircuitUiState {

data class Item(
val name: String,
val file: File,
val isSelected: Boolean,
val state: SyncState,
)
}
}

Expand Down Expand Up @@ -113,20 +89,29 @@ internal fun GalleryPresenter(
sync: SyncManager,
): GalleryScreen.State {
val itemList by produceState(emptyList<Image>(), images) {
images.list().collect { value = it }
images.list.collect { value = it }
}

var selected by remember { mutableStateOf(emptyList<Image>()) }
var takePhoto by remember { mutableStateOf(false) }

val syncState by sync.state().collectAsState(emptyMap())
val syncState by produceState(emptyMap<String, SyncState>()) {
sync.state.collect { value = it }
}

val coroutineScope = rememberCoroutineScope()

return GalleryScreen.State.Success(
itemList = itemList,
isSelected = { it in selected },
state = { syncState[it.name] ?: SyncState.NOT_SYNCED },
return GalleryScreen.State(
itemList = itemList.map {
GalleryScreen.State.Item(
name = it.name,
file = File(it.path),
isSelected = it in selected,
state = syncState[it.name] ?: SyncState.NOT_SYNCED,
)
},
showCapture = takePhoto,
isLoggedIn = false,
) { event ->
when (event) {
is GalleryScreen.Event.Capture -> takePhoto = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import androidx.compose.material.icons.filled.Sync
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -89,18 +88,17 @@ internal fun GalleryScreen(
manager: StorageManager,
modifier: Modifier = Modifier,
) {
val isSelecting = state is GalleryScreen.State.Success && state.itemList.any { it.isSelected }
val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState())
val isSelecting = state.itemList.any { it.isSelected }

Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { GalleryTopAppBar(scrollBehavior) },
bottomBar = { GalleryBottomBar(state, isSelecting) },
) { paddingValues ->
when (state) {
GalleryScreen.State.Loading -> GalleryProgressIndicator()
is GalleryScreen.State.Empty -> GalleryEmpty()
is GalleryScreen.State.Success -> {
when {
state.itemList.isEmpty() -> GalleryEmpty()
else -> {
GalleryGrid(
itemList = state.itemList.toImmutableList(),
isSelecting = isSelecting,
Expand All @@ -121,7 +119,7 @@ internal fun GalleryScreen(

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun GalleryTopAppBar(
internal fun GalleryTopAppBar(
scrollBehavior: TopAppBarScrollBehavior,
title: String = "Gallery",
modifier: Modifier = Modifier,
Expand All @@ -143,19 +141,7 @@ private fun GalleryTopAppBar(
}

@Composable
private fun GalleryProgressIndicator(modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onSurface,
)
}
}

@Composable
private fun GalleryEmpty(modifier: Modifier = Modifier) {
internal fun GalleryEmpty(modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
Expand All @@ -166,8 +152,8 @@ private fun GalleryEmpty(modifier: Modifier = Modifier) {

@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun GalleryGrid(
itemList: ImmutableList<GalleryScreen.State.Success.Item>,
internal fun GalleryGrid(
itemList: ImmutableList<GalleryScreen.State.Item>,
isSelecting: Boolean = false,
modifier: Modifier = Modifier,
onSelect: (Int) -> Unit,
Expand Down Expand Up @@ -250,7 +236,7 @@ private fun GalleryGrid(
}

@Composable
private fun SyncIndicator(isSyncing: Boolean, modifier: Modifier = Modifier) {
internal fun SyncIndicator(isSyncing: Boolean, modifier: Modifier = Modifier) {
val tint by animateColorAsState(if (isSyncing) Color.Orange else Color.LightGreen)
val scale by animateFloatAsState(if (isSyncing) 0.75f else 1f)

Expand Down Expand Up @@ -298,7 +284,7 @@ private fun SyncIndicator(isSyncing: Boolean, modifier: Modifier = Modifier) {
}

@Composable
private fun GalleryCapture(
internal fun GalleryCapture(
manager: StorageManager,
modifier: Modifier = Modifier,
eventSink: (GalleryScreen.Event) -> Unit,
Expand All @@ -311,33 +297,25 @@ private fun GalleryCapture(
}

@Composable
private fun GalleryBottomBar(
internal fun GalleryBottomBar(
state: GalleryScreen.State,
isSelecting: Boolean,
modifier: Modifier = Modifier,
) {
val eventSink = when (state) {
is GalleryScreen.State.Success -> state.eventSink
is GalleryScreen.State.Empty -> state.eventSink
else -> null
}
val eventSink = state.eventSink

BottomAppBar(
actions = {
AnimatedVisibility(
visible = eventSink != null && isSelecting,
visible = isSelecting,
enter = slideIn(initialOffset = { IntOffset(0, 200) }),
exit = slideOut(targetOffset = { IntOffset(0, 200) }),
) {
check(eventSink != null) { "Event sink cannot be null" }

Row {
Box(modifier = Modifier.padding(horizontal = 4.dp)) {
IconButton(
onClick = { eventSink(GalleryScreen.Event.Sync) },
enabled = state is GalleryScreen.State.Success && state.itemList.none {
it.state == SyncState.SYNCING
},
enabled = state.itemList.none { it.state == SyncState.SYNCING },
) {
Icon(
imageVector = Icons.Filled.Sync,
Expand All @@ -359,19 +337,11 @@ private fun GalleryBottomBar(
},
modifier = modifier,
floatingActionButton = {
AnimatedVisibility(
visible = eventSink != null,
enter = fadeIn(),
exit = fadeOut(),
) {
check(eventSink != null) { "Event sink cannot be null" }

FloatingActionButton(onClick = { eventSink(GalleryScreen.Event.Capture) }) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add",
)
}
FloatingActionButton(onClick = { eventSink(GalleryScreen.Event.Capture) }) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add",
)
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.ashdavies.playground.mapToList
import kotlinx.coroutines.flow.Flow

internal interface ImageManager {
fun list(): Flow<List<Image>>
val list: Flow<List<Image>>
fun add(file: File)
fun remove(image: Image)
}
Expand All @@ -14,9 +14,9 @@ internal fun ImageManager(
queries: ImageQueries,
): ImageManager = object : ImageManager {

override fun list(): Flow<List<Image>> {
return queries.selectAll().mapToList()
}
override val list: Flow<List<Image>> = queries
.selectAll()
.mapToList()

override fun add(file: File) {
val image = Image(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.ashdavies.content.PlatformContext

internal interface StorageManager {
fun create(context: PlatformContext): File
fun list(): List<File>
fun delete(file: File): Boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.update
import java.util.concurrent.atomic.AtomicBoolean

internal interface SyncManager {
fun state(): Flow<Map<String, SyncState>>
val state: Flow<Map<String, SyncState>>
suspend fun sync(path: String)
}

Expand All @@ -31,22 +31,22 @@ internal fun SyncManager(
reader: File.() -> ByteReadChannel = File::readChannel,
): SyncManager = object : SyncManager {

private val state = MutableStateFlow<Map<String, SyncState>>(emptyMap())
private val _state = MutableStateFlow<Map<String, SyncState>>(emptyMap())
private val initialised = AtomicBoolean(false)

override fun state(): Flow<Map<String, SyncState>> = channelFlow {
override val state: Flow<Map<String, SyncState>> = channelFlow {
if (initialised.compareAndSet(false, true)) {
val initialValue = client.get("/").body<List<String>>()
state.value = initialValue.associateWith { SyncState.SYNCED }
_state.value = initialValue.associateWith { SyncState.SYNCED }
}

state.collect(::send)
_state.collect(::send)
}

override suspend fun sync(path: String) = with(File(path)) {
val initialState = state.value[getName()] ?: SyncState.NOT_SYNCED
val initialState = _state.value[getName()] ?: SyncState.NOT_SYNCED

state.update { it + (getName() to SyncState.SYNCING) }
_state.update { it + (getName() to SyncState.SYNCING) }

when (initialState) {
SyncState.NOT_SYNCED -> client.post(getName()) {
Expand All @@ -57,7 +57,7 @@ internal fun SyncManager(
else -> client.put(getName())
}

state.update { it + (getName() to SyncState.SYNCED) }
_state.update { it + (getName() to SyncState.SYNCED) }
}
}

Expand Down
Loading

0 comments on commit 2f10c2c

Please sign in to comment.