Skip to content

Commit 163148e

Browse files
committed
feat(swiper): Implement media skip functionality
This commit introduces the ability for users to skip a media item during a sorting session, allowing them to bypass an item without making a keep/delete/move decision. Key changes include: - **New "Skip" Button:** An icon button has been added to the central control bar on the Swiper screen. - **User Setting:** The button's visibility is governed by a new toggle in "Settings -> Behavior" named "Hide Skip Button". This setting is enabled by default to maintain a clean UI for users who do not need this feature. - **Session Tracking:** The `SwiperViewModel` now tracks skipped items for the duration of the session, ensuring they are not presented again until the session is reset. - **End-of-Session Summary:** The "All media has been organized" dialog now displays a count of any items that were skipped during the session, providing useful feedback to the user.
1 parent 4dc2521 commit 163148e

File tree

5 files changed

+95
-9
lines changed

5 files changed

+95
-9
lines changed

app/src/main/java/com/cleansweep/data/repository/PreferencesRepository.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class PreferencesRepository @Inject constructor(
105105
val SCREENSHOT_DELETES_VIDEO = booleanPreferencesKey("screenshot_deletes_video")
106106
val SCREENSHOT_JPEG_QUALITY = stringPreferencesKey("screenshot_jpeg_quality")
107107
val SIMILARITY_THRESHOLD_LEVEL = stringPreferencesKey("similarity_threshold_level")
108+
val HIDE_SWIPER_SKIP_BUTTON = booleanPreferencesKey("hide_swiper_skip_button")
108109

109110
val DEFAULT_VIDEO_SPEED = floatPreferencesKey("default_video_speed")
110111

@@ -192,6 +193,11 @@ class PreferencesRepository @Inject constructor(
192193
preferences[PreferencesKeys.UNFAVORITE_REMOVES_FROM_BAR] ?: false
193194
}
194195

196+
val hideSkipButtonFlow: Flow<Boolean> = context.dataStore.data
197+
.map { preferences ->
198+
preferences[PreferencesKeys.HIDE_SWIPER_SKIP_BUTTON] ?: false
199+
}
200+
195201
val processedMediaPathsFlow: Flow<Set<String>> = context.dataStore.data
196202
.map { preferences ->
197203
val pathsString = preferences[PreferencesKeys.PROCESSED_MEDIA_PATHS] ?: ""
@@ -433,6 +439,12 @@ class PreferencesRepository @Inject constructor(
433439
}
434440
}
435441

442+
suspend fun setHideSkipButton(enabled: Boolean) {
443+
context.dataStore.edit { preferences ->
444+
preferences[PreferencesKeys.HIDE_SWIPER_SKIP_BUTTON] = enabled
445+
}
446+
}
447+
436448
suspend fun addProcessedMediaPaths(mediaPaths: Set<String>) {
437449
if (mediaPaths.isEmpty()) return
438450
context.dataStore.edit { preferences ->

app/src/main/java/com/cleansweep/ui/screens/settings/SettingsScreen.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ fun SettingsScreen(
9090
val folderSelectionMode by viewModel.folderSelectionMode.collectAsState()
9191
val rememberProcessedMedia by viewModel.rememberProcessedMedia.collectAsState()
9292
val unfavoriteRemovesFromBar by viewModel.unfavoriteRemovesFromBar.collectAsState()
93+
val hideSkipButton by viewModel.hideSkipButton.collectAsState()
9394
val defaultPath by viewModel.defaultAlbumCreationPath.collectAsState()
9495
val showFavoritesInSetup by viewModel.showFavoritesInSetup.collectAsState()
9596
val searchAutofocusEnabled by viewModel.searchAutofocusEnabled.collectAsState()
@@ -375,6 +376,14 @@ fun SettingsScreen(
375376
checked = invertSwipe,
376377
onCheckedChange = { viewModel.setInvertSwipe(it) })
377378
},
379+
SettingItem(keywords = listOf("hide skip button")) {
380+
SettingSwitch(
381+
title = "Hide Skip Button",
382+
description = "If enabled, the 'Skip' button will be hidden from the bottom bar.",
383+
checked = hideSkipButton,
384+
onCheckedChange = { viewModel.setHideSkipButton(it) }
385+
)
386+
},
378387
SettingItem(keywords = listOf("add to favorites by default", "target folder")) {
379388
SettingSwitch(
380389
title = "Add to Favorites by Default",

app/src/main/java/com/cleansweep/ui/screens/settings/SettingsViewModel.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ class SettingsViewModel @Inject constructor(
147147
initialValue = true
148148
)
149149

150+
val hideSkipButton: StateFlow<Boolean> =
151+
preferencesRepository.hideSkipButtonFlow
152+
.stateIn(
153+
scope = viewModelScope,
154+
started = SharingStarted.WhileSubscribed(5000),
155+
initialValue = true
156+
)
157+
150158
val defaultAlbumCreationPath: StateFlow<String> =
151159
preferencesRepository.defaultAlbumCreationPathFlow
152160
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
@@ -486,6 +494,12 @@ class SettingsViewModel @Inject constructor(
486494
}
487495
}
488496

497+
fun setHideSkipButton(enabled: Boolean) {
498+
viewModelScope.launch {
499+
preferencesRepository.setHideSkipButton(enabled)
500+
}
501+
}
502+
489503
fun setShowFavoritesInSetup(enabled: Boolean) {
490504
viewModelScope.launch {
491505
preferencesRepository.setShowFavoritesFirstInSetup(enabled)

app/src/main/java/com/cleansweep/ui/screens/swiper/SwiperScreen.kt

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.compose.foundation.verticalScroll
3232
import androidx.compose.material.icons.Icons
3333
import androidx.compose.material.icons.automirrored.filled.ArrowBack
3434
import androidx.compose.material.icons.automirrored.filled.OpenInNew
35+
import androidx.compose.material.icons.automirrored.filled.Redo
3536
import androidx.compose.material.icons.automirrored.filled.Undo
3637
import androidx.compose.material.icons.automirrored.filled.VolumeOff
3738
import androidx.compose.material.icons.automirrored.filled.VolumeUp
@@ -94,6 +95,8 @@ import androidx.compose.foundation.lazy.grid.GridCells
9495
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
9596
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
9697
import androidx.compose.foundation.lazy.grid.items
98+
import androidx.compose.material.icons.automirrored.filled.ArrowForward
99+
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
97100
import androidx.compose.material3.ButtonDefaults
98101
import com.cleansweep.ui.components.FastScrollbar
99102
import java.text.DecimalFormat
@@ -312,12 +315,14 @@ fun SwiperScreen(
312315
pendingChangesCount = uiState.pendingChanges.size,
313316
currentItem = uiState.currentItem,
314317
targetFavorites = uiState.targetFavorites,
318+
isSkipButtonHidden = uiState.isSkipButtonHidden,
315319
onSelectFolder = viewModel::moveToFolder,
316320
onLongPressFolder = viewModel::showFolderMenu,
317321
onCreateNewAlbum = viewModel::showAddTargetFolderDialog,
318322
onToggleExpansion = viewModel::toggleFolderBarExpansion,
319323
onShowSummary = viewModel::showSummarySheet,
320324
onUndo = viewModel::revertLastChange,
325+
onSkip = viewModel::handleSkip,
321326
layout = FolderBarLayout.VERTICAL,
322327
folderName = if (folderNameLayout == FolderNameLayout.BELOW) uiState.currentItem!!.bucketName else null
323328
)
@@ -363,12 +368,14 @@ fun SwiperScreen(
363368
pendingChangesCount = uiState.pendingChanges.size,
364369
currentItem = uiState.currentItem,
365370
targetFavorites = uiState.targetFavorites,
371+
isSkipButtonHidden = uiState.isSkipButtonHidden,
366372
onSelectFolder = viewModel::moveToFolder,
367373
onLongPressFolder = viewModel::showFolderMenu,
368374
onCreateNewAlbum = viewModel::showAddTargetFolderDialog,
369375
onToggleExpansion = viewModel::toggleFolderBarExpansion,
370376
onShowSummary = viewModel::showSummarySheet,
371377
onUndo = viewModel::revertLastChange,
378+
onSkip = viewModel::handleSkip,
372379
layout = folderBarLayout,
373380
folderName = if (folderNameLayout == FolderNameLayout.BELOW) uiState.currentItem!!.bucketName else null
374381
)
@@ -382,6 +389,7 @@ fun SwiperScreen(
382389
showResetHistoryButton = rememberProcessedMedia,
383390
onResetHistory = viewModel::resetProcessedMedia,
384391
onResetSingleFolderHistory = viewModel::showForgetMediaInFolderDialog,
392+
skippedCount = uiState.sessionSkippedCount,
385393
onClose = { (appContext as? Activity)?.finish() }
386394
)
387395
}
@@ -735,10 +743,12 @@ private fun ControlBar(
735743
isExpanded: Boolean,
736744
showExpandButton: Boolean,
737745
hasPendingChanges: Boolean,
746+
isSkipButtonHidden: Boolean,
738747
onToggleExpansion: () -> Unit,
739748
onCreateNewAlbum: () -> Unit,
740749
onShowSummary: () -> Unit,
741-
onUndo: () -> Unit
750+
onUndo: () -> Unit,
751+
onSkip: () -> Unit
742752
) {
743753
Box(
744754
modifier = Modifier
@@ -764,6 +774,12 @@ private fun ControlBar(
764774
}
765775
}
766776
Spacer(modifier = Modifier.width(8.dp))
777+
AnimatedVisibility(visible = !isSkipButtonHidden) {
778+
IconButton(onClick = onSkip) {
779+
Icon(Icons.AutoMirrored.Filled.ArrowForwardIos , "Skip Item")
780+
}
781+
}
782+
Spacer(modifier = Modifier.width(8.dp))
767783
AnimatedVisibility(visible = hasPendingChanges) {
768784
IconButton(onClick = onUndo) {
769785
Icon(Icons.AutoMirrored.Filled.Undo, "Undo Last Action")
@@ -802,12 +818,14 @@ private fun BottomFolderBar(
802818
pendingChangesCount: Int,
803819
currentItem: MediaItem?,
804820
targetFavorites: Set<String>,
821+
isSkipButtonHidden: Boolean,
805822
onSelectFolder: (String) -> Unit,
806823
onLongPressFolder: (String, DpOffset) -> Unit,
807824
onCreateNewAlbum: () -> Unit,
808825
onToggleExpansion: () -> Unit,
809826
onShowSummary: () -> Unit,
810827
onUndo: () -> Unit,
828+
onSkip: () -> Unit,
811829
layout: FolderBarLayout,
812830
folderName: String?
813831
) {
@@ -856,10 +874,12 @@ private fun BottomFolderBar(
856874
isExpanded = isFolderBarExpanded,
857875
showExpandButton = showExpandButton,
858876
hasPendingChanges = pendingChangesCount > 0,
877+
isSkipButtonHidden = isSkipButtonHidden,
859878
onToggleExpansion = onToggleExpansion,
860879
onCreateNewAlbum = onCreateNewAlbum,
861880
onShowSummary = onShowSummary,
862-
onUndo = onUndo
881+
onUndo = onUndo,
882+
onSkip = onSkip
863883
)
864884

865885
if (targetFolders.isNotEmpty()) {
@@ -1443,6 +1463,7 @@ private fun AlreadyOrganizedDialog(
14431463
showResetHistoryButton: Boolean,
14441464
onResetHistory: () -> Unit,
14451465
onResetSingleFolderHistory: () -> Unit,
1466+
skippedCount: Int,
14461467
onClose: () -> Unit
14471468
) {
14481469
Column(
@@ -1464,6 +1485,15 @@ private fun AlreadyOrganizedDialog(
14641485
style = MaterialTheme.typography.headlineSmall,
14651486
textAlign = TextAlign.Center
14661487
)
1488+
if (skippedCount > 0) {
1489+
Spacer(modifier = Modifier.height(8.dp))
1490+
val itemText = if (skippedCount == 1) "item" else "items"
1491+
Text(
1492+
"You skipped $skippedCount $itemText this session.",
1493+
style = MaterialTheme.typography.bodyLarge,
1494+
color = MaterialTheme.colorScheme.onSurfaceVariant
1495+
)
1496+
}
14671497
Spacer(modifier = Modifier.height(32.dp))
14681498
Button(
14691499
onClick = onSelectNewFolders,
@@ -1524,4 +1554,4 @@ private fun AlreadyOrganizedDialog(
15241554
Text("Close App")
15251555
}
15261556
}
1527-
}
1557+
}

app/src/main/java/com/cleansweep/ui/screens/swiper/SwiperViewModel.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ data class SwiperUiState(
108108
val showConfirmExitDialog: Boolean = false,
109109
val currentTheme: AppTheme = AppTheme.SYSTEM,
110110
val isCurrentItemPendingConversion: Boolean = false,
111+
val isSkipButtonHidden: Boolean = true,
112+
val sessionSkippedCount: Int = 0,
111113

112114
// Pre-processed lists for Summary Sheet performance
113115
val toDelete: List<PendingChange> = emptyList(),
@@ -141,6 +143,7 @@ class SwiperViewModel @Inject constructor(
141143

142144
private val newlyAddedTargetFolders = MutableStateFlow<Map<String, String>>(emptyMap())
143145
private val sessionHiddenTargetFolders = MutableStateFlow<Set<String>>(emptySet())
146+
private val sessionSkippedMediaIds = mutableSetOf<String>()
144147

145148
val invertSwipe: StateFlow<Boolean> = preferencesRepository.invertSwipeFlow
146149
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
@@ -332,6 +335,11 @@ class SwiperViewModel @Inject constructor(
332335
_uiState.update { it.copy(currentTheme = theme) }
333336
}
334337
}
338+
viewModelScope.launch {
339+
preferencesRepository.hideSkipButtonFlow.collectLatest { isHidden ->
340+
_uiState.update { it.copy(isSkipButtonHidden = isHidden) }
341+
}
342+
}
335343
viewModelScope.launch {
336344
val initialExpandedState = preferencesRepository.bottomBarExpandedFlow.first()
337345
_uiState.update { it.copy(isFolderBarExpanded = initialExpandedState) }
@@ -438,7 +446,7 @@ class SwiperViewModel @Inject constructor(
438446
allItems.addAll(newBatch)
439447

440448
if (!initialItemFound) {
441-
val allProcessedIds = sessionProcessedMediaIds + processedMediaIds
449+
val allProcessedIds = sessionProcessedMediaIds + processedMediaIds + sessionSkippedMediaIds
442450
val firstUnprocessedIndex = allItems.indexOfFirst { it.id !in allProcessedIds }
443451
if (firstUnprocessedIndex != -1) {
444452
initialItemFound = true
@@ -506,7 +514,8 @@ class SwiperViewModel @Inject constructor(
506514
val currentState = _uiState.value
507515
val allProcessedIds = sessionProcessedMediaIds +
508516
(if (_rememberProcessedMediaEnabled) processedMediaIds else emptySet()) +
509-
currentState.pendingChanges.map { it.item.id }.toSet()
517+
currentState.pendingChanges.map { it.item.id }.toSet() +
518+
sessionSkippedMediaIds
510519

511520
val searchStartIndex = if (isDeletion) currentState.currentIndex else currentState.currentIndex + 1
512521

@@ -528,7 +537,7 @@ class SwiperViewModel @Inject constructor(
528537
)
529538
}
530539
} else {
531-
_uiState.update { it.copy(currentItem = null, isSortingComplete = true, showSummarySheet = it.pendingChanges.isNotEmpty(), isCurrentItemPendingConversion = false) }
540+
_uiState.update { it.copy(currentItem = null, isSortingComplete = true, showSummarySheet = it.pendingChanges.isNotEmpty(), isCurrentItemPendingConversion = false, sessionSkippedCount = sessionSkippedMediaIds.size) }
532541
}
533542
} else {
534543
val hasPendingChanges = currentState.pendingChanges.isNotEmpty()
@@ -537,7 +546,8 @@ class SwiperViewModel @Inject constructor(
537546
isSortingComplete = true,
538547
showSummarySheet = hasPendingChanges,
539548
videoPlaybackPosition = 0L,
540-
isCurrentItemPendingConversion = false
549+
isCurrentItemPendingConversion = false,
550+
sessionSkippedCount = sessionSkippedMediaIds.size
541551
) }
542552
}
543553
}
@@ -554,6 +564,12 @@ class SwiperViewModel @Inject constructor(
554564
advanceState()
555565
}
556566

567+
fun handleSkip() {
568+
val currentItem = _uiState.value.currentItem ?: return
569+
sessionSkippedMediaIds.add(currentItem.id)
570+
advanceState()
571+
}
572+
557573
fun moveToFolder(folderPath: String) {
558574
val currentItem = _uiState.value.currentItem ?: return
559575
addPendingChange(PendingChange(currentItem, SwiperAction.Move(currentItem, folderPath)))
@@ -1061,6 +1077,7 @@ class SwiperViewModel @Inject constructor(
10611077
_uiState.update { it.copy(showConfirmExitDialog = true) }
10621078
} else {
10631079
sessionHiddenTargetFolders.value = emptySet()
1080+
sessionSkippedMediaIds.clear()
10641081
viewModelScope.launch {
10651082
_navigationEvent.emit(NavigationEvent.NavigateUp)
10661083
}
@@ -1071,6 +1088,7 @@ class SwiperViewModel @Inject constructor(
10711088
viewModelScope.launch {
10721089
logJitSummary()
10731090
sessionHiddenTargetFolders.value = emptySet()
1091+
sessionSkippedMediaIds.clear()
10741092
_uiState.update { it.copy(showConfirmExitDialog = false) }
10751093
_navigationEvent.emit(NavigationEvent.NavigateUp)
10761094
}
@@ -1157,10 +1175,12 @@ class SwiperViewModel @Inject constructor(
11571175

11581176
fun resetPendingChanges() {
11591177
val emptyChanges = emptyList<PendingChange>()
1178+
sessionSkippedMediaIds.clear()
11601179
_uiState.update { it.copy(
11611180
pendingChanges = emptyChanges,
11621181
showSummarySheet = false,
1163-
isCurrentItemPendingConversion = false // Reset conversion state
1182+
isCurrentItemPendingConversion = false, // Reset conversion state
1183+
sessionSkippedCount = 0
11641184
) }
11651185
processPendingChangesForSummary(emptyChanges)
11661186
savedStateHandle["pendingChanges"] = null
@@ -1188,7 +1208,8 @@ class SwiperViewModel @Inject constructor(
11881208
preferencesRepository.clearProcessedMediaPaths()
11891209
preferencesRepository.clearPermanentlySortedFolders()
11901210
sessionProcessedMediaIds.clear()
1191-
_uiState.update { it.copy(toastMessage = "Sorted media history has been reset.") }
1211+
sessionSkippedMediaIds.clear()
1212+
_uiState.update { it.copy(toastMessage = "Sorted media history has been reset.", sessionSkippedCount = 0) }
11921213
initializeMedia(bucketIds)
11931214
}
11941215
}

0 commit comments

Comments
 (0)