Skip to content

Commit 7adff7e

Browse files
committed
Moving more stuff to ViewModels
1 parent 77ea17d commit 7adff7e

File tree

4 files changed

+171
-98
lines changed

4 files changed

+171
-98
lines changed

app/src/main/kotlin/com/darkrockstudios/app/securecamera/AppModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.darkrockstudios.app.securecamera
33
import com.darkrockstudios.app.securecamera.auth.AuthorizationManager
44
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
55
import com.darkrockstudios.app.securecamera.camera.ThumbnailCache
6+
import com.darkrockstudios.app.securecamera.gallery.GalleryViewModel
67
import com.darkrockstudios.app.securecamera.obfuscation.ObfuscatePhotoViewModel
78
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesManager
89
import com.darkrockstudios.app.securecamera.usecases.PinStrengthCheckUseCase
@@ -27,4 +28,5 @@ val appModule = module {
2728

2829
viewModelOf(::ObfuscatePhotoViewModel)
2930
viewModelOf(::ViewPhotoViewModel)
31+
viewModelOf(::GalleryViewModel)
3032
}

app/src/main/kotlin/com/darkrockstudios/app/securecamera/gallery/GalleryContent.kt

Lines changed: 34 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -13,133 +13,67 @@ import androidx.compose.material.icons.filled.CheckCircle
1313
import androidx.compose.material.icons.filled.Warning
1414
import androidx.compose.material3.*
1515
import androidx.compose.runtime.*
16-
import androidx.compose.runtime.getValue
17-
import androidx.compose.runtime.saveable.rememberSaveable
1816
import androidx.compose.ui.Alignment
1917
import androidx.compose.ui.Modifier
2018
import androidx.compose.ui.draw.alpha
2119
import androidx.compose.ui.graphics.Color
2220
import androidx.compose.ui.graphics.ImageBitmap
23-
import androidx.compose.ui.graphics.Shape
2421
import androidx.compose.ui.graphics.asImageBitmap
2522
import androidx.compose.ui.layout.ContentScale
2623
import androidx.compose.ui.platform.LocalContext
2724
import androidx.compose.ui.res.stringResource
2825
import androidx.compose.ui.unit.dp
26+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2927
import androidx.navigation.NavController
3028
import com.darkrockstudios.app.securecamera.ConfirmDeletePhotoDialog
3129
import com.darkrockstudios.app.securecamera.R
3230
import com.darkrockstudios.app.securecamera.camera.PhotoDef
3331
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
3432
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
35-
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesManager
36-
import com.darkrockstudios.app.securecamera.sharePhotosData
37-
import kotlinx.coroutines.*
33+
import com.darkrockstudios.app.securecamera.ui.HandleUiEvents
34+
import kotlinx.coroutines.CoroutineDispatcher
35+
import kotlinx.coroutines.CoroutineScope
36+
import kotlinx.coroutines.Dispatchers
37+
import kotlinx.coroutines.launch
38+
import org.koin.androidx.compose.koinViewModel
3839
import org.koin.compose.koinInject
3940

4041
@OptIn(ExperimentalMaterial3Api::class)
4142
@Composable
4243
fun GalleryContent(
4344
modifier: Modifier = Modifier,
4445
navController: NavController,
45-
paddingValues: PaddingValues
46+
paddingValues: PaddingValues,
47+
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
4648
) {
47-
val imageManager = koinInject<SecureImageManager>()
48-
val preferencesManager = koinInject<AppPreferencesManager>()
49-
var photos by remember { mutableStateOf<List<PhotoDef>>(emptyList()) }
50-
var isLoading by rememberSaveable { mutableStateOf(true) }
5149
val context = LocalContext.current
52-
val scope = rememberCoroutineScope()
53-
54-
// Selection state
55-
var isSelectionMode by rememberSaveable { mutableStateOf(false) }
56-
var selectedPhotos by rememberSaveable { mutableStateOf<Set<String>>(emptySet()) }
57-
var showDeleteConfirmation by rememberSaveable { mutableStateOf(false) }
58-
59-
val sanitizeFileName by
60-
preferencesManager.sanitizeFileName.collectAsState(preferencesManager.sanitizeFileNameDefault)
61-
val sanitizeMetadata by
62-
preferencesManager.sanitizeMetadata.collectAsState(preferencesManager.sanitizeMetadataDefault)
63-
64-
// Function to toggle selection of a photo
65-
val togglePhotoSelection = { photoName: String ->
66-
selectedPhotos = if (selectedPhotos.contains(photoName)) {
67-
selectedPhotos - photoName
68-
} else {
69-
selectedPhotos + photoName
70-
}
50+
val viewModel: GalleryViewModel = koinViewModel()
51+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
7152

72-
// If no photos are selected, exit selection mode
73-
if (selectedPhotos.isEmpty()) {
74-
isSelectionMode = false
75-
}
76-
}
77-
78-
val startSelectionMode = { photoName: String ->
79-
isSelectionMode = true
80-
selectedPhotos = setOf(photoName)
53+
val startSelectionWithVibration = { photoName: String ->
54+
viewModel.startSelectionMode(photoName)
8155
vibrateDevice(context)
8256
}
8357

84-
val clearSelection = {
85-
isSelectionMode = false
86-
selectedPhotos = emptySet()
87-
}
88-
89-
val handleDelete = {
90-
showDeleteConfirmation = true
91-
}
92-
93-
val performDelete = {
94-
val photoDefs = selectedPhotos.mapNotNull { imageManager.getPhotoByName(it) }
95-
imageManager.deleteImages(photoDefs)
96-
clearSelection()
97-
photos = photos.filter { it !in photoDefs }
98-
showDeleteConfirmation = false
99-
}
100-
101-
val handleShare = {
102-
val photoDefs = selectedPhotos.mapNotNull { imageManager.getPhotoByName(it) }
103-
if (photoDefs.isNotEmpty()) {
104-
scope.launch(Dispatchers.IO) {
105-
sharePhotosData(
106-
photos = photoDefs,
107-
sanitizeName = sanitizeFileName,
108-
sanitizeMetadata = sanitizeMetadata,
109-
imageManager = imageManager,
110-
context = context
111-
)
112-
withContext(Dispatchers.Main) {
113-
clearSelection()
114-
}
115-
}
116-
}
117-
}
118-
119-
LaunchedEffect(Unit) {
120-
photos = imageManager.getPhotos().sortedByDescending { it.dateTaken() }
121-
isLoading = false
122-
}
123-
12458
Column(
12559
modifier = modifier
12660
.fillMaxSize()
12761
.background(MaterialTheme.colorScheme.background)
12862
) {
12963
GalleryTopNav(
13064
navController = navController,
131-
onDeleteClick = handleDelete,
132-
onShareClick = handleShare,
133-
isSelectionMode = isSelectionMode,
134-
selectedCount = selectedPhotos.size,
135-
onCancelSelection = clearSelection
65+
onDeleteClick = { viewModel.showDeleteConfirmation() },
66+
onShareClick = { viewModel.shareSelectedPhotos(context) },
67+
isSelectionMode = uiState.isSelectionMode,
68+
selectedCount = uiState.selectedPhotos.size,
69+
onCancelSelection = { viewModel.clearSelection() }
13670
)
13771

138-
if (showDeleteConfirmation) {
72+
if (uiState.showDeleteConfirmation) {
13973
ConfirmDeletePhotoDialog(
140-
selectedCount = selectedPhotos.size,
141-
onConfirm = performDelete,
142-
onDismiss = { showDeleteConfirmation = false }
74+
selectedCount = uiState.selectedPhotos.size,
75+
onConfirm = { viewModel.deleteSelectedPhotos() },
76+
onDismiss = { viewModel.dismissDeleteConfirmation() }
14377
)
14478
}
14579

@@ -154,19 +88,18 @@ fun GalleryContent(
15488
.fillMaxSize(),
15589
contentAlignment = Alignment.Center
15690
) {
157-
if (isLoading) {
158-
// Show a loading indicator or placeholder
91+
if (uiState.isLoading) {
15992
Text(text = stringResource(id = R.string.gallery_loading))
160-
} else if (photos.isEmpty()) {
93+
} else if (uiState.photos.isEmpty()) {
16194
Text(text = stringResource(id = R.string.gallery_empty))
16295
} else {
16396
PhotoGrid(
164-
photos = photos,
165-
selectedPhotoNames = selectedPhotos,
166-
onPhotoLongClick = startSelectionMode,
97+
photos = uiState.photos,
98+
selectedPhotoNames = uiState.selectedPhotos,
99+
onPhotoLongClick = startSelectionWithVibration,
167100
onPhotoClick = { photoName ->
168-
if (isSelectionMode) {
169-
togglePhotoSelection(photoName)
101+
if (uiState.isSelectionMode) {
102+
viewModel.togglePhotoSelection(photoName)
170103
} else {
171104
navController.navigate(AppDestinations.createViewPhotoRoute(photoName))
172105
}
@@ -175,6 +108,8 @@ fun GalleryContent(
175108
}
176109
}
177110
}
111+
112+
HandleUiEvents(viewModel.events, snackbarHostState, navController)
178113
}
179114

180115
@Composable
@@ -267,7 +202,9 @@ private fun PhotoItem(
267202
photo.photoName
268203
),
269204
contentScale = ContentScale.Crop,
270-
modifier = Modifier.fillMaxSize().alpha(imageAlpha)
205+
modifier = Modifier
206+
.fillMaxSize()
207+
.alpha(imageAlpha)
271208
)
272209
} ?: run {
273210
Box(modifier = Modifier.fillMaxSize()) {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.darkrockstudios.app.securecamera.gallery
2+
3+
import android.content.Context
4+
import androidx.lifecycle.viewModelScope
5+
import com.darkrockstudios.app.securecamera.BaseViewModel
6+
import com.darkrockstudios.app.securecamera.camera.PhotoDef
7+
import com.darkrockstudios.app.securecamera.camera.SecureImageManager
8+
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesManager
9+
import com.darkrockstudios.app.securecamera.sharePhotosData
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.flow.update
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.withContext
14+
15+
class GalleryViewModel(
16+
private val imageManager: SecureImageManager,
17+
private val preferencesManager: AppPreferencesManager
18+
) : BaseViewModel<GalleryUiState>() {
19+
20+
override fun createState() = GalleryUiState()
21+
22+
init {
23+
loadPhotos()
24+
observePreferences()
25+
}
26+
27+
private fun loadPhotos() {
28+
viewModelScope.launch {
29+
_uiState.update { it.copy(isLoading = true) }
30+
val photos = imageManager.getPhotos().sortedByDescending { it.dateTaken() }
31+
_uiState.update { it.copy(photos = photos, isLoading = false) }
32+
}
33+
}
34+
35+
private fun observePreferences() {
36+
viewModelScope.launch {
37+
preferencesManager.sanitizeFileName.collect { sanitizeFileName ->
38+
_uiState.update { it.copy(sanitizeFileName = sanitizeFileName) }
39+
}
40+
}
41+
42+
viewModelScope.launch {
43+
preferencesManager.sanitizeMetadata.collect { sanitizeMetadata ->
44+
_uiState.update { it.copy(sanitizeMetadata = sanitizeMetadata) }
45+
}
46+
}
47+
}
48+
49+
fun togglePhotoSelection(photoName: String) {
50+
val currentSelectedPhotos = uiState.value.selectedPhotos
51+
val newSelectedPhotos = if (currentSelectedPhotos.contains(photoName)) {
52+
currentSelectedPhotos - photoName
53+
} else {
54+
currentSelectedPhotos + photoName
55+
}
56+
57+
_uiState.update {
58+
it.copy(
59+
selectedPhotos = newSelectedPhotos,
60+
isSelectionMode = newSelectedPhotos.isNotEmpty()
61+
)
62+
}
63+
}
64+
65+
fun startSelectionMode(photoName: String) {
66+
_uiState.update {
67+
it.copy(
68+
isSelectionMode = true,
69+
selectedPhotos = setOf(photoName)
70+
)
71+
}
72+
}
73+
74+
fun clearSelection() {
75+
_uiState.update {
76+
it.copy(
77+
isSelectionMode = false,
78+
selectedPhotos = emptySet()
79+
)
80+
}
81+
}
82+
83+
fun showDeleteConfirmation() {
84+
_uiState.update { it.copy(showDeleteConfirmation = true) }
85+
}
86+
87+
fun dismissDeleteConfirmation() {
88+
_uiState.update { it.copy(showDeleteConfirmation = false) }
89+
}
90+
91+
fun deleteSelectedPhotos() {
92+
val photoDefs = uiState.value.selectedPhotos.mapNotNull { imageManager.getPhotoByName(it) }
93+
imageManager.deleteImages(photoDefs)
94+
95+
val updatedPhotos = uiState.value.photos.filter { it !in photoDefs }
96+
_uiState.update {
97+
it.copy(
98+
photos = updatedPhotos,
99+
selectedPhotos = emptySet(),
100+
isSelectionMode = false,
101+
showDeleteConfirmation = false
102+
)
103+
}
104+
}
105+
106+
fun shareSelectedPhotos(context: Context) {
107+
val photoDefs = uiState.value.selectedPhotos.mapNotNull { imageManager.getPhotoByName(it) }
108+
if (photoDefs.isNotEmpty()) {
109+
viewModelScope.launch(Dispatchers.IO) {
110+
sharePhotosData(
111+
photos = photoDefs,
112+
sanitizeName = uiState.value.sanitizeFileName,
113+
sanitizeMetadata = uiState.value.sanitizeMetadata,
114+
imageManager = imageManager,
115+
context = context
116+
)
117+
withContext(Dispatchers.Main) {
118+
clearSelection()
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
data class GalleryUiState(
126+
val photos: List<PhotoDef> = emptyList(),
127+
val isLoading: Boolean = true,
128+
val isSelectionMode: Boolean = false,
129+
val selectedPhotos: Set<String> = emptySet(),
130+
val showDeleteConfirmation: Boolean = false,
131+
val sanitizeFileName: Boolean = true,
132+
val sanitizeMetadata: Boolean = true
133+
)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/navigation/AppNavigation.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ fun AppNavHost(
9797
GalleryContent(
9898
navController = navController,
9999
modifier = Modifier.fillMaxSize(),
100-
paddingValues = paddingValues
100+
paddingValues = paddingValues,
101+
snackbarHostState = snackbarHostState
101102
)
102103
} else {
103104
Box(modifier = Modifier.fillMaxSize()) {

0 commit comments

Comments
 (0)