Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions shared/src/commonMain/composeResources/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,6 @@
<string name="profile_disco_unlocked_body">Aktiviere den Disco-Modus in den LED-Einstellungen, damit dein Trainer Party macht.</string>
<string name="profile_disco_unlocked_action">Party starten</string>
<string name="profile_adult_enable_partial_failure">Die Altersbestätigung wurde gespeichert, aber der vulgäre Modus konnte nicht aktiviert werden. Versuche es erneut.</string>
<string name="label_previously_completed">Zuvor abgeschlossen</string>
<string name="cd_filter_previously_completed">Zuvor abgeschlossene Übungen filtern</string>
</resources>
2 changes: 2 additions & 0 deletions shared/src/commonMain/composeResources/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,6 @@
<string name="profile_disco_unlocked_body">Activa el modo disco en las preferencias LED para que tu entrenador se una a la fiesta.</string>
<string name="profile_disco_unlocked_action">¡A bailar!</string>
<string name="profile_adult_enable_partial_failure">La confirmación de edad se guardó, pero no se pudo activar el modo vulgar. Inténtalo de nuevo.</string>
<string name="label_previously_completed">Completado anteriormente</string>
<string name="cd_filter_previously_completed">Filtrar ejercicios completados anteriormente</string>
</resources>
2 changes: 2 additions & 0 deletions shared/src/commonMain/composeResources/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,6 @@
<string name="profile_disco_unlocked_body">Activez le mode disco dans les préférences LED pour faire danser votre machine.</string>
<string name="profile_disco_unlocked_action">C\'est parti !</string>
<string name="profile_adult_enable_partial_failure">La confirmation de l\'âge a été enregistrée, mais le mode vulgaire n\'a pas pu être activé. Réessayez.</string>
<string name="label_previously_completed">Déjà terminés</string>
<string name="cd_filter_previously_completed">Filtrer les exercices déjà terminés</string>
</resources>
2 changes: 2 additions & 0 deletions shared/src/commonMain/composeResources/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,6 @@
<string name="set_type_warmup">Warm-up Set %1$d</string>
<string name="set_type_working">Working Set</string>

<string name="label_previously_completed">Completati in precedenza</string>
<string name="cd_filter_previously_completed">Filtra gli esercizi completati in precedenza</string>
</resources>
2 changes: 2 additions & 0 deletions shared/src/commonMain/composeResources/values-nl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -775,4 +775,6 @@
<string name="profile_disco_unlocked_body">Schakel de discomodus in bij de LED-voorkeuren en laat je trainer feesten.</string>
<string name="profile_disco_unlocked_action">Tijd voor een feestje</string>
<string name="profile_adult_enable_partial_failure">De leeftijdsbevestiging is opgeslagen, maar de vulgaire modus kon niet worden ingeschakeld. Probeer het opnieuw.</string>
<string name="label_previously_completed">Eerder voltooid</string>
<string name="cd_filter_previously_completed">Eerder voltooide oefeningen filteren</string>
</resources>
2 changes: 2 additions & 0 deletions shared/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -931,4 +931,6 @@
<string name="profile_disco_unlocked_body">Turn on Disco Mode in LED preferences to make your trainer party.</string>
<string name="profile_disco_unlocked_action">Let\'s party</string>
<string name="profile_adult_enable_partial_failure">Age confirmation was saved, but Vulgar Mode could not be enabled. Try again.</string>
<string name="label_previously_completed">Previously completed</string>
<string name="cd_filter_previously_completed">Filter exercises previously completed</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
Expand All @@ -66,7 +65,9 @@ import com.devil.phoenixproject.data.repository.ExerciseVideoEntity
import com.devil.phoenixproject.domain.model.Exercise
import com.devil.phoenixproject.presentation.components.exercisepicker.ExerciseFilterShelf
import com.devil.phoenixproject.presentation.components.exercisepicker.ExerciseListEmptyState
import com.devil.phoenixproject.presentation.components.exercisepicker.ExercisePickerFilterState
import com.devil.phoenixproject.presentation.components.exercisepicker.GroupedExerciseList
import com.devil.phoenixproject.presentation.components.exercisepicker.filterExercisePickerCandidates
import com.devil.phoenixproject.presentation.util.isCompactAccessibilityLayout
import com.devil.phoenixproject.ui.theme.ThemeMode
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -110,48 +111,69 @@ fun ExercisePickerDialog(
fullScreen: Boolean = false,
themeMode: ThemeMode = ThemeMode.DARK,
enableCustomExercises: Boolean = true,
enablePreviouslyCompletedFilter: Boolean = false,
completedExerciseIds: Set<String> = emptySet(),
completedExerciseIdsLoading: Boolean = false,
) {
if (!showDialog) return

val coroutineScope = rememberCoroutineScope()
var searchQuery by remember { mutableStateOf("") }
var showFavoritesOnly by remember { mutableStateOf(false) }
var showCustomOnly by remember { mutableStateOf(false) }
var showPreviouslyCompletedOnly by remember { mutableStateOf(false) }
LaunchedEffect(showDialog) {
if (showDialog) showPreviouslyCompletedOnly = false
}
var selectedMuscles by remember { mutableStateOf(setOf<String>()) }
var selectedEquipment by remember { mutableStateOf(setOf<String>()) }
var showCreateDialog by remember { mutableStateOf(false) }
var exerciseToEdit by remember { mutableStateOf<Exercise?>(null) }

val customExercises by exerciseRepository.getCustomExercises().collectAsState(initial = emptyList())

val allExercises by remember(searchQuery, showFavoritesOnly, showCustomOnly) {
val candidateExercises by remember(searchQuery) {
when {
showCustomOnly -> exerciseRepository.getCustomExercises()
showFavoritesOnly -> exerciseRepository.getFavorites()
searchQuery.isNotBlank() -> exerciseRepository.searchExercises(searchQuery)
else -> exerciseRepository.getAllExercises()
}
}.collectAsState(initial = emptyList())

val exercises = remember(allExercises, selectedMuscles, selectedEquipment) {
allExercises.filter { exercise ->
val matchesMuscle = selectedMuscles.isEmpty() ||
selectedMuscles.any { muscle ->
exercise.muscleGroups.contains(muscle, ignoreCase = true)
}
val matchesEquipment = selectedEquipment.isEmpty() ||
selectedEquipment.any { equipment ->
val databaseValues = getEquipmentDatabaseValues(equipment)
val equipmentList = exercise.equipment.uppercase().split(",").map { it.trim() }
databaseValues.any { dbValue -> equipmentList.contains(dbValue.uppercase()) }
}
matchesMuscle && matchesEquipment
val isCompletedFilterLoading =
enablePreviouslyCompletedFilter && showPreviouslyCompletedOnly && completedExerciseIdsLoading
val exercises = remember(
candidateExercises,
showFavoritesOnly,
showCustomOnly,
selectedMuscles,
selectedEquipment,
showPreviouslyCompletedOnly,
completedExerciseIds,
isCompletedFilterLoading,
) {
if (isCompletedFilterLoading) {
emptyList()
} else {
filterExercisePickerCandidates(
candidates = candidateExercises,
filters = ExercisePickerFilterState(
showFavoritesOnly = showFavoritesOnly,
showCustomOnly = showCustomOnly,
selectedMuscles = selectedMuscles,
selectedEquipment = selectedEquipment,
showPreviouslyCompletedOnly =
enablePreviouslyCompletedFilter && showPreviouslyCompletedOnly,
),
completedExerciseIds = completedExerciseIds,
)
}
}

fun clearAllFilters() {
searchQuery = ""
showFavoritesOnly = false
showCustomOnly = false
showPreviouslyCompletedOnly = false
selectedMuscles = emptySet()
selectedEquipment = emptySet()
}
Expand Down Expand Up @@ -232,14 +254,13 @@ fun ExercisePickerDialog(
searchQuery = searchQuery,
onSearchQueryChange = { searchQuery = it },
showFavoritesOnly = showFavoritesOnly,
onToggleFavorites = {
showFavoritesOnly = !showFavoritesOnly
if (showFavoritesOnly) showCustomOnly = false
},
onToggleFavorites = { showFavoritesOnly = !showFavoritesOnly },
showCustomOnly = showCustomOnly,
onToggleCustom = {
showCustomOnly = !showCustomOnly
if (showCustomOnly) showFavoritesOnly = false
onToggleCustom = { showCustomOnly = !showCustomOnly },
enablePreviouslyCompletedFilter = enablePreviouslyCompletedFilter,
showPreviouslyCompletedOnly = showPreviouslyCompletedOnly,
onTogglePreviouslyCompleted = {
showPreviouslyCompletedOnly = !showPreviouslyCompletedOnly
},
customExerciseCount = customExercises.size,
selectedMuscles = selectedMuscles,
Expand Down Expand Up @@ -275,6 +296,7 @@ fun ExercisePickerDialog(
enableCustomExercises = enableCustomExercises,
onCreateExercise = { showCreateDialog = true },
onEditExercise = { exercise -> exerciseToEdit = exercise },
isLoading = isCompletedFilterLoading,
fullScreen = true,
)
}
Expand All @@ -295,14 +317,13 @@ fun ExercisePickerDialog(
searchQuery = searchQuery,
onSearchQueryChange = { searchQuery = it },
showFavoritesOnly = showFavoritesOnly,
onToggleFavorites = {
showFavoritesOnly = !showFavoritesOnly
if (showFavoritesOnly) showCustomOnly = false
},
onToggleFavorites = { showFavoritesOnly = !showFavoritesOnly },
showCustomOnly = showCustomOnly,
onToggleCustom = {
showCustomOnly = !showCustomOnly
if (showCustomOnly) showFavoritesOnly = false
onToggleCustom = { showCustomOnly = !showCustomOnly },
enablePreviouslyCompletedFilter = enablePreviouslyCompletedFilter,
showPreviouslyCompletedOnly = showPreviouslyCompletedOnly,
onTogglePreviouslyCompleted = {
showPreviouslyCompletedOnly = !showPreviouslyCompletedOnly
},
customExerciseCount = customExercises.size,
selectedMuscles = selectedMuscles,
Expand Down Expand Up @@ -338,6 +359,7 @@ fun ExercisePickerDialog(
enableCustomExercises = enableCustomExercises,
onCreateExercise = { showCreateDialog = true },
onEditExercise = { exercise -> exerciseToEdit = exercise },
isLoading = isCompletedFilterLoading,
fullScreen = false,
)
}
Expand All @@ -361,6 +383,9 @@ fun ExercisePickerContent(
onToggleFavorites: () -> Unit,
showCustomOnly: Boolean,
onToggleCustom: () -> Unit,
enablePreviouslyCompletedFilter: Boolean = false,
showPreviouslyCompletedOnly: Boolean = false,
onTogglePreviouslyCompleted: () -> Unit = {},
customExerciseCount: Int,
selectedMuscles: Set<String>,
onToggleMuscle: (String) -> Unit,
Expand Down Expand Up @@ -389,6 +414,7 @@ fun ExercisePickerContent(
val hasActiveFilters = searchQuery.isNotBlank() ||
showFavoritesOnly ||
showCustomOnly ||
showPreviouslyCompletedOnly ||
selectedMuscles.isNotEmpty() ||
selectedEquipment.isNotEmpty()

Expand Down Expand Up @@ -466,6 +492,9 @@ fun ExercisePickerContent(
onToggleFavorites = onToggleFavorites,
showCustomOnly = showCustomOnly,
onToggleCustom = onToggleCustom,
enablePreviouslyCompletedFilter = enablePreviouslyCompletedFilter,
showPreviouslyCompletedOnly = showPreviouslyCompletedOnly,
onTogglePreviouslyCompleted = onTogglePreviouslyCompleted,
selectedMuscles = selectedMuscles,
onToggleMuscle = onToggleMuscle,
selectedEquipment = selectedEquipment,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.devil.phoenixproject.presentation.components.exercisepicker

import com.devil.phoenixproject.domain.model.WorkoutSession

data class CompletedExerciseIdsState(
val profileId: String?,
val ids: Set<String> = emptySet(),
val isLoading: Boolean,
)

internal fun completedExerciseIdsFromHistory(
sessions: List<WorkoutSession>,
): Set<String> = sessions.mapNotNullTo(linkedSetOf()) { session ->
session.exerciseId?.trim()?.takeIf(String::isNotEmpty)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import vitruvianprojectphoenix.shared.generated.resources.*
Expand All @@ -37,6 +39,9 @@ fun ExerciseFilterShelf(
onToggleFavorites: () -> Unit,
showCustomOnly: Boolean,
onToggleCustom: () -> Unit,
enablePreviouslyCompletedFilter: Boolean = false,
showPreviouslyCompletedOnly: Boolean = false,
onTogglePreviouslyCompleted: () -> Unit = {},
selectedMuscles: Set<String>,
onToggleMuscle: (String) -> Unit,
selectedEquipment: Set<String>,
Expand All @@ -57,7 +62,9 @@ fun ExerciseFilterShelf(
"Bodyweight",
)

val hasActiveFilters = showFavoritesOnly || showCustomOnly ||
val previouslyCompletedDescription =
stringResource(Res.string.cd_filter_previously_completed)
val hasActiveFilters = showFavoritesOnly || showCustomOnly || showPreviouslyCompletedOnly ||
selectedMuscles.isNotEmpty() || selectedEquipment.isNotEmpty()

LazyRow(
Expand Down Expand Up @@ -128,6 +135,23 @@ fun ExerciseFilterShelf(
)
}

if (enablePreviouslyCompletedFilter) {
item {
FilterChip(
selected = showPreviouslyCompletedOnly,
onClick = onTogglePreviouslyCompleted,
label = { Text(stringResource(Res.string.label_previously_completed)) },
modifier = Modifier.semantics {
contentDescription = previouslyCompletedDescription
},
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
)
}
}

// Divider
item {
VerticalDivider(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.devil.phoenixproject.presentation.components.exercisepicker

import com.devil.phoenixproject.domain.model.Exercise
import com.devil.phoenixproject.presentation.components.getEquipmentDatabaseValues

internal data class ExercisePickerFilterState(
val showFavoritesOnly: Boolean = false,
val showCustomOnly: Boolean = false,
val selectedMuscles: Set<String> = emptySet(),
val selectedEquipment: Set<String> = emptySet(),
val showPreviouslyCompletedOnly: Boolean = false,
)

/**
* Applies every exercise-picker narrowing predicate to the repository-selected candidates.
* The source list controls search semantics and ordering; this helper only removes entries.
*/
internal fun filterExercisePickerCandidates(
candidates: List<Exercise>,
filters: ExercisePickerFilterState,
completedExerciseIds: Set<String> = emptySet(),
): List<Exercise> = candidates.filter { exercise ->
val matchesFavorites = !filters.showFavoritesOnly || exercise.isFavorite
val matchesCustom = !filters.showCustomOnly || exercise.isCustom
val matchesMuscle = filters.selectedMuscles.isEmpty() ||
filters.selectedMuscles.any { muscle ->
exercise.muscleGroups.contains(muscle, ignoreCase = true)
}
val matchesEquipment = filters.selectedEquipment.isEmpty() ||
filters.selectedEquipment.any { equipment ->
val databaseValues = getEquipmentDatabaseValues(equipment)
val exerciseEquipment = exercise.equipment.uppercase().split(",").map { it.trim() }
databaseValues.any { databaseValue ->
databaseValue.uppercase() in exerciseEquipment
}
}
val matchesPreviouslyCompleted = !filters.showPreviouslyCompletedOnly ||
(exercise.id?.trim()?.takeIf(String::isNotEmpty) in completedExerciseIds)

matchesFavorites && matchesCustom && matchesMuscle && matchesEquipment && matchesPreviouslyCompleted
}
Loading
Loading