diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fd5ac6d9..609f12e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- +- A new toggle to show images was introduced (off by default). [#29](https://github.com/LibreFitOrg/LibreFit/issues/29) ### Deprecated diff --git a/app/src/main/java/org/librefit/db/repository/UserPreferencesRepository.kt b/app/src/main/java/org/librefit/db/repository/UserPreferencesRepository.kt index b15b829f5..46075b732 100644 --- a/app/src/main/java/org/librefit/db/repository/UserPreferencesRepository.kt +++ b/app/src/main/java/org/librefit/db/repository/UserPreferencesRepository.kt @@ -53,6 +53,7 @@ private val SHOW_KEEP_ANDROID_OPEN_KEY = booleanPreferencesKey("showKeepAndroidO private val USE_SCROLL_WHEEL_FOR_INPUT_KEY = booleanPreferencesKey("use_number_picker") private val DISMISS_SCROLL_WHELL_INPUT_AUTOMATICALLY = booleanPreferencesKey("dismiss_input_modal_bottom_sheet_automatically_key") +private val SHOW_EXERCISES_IMAGES_KEY = booleanPreferencesKey("show_exercises_images_key") private val UNIT_SYSTEM_KEY = stringPreferencesKey("unit_system") /** * Central repository managing application-level preferences, including theme, unit systems, and language. @@ -169,6 +170,14 @@ class UserPreferencesRepository @Inject constructor( initialValue = true ) + val showExercisesImages: StateFlow = dataStore.data + .map { preferences -> preferences[SHOW_EXERCISES_IMAGES_KEY] } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null + ) + val dismissScrollWheelInputAutomatically: StateFlow = dataStore.data .map { preferences -> preferences[DISMISS_SCROLL_WHELL_INPUT_AUTOMATICALLY] == true } .stateIn( @@ -314,6 +323,12 @@ class UserPreferencesRepository @Inject constructor( dataStore.edit { preferences -> preferences[USE_SCROLL_WHEEL_FOR_INPUT_KEY] = useScroll } } + suspend fun saveShowExercisesImages(show: Boolean) { + dataStore.edit { preferences -> + preferences[SHOW_EXERCISES_IMAGES_KEY] = show + } + } + suspend fun saveDismissScrollWheelInputAutomatically(dismissAutomatically: Boolean) { dataStore.edit { preferences -> preferences[DISMISS_SCROLL_WHELL_INPUT_AUTOMATICALLY] = dismissAutomatically diff --git a/app/src/main/java/org/librefit/ui/components/ExerciseCard.kt b/app/src/main/java/org/librefit/ui/components/ExerciseCard.kt index 65a1d61ae..eb01cb08e 100644 --- a/app/src/main/java/org/librefit/ui/components/ExerciseCard.kt +++ b/app/src/main/java/org/librefit/ui/components/ExerciseCard.kt @@ -141,6 +141,7 @@ import kotlin.time.Duration.Companion.seconds * @param isDragging when `true`, it applies a shadow to further emphasize with a shadow that the card is dragged. * @param useScrollWheelForInput If `true`, [InputModalBottomSheet] appears instead of keyboard * @param dismissScrollWheelInputAutomatically If both this and [useScrollWheelForInput] are `true`, the [InputModalBottomSheet] will be dismissed automatically after first edit. + * @param showExercisesImages If `true`, it shows image of exercise * @param updateExerciseNotes A function to update notes based on [UiExercise.id]. For more details, refer to * [org.librefit.ui.screens.workout.WorkoutScreenViewModel.updateExerciseNotes] and * [org.librefit.ui.screens.editWorkout.EditWorkoutScreenViewModel.updateExerciseNotes]. @@ -195,6 +196,7 @@ fun SharedTransitionScope.ExerciseCard( isDragging: Boolean, useScrollWheelForInput: Boolean, dismissScrollWheelInputAutomatically: Boolean, + showExercisesImages: Boolean?, onReorderRequest: () -> Unit, deleteSet: (Long) -> Unit, updateExerciseNotes: (String, Long) -> Unit, @@ -244,23 +246,25 @@ fun SharedTransitionScope.ExerciseCard( ) { val model = remember(exerciseWithSets.exerciseDC.images) { exerciseWithSets.exerciseDC.images.firstOrNull() } - AsyncImage( - model = model?.let { "file:///android_asset/${it}" }, - fallback = painterResource(R.drawable.no_image), - contentDescription = exerciseWithSets.exerciseDC.name, - contentScale = ContentScale.Crop, - colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, - modifier = Modifier - .padding(end = 10.dp) - .sharedElement( - sharedContentState = rememberSharedContentState( - key = exerciseWithSets.exercise.id.toString() + exerciseWithSets.exerciseDC.id - ), - animatedVisibilityScope = animatedVisibilityScope - ) - .size(50.dp) - .clip(MaterialTheme.shapes.medium) - ) + if (showExercisesImages == true) { + AsyncImage( + model = model?.let { "file:///android_asset/${it}" }, + fallback = painterResource(R.drawable.no_image), + contentDescription = exerciseWithSets.exerciseDC.name, + contentScale = ContentScale.Crop, + colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, + modifier = Modifier + .padding(end = 10.dp) + .sharedElement( + sharedContentState = rememberSharedContentState( + key = exerciseWithSets.exercise.id.toString() + exerciseWithSets.exerciseDC.id + ), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(50.dp) + .clip(MaterialTheme.shapes.medium) + ) + } Text( text = exerciseWithSets.exerciseDC.name, style = MaterialTheme.typography.headlineSmall, @@ -1027,6 +1031,7 @@ private fun ExerciseCardPreview() { isDragging = false, useScrollWheelForInput = false, dismissScrollWheelInputAutomatically = false, + showExercisesImages = false, updateExerciseNotes = { notes, _ -> e.value = e.value.copy(exercise = e.value.exercise.copy(notes = notes)) }, diff --git a/app/src/main/java/org/librefit/ui/components/ExerciseCardSmall.kt b/app/src/main/java/org/librefit/ui/components/ExerciseCardSmall.kt index 630fd02c7..8bed7330b 100644 --- a/app/src/main/java/org/librefit/ui/components/ExerciseCardSmall.kt +++ b/app/src/main/java/org/librefit/ui/components/ExerciseCardSmall.kt @@ -79,6 +79,7 @@ import org.librefit.util.Formatter.formatTime fun SharedTransitionScope.ExerciseCardSmall( exerciseWithSets: UiExerciseWithSets, isRoutine: Boolean = false, + showExercisesImages: Boolean?, animatedVisibilityScope: AnimatedVisibilityScope, onDetail: () -> Unit ) { @@ -103,23 +104,25 @@ fun SharedTransitionScope.ExerciseCardSmall( ) { val model = remember(exerciseWithSets.exerciseDC.images) { exerciseWithSets.exerciseDC.images.firstOrNull() } - AsyncImage( - model = model?.let { "file:///android_asset/${it}" }, - fallback = painterResource(R.drawable.no_image), - contentDescription = exerciseWithSets.exerciseDC.name, - contentScale = ContentScale.Crop, - colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, - modifier = Modifier - .padding(end = 10.dp) - .sharedElement( - sharedContentState = rememberSharedContentState( - key = exerciseWithSets.exercise.id.toString() + exerciseWithSets.exerciseDC.id - ), - animatedVisibilityScope = animatedVisibilityScope - ) - .size(50.dp) - .clip(MaterialTheme.shapes.medium) - ) + if (showExercisesImages == true) { + AsyncImage( + model = model?.let { "file:///android_asset/${it}" }, + fallback = painterResource(R.drawable.no_image), + contentDescription = exerciseWithSets.exerciseDC.name, + contentScale = ContentScale.Crop, + colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, + modifier = Modifier + .padding(end = 10.dp) + .sharedElement( + sharedContentState = rememberSharedContentState( + key = exerciseWithSets.exercise.id.toString() + exerciseWithSets.exerciseDC.id + ), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(50.dp) + .clip(MaterialTheme.shapes.medium) + ) + } Text( modifier = Modifier.weight(1f), text = exerciseWithSets.exerciseDC.name, @@ -289,6 +292,7 @@ private fun ExerciseCardSmallPreview() { ), sets = persistentListOf(UiSet(completed = true), UiSet(reps = 10), UiSet()) ), + showExercisesImages = null, animatedVisibilityScope = this ) { } } diff --git a/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreen.kt b/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreen.kt index 40e8fa61c..f17758086 100644 --- a/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreen.kt @@ -87,6 +87,8 @@ fun SharedTransitionScope.EditExerciseScreen( val exerciseDC by viewModel.exerciseDC.collectAsStateWithLifecycle() + val showExercisesImages by viewModel.showExercisesImages.collectAsStateWithLifecycle() + val isCreateMode = exerciseDC.id.isBlank() EditExerciseScreenContent( @@ -103,6 +105,7 @@ fun SharedTransitionScope.EditExerciseScreen( instructions = exerciseDC.instructions, category = exerciseDC.category, images = exerciseDC.images, + showExercisesImages = showExercisesImages, navigateBack = navController::navigateUp, animatedVisibilityScope = animatedVisibilityScope, updateValue = viewModel::updateValue, @@ -141,6 +144,7 @@ private fun SharedTransitionScope.EditExerciseScreenContent( instructions: List, images: List, category: Category, + showExercisesImages: Boolean?, animatedVisibilityScope: AnimatedVisibilityScope, navigateBack: () -> Unit, updateValue: (ExerciseProperty) -> Unit, @@ -191,26 +195,28 @@ private fun SharedTransitionScope.EditExerciseScreenContent( item { // TODO: implement display all images (like in a horizontal pager) val model = remember(images) { images.firstOrNull() } - AsyncImage( - model = model?.let { "file:///android_asset/${it}" }, - fallback = painterResource(R.drawable.no_image), - contentDescription = name, - contentScale = ContentScale.Crop, - filterQuality = FilterQuality.High, - colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, - modifier = Modifier - .sharedElement( - sharedContentState = rememberSharedContentState(stringId + exerciseDcId), - animatedVisibilityScope = animatedVisibilityScope - ) - .fillMaxWidth() - .clip(MaterialTheme.shapes.extraLarge) - .border( - 0.5.dp, - color = MaterialTheme.colorScheme.outlineVariant, - shape = MaterialTheme.shapes.extraLarge - ), - ) + if (showExercisesImages == true) { + AsyncImage( + model = model?.let { "file:///android_asset/${it}" }, + fallback = painterResource(R.drawable.no_image), + contentDescription = name, + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.High, + colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, + modifier = Modifier + .sharedElement( + sharedContentState = rememberSharedContentState(stringId + exerciseDcId), + animatedVisibilityScope = animatedVisibilityScope + ) + .fillMaxWidth() + .clip(MaterialTheme.shapes.extraLarge) + .border( + 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = MaterialTheme.shapes.extraLarge + ), + ) + } } item { } @@ -420,6 +426,7 @@ private fun EditExerciseScreenContentPreview() { instructions = e.instructions, category = e.category, images = e.images, + showExercisesImages = null, animatedVisibilityScope = this, saveExercise = {}, updatePrimaryMuscles = {}, diff --git a/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreenViewModel.kt index e387782e8..b97d0012f 100644 --- a/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/editExercise/EditExerciseScreenViewModel.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.librefit.db.repository.DatasetRepository +import org.librefit.db.repository.UserPreferencesRepository import org.librefit.enums.exercise.Category import org.librefit.enums.exercise.Equipment import org.librefit.enums.exercise.ExerciseProperty @@ -38,9 +39,12 @@ import kotlin.uuid.Uuid @HiltViewModel class EditExerciseScreenViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val datasetRepository: DatasetRepository + private val datasetRepository: DatasetRepository, + userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { + val showExercisesImages = userPreferencesRepository.showExercisesImages + val exerciseDCid = savedStateHandle.toRoute().exerciseDCid @OptIn(ExperimentalUuidApi::class) diff --git a/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreen.kt b/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreen.kt index d62f870ba..e6e45e9b2 100644 --- a/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreen.kt @@ -84,6 +84,8 @@ fun SharedTransitionScope.EditWorkoutScreen( val useScrollWheelForInput by viewModel.useScrollWheelForInput.collectAsStateWithLifecycle() + val showExercisesImages by viewModel.showExercisesImages.collectAsStateWithLifecycle() + val dismissInputAutomatically by viewModel.dismissScrollWheelInputAutomatically.collectAsStateWithLifecycle() LaunchedEffect(Unit) { @@ -117,6 +119,7 @@ fun SharedTransitionScope.EditWorkoutScreen( isTitleEmpty = viewModel.isTitleEmpty(), dismissInputAutomatically = dismissInputAutomatically, useScrollWheelForInput = useScrollWheelForInput, + showExercisesImages = showExercisesImages, updateTitle = viewModel::updateTitle, updateNotes = viewModel::updateNotes, updateSetTime = viewModel::updateSetTime, @@ -149,6 +152,7 @@ private fun SharedTransitionScope.EditWorkoutScreenContent( isTitleEmpty: Boolean, dismissInputAutomatically: Boolean, useScrollWheelForInput: Boolean, + showExercisesImages: Boolean?, updateTitle: (String) -> Unit, updateNotes: (String) -> Unit, deleteSet: (Long) -> Unit, @@ -339,6 +343,7 @@ private fun SharedTransitionScope.EditWorkoutScreenContent( addSet = addSetToExercise, isDragging = isDragging, useScrollWheelForInput = useScrollWheelForInput, + showExercisesImages = showExercisesImages, dismissScrollWheelInputAutomatically = dismissInputAutomatically, onDetail = { id, idExerciseDC -> navController.navigate( @@ -425,6 +430,7 @@ private fun EditWorkoutScreenPreview() { isTitleEmpty = false, useScrollWheelForInput = false, dismissInputAutomatically = false, + showExercisesImages = null, updateTitle = { _ -> }, updateNotes = { _ -> }, addSetToExercise = { _ -> }, diff --git a/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreenViewModel.kt index fd016e545..08dc127c9 100644 --- a/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/editWorkout/EditWorkoutScreenViewModel.kt @@ -49,7 +49,7 @@ class EditWorkoutScreenViewModel @Inject constructor( @param:IoDispatcher private val ioDispatcher: CoroutineDispatcher, userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { - + val showExercisesImages = userPreferencesRepository.showExercisesImages val useScrollWheelForInput = userPreferencesRepository.useScrollWheelForInput val dismissScrollWheelInputAutomatically = diff --git a/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreen.kt b/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreen.kt index ea459689a..5196fa8e6 100644 --- a/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreen.kt @@ -99,7 +99,7 @@ fun SharedTransitionScope.ExercisesScreen( val isSupporter by viewModel.isSupporter.collectAsStateWithLifecycle() - + val showExercisesImages by viewModel.showExercisesImages.collectAsStateWithLifecycle() var showConfirmDialog by remember { mutableStateOf(false) } @@ -134,6 +134,7 @@ fun SharedTransitionScope.ExercisesScreen( filteredExerciseList = filteredExerciseList, query = query, filterValue = filterValue, + showExercisesImages = showExercisesImages, animatedVisibilityScope = animatedVisibilityScope, toggleSelectedExercise = viewModel::toggleSelectedExercise, updateQuery = viewModel::updateQuery, @@ -160,6 +161,7 @@ private fun SharedTransitionScope.ExercisesScreenContent( filteredExerciseList: List, query: String, filterValue: FilterValue, + showExercisesImages: Boolean?, animatedVisibilityScope: AnimatedVisibilityScope, toggleSelectedExercise: (String) -> Unit, updateQuery: (String) -> Unit, @@ -261,6 +263,7 @@ private fun SharedTransitionScope.ExercisesScreenContent( modifier = Modifier.animateItem(), addExercises = addExercises, exercise = exercise, + showExercisesImages = showExercisesImages, animatedVisibilityScope = animatedVisibilityScope, onAddToggle = { toggleSelectedExercise(exercise.id) }, isSelected = exercise.id in selectedExercisesIdList, @@ -278,6 +281,7 @@ private fun SharedTransitionScope.ItemExerciseDC( addExercises: Boolean, exercise: UiExerciseDC, isSelected: Boolean, + showExercisesImages: Boolean?, animatedVisibilityScope: AnimatedVisibilityScope, onAddToggle: () -> Unit, onInfo: () -> Unit, @@ -303,23 +307,27 @@ private fun SharedTransitionScope.ItemExerciseDC( verticalAlignment = Alignment.CenterVertically, ) { val model = remember { exercise.images.firstOrNull() } - AsyncImage( - model = model?.let { "file:///android_asset/${it}" }, - fallback = painterResource(R.drawable.no_image), - contentDescription = exercise.name, - contentScale = ContentScale.Crop, - colorFilter = if (model == null) ColorFilter.tint(LocalContentColor.current) else null, - filterQuality = FilterQuality.High, - modifier = Modifier - .sharedElement( - sharedContentState = rememberSharedContentState(exercise.id), - animatedVisibilityScope = animatedVisibilityScope - ) - .size(100.dp) - .clip(MaterialTheme.shapes.large) - ) + if (showExercisesImages == true) { + AsyncImage( + model = model?.let { "file:///android_asset/${it}" }, + fallback = painterResource(R.drawable.no_image), + contentDescription = exercise.name, + contentScale = ContentScale.Crop, + colorFilter = if (model == null) ColorFilter.tint(LocalContentColor.current) else null, + filterQuality = FilterQuality.High, + modifier = Modifier + .sharedElement( + sharedContentState = rememberSharedContentState(exercise.id), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(100.dp) + .clip(MaterialTheme.shapes.large) + ) + } Column( - modifier = Modifier.padding(start = 20.dp), + modifier = Modifier.padding( + start = (if (showExercisesImages == true) 20 else 10).dp + ), verticalArrangement = Arrangement.Center ) { Text( @@ -417,6 +425,7 @@ private fun ExercisesScreenPreview() { query = query, filterValue = filterValue, toggleSelectedExercise = {}, + showExercisesImages = false, updateQuery = { query = it }, updateFilter = { filterValue = it }, actions = persistentListOf({}), diff --git a/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModel.kt index 4f7d77247..726fb4290 100644 --- a/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModel.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -27,12 +28,15 @@ import org.librefit.enums.exercise.FilterValue import org.librefit.ui.models.UiExerciseDC import org.librefit.util.fuzzySearch.FuzzySearch import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class ExercisesScreenViewModel @Inject constructor( datasetRepository: DatasetRepository, userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { + val showExercisesImages = userPreferencesRepository.showExercisesImages + private val _query = MutableStateFlow("") val query = _query.asStateFlow() @@ -41,14 +45,9 @@ class ExercisesScreenViewModel @Inject constructor( } @OptIn(FlowPreview::class) - val debouncedQuery: StateFlow = _query - .debounce(300L) + val debouncedQuery: Flow = _query + .debounce(300.milliseconds) .distinctUntilChanged() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = "" - ) @@ -73,7 +72,6 @@ class ExercisesScreenViewModel @Inject constructor( .sortedByDescending { it.second } .map { it.first } } - .distinctUntilChanged() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), @@ -112,7 +110,6 @@ class ExercisesScreenViewModel @Inject constructor( selectedExerciseIds, dataset ) { ids, list -> list.filter { it.id in ids } } - .distinctUntilChanged() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), diff --git a/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt b/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt index 1e7c6f9a3..bdccaa8df 100644 --- a/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt @@ -8,15 +8,21 @@ package org.librefit.ui.screens.infoExercise +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -37,11 +43,11 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonGroup import androidx.compose.material3.Checkbox import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -102,6 +108,7 @@ import org.librefit.enums.pages.InfoExercisePages import org.librefit.enums.userPreferences.ThemeMode import org.librefit.nav.Route import org.librefit.ui.components.HeadlineText +import org.librefit.ui.components.LibreFitButton import org.librefit.ui.components.LibreFitLazyColumn import org.librefit.ui.components.LibreFitScaffold import org.librefit.ui.components.animations.EmptyLottie @@ -131,6 +138,7 @@ fun SharedTransitionScope.InfoExerciseScreen( navController: NavHostController, viewModel: InfoExerciseScreenViewModel = hiltViewModel() ) { + val showExercisesImages by viewModel.showExercisesImages.collectAsStateWithLifecycle() val workoutsWithExercises by viewModel.workoutsWithExercises.collectAsStateWithLifecycle() @@ -145,9 +153,12 @@ fun SharedTransitionScope.InfoExerciseScreen( exerciseDC = uiExerciseDC, animatedVisibilityScope = animatedVisibilityScope, workoutsWithExercises = workoutsWithExercises, + showExercisesImages = showExercisesImages, points = points, exerciseChart = exerciseChart, navController = navController, + setTrueShowExercisesImages = viewModel::setTrueShowExercisesImages, + setFalseShowExercisesImages = viewModel::setFalseShowExercisesImages, updateExerciseChart = viewModel::updateExerciseChart, deleteExercise = viewModel::deleteExercise ) @@ -159,11 +170,14 @@ fun SharedTransitionScope.InfoExerciseScreen( private fun SharedTransitionScope.InfoExerciseScreenContent( id: Long, exerciseDC: UiExerciseDC, + showExercisesImages: Boolean?, animatedVisibilityScope: AnimatedVisibilityScope, workoutsWithExercises: List, points: List, exerciseChart: ExerciseChart, navController: NavHostController, + setTrueShowExercisesImages: () -> Unit, + setFalseShowExercisesImages: () -> Unit, updateExerciseChart: (ExerciseChart) -> Unit, deleteExercise: () -> Unit ) { @@ -235,7 +249,20 @@ private fun SharedTransitionScope.InfoExerciseScreenContent( ) } item { - AlternatingImages(stringId, exerciseDC, animatedVisibilityScope) + AnimatedVisibility( + visible = showExercisesImages != false, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + AlternatingImages( + stringId = stringId, + exercise = exerciseDC, + showExercisesImages = showExercisesImages, + setTrueShowExercisesImages = setTrueShowExercisesImages, + setFalseShowExercisesImages = setFalseShowExercisesImages, + animatedVisibilityScope = animatedVisibilityScope + ) + } } stickyHeader { PrimaryTabRow(selectedTabIndex = pagerState.currentPage) { @@ -750,14 +777,15 @@ private fun SharedTransitionScope.HistoryPage( private fun SharedTransitionScope.AlternatingImages( stringId: String, exercise: UiExerciseDC, + showExercisesImages: Boolean?, + setTrueShowExercisesImages: () -> Unit, + setFalseShowExercisesImages: () -> Unit, animatedVisibilityScope: AnimatedVisibilityScope ) { var currentImageIndex by remember { mutableIntStateOf(0) } var isRunning by rememberSaveable { mutableStateOf(true) } - var showWarning by rememberSaveable { mutableStateOf(true) } - LaunchedEffect(exercise.images) { while (exercise.images.isNotEmpty()) { @@ -768,84 +796,123 @@ private fun SharedTransitionScope.AlternatingImages( } } - Column { - Box( - modifier = Modifier.padding(15.dp), - contentAlignment = Alignment.BottomEnd - ) { - val model = remember(currentImageIndex) { exercise.images.getOrNull(currentImageIndex) } - AsyncImage( - model = model?.let { "file:///android_asset/${it}" }, - fallback = painterResource(R.drawable.no_image), - contentDescription = exercise.name, - contentScale = ContentScale.Crop, - colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, - filterQuality = FilterQuality.High, - modifier = Modifier - .sharedElement( - sharedContentState = rememberSharedContentState(stringId + exercise.id), - animatedVisibilityScope = animatedVisibilityScope - ) - .aspectRatio( - ratio = rememberAssetAspectRatio(model, 16f / 9) + AnimatedContent(showExercisesImages) { show -> + Column { + if (show == true) { + Box( + modifier = Modifier.padding(15.dp), + contentAlignment = Alignment.BottomEnd + ) { + val model = + remember(currentImageIndex) { exercise.images.getOrNull(currentImageIndex) } + AsyncImage( + model = model?.let { "file:///android_asset/${it}" }, + fallback = painterResource(R.drawable.no_image), + contentDescription = exercise.name, + contentScale = ContentScale.Crop, + colorFilter = if (model == null) ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) else null, + filterQuality = FilterQuality.High, + modifier = Modifier + .sharedElement( + sharedContentState = rememberSharedContentState(stringId + exercise.id), + animatedVisibilityScope = animatedVisibilityScope + ) + .aspectRatio( + ratio = rememberAssetAspectRatio(model, 16f / 9) + ) + .fillMaxWidth() + .clip(MaterialTheme.shapes.extraLarge) + .border( + width = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant, + MaterialTheme.shapes.extraLarge + ), ) - .fillMaxWidth() - .clip(MaterialTheme.shapes.extraLarge) - .border( - width = 0.5.dp, - color = MaterialTheme.colorScheme.outlineVariant, - MaterialTheme.shapes.extraLarge - ), - ) - if (exercise.images.isNotEmpty()) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - FilledTonalIconToggleButton( - checked = showWarning, - modifier = Modifier.padding(10.dp), - onCheckedChange = { showWarning = it }, - ) { - Icon( - painter = painterResource(R.drawable.ic_warning), - contentDescription = stringResource(R.string.warning), - ) - } + if (exercise.images.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { - ToggleButton( - checked = isRunning, - modifier = Modifier.padding(10.dp), - onCheckedChange = { isRunning = it }, - shapes = ToggleButtonDefaults.shapes() - ) { - Icon( - painter = painterResource( - if (isRunning) R.drawable.ic_pause else R.drawable.ic_play_arrow - ), - contentDescription = stringResource(if (isRunning) R.string.pause else R.string.resume), - ) + ToggleButton( + checked = isRunning, + modifier = Modifier.padding(10.dp), + onCheckedChange = { isRunning = it }, + shapes = ToggleButtonDefaults.shapes() + ) { + Icon( + painter = painterResource( + if (isRunning) R.drawable.ic_pause else R.drawable.ic_play_arrow + ), + contentDescription = stringResource(if (isRunning) R.string.pause else R.string.resume), + ) + } + } } } - } - } - if (exercise.images.isNotEmpty()) { - AnimatedVisibility(visible = showWarning) { - OutlinedCard( - modifier = Modifier.padding(start= 15.dp, end= 15.dp), - shape = MaterialTheme.shapes.large - ){ - Column(Modifier.padding(10.dp)) { + + } else { + ElevatedCard( + shape = MaterialTheme.shapes.largeIncreased, + modifier = Modifier.padding(15.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { Text( + modifier = Modifier.padding(15.dp), text = stringResource(R.string.ai_images_warning), - style = MaterialTheme.typography.labelSmall + textAlign = TextAlign.Center ) + val interactionSources = remember { List(2) { MutableInteractionSource() } } + + ButtonGroup( + overflowIndicator = {} + ) { + customItem( + menuContent = {}, + buttonGroupContent = { + LibreFitButton( + modifier = Modifier + .weight(1f) + .animateWidth(interactionSources[0]), + text = stringResource(R.string.show), + icon = painterResource(R.drawable.ic_image), + interactionSource = interactionSources[0] + ) { + setTrueShowExercisesImages() + } + } + ) + customItem( + menuContent = {}, + buttonGroupContent = { + LibreFitButton( + modifier = Modifier + .weight(1f) + .animateWidth(interactionSources[1]), + text = stringResource(R.string.hide), + icon = painterResource(R.drawable.ic_hide_image), + interactionSource = interactionSources[1], + elevated = false + ) { + setFalseShowExercisesImages() + } + } + ) + } + } } } } } + } @OptIn(ExperimentalSharedTransitionApi::class) @@ -875,6 +942,7 @@ private fun InfoExercisePreview() { category = Category.STRENGTH, images = persistentListOf("3_4_Sit-Up/0.jpg", "3_4_Sit-Up/1.jpg") ), + showExercisesImages = false, animatedVisibilityScope = this, workoutsWithExercises = listOf( UiWorkoutWithExercisesAndSets( @@ -912,6 +980,8 @@ private fun InfoExercisePreview() { ), points = emptyList(), navController = rememberNavController(), + setTrueShowExercisesImages = {}, + setFalseShowExercisesImages = {}, updateExerciseChart = {}, deleteExercise = {} ) diff --git a/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreenViewModel.kt index 4d8f79b18..cd1813da0 100644 --- a/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreenViewModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.librefit.db.repository.DatasetRepository +import org.librefit.db.repository.UserPreferencesRepository import org.librefit.db.repository.WorkoutRepository import org.librefit.enums.chart.BodyweightChart import org.librefit.enums.chart.ExerciseChart @@ -49,9 +50,27 @@ class InfoExerciseScreenViewModel @Inject constructor( savedStateHandle: SavedStateHandle, workoutRepository: WorkoutRepository, dataHelper: DataHelper, - private val datasetRepository: DatasetRepository + private val datasetRepository: DatasetRepository, + private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { + val showExercisesImages = userPreferencesRepository.showExercisesImages + + fun setTrueShowExercisesImages() { + onShowExercisesImagesChange(true) + } + + fun setFalseShowExercisesImages() { + onShowExercisesImagesChange(false) + } + + fun onShowExercisesImagesChange(value: Boolean) { + viewModelScope.launch { + userPreferencesRepository.saveShowExercisesImages(value) + } + } + + private val idExerciseDC = savedStateHandle.toRoute().idExerciseDC // Keeps track of changes (e.g. the user edits the exercise) diff --git a/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreen.kt b/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreen.kt index 8e7c85217..333bf9a4f 100644 --- a/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreen.kt @@ -84,6 +84,8 @@ fun SharedTransitionScope.InfoWorkoutScreen( viewModel: InfoWorkoutScreenViewModel = hiltViewModel() ) { + val showExercisesImages by viewModel.showExercisesImages.collectAsStateWithLifecycle() + val points by viewModel.points.collectAsStateWithLifecycle() val workout by viewModel.workout.collectAsStateWithLifecycle() @@ -109,6 +111,7 @@ fun SharedTransitionScope.InfoWorkoutScreen( workoutChart = workoutChartMode, exercises = exercises, points = points, + showExercisesImages = showExercisesImages, deleteWorkout = viewModel::deleteWorkout, updateChartMode = viewModel::updateChartMode, detachWorkoutFromRoutine = viewModel::detachWorkoutFromRoutine, @@ -129,6 +132,7 @@ private fun SharedTransitionScope.InfoWorkoutScreenContent( workoutChart: WorkoutChart, exercises: List, points: List, + showExercisesImages: Boolean?, deleteWorkout: () -> Unit, detachWorkoutFromRoutine: () -> Unit, updateChartMode: (WorkoutChart) -> Unit @@ -406,6 +410,7 @@ private fun SharedTransitionScope.InfoWorkoutScreenContent( ExerciseCardSmall( exerciseWithSets = e, isRoutine = isRoutine, + showExercisesImages = showExercisesImages, animatedVisibilityScope = animatedVisibilityScope ) { navController.navigate( @@ -438,6 +443,7 @@ private fun InfoRoutineScreenPreview() { workoutDate = Formatter.getFullDateFromLocalDate(LocalDateTime.now()), volumeExercises = Weight.kilograms(100.0), workoutChart = WorkoutChart.REPS, + showExercisesImages = null, exercises = listOf( UiExerciseWithSets( exerciseDC = UiExerciseDC( diff --git a/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreenViewModel.kt index 571bc1f38..8a600f93a 100644 --- a/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/infoWorkout/InfoWorkoutScreenViewModel.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.librefit.db.relations.WorkoutWithExercisesAndSets +import org.librefit.db.repository.UserPreferencesRepository import org.librefit.db.repository.WorkoutRepository import org.librefit.di.qualifiers.IoDispatcher import org.librefit.enums.WorkoutState @@ -45,9 +46,12 @@ class InfoWorkoutScreenViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val workoutRepository: WorkoutRepository, dataHelper: DataHelper, - @param:IoDispatcher private val ioDispatcher: CoroutineDispatcher + @param:IoDispatcher private val ioDispatcher: CoroutineDispatcher, + userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { + val showExercisesImages = userPreferencesRepository.showExercisesImages + private val workoutId = savedStateHandle.toRoute().workoutId private val _volume = MutableStateFlow(Weight.zero()) diff --git a/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreen.kt index 94a83d1c3..4bf79dc4f 100644 --- a/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -56,6 +57,7 @@ import org.librefit.nav.Route import org.librefit.ui.components.HeadlineText import org.librefit.ui.components.LibreFitLazyColumn import org.librefit.ui.components.LibreFitScaffold +import org.librefit.ui.components.dialogs.ConfirmDialog import org.librefit.ui.components.dialogs.PreferenceDialog import org.librefit.ui.theme.LibreFitTheme import org.librefit.util.Formatter @@ -89,6 +91,8 @@ fun SettingsScreen( val useScrollWheelForInput by viewModel.useScrollWheelForInput.collectAsStateWithLifecycle() + val showExercisesImages by viewModel.showExercisesImages.collectAsStateWithLifecycle() + val dismissScrollWheelInputAutomatically by viewModel.dismissScrollWheelInputAutomatically.collectAsStateWithLifecycle() preferences?.let { @@ -102,6 +106,26 @@ fun SettingsScreen( } + + var showConfirmDialogDisplayExerciseImages by rememberSaveable { mutableStateOf(false) } + + + if (showConfirmDialogDisplayExerciseImages) { + ConfirmDialog( + title = stringResource(R.string.show_images), + text = stringResource(R.string.ai_images_warning), + confirmText = stringResource(R.string.show), + onConfirm = { + viewModel.saveShowExercisesImages(true) + + showConfirmDialogDisplayExerciseImages = false + }, + onDismiss = { + showConfirmDialogDisplayExerciseImages = false + } + ) + } + SettingsScreenContent( navController = navController, selectedTheme = selectedTheme, @@ -111,6 +135,7 @@ fun SettingsScreen( restTimerSoundOn = restTimerSoundOn, isSupporter = isSupporter, useScrollWheelForInput = useScrollWheelForInput, + showExercisesImages = showExercisesImages, isWorkoutHeaderSticky = isWorkoutHeaderSticky, dismissScrollWheelInputAutomatically = dismissScrollWheelInputAutomatically, unitSystem = unitSystem, @@ -120,6 +145,10 @@ fun SettingsScreen( onRestTimerSoundOnChange = viewModel::saveRestTimerSoundOn, onIsWorkoutHeaderStickyChange = viewModel::saveIsWorkoutHeaderSticky, onUseScrollWheelForInputChange = viewModel::saveUseScrollWheelForInput, + onShowExercisesImagesChange = viewModel::saveShowExercisesImages, + showConfirmDialogShowExerciseImages = { + showConfirmDialogDisplayExerciseImages = true + }, onDismissScrollWhellInputAutomaticallyChange = viewModel::saveDismissScrollWheelInputAutomatically ) } @@ -136,6 +165,7 @@ private fun SettingsScreenContent( isSupporter: Boolean, isWorkoutHeaderSticky: Boolean, useScrollWheelForInput: Boolean, + showExercisesImages: Boolean?, dismissScrollWheelInputAutomatically: Boolean, unitSystem: UnitSystem, updatePreferences: (List) -> Unit, @@ -144,6 +174,8 @@ private fun SettingsScreenContent( onRestTimerSoundOnChange: (Boolean) -> Unit, onIsWorkoutHeaderStickyChange: (Boolean) -> Unit, onUseScrollWheelForInputChange: (Boolean) -> Unit, + onShowExercisesImagesChange: (Boolean) -> Unit, + showConfirmDialogShowExerciseImages: () -> Unit, onDismissScrollWhellInputAutomaticallyChange: (Boolean) -> Unit, ) { LibreFitScaffold( @@ -247,6 +279,22 @@ private fun SettingsScreenContent( ) } + item { + SettingItem( + isChecked = showExercisesImages == true, + onClick = { + if (showExercisesImages != null) { + onShowExercisesImagesChange(!showExercisesImages) + } else { + showConfirmDialogShowExerciseImages() + } + }, + icon = painterResource(if (showExercisesImages == true) R.drawable.ic_image else R.drawable.ic_hide_image), + settingName = stringResource(R.string.show_images), + settingDesc = stringResource(if (showExercisesImages == true) R.string.show_images_desc else R.string.hide_images_desc) + ) + } + item { SettingItem( isChecked = useScrollWheelForInput, @@ -347,7 +395,9 @@ fun SettingsScreenPreview() { var keepWorkoutScreenOn by remember { mutableStateOf(Random.nextBoolean()) } var restTimerSoundOn by remember { mutableStateOf(Random.nextBoolean()) } var isWorkoutHeaderSticky by remember { mutableStateOf(Random.nextBoolean()) } + var dismissScrollWheelInputAutomatically by remember { mutableStateOf(Random.nextBoolean()) } var useScrollWheelForInput by remember { mutableStateOf(Random.nextBoolean()) } + var displayExercisesImages by remember { mutableStateOf(Random.nextBoolean()) } val theme = ThemeMode.entries.random() @@ -363,14 +413,19 @@ fun SettingsScreenPreview() { isSupporter = Random.nextBoolean(), isWorkoutHeaderSticky = isWorkoutHeaderSticky, useScrollWheelForInput = useScrollWheelForInput, - dismissScrollWheelInputAutomatically = Random.nextBoolean(), + showExercisesImages = displayExercisesImages, + dismissScrollWheelInputAutomatically = dismissScrollWheelInputAutomatically, unitSystem = UnitSystem.entries.random(), onMaterialModeChange = { materialModeOn = it }, onKeepWorkoutScreenOnChange = { keepWorkoutScreenOn = it }, onRestTimerSoundOnChange = { restTimerSoundOn = it }, onIsWorkoutHeaderStickyChange = { isWorkoutHeaderSticky = it }, onUseScrollWheelForInputChange = { useScrollWheelForInput = it }, - onDismissScrollWhellInputAutomaticallyChange = {} + onShowExercisesImagesChange = { displayExercisesImages = it }, + showConfirmDialogShowExerciseImages = {}, + onDismissScrollWhellInputAutomaticallyChange = { + dismissScrollWheelInputAutomatically = it + } ) } } \ No newline at end of file diff --git a/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreenViewModel.kt index 3bb3ba7d6..b402240f5 100644 --- a/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/settings/SettingsScreenViewModel.kt @@ -39,6 +39,7 @@ class SettingsScreenViewModel @Inject constructor( val isSupporter = userPreferences.isSupporter val isWorkoutHeaderSticky = userPreferences.isWorkoutHeaderSticky val useScrollWheelForInput = userPreferences.useScrollWheelForInput + val showExercisesImages = userPreferences.showExercisesImages val dismissScrollWheelInputAutomatically = userPreferences.dismissScrollWheelInputAutomatically val unitSystem = userPreferences.unitSystem @@ -76,6 +77,12 @@ class SettingsScreenViewModel @Inject constructor( } } + fun saveShowExercisesImages(display: Boolean) { + viewModelScope.launch { + userPreferences.saveShowExercisesImages(display) + } + } + fun saveUnitSystem(unitSystem: UnitSystem) { viewModelScope.launch { userPreferences.saveUnitSystem(unitSystem) } } diff --git a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt index fad2ec1ee..52ded1840 100644 --- a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt @@ -137,6 +137,8 @@ fun SharedTransitionScope.WorkoutScreen( val useScrollWheelForInput by viewModel.useScrollWheelForInput.collectAsStateWithLifecycle() + val showExercisesImages by viewModel.displayExercisesImages.collectAsStateWithLifecycle() + val dismissScrollWheelInputAutomatically by viewModel.dismissScrollWheelInputAutomatically.collectAsStateWithLifecycle() @@ -222,6 +224,7 @@ fun SharedTransitionScope.WorkoutScreen( isHeaderSticky = isHeaderSticky, useScrollWheelForInput = useScrollWheelForInput, dismissScrollWheelInputAutomatically = dismissScrollWheelInputAutomatically, + showExercisesImages = showExercisesImages, toggleStopwatch = viewModel::toggleStopwatch, updateIdSetWithRunningStopwatch = viewModel::updateIdSetWithRunningStopwatch, onSelectedExerciseIdChange = { id, idExerciseDC -> @@ -283,6 +286,7 @@ private fun SharedTransitionScope.WorkoutScreenContent( idSetWithRunningStopwatch: Long?, isHeaderSticky: Boolean, useScrollWheelForInput: Boolean, + showExercisesImages: Boolean?, dismissScrollWheelInputAutomatically: Boolean, toggleStopwatch: () -> Unit, updateIdSetWithRunningStopwatch: (Long?) -> Unit, @@ -412,6 +416,7 @@ private fun SharedTransitionScope.WorkoutScreenContent( addSet = addSetToExercise, onDetail = onSelectedExerciseIdChange, onDelete = deleteExercise, + showExercisesImages = showExercisesImages, isCollapsed = isReorderingEnabled, dragHandleModifier = Modifier.draggableHandle( onDragStarted = { @@ -648,6 +653,7 @@ private fun WorkoutScreenPreview() { workoutProgress = workoutProgress, isHeaderSticky = true, useScrollWheelForInput = true, + showExercisesImages = null, dismissScrollWheelInputAutomatically = false, toggleStopwatch = {}, addSetToExercise = {}, diff --git a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt index a2c2c5c1b..3a04c8312 100644 --- a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt @@ -586,4 +586,6 @@ class WorkoutScreenViewModel @Inject constructor( val useScrollWheelForInput = userPreferences.useScrollWheelForInput val dismissScrollWheelInputAutomatically = userPreferences.dismissScrollWheelInputAutomatically + + val displayExercisesImages = userPreferences.showExercisesImages } diff --git a/app/src/main/res/drawable/ic_hide_image.xml b/app/src/main/res/drawable/ic_hide_image.xml new file mode 100644 index 000000000..016808708 --- /dev/null +++ b/app/src/main/res/drawable/ic_hide_image.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_image.xml b/app/src/main/res/drawable/ic_image.xml new file mode 100644 index 000000000..c7b30155c --- /dev/null +++ b/app/src/main/res/drawable/ic_image.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18c30eaea..e2608adb7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -232,6 +232,8 @@ Delete exercise from dataset? Warning: all associated exercises will be deleted! This action cannot be reversed. ⚠️ AI generated images are continuously reviewed but may contain inaccuracies and/or artifacts. For a more accurate exercise description, see the relative instructions. + Hide + Show @@ -317,6 +319,9 @@ Dismiss scroll wheel automatically The scroll wheel will be dismissed automatically. The scroll wheel will have to be dismissed manually. + Show images + Images of exercises will be hidden + Images of exercises will be shown Location Unit system Metric diff --git a/app/src/test/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModelTest.kt b/app/src/test/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModelTest.kt index 9d6feea0a..326a4aa83 100644 --- a/app/src/test/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModelTest.kt +++ b/app/src/test/java/org/librefit/ui/screens/exercises/ExercisesScreenViewModelTest.kt @@ -42,6 +42,8 @@ class ExercisesScreenViewModelTest { private lateinit var isSupporterFlow: MutableStateFlow + private lateinit var showExercisesImages: MutableStateFlow + // Test dataset private val dataset = listOf( UiExerciseDC(name = "Pull exercise", force = Force.PULL), @@ -59,10 +61,12 @@ class ExercisesScreenViewModelTest { userPreferencesRepository = mockk() isSupporterFlow = MutableStateFlow(false) + showExercisesImages = MutableStateFlow(null) // Arrange: Tell the mock what to return when a variable is accessed every { datasetRepository.dataset } returns datasetFlow every { userPreferencesRepository.isSupporter } returns isSupporterFlow + every { userPreferencesRepository.showExercisesImages } returns showExercisesImages // Instantiate the ViewModel directly, passing in test data viewModel = ExercisesScreenViewModel( @@ -78,7 +82,9 @@ class ExercisesScreenViewModelTest { @Test fun `initial state - debounced query is empty`() = runTest { - assertThat(viewModel.debouncedQuery.value).isEmpty() + viewModel.debouncedQuery.test { + assertThat(awaitItem()).isEmpty() + } } @Test diff --git a/app/src/test/java/org/librefit/ui/screens/settings/SettingsScreenViewModelTest.kt b/app/src/test/java/org/librefit/ui/screens/settings/SettingsScreenViewModelTest.kt index 2943b3201..ab755eb92 100644 --- a/app/src/test/java/org/librefit/ui/screens/settings/SettingsScreenViewModelTest.kt +++ b/app/src/test/java/org/librefit/ui/screens/settings/SettingsScreenViewModelTest.kt @@ -47,6 +47,7 @@ class SettingsScreenViewModelTest { private lateinit var isWorkoutHeaderSticky: MutableStateFlow private lateinit var useScrollWheelForInput: MutableStateFlow private lateinit var dismissScrollWheelAutomatically: MutableStateFlow + private lateinit var showExercisesImages: MutableStateFlow private lateinit var unitSystem: MutableStateFlow @Before @@ -62,6 +63,7 @@ class SettingsScreenViewModelTest { isWorkoutHeaderSticky = MutableStateFlow(true) useScrollWheelForInput = MutableStateFlow(true) dismissScrollWheelAutomatically = MutableStateFlow(false) + showExercisesImages = MutableStateFlow(null) unitSystem = MutableStateFlow(UnitSystem.METRIC) // Arrange: Tell the mock what to return when these are accessed @@ -74,6 +76,7 @@ class SettingsScreenViewModelTest { every { userPreferencesRepository.isWorkoutHeaderSticky } returns isWorkoutHeaderSticky every { userPreferencesRepository.useScrollWheelForInput } returns useScrollWheelForInput every { userPreferencesRepository.dismissScrollWheelInputAutomatically } returns dismissScrollWheelAutomatically + every { userPreferencesRepository.showExercisesImages } returns showExercisesImages every { userPreferencesRepository.unitSystem } returns unitSystem every { userPreferencesRepository.saveLanguage(any()) } answers { @@ -103,6 +106,9 @@ class SettingsScreenViewModelTest { coEvery { userPreferencesRepository.saveDismissScrollWheelInputAutomatically(any()) } answers { dismissScrollWheelAutomatically.value = firstArg() } + coEvery { userPreferencesRepository.saveShowExercisesImages(any()) } answers { + showExercisesImages.value = firstArg() + } // Arrange: Create the ViewModel instance with the mock repository viewModel = SettingsScreenViewModel(userPreferencesRepository) @@ -143,6 +149,22 @@ class SettingsScreenViewModelTest { assertThat(viewModel.dismissScrollWheelInputAutomatically.value).isFalse() } + @Test + fun `initial state - show images is null`() = runTest { + assertThat(viewModel.showExercisesImages.value).isNull() + } + + @Test + fun `show exercises images updates correctly`() = runTest { + val expected = true + + viewModel.showExercisesImages.test { + assertThat(awaitItem()).isNull() + viewModel.saveShowExercisesImages(expected) + assertThat(awaitItem()).isEqualTo(expected) + } + } + @Test fun `when updating preferences - preferences must match the update input`() = runTest { viewModel.preferences.test {