diff --git a/.gitignore b/.gitignore
index a90ddc1..653c40c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,5 @@ fastlane/readme.md
app/schemas/com.example.android.january2022.db.GymDatabase/1.json
*.json
/app/signing/
+.idea
+.DS_Store
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index e773e77..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-January 2022
\ No newline at end of file
diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
deleted file mode 100644
index 7a6e8f8..0000000
--- a/.idea/androidTestResultsUserPreferences.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index fb7f4a8..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index ea46863..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 05b4063..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 180dfd3..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/android/january2022/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/android/january2022/ExampleInstrumentedTest.kt
index 9791867..fd5165a 100644
--- a/app/src/androidTest/java/com/example/android/january2022/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/example/android/january2022/ExampleInstrumentedTest.kt
@@ -1,13 +1,11 @@
package com.example.android.january2022
-import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
-
+import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.Assert.*
-
/**
* Instrumented test, which will execute on an Android device.
*
@@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.android.january2022", appContext.packageName)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 65fdae6..54e1434 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,7 +15,6 @@
diff --git a/app/src/main/java/com/example/android/january2022/GymApp.kt b/app/src/main/java/com/example/android/january2022/GymApp.kt
index f25b197..73c98ec 100644
--- a/app/src/main/java/com/example/android/january2022/GymApp.kt
+++ b/app/src/main/java/com/example/android/january2022/GymApp.kt
@@ -9,24 +9,24 @@ import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class GymApp : Application() {
- override fun onCreate() {
- super.onCreate()
- val channel = NotificationChannel(
- TimerService.CHANNEL_ID,
- "Workout Timer",
- NotificationManager.IMPORTANCE_DEFAULT
- ).also {
- it.setSound(null, null)
+ override fun onCreate() {
+ super.onCreate()
+ val channel = NotificationChannel(
+ TimerService.CHANNEL_ID,
+ "Workout Timer",
+ NotificationManager.IMPORTANCE_DEFAULT,
+ ).also {
+ it.setSound(null, null)
+ }
+ val alertChannel = NotificationChannel(
+ TimerService.ALERT_CHANNEL_ID,
+ "Workout Timer Alerts",
+ NotificationManager.IMPORTANCE_HIGH,
+ ).also {
+ it.enableVibration(true)
+ }
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ notificationManager.createNotificationChannel(alertChannel)
}
- val alertChannel = NotificationChannel(
- TimerService.ALERT_CHANNEL_ID,
- "Workout Timer Alerts",
- NotificationManager.IMPORTANCE_HIGH
- ).also {
- it.enableVibration(true)
- }
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.createNotificationChannel(channel)
- notificationManager.createNotificationChannel(alertChannel)
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/MainActivity.kt b/app/src/main/java/com/example/android/january2022/MainActivity.kt
index 0f3d43d..58bcb0c 100644
--- a/app/src/main/java/com/example/android/january2022/MainActivity.kt
+++ b/app/src/main/java/com/example/android/january2022/MainActivity.kt
@@ -16,42 +16,41 @@ import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import timber.log.Timber.DebugTree
-
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (BuildConfig.DEBUG) {
- Timber.plant(DebugTree())
- }
- setContent {
- WorkoutTheme {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- ActivityCompat.requestPermissions(
- this,
- arrayOf(Manifest.permission.POST_NOTIFICATIONS),
- 0
- )
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (BuildConfig.DEBUG) {
+ Timber.plant(DebugTree())
+ }
+ setContent {
+ WorkoutTheme {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 0,
+ )
+ }
+ val navController = rememberNavController()
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ NavHost(navController)
+ }
}
- val navController = rememberNavController()
- WindowCompat.setDecorFitsSystemWindows(window, false)
- NavHost(navController)
- }
}
- }
- override fun onStart() {
- super.onStart()
- this.sendTimerIntent {
- it.action = TimerService.Actions.MOVE_TO_BACKGROUND.toString()
+ override fun onStart() {
+ super.onStart()
+ this.sendTimerIntent {
+ it.action = TimerService.Actions.MOVE_TO_BACKGROUND.toString()
+ }
}
- }
- override fun onPause() {
- super.onPause()
- this.sendTimerIntent {
- it.action = TimerService.Actions.MOVE_TO_FOREGROUND.toString()
+ override fun onPause() {
+ super.onPause()
+ this.sendTimerIntent {
+ it.action = TimerService.Actions.MOVE_TO_FOREGROUND.toString()
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/Equipment.kt b/app/src/main/java/com/example/android/january2022/db/Equipment.kt
index 0d5eb5e..2ff8b97 100644
--- a/app/src/main/java/com/example/android/january2022/db/Equipment.kt
+++ b/app/src/main/java/com/example/android/january2022/db/Equipment.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.db
-
object Equipment {
const val ATLAS_STONE = "Atlas Stone"
const val BARBELL = "Barbell"
@@ -40,7 +39,7 @@ object Equipment {
SMITH_MACHINE,
STABILITY_BALL,
SUSPENSION,
- WEIGHT
+ WEIGHT,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/GymDAO.kt b/app/src/main/java/com/example/android/january2022/db/GymDAO.kt
index 38b9f9b..7513509 100644
--- a/app/src/main/java/com/example/android/january2022/db/GymDAO.kt
+++ b/app/src/main/java/com/example/android/january2022/db/GymDAO.kt
@@ -1,89 +1,96 @@
package com.example.android.january2022.db
-import androidx.room.*
-import com.example.android.january2022.db.entities.*
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import com.example.android.january2022.db.entities.Exercise
+import com.example.android.january2022.db.entities.GymSet
+import com.example.android.january2022.db.entities.Session
+import com.example.android.january2022.db.entities.SessionExercise
+import com.example.android.january2022.db.entities.SessionExerciseWithExercise
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
-
@Dao
interface GymDAO {
- @Query("SELECT * FROM sessions WHERE sessionId = :sessionId")
- fun getSessionById(sessionId: Long): Session
+ @Query("SELECT * FROM sessions WHERE sessionId = :sessionId")
+ fun getSessionById(sessionId: Long): Session
- @Query("SELECT * FROM sets ORDER BY setId ASC")
- fun getAllSets(): Flow>
+ @Query("SELECT * FROM sets ORDER BY setId ASC")
+ fun getAllSets(): Flow>
- @Query("SELECT * FROM sessions ORDER BY start DESC")
- fun getAllSessions(): Flow>
+ @Query("SELECT * FROM sessions ORDER BY start DESC")
+ fun getAllSessions(): Flow>
- @Query("SELECT * FROM sessions ORDER BY sessionId DESC LIMIT 1")
- fun getLastSession(): Session
+ @Query("SELECT * FROM sessions ORDER BY sessionId DESC LIMIT 1")
+ fun getLastSession(): Session
- @Query("SELECT * FROM exercises ORDER BY title ASC")
- fun getAllExercises(): Flow>
+ @Query("SELECT * FROM exercises ORDER BY title ASC")
+ fun getAllExercises(): Flow>
- @Query("SELECT * FROM sessionExercises join exercises ON sessionExercises.parentExerciseId = exercises.id")
- fun getAllSessionExercises(): Flow>
+ @Query("SELECT * FROM sessionExercises join exercises ON sessionExercises.parentExerciseId = exercises.id")
+ fun getAllSessionExercises(): Flow>
- @Query("SELECT * FROM sessionExercises JOIN exercises ON sessionExercises.parentExerciseId = exercises.id WHERE parentSessionId = :sessionId")
- fun getExercisesForSession(sessionId: Long): Flow>
+ @Query("SELECT * FROM sessionExercises JOIN exercises ON sessionExercises.parentExerciseId = exercises.id WHERE parentSessionId = :sessionId")
+ fun getExercisesForSession(sessionId: Long): Flow>
- @Query("SELECT * FROM sets WHERE parentSessionExerciseId = :id ORDER BY setId ASC")
- fun getSetsForExercise(id: Long): Flow>
+ @Query("SELECT * FROM sets WHERE parentSessionExerciseId = :id ORDER BY setId ASC")
+ fun getSetsForExercise(id: Long): Flow>
- @Query("SELECT GROUP_CONCAT(targets,'|') FROM exercises as e JOIN sessionExercises as se ON e.id = se.parentExerciseId WHERE se.parentSessionId = :sessionId")
- fun getMuscleGroupsForSession(sessionId: Long): Flow
+ @Query("SELECT GROUP_CONCAT(targets,'|') FROM exercises as e JOIN sessionExercises as se ON e.id = se.parentExerciseId WHERE se.parentSessionId = :sessionId")
+ fun getMuscleGroupsForSession(sessionId: Long): Flow
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insertSession(session: Session): Long
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertSession(session: Session): Long
- @Delete
- suspend fun removeSession(session: Session)
+ @Delete
+ suspend fun removeSession(session: Session)
- @Update
- suspend fun updateSession(session: Session)
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insertExercise(exercise: Exercise): Long
+ @Update
+ suspend fun updateSession(session: Session)
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insertSessionExercise(sessionExercise: SessionExercise): Long
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertExercise(exercise: Exercise): Long
- @Delete
- suspend fun removeSessionExercise(sessionExercise: SessionExercise)
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertSessionExercise(sessionExercise: SessionExercise): Long
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insertSet(set: GymSet): Long
+ @Delete
+ suspend fun removeSessionExercise(sessionExercise: SessionExercise)
- @Update
- suspend fun updateSet(set: GymSet)
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertSet(set: GymSet): Long
- @Delete
- suspend fun deleteSet(set: GymSet)
+ @Update
+ suspend fun updateSet(set: GymSet)
- @Query("SELECT * FROM sessions")
- fun getSessionList(): List
+ @Delete
+ suspend fun deleteSet(set: GymSet)
- @Query("SELECT * FROM exercises")
- fun getExerciseList(): List
+ @Query("SELECT * FROM sessions")
+ fun getSessionList(): List
- @Query("SELECT * FROM sessionExercises")
- fun getSessionExerciseList(): List
+ @Query("SELECT * FROM exercises")
+ fun getExerciseList(): List
- @Query("SELECT * FROM sets")
- fun getSetList(): List
+ @Query("SELECT * FROM sessionExercises")
+ fun getSessionExerciseList(): List
- @Query("DELETE FROM sessions")
- suspend fun clearSessions()
+ @Query("SELECT * FROM sets")
+ fun getSetList(): List
- @Query("DELETE FROM sessionExercises")
- suspend fun clearSessionExercises()
+ @Query("DELETE FROM sessions")
+ suspend fun clearSessions()
- @Query("DELETE FROM sets")
- suspend fun clearSets()
+ @Query("DELETE FROM sessionExercises")
+ suspend fun clearSessionExercises()
- @Query("DELETE FROM exercises")
- suspend fun clearExercises()
-}
+ @Query("DELETE FROM sets")
+ suspend fun clearSets()
+ @Query("DELETE FROM exercises")
+ suspend fun clearExercises()
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/GymDatabase.kt b/app/src/main/java/com/example/android/january2022/db/GymDatabase.kt
index 6b2fe06..e0d60d4 100644
--- a/app/src/main/java/com/example/android/january2022/db/GymDatabase.kt
+++ b/app/src/main/java/com/example/android/january2022/db/GymDatabase.kt
@@ -1,23 +1,24 @@
package com.example.android.january2022.db
-import androidx.room.*
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
import com.example.android.january2022.db.entities.Exercise
import com.example.android.january2022.db.entities.GymSet
import com.example.android.january2022.db.entities.Session
import com.example.android.january2022.db.entities.SessionExercise
import com.example.android.january2022.utils.Converters
-
@Database(
entities = [
Session::class,
Exercise::class,
SessionExercise::class,
- GymSet::class
- ],
- autoMigrations = [
+ GymSet::class,
],
- version = 2, exportSchema = true
+ autoMigrations = [],
+ version = 2,
+ exportSchema = true,
)
@TypeConverters(Converters::class)
abstract class GymDatabase : RoomDatabase() {
@@ -26,5 +27,4 @@ abstract class GymDatabase : RoomDatabase() {
* Connects the database to the DAO.
*/
abstract val dao: GymDAO
-
}
diff --git a/app/src/main/java/com/example/android/january2022/db/GymRepository.kt b/app/src/main/java/com/example/android/january2022/db/GymRepository.kt
index e32498f..5e40d8a 100644
--- a/app/src/main/java/com/example/android/january2022/db/GymRepository.kt
+++ b/app/src/main/java/com/example/android/january2022/db/GymRepository.kt
@@ -1,89 +1,94 @@
package com.example.android.january2022.db
-import com.example.android.january2022.db.entities.*
+import com.example.android.january2022.db.entities.Exercise
+import com.example.android.january2022.db.entities.GymSet
+import com.example.android.january2022.db.entities.Session
+import com.example.android.january2022.db.entities.SessionExercise
+import com.example.android.january2022.db.entities.SessionExerciseWithExercise
import com.example.android.january2022.ui.DatabaseModel
import com.example.android.january2022.utils.turnTargetIntoMuscleGroups
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapNotNull
import timber.log.Timber
-
class GymRepository(
- private val dao: GymDAO
+ private val dao: GymDAO,
) {
- fun getSessionById(sessionId: Long) = dao.getSessionById(sessionId)
+ fun getSessionById(sessionId: Long) = dao.getSessionById(sessionId)
- fun getAllSessions() = dao.getAllSessions()
+ fun getAllSessions() = dao.getAllSessions()
- fun getAllSets() = dao.getAllSets()
- fun getAllExercises() = dao.getAllExercises()
+ fun getAllSets() = dao.getAllSets()
+ fun getAllExercises() = dao.getAllExercises()
- fun getLastSession() = dao.getLastSession()
+ fun getLastSession() = dao.getLastSession()
- fun getAllSessionExercises() = dao.getAllSessionExercises()
+ fun getAllSessionExercises() = dao.getAllSessionExercises()
- @OptIn(ExperimentalCoroutinesApi::class)
- fun getExercisesForSession(session: Flow): Flow> {
- return session.flatMapLatest {
- dao.getExercisesForSession(it.sessionId)
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getExercisesForSession(session: Flow): Flow> {
+ return session.flatMapLatest {
+ dao.getExercisesForSession(it.sessionId)
+ }
}
- }
-
- fun getExercisesForSession(session: Session): Flow> {
- Timber.d("Retrieving exercises for session: $session")
- return dao.getExercisesForSession(session.sessionId)
- }
-
- fun getSetsForExercise(sessionExerciseId: Long) = dao.getSetsForExercise(sessionExerciseId)
-
- fun getMuscleGroupsForSession(session: Session): Flow> {
- val list = dao.getMuscleGroupsForSession(session.sessionId).mapNotNull {
- Timber.d("MuscleGroup flow created.")
- try {
- turnTargetIntoMuscleGroups(it)
- } catch (_: Exception) {
- Timber.d("Error when converting target.")
- emptyList()
- }
+
+ fun getExercisesForSession(session: Session): Flow> {
+ Timber.d("Retrieving exercises for session: $session")
+ return dao.getExercisesForSession(session.sessionId)
}
- return list
- }
- suspend fun insertExercise(exercise: Exercise) = dao.insertExercise(exercise)
+ fun getSetsForExercise(sessionExerciseId: Long) = dao.getSetsForExercise(sessionExerciseId)
+
+ fun getMuscleGroupsForSession(session: Session): Flow> {
+ val list = dao.getMuscleGroupsForSession(session.sessionId).mapNotNull {
+ Timber.d("MuscleGroup flow created.")
+ try {
+ turnTargetIntoMuscleGroups(it)
+ } catch (_: Exception) {
+ Timber.d("Error when converting target.")
+ emptyList()
+ }
+ }
+ return list
+ }
- suspend fun insertSession(session: Session) = dao.insertSession(session)
+ suspend fun insertExercise(exercise: Exercise) = dao.insertExercise(exercise)
- suspend fun removeSession(session: Session) = dao.removeSession(session)
+ suspend fun insertSession(session: Session) = dao.insertSession(session)
- suspend fun updateSession(session: Session) = dao.updateSession(session)
+ suspend fun removeSession(session: Session) = dao.removeSession(session)
- suspend fun insertSessionExercise(sessionExercise: SessionExercise) =
- dao.insertSessionExercise(sessionExercise)
+ suspend fun updateSession(session: Session) = dao.updateSession(session)
- suspend fun removeSessionExercise(sessionExercise: SessionExercise) =
- dao.removeSessionExercise(sessionExercise)
+ suspend fun insertSessionExercise(sessionExercise: SessionExercise) =
+ dao.insertSessionExercise(sessionExercise)
- suspend fun insertSet(gymSet: GymSet) = dao.insertSet(gymSet)
+ suspend fun removeSessionExercise(sessionExercise: SessionExercise) =
+ dao.removeSessionExercise(sessionExercise)
- suspend fun updateSet(set: GymSet) = dao.updateSet(set)
- suspend fun deleteSet(set: GymSet) = dao.deleteSet(set)
+ suspend fun insertSet(gymSet: GymSet) = dao.insertSet(gymSet)
- suspend fun createSet(sessionExercise: SessionExercise) =
- dao.insertSet(GymSet(parentSessionExerciseId = sessionExercise.sessionExerciseId))
+ suspend fun updateSet(set: GymSet) = dao.updateSet(set)
+ suspend fun deleteSet(set: GymSet) = dao.deleteSet(set)
- fun getDatabaseModel() =
- DatabaseModel(
- sessions = dao.getSessionList(),
- exercises = dao.getExerciseList(),
- sessionExercises = dao.getSessionExerciseList(),
- sets = dao.getSetList()
- )
+ suspend fun createSet(sessionExercise: SessionExercise) =
+ dao.insertSet(GymSet(parentSessionExerciseId = sessionExercise.sessionExerciseId))
- suspend fun clearDatabase() {
- dao.clearSessions()
- dao.clearSessionExercises()
- dao.clearExercises()
- dao.clearSets()
- }
+ fun getDatabaseModel() =
+ DatabaseModel(
+ sessions = dao.getSessionList(),
+ exercises = dao.getExerciseList(),
+ sessionExercises = dao.getSessionExerciseList(),
+ sets = dao.getSetList(),
+ )
+
+ suspend fun clearDatabase() {
+ dao.clearSessions()
+ dao.clearSessionExercises()
+ dao.clearExercises()
+ dao.clearSets()
+ }
}
diff --git a/app/src/main/java/com/example/android/january2022/db/MuscleGroup.kt b/app/src/main/java/com/example/android/january2022/db/MuscleGroup.kt
index 7ef7276..7190b10 100644
--- a/app/src/main/java/com/example/android/january2022/db/MuscleGroup.kt
+++ b/app/src/main/java/com/example/android/january2022/db/MuscleGroup.kt
@@ -33,7 +33,7 @@ object MuscleGroup {
FOREARMS,
TRICEPS,
CHEST,
- SHOULDERS
+ SHOULDERS,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/SetType.kt b/app/src/main/java/com/example/android/january2022/db/SetType.kt
index f1f056f..4c948ec 100644
--- a/app/src/main/java/com/example/android/january2022/db/SetType.kt
+++ b/app/src/main/java/com/example/android/january2022/db/SetType.kt
@@ -7,13 +7,12 @@ object SetType {
const val HARD = "Hard"
const val DROP = "Drop"
-
- private val order by lazy { listOf(WARMUP, EASY, NORMAL, HARD, DROP)}
+ private val order by lazy { listOf(WARMUP, EASY, NORMAL, HARD, DROP) }
fun add(current: String): String {
val currentIndex: Int = order.indexOf(current)
- val nextIndex = (currentIndex+1) % order.size
+ val nextIndex = (currentIndex + 1) % order.size
return order[nextIndex]
}
fun next(current: String): String = add(current)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/StartingExercises.kt b/app/src/main/java/com/example/android/january2022/db/StartingExercises.kt
index 96df19f..2862275 100644
--- a/app/src/main/java/com/example/android/january2022/db/StartingExercises.kt
+++ b/app/src/main/java/com/example/android/january2022/db/StartingExercises.kt
@@ -19,73 +19,71 @@ import javax.inject.Inject
import javax.inject.Provider
class StartingExercises @Inject constructor(
- private val repository: Provider,
- @ApplicationScope private val applicationScope: CoroutineScope,
- @ApplicationContext private val context: Context
+ private val repository: Provider,
+ @ApplicationScope private val applicationScope: CoroutineScope,
+ @ApplicationContext private val context: Context,
) : RoomDatabase.Callback() {
- override fun onCreate(db: SupportSQLiteDatabase) {
- super.onCreate(db)
- applicationScope.launch {
- fillWithStartingExercises(context, repository.get())
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ super.onCreate(db)
+ applicationScope.launch {
+ fillWithStartingExercises(context, repository.get())
+ }
}
- }
- private suspend fun fillWithStartingExercises(
- context: Context,
- repository: GymRepository
- ) {
- Timber.d("Starting process")
- try {
- val exercises = loadJSONArray(context)
- for (i in 0 until exercises.length()) {
- val item = exercises.getJSONObject(i)
- val title = item.getString("title")
- val type = item.getString("type")
- val force = item.parseToList("force")
- val equipment = item.parseToList("equipment")
- val targets = item.parseToList("targets")
- val synergists = item.parseToList("synergists")
- val stabilizers = item.parseToList("stabilizers")
+ private suspend fun fillWithStartingExercises(
+ context: Context,
+ repository: GymRepository,
+ ) {
+ Timber.d("Starting process")
+ try {
+ val exercises = loadJSONArray(context)
+ for (i in 0 until exercises.length()) {
+ val item = exercises.getJSONObject(i)
+ val title = item.getString("title")
+ val type = item.getString("type")
+ val force = item.parseToList("force")
+ val equipment = item.parseToList("equipment")
+ val targets = item.parseToList("targets")
+ val synergists = item.parseToList("synergists")
+ val stabilizers = item.parseToList("stabilizers")
- val exercise = Exercise(
- title = title,
- force = force,
- type = type,
- equipment = equipment,
- targets = targets,
- synergists = synergists,
- stabilizers = stabilizers
- )
- val result = repository.insertExercise(exercise)
- if (result == -1L) Timber.d("Exercise insertion failed for $exercise")
- }
-
- } catch (e: Exception) {
- Timber.d("fillWithStartingExercises: $e")
- Timber.d("Error caught")
+ val exercise = Exercise(
+ title = title,
+ force = force,
+ type = type,
+ equipment = equipment,
+ targets = targets,
+ synergists = synergists,
+ stabilizers = stabilizers,
+ )
+ val result = repository.insertExercise(exercise)
+ if (result == -1L) Timber.d("Exercise insertion failed for $exercise")
+ }
+ } catch (e: Exception) {
+ Timber.d("fillWithStartingExercises: $e")
+ Timber.d("Error caught")
+ }
}
- }
- private fun JSONObject.parseToList(propertyName: String): List {
- val gson = GsonBuilder().create()
- return try {
- Timber.d("Parsing $propertyName")
- val list = gson.fromJson(this.getString(propertyName), Array::class.java).toList()
- Timber.d("List: $list")
- return list
- } catch (e: JSONException) {
- println("Error parsing $propertyName: ${e.message}")
- emptyList()
+ private fun JSONObject.parseToList(propertyName: String): List {
+ val gson = GsonBuilder().create()
+ return try {
+ Timber.d("Parsing $propertyName")
+ val list = gson.fromJson(this.getString(propertyName), Array::class.java).toList()
+ Timber.d("List: $list")
+ return list
+ } catch (e: JSONException) {
+ println("Error parsing $propertyName: ${e.message}")
+ emptyList()
+ }
}
- }
-
- private fun loadJSONArray(context: Context): JSONArray {
- Timber.d("loading JSON array")
- val inputStream = context.resources.openRawResource(R.raw.exercises)
+ private fun loadJSONArray(context: Context): JSONArray {
+ Timber.d("loading JSON array")
+ val inputStream = context.resources.openRawResource(R.raw.exercises)
- BufferedReader(inputStream.reader()).use {
- return JSONArray(it.readText())
+ BufferedReader(inputStream.reader()).use {
+ return JSONArray(it.readText())
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/entities/Exercise.kt b/app/src/main/java/com/example/android/january2022/db/entities/Exercise.kt
index 20730f6..fa71c06 100644
--- a/app/src/main/java/com/example/android/january2022/db/entities/Exercise.kt
+++ b/app/src/main/java/com/example/android/january2022/db/entities/Exercise.kt
@@ -5,26 +5,25 @@ import androidx.room.PrimaryKey
import com.example.android.january2022.utils.FuzzySearch
import com.example.android.january2022.utils.turnTargetIntoMuscleGroups
-
@Entity(tableName = "exercises")
data class Exercise(
- @PrimaryKey(autoGenerate = true)
- var id: Long = 0L,
- var title: String = "Exercise",
- var type: String? = null,
- var force: List = emptyList(),
- var equipment: List = emptyList(),
- var targets: List = emptyList(),
- var synergists: List = emptyList(),
- var stabilizers: List = emptyList()
+ @PrimaryKey(autoGenerate = true)
+ var id: Long = 0L,
+ var title: String = "Exercise",
+ var type: String? = null,
+ var force: List = emptyList(),
+ var equipment: List = emptyList(),
+ var targets: List = emptyList(),
+ var synergists: List = emptyList(),
+ var stabilizers: List = emptyList(),
) {
- fun getMuscleGroups(exercise: Exercise = this): List {
- return exercise.targets.flatMap {
- turnTargetIntoMuscleGroups(it)
- }.distinct()
- }
+ fun getMuscleGroups(exercise: Exercise = this): List {
+ return exercise.targets.flatMap {
+ turnTargetIntoMuscleGroups(it)
+ }.distinct()
+ }
- fun getStringMatch(string: String): Boolean {
- return FuzzySearch.regexMatch(string, title)
- }
-}
\ No newline at end of file
+ fun getStringMatch(string: String): Boolean {
+ return FuzzySearch.regexMatch(string, title)
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/db/entities/Session.kt b/app/src/main/java/com/example/android/january2022/db/entities/Session.kt
index 4ab79f5..ada8053 100644
--- a/app/src/main/java/com/example/android/january2022/db/entities/Session.kt
+++ b/app/src/main/java/com/example/android/january2022/db/entities/Session.kt
@@ -4,14 +4,13 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import java.time.LocalDateTime
-
/**
* A workout-session contains multiple SessionExercises. Has a start and end-time.
*/
@Entity(tableName = "sessions")
data class Session(
- @PrimaryKey(autoGenerate = true)
- val sessionId: Long = 0L,
- val start: LocalDateTime = LocalDateTime.now(),
- val end: LocalDateTime? = null
-)
\ No newline at end of file
+ @PrimaryKey(autoGenerate = true)
+ val sessionId: Long = 0L,
+ val start: LocalDateTime = LocalDateTime.now(),
+ val end: LocalDateTime? = null,
+)
diff --git a/app/src/main/java/com/example/android/january2022/db/entities/SessionExercise.kt b/app/src/main/java/com/example/android/january2022/db/entities/SessionExercise.kt
index d3db9e3..2e7338b 100644
--- a/app/src/main/java/com/example/android/january2022/db/entities/SessionExercise.kt
+++ b/app/src/main/java/com/example/android/january2022/db/entities/SessionExercise.kt
@@ -7,27 +7,27 @@ import androidx.room.PrimaryKey
@Entity(tableName = "sessionExercises")
data class SessionExercise(
- @PrimaryKey(autoGenerate = true)
- val sessionExerciseId: Long = 0,
- @ColumnInfo(index = true)
- val parentSessionId: Long,
- @ColumnInfo(index = true)
- val parentExerciseId: Long,
- val comment: String? = null
+ @PrimaryKey(autoGenerate = true)
+ val sessionExerciseId: Long = 0,
+ @ColumnInfo(index = true)
+ val parentSessionId: Long,
+ @ColumnInfo(index = true)
+ val parentExerciseId: Long,
+ val comment: String? = null,
)
/**
* Holds a sessionExercise and it's associated exercise. Embedded = bad? it works though.
*/
data class SessionExerciseWithExercise(
- @Embedded
- val sessionExercise: SessionExercise,
- @Embedded
- val exercise: Exercise
+ @Embedded
+ val sessionExercise: SessionExercise,
+ @Embedded
+ val exercise: Exercise,
)
data class SessionWithSessionExerciseWithExercise(
- @Embedded val session: Session,
- @Embedded val sessionExercise: SessionExercise,
- @Embedded val exercise: Exercise
+ @Embedded val session: Session,
+ @Embedded val sessionExercise: SessionExercise,
+ @Embedded val exercise: Exercise,
)
diff --git a/app/src/main/java/com/example/android/january2022/db/entities/Set.kt b/app/src/main/java/com/example/android/january2022/db/entities/Set.kt
index 8495135..cfc1b22 100644
--- a/app/src/main/java/com/example/android/january2022/db/entities/Set.kt
+++ b/app/src/main/java/com/example/android/january2022/db/entities/Set.kt
@@ -3,23 +3,21 @@ package com.example.android.january2022.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
-import com.example.android.january2022.db.Equipment
import com.example.android.january2022.db.SetType
-
/**
* A SessionExercise can have multiple Set:s associated with it.
* Each Set is a number of reps with a specific weight (if applicable)
*/
@Entity(tableName = "sets")
data class GymSet(
- @PrimaryKey(autoGenerate = true)
- val setId: Long = 0L,
- @ColumnInfo(index = true)
- val parentSessionExerciseId: Long,
- val reps: Int? = null,
- val weight: Float? = null,
- val time: Long? = null,
- val distance: Float? = null,
- val setType: String = SetType.NORMAL
-)
\ No newline at end of file
+ @PrimaryKey(autoGenerate = true)
+ val setId: Long = 0L,
+ @ColumnInfo(index = true)
+ val parentSessionExerciseId: Long,
+ val reps: Int? = null,
+ val weight: Float? = null,
+ val time: Long? = null,
+ val distance: Float? = null,
+ val setType: String = SetType.NORMAL,
+)
diff --git a/app/src/main/java/com/example/android/january2022/di/AppModule.kt b/app/src/main/java/com/example/android/january2022/di/AppModule.kt
index 95e410c..93f361c 100644
--- a/app/src/main/java/com/example/android/january2022/di/AppModule.kt
+++ b/app/src/main/java/com/example/android/january2022/di/AppModule.kt
@@ -14,42 +14,41 @@ import kotlinx.coroutines.SupervisorJob
import javax.inject.Qualifier
import javax.inject.Singleton
-
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
- @Provides
- @Singleton
- fun provideGymDatabase(
- app: Application,
- callback: StartingExercises
- ): GymDatabase {
- return Room
- .databaseBuilder(
- app,
- GymDatabase::class.java,
- "gym_database.db"
- )
- .fallbackToDestructiveMigration()
- .addCallback(callback)
- .build()
- }
+ @Provides
+ @Singleton
+ fun provideGymDatabase(
+ app: Application,
+ callback: StartingExercises,
+ ): GymDatabase {
+ return Room
+ .databaseBuilder(
+ app,
+ GymDatabase::class.java,
+ "gym_database.db",
+ )
+ .fallbackToDestructiveMigration()
+ .addCallback(callback)
+ .build()
+ }
- @Provides
- @Singleton
- fun provideGymRepository(db: GymDatabase): GymRepository {
- return GymRepository(db.dao)
- }
+ @Provides
+ @Singleton
+ fun provideGymRepository(db: GymDatabase): GymRepository {
+ return GymRepository(db.dao)
+ }
- @ApplicationScope
- @Provides
- @Singleton
- fun provideApplicationScope() = CoroutineScope(SupervisorJob())
+ @ApplicationScope
+ @Provides
+ @Singleton
+ fun provideApplicationScope() = CoroutineScope(SupervisorJob())
}
// detta används typ för att man ska kunna använda olika provideApplicationScopes
// behövs tekniskt sätt inte då jag bara har ett scope
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
-annotation class ApplicationScope
\ No newline at end of file
+annotation class ApplicationScope
diff --git a/app/src/main/java/com/example/android/january2022/timer/TimerService.kt b/app/src/main/java/com/example/android/january2022/timer/TimerService.kt
index 525f206..226bf31 100644
--- a/app/src/main/java/com/example/android/january2022/timer/TimerService.kt
+++ b/app/src/main/java/com/example/android/january2022/timer/TimerService.kt
@@ -17,225 +17,227 @@ import kotlin.math.roundToInt
class TimerService : Service() {
- private var running = false
- private var time = 0L
- private var maxTime = 60000L
- private val increment = 30 * 1000L
-
- private var showNotification = false
-
- private var timer: WorkoutTimer? = null
-
- private lateinit var notificationManager: NotificationManager
-
- override fun onBind(intent: Intent?): IBinder? {
- return null
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- notificationManager =
- ContextCompat.getSystemService(this, NotificationManager::class.java) as NotificationManager
-
- val action = intent?.action
- Timber.d("Start Timer with action: $action")
- when (action) {
- Actions.TOGGLE.toString() -> toggle()
- Actions.RESET.toString() -> reset()
- Actions.INCREMENT.toString() -> increment()
- Actions.DECREMENT.toString() -> decrement()
- Actions.STOP.toString() -> stop()
- Actions.MOVE_TO_FOREGROUND.toString() -> toForeground()
- Actions.MOVE_TO_BACKGROUND.toString() -> toBackground()
- Actions.QUERY.toString() -> sendStatus()
- }
- return super.onStartCommand(intent, flags, startId)
- }
-
- private fun toForeground() {
- if (running) {
- showNotification = true
- startForeground(1, buildStatusNotification())
- }
- }
-
- private fun toBackground() {
- showNotification = false
- stopForeground(STOP_FOREGROUND_REMOVE)
- }
-
- private fun toggle() {
- if (running) stop()
- else if (time <= 0L) start() else resume()
- }
-
- private fun start() {
- running = true
- timer?.cancel()
- Timber.d("maxTime: $maxTime")
- timer = WorkoutTimer(maxTime).apply { start() }
- sendStatus()
- }
-
- private fun stop() {
- timer?.cancel()
- running = false
- sendStatus()
- }
-
- private fun resume() {
- timer?.cancel()
- timer = WorkoutTimer(time)
- timer?.start()
- running = true
- sendStatus()
- }
-
- private fun increment() {
- if (running) {
- stop()
- maxTime += increment
- time += increment
- resume()
- } else {
- maxTime += increment
- if (time > 0L) time += increment
- }
- sendStatus()
- }
-
- private fun decrement() {
- if (running) {
- stop()
- time = time.minus(increment).coerceAtLeast(0L)
- if (time <= 0L) reset() else resume()
- } else {
- maxTime = maxTime.minus(increment).coerceAtLeast(0L)
- time = time.minus(increment).coerceAtLeast(0L)
- }
- sendStatus()
- }
-
- private fun reset() {
- timer?.cancel()
- timer = null
- time = 0L
- running = false
- sendStatus()
- }
-
- private fun sendStatus() {
- val statusIntent = Intent().also {
- it.action = Intents.STATUS.toString()
- it.putExtra(Intents.Extras.IS_RUNNING.toString(), running)
- it.putExtra(Intents.Extras.TIME.toString(), time)
- it.putExtra(Intents.Extras.MAX_TIME.toString(), maxTime)
- }
- sendBroadcast(statusIntent)
- Timber.d("Broadcasting status: $running, $time, $maxTime")
- }
-
- private fun buildStatusNotification() = notification(
- subText = time.toTimerString(),
- channelId = CHANNEL_ID
- )
-
- private fun buildFinishedNotification() = notification(
- contentText = "Timer finished, tap to return.",
- progressBar = false,
- channelId = ALERT_CHANNEL_ID
- )
-
- private fun notification(
- contentText: String? = null,
- subText: String? = null,
- progressBar: Boolean = true,
- channelId: String
- ): Notification {
- val max = if (progressBar) maxTime.toInt() else 0
- val progress = if (progressBar) time.toInt() else 0
-
- val intent = Intent(this, MainActivity::class.java).also {
- it.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
- }
- val pendingIntent = PendingIntent.getActivity(
- this,
- 0,
- intent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ private var running = false
+ private var time = 0L
+ private var maxTime = 60000L
+ private val increment = 30 * 1000L
+
+ private var showNotification = false
+
+ private var timer: WorkoutTimer? = null
+
+ private lateinit var notificationManager: NotificationManager
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ notificationManager =
+ ContextCompat.getSystemService(this, NotificationManager::class.java) as NotificationManager
+
+ val action = intent?.action
+ Timber.d("Start Timer with action: $action")
+ when (action) {
+ Actions.TOGGLE.toString() -> toggle()
+ Actions.RESET.toString() -> reset()
+ Actions.INCREMENT.toString() -> increment()
+ Actions.DECREMENT.toString() -> decrement()
+ Actions.STOP.toString() -> stop()
+ Actions.MOVE_TO_FOREGROUND.toString() -> toForeground()
+ Actions.MOVE_TO_BACKGROUND.toString() -> toBackground()
+ Actions.QUERY.toString() -> sendStatus()
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun toForeground() {
+ if (running) {
+ showNotification = true
+ startForeground(1, buildStatusNotification())
+ }
+ }
+
+ private fun toBackground() {
+ showNotification = false
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+
+ private fun toggle() {
+ if (running) {
+ stop()
+ } else if (time <= 0L) start() else resume()
+ }
+
+ private fun start() {
+ running = true
+ timer?.cancel()
+ Timber.d("maxTime: $maxTime")
+ timer = WorkoutTimer(maxTime).apply { start() }
+ sendStatus()
+ }
+
+ private fun stop() {
+ timer?.cancel()
+ running = false
+ sendStatus()
+ }
+
+ private fun resume() {
+ timer?.cancel()
+ timer = WorkoutTimer(time)
+ timer?.start()
+ running = true
+ sendStatus()
+ }
+
+ private fun increment() {
+ if (running) {
+ stop()
+ maxTime += increment
+ time += increment
+ resume()
+ } else {
+ maxTime += increment
+ if (time > 0L) time += increment
+ }
+ sendStatus()
+ }
+
+ private fun decrement() {
+ if (running) {
+ stop()
+ time = time.minus(increment).coerceAtLeast(0L)
+ if (time <= 0L) reset() else resume()
+ } else {
+ maxTime = maxTime.minus(increment).coerceAtLeast(0L)
+ time = time.minus(increment).coerceAtLeast(0L)
+ }
+ sendStatus()
+ }
+
+ private fun reset() {
+ timer?.cancel()
+ timer = null
+ time = 0L
+ running = false
+ sendStatus()
+ }
+
+ private fun sendStatus() {
+ val statusIntent = Intent().also {
+ it.action = Intents.STATUS.toString()
+ it.putExtra(Intents.Extras.IS_RUNNING.toString(), running)
+ it.putExtra(Intents.Extras.TIME.toString(), time)
+ it.putExtra(Intents.Extras.MAX_TIME.toString(), maxTime)
+ }
+ sendBroadcast(statusIntent)
+ Timber.d("Broadcasting status: $running, $time, $maxTime")
+ }
+
+ private fun buildStatusNotification() = notification(
+ subText = time.toTimerString(),
+ channelId = CHANNEL_ID,
+ )
+
+ private fun buildFinishedNotification() = notification(
+ contentText = "Timer finished, tap to return.",
+ progressBar = false,
+ channelId = ALERT_CHANNEL_ID,
)
- return NotificationCompat.Builder(this, channelId)
- .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
- .setContentIntent(pendingIntent)
- .setAutoCancel(true)
- .setProgress(max, progress, false)
- .setSmallIcon(R.drawable.ic_launcher_foreground)
- .setContentTitle("Workout Timer")
- .setContentText(contentText)
- .setSubText(subText)
- .build()
- }
-
- private fun notify(notification: Notification) {
- notificationManager.notify(1, notification)
- }
-
- private fun alert(notification: Notification) {
- notificationManager.notify(2, notification)
- }
-
- enum class Actions {
- TOGGLE, INCREMENT, DECREMENT, RESET, STOP, MOVE_TO_BACKGROUND, MOVE_TO_FOREGROUND, QUERY
- }
-
- enum class Intents {
- STATUS;
-
- enum class Extras {
- TIME, IS_RUNNING, MAX_TIME
- }
- }
-
- inner class WorkoutTimer(length: Long, interval: Long = 1000L) :
- CountDownTimer(length, interval) {
-
- override fun onTick(millisUntilFinished: Long) {
- time = millisUntilFinished
- if (showNotification) notify(buildStatusNotification())
- if (time <= 0L) onFinish()
- sendStatus()
- }
-
- override fun onFinish() {
- Timber.d("Timer finished")
- time = maxTime
- alert(buildFinishedNotification())
- reset()
- }
- }
-
- companion object {
- const val CHANNEL_ID = "workout_timer"
- const val ALERT_CHANNEL_ID = "workout_alert_timer"
- }
+
+ private fun notification(
+ contentText: String? = null,
+ subText: String? = null,
+ progressBar: Boolean = true,
+ channelId: String,
+ ): Notification {
+ val max = if (progressBar) maxTime.toInt() else 0
+ val progress = if (progressBar) time.toInt() else 0
+
+ val intent = Intent(this, MainActivity::class.java).also {
+ it.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+ }
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+ return NotificationCompat.Builder(this, channelId)
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .setProgress(max, progress, false)
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentTitle("Workout Timer")
+ .setContentText(contentText)
+ .setSubText(subText)
+ .build()
+ }
+
+ private fun notify(notification: Notification) {
+ notificationManager.notify(1, notification)
+ }
+
+ private fun alert(notification: Notification) {
+ notificationManager.notify(2, notification)
+ }
+
+ enum class Actions {
+ TOGGLE, INCREMENT, DECREMENT, RESET, STOP, MOVE_TO_BACKGROUND, MOVE_TO_FOREGROUND, QUERY
+ }
+
+ enum class Intents {
+ STATUS,
+ ;
+
+ enum class Extras {
+ TIME, IS_RUNNING, MAX_TIME
+ }
+ }
+
+ inner class WorkoutTimer(length: Long, interval: Long = 1000L) :
+ CountDownTimer(length, interval) {
+
+ override fun onTick(millisUntilFinished: Long) {
+ time = millisUntilFinished
+ if (showNotification) notify(buildStatusNotification())
+ if (time <= 0L) onFinish()
+ sendStatus()
+ }
+
+ override fun onFinish() {
+ Timber.d("Timer finished")
+ time = maxTime
+ alert(buildFinishedNotification())
+ reset()
+ }
+ }
+
+ companion object {
+ const val CHANNEL_ID = "workout_timer"
+ const val ALERT_CHANNEL_ID = "workout_alert_timer"
+ }
}
fun Long.toTimerString(): String {
- val totalSeconds = this.toFloat().div(1000).roundToInt()
- val minutes = totalSeconds / 60
- val seconds = totalSeconds % 60
- val displayedSeconds = if (seconds < 10) "0$seconds" else seconds
- return "$minutes:$displayedSeconds"
+ val totalSeconds = this.toFloat().div(1000).roundToInt()
+ val minutes = totalSeconds / 60
+ val seconds = totalSeconds % 60
+ val displayedSeconds = if (seconds < 10) "0$seconds" else seconds
+ return "$minutes:$displayedSeconds"
}
fun Context.sendTimerAction(action: TimerService.Actions) {
- this.sendTimerIntent {
- it.action = action.toString()
- }
+ this.sendTimerIntent {
+ it.action = action.toString()
+ }
}
fun Context.sendTimerIntent(also: (Intent) -> Unit) {
- Intent(this, TimerService::class.java).also {
- also(it)
- startService(it)
- }
-}
\ No newline at end of file
+ Intent(this, TimerService::class.java).also {
+ also(it)
+ startService(it)
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/NavHost.kt b/app/src/main/java/com/example/android/january2022/ui/NavHost.kt
index 2183572..f3419e0 100644
--- a/app/src/main/java/com/example/android/january2022/ui/NavHost.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/NavHost.kt
@@ -16,52 +16,50 @@ import com.example.android.january2022.utils.UiEvent
@Composable
fun NavHost(
- navController: NavHostController
+ navController: NavHostController,
) {
-
- NavHost(
- navController = navController,
- startDestination = Routes.HOME
- ) {
-
- composable(Routes.HOME) {
- HomeScreen(
- onNavigate = { navController.navigationEvent(event = it) },
- )
- }
- composable(
- route = "${Routes.SESSION}/{session_id}",
- arguments = listOf(
- navArgument("session_id") {
- type = NavType.LongType
- }
- )
+ NavHost(
+ navController = navController,
+ startDestination = Routes.HOME,
) {
- SessionScreen(
- onNavigate = { navController.navigationEvent(event = it) },
- )
- }
- composable(
- route = "${Routes.EXERCISE_PICKER}/{session_id}",
- arguments = listOf(
- navArgument("session_id") {
- type = NavType.LongType
+ composable(Routes.HOME) {
+ HomeScreen(
+ onNavigate = { navController.navigationEvent(event = it) },
+ )
+ }
+ composable(
+ route = "${Routes.SESSION}/{session_id}",
+ arguments = listOf(
+ navArgument("session_id") {
+ type = NavType.LongType
+ },
+ ),
+ ) {
+ SessionScreen(
+ onNavigate = { navController.navigationEvent(event = it) },
+ )
+ }
+ composable(
+ route = "${Routes.EXERCISE_PICKER}/{session_id}",
+ arguments = listOf(
+ navArgument("session_id") {
+ type = NavType.LongType
+ },
+ ),
+ ) {
+ ExercisePickerScreen(
+ navController = navController,
+ )
+ }
+ composable(Routes.SETTINGS) {
+ SettingsScreen()
}
- )
- ) {
- ExercisePickerScreen(
- navController = navController,
- )
- }
- composable(Routes.SETTINGS) {
- SettingsScreen()
}
- }
}
fun NavController.navigationEvent(event: UiEvent.Navigate) {
- navigate(event.route) {
- if (event.popBackStack) currentDestination?.route?.let { popUpTo(it) { inclusive = true } }
- launchSingleTop = true
- }
+ navigate(event.route) {
+ if (event.popBackStack) currentDestination?.route?.let { popUpTo(it) { inclusive = true } }
+ launchSingleTop = true
+ }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/UiState.kt b/app/src/main/java/com/example/android/january2022/ui/UiState.kt
index b1c405a..a7f0c5d 100644
--- a/app/src/main/java/com/example/android/january2022/ui/UiState.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/UiState.kt
@@ -6,25 +6,25 @@ import com.example.android.january2022.db.entities.Session
import com.example.android.january2022.db.entities.SessionExercise
data class SessionWrapper(
- val session: Session,
- val muscleGroups: List
+ val session: Session,
+ val muscleGroups: List,
)
data class ExerciseWrapper(
- val sessionExercise: SessionExercise,
- val exercise: Exercise,
- val sets: List
+ val sessionExercise: SessionExercise,
+ val exercise: Exercise,
+ val sets: List,
)
data class TimerState(
- val time: Long,
- val running: Boolean,
- val maxTime: Long
+ val time: Long,
+ val running: Boolean,
+ val maxTime: Long,
)
data class DatabaseModel(
- val sessions: List,
- val exercises: List,
- val sessionExercises: List,
- val sets: List
-)
\ No newline at end of file
+ val sessions: List,
+ val exercises: List,
+ val sessionExercises: List,
+ val sets: List,
+)
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Composables.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Composables.kt
index e758142..955bd16 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Composables.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Composables.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.datetimedialog
-
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.MaterialTheme
@@ -15,23 +14,23 @@ import androidx.compose.ui.unit.sp
@Composable
internal fun DialogTitle(text: String, modifier: Modifier = Modifier) {
- Text(
- text,
- modifier = modifier
- .fillMaxWidth()
- .wrapContentWidth(Alignment.CenterHorizontally),
- color = MaterialTheme.colors.onBackground,
- fontSize = 20.sp,
- style = TextStyle(fontWeight = FontWeight.W600)
- )
+ Text(
+ text,
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentWidth(Alignment.CenterHorizontally),
+ color = MaterialTheme.colors.onBackground,
+ fontSize = 20.sp,
+ style = TextStyle(fontWeight = FontWeight.W600),
+ )
}
@Composable
internal fun isSmallDevice(): Boolean {
- return LocalConfiguration.current.screenWidthDp <= 360
+ return LocalConfiguration.current.screenWidthDp <= 360
}
@Composable
internal fun isLargeDevice(): Boolean {
- return LocalConfiguration.current.screenWidthDp <= 600
+ return LocalConfiguration.current.screenWidthDp <= 600
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Extensions.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Extensions.kt
index 95ae68d..b2b47ea 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Extensions.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/Extensions.kt
@@ -1,35 +1,38 @@
package com.example.android.january2022.ui.datetimedialog
-
import androidx.compose.ui.geometry.Offset
-import java.time.*
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.Month
+import java.time.YearMonth
import java.util.*
import kotlin.math.cos
import kotlin.math.sin
internal fun Float.getOffset(angle: Double): Offset =
- Offset((this * cos(angle)).toFloat(), (this * sin(angle)).toFloat())
+ Offset((this * cos(angle)).toFloat(), (this * sin(angle)).toFloat())
internal val LocalDate.yearMonth: YearMonth
- get() = YearMonth.of(this.year, this.month)
+ get() = YearMonth.of(this.year, this.month)
internal val LocalTime.isAM: Boolean
- get() = this.hour in 0..11
+ get() = this.hour in 0..11
internal val LocalTime.simpleHour: Int
- get() {
- val tempHour = this.hour % 12
- return if (tempHour == 0) 12 else tempHour
- }
+ get() {
+ val tempHour = this.hour % 12
+ return if (tempHour == 0) 12 else tempHour
+ }
internal fun Month.getShortLocalName(locale: Locale): String =
- this.getDisplayName(java.time.format.TextStyle.SHORT, locale)
+ this.getDisplayName(java.time.format.TextStyle.SHORT, locale)
internal fun Month.getFullLocalName(locale: Locale) =
- this.getDisplayName(java.time.format.TextStyle.FULL_STANDALONE, locale)
+ this.getDisplayName(java.time.format.TextStyle.FULL_STANDALONE, locale)
internal fun DayOfWeek.getShortLocalName(locale: Locale) =
- this.getDisplayName(java.time.format.TextStyle.SHORT, locale)
+ this.getDisplayName(java.time.format.TextStyle.SHORT, locale)
internal fun LocalTime.toAM(): LocalTime = if (this.isAM) this else this.minusHours(12)
internal fun LocalTime.toPM(): LocalTime = if (!this.isAM) this else this.plusHours(12)
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialog.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialog.kt
index e50a1ca..50f80a5 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialog.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialog.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.datetimedialog
-
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
@@ -43,111 +42,111 @@ import kotlin.math.min
* within a [MaterialDialog]'s content parameter
*/
interface MaterialDialogScope {
- val dialogState: MaterialDialogState
- val dialogButtons: MaterialDialogButtons
-
- val callbacks: SnapshotStateMap Unit>
- val positiveButtonEnabled: SnapshotStateMap
-
- val autoDismiss: Boolean
-
- /**
- * Hides the dialog and calls any callbacks from components in the dialog
- */
- fun submit()
-
- /**
- * Clears the dialog's state
- */
- fun reset()
-
- /**
- * Adds a value to the [positiveButtonEnabled] map and updates the value in the map when
- * [valid] changes
- *
- * @param valid boolean value to initialise the index in the list
- * @param onDispose cleanup callback when component calling this gets destroyed
- */
- @Composable
- fun PositiveButtonEnabled(valid: Boolean, onDispose: () -> Unit)
-
- /**
- * Adds a callback to the dialog which is called on positive button press
- *
- * @param callback called when positive button is pressed
- */
- @Composable
- fun DialogCallback(callback: () -> Unit)
+ val dialogState: MaterialDialogState
+ val dialogButtons: MaterialDialogButtons
+
+ val callbacks: SnapshotStateMap Unit>
+ val positiveButtonEnabled: SnapshotStateMap
+
+ val autoDismiss: Boolean
+
+ /**
+ * Hides the dialog and calls any callbacks from components in the dialog
+ */
+ fun submit()
+
+ /**
+ * Clears the dialog's state
+ */
+ fun reset()
+
+ /**
+ * Adds a value to the [positiveButtonEnabled] map and updates the value in the map when
+ * [valid] changes
+ *
+ * @param valid boolean value to initialise the index in the list
+ * @param onDispose cleanup callback when component calling this gets destroyed
+ */
+ @Composable
+ fun PositiveButtonEnabled(valid: Boolean, onDispose: () -> Unit)
+
+ /**
+ * Adds a callback to the dialog which is called on positive button press
+ *
+ * @param callback called when positive button is pressed
+ */
+ @Composable
+ fun DialogCallback(callback: () -> Unit)
}
internal class MaterialDialogScopeImpl(
- override val dialogState: MaterialDialogState,
- override val autoDismiss: Boolean = true
+ override val dialogState: MaterialDialogState,
+ override val autoDismiss: Boolean = true,
) : MaterialDialogScope {
- override val dialogButtons = MaterialDialogButtons(this)
+ override val dialogButtons = MaterialDialogButtons(this)
- override val callbacks = mutableStateMapOf Unit>()
- private val callbackCounter = AtomicInteger(0)
+ override val callbacks = mutableStateMapOf Unit>()
+ private val callbackCounter = AtomicInteger(0)
+
+ override val positiveButtonEnabled = mutableStateMapOf()
+ private val positiveEnabledCounter = AtomicInteger(0)
+
+ /**
+ * Hides the dialog and calls any callbacks from components in the dialog
+ */
+ override fun submit() {
+ dialogState.hide()
+ callbacks.values.forEach {
+ it()
+ }
+ }
- override val positiveButtonEnabled = mutableStateMapOf()
- private val positiveEnabledCounter = AtomicInteger(0)
+ /**
+ * Clears the dialog callbacks and positive button enables values along with their
+ * respective counters
+ */
+ override fun reset() {
+ positiveButtonEnabled.clear()
+ callbacks.clear()
- /**
- * Hides the dialog and calls any callbacks from components in the dialog
- */
- override fun submit() {
- dialogState.hide()
- callbacks.values.forEach {
- it()
+ positiveEnabledCounter.set(0)
+ callbackCounter.set(0)
}
- }
-
- /**
- * Clears the dialog callbacks and positive button enables values along with their
- * respective counters
- */
- override fun reset() {
- positiveButtonEnabled.clear()
- callbacks.clear()
-
- positiveEnabledCounter.set(0)
- callbackCounter.set(0)
- }
-
- /**
- * Adds a value to the [positiveButtonEnabled] map and updates the value in the map when
- * [valid] changes
- *
- * @param valid boolean value to initialise the index in the list
- * @param onDispose cleanup callback when component calling this gets destroyed
- */
- @Composable
- override fun PositiveButtonEnabled(valid: Boolean, onDispose: () -> Unit) {
- val positiveEnabledIndex = remember { positiveEnabledCounter.getAndIncrement() }
-
- DisposableEffect(valid) {
- positiveButtonEnabled[positiveEnabledIndex] = valid
- onDispose {
- positiveButtonEnabled[positiveEnabledIndex] = true
- onDispose()
- }
+
+ /**
+ * Adds a value to the [positiveButtonEnabled] map and updates the value in the map when
+ * [valid] changes
+ *
+ * @param valid boolean value to initialise the index in the list
+ * @param onDispose cleanup callback when component calling this gets destroyed
+ */
+ @Composable
+ override fun PositiveButtonEnabled(valid: Boolean, onDispose: () -> Unit) {
+ val positiveEnabledIndex = remember { positiveEnabledCounter.getAndIncrement() }
+
+ DisposableEffect(valid) {
+ positiveButtonEnabled[positiveEnabledIndex] = valid
+ onDispose {
+ positiveButtonEnabled[positiveEnabledIndex] = true
+ onDispose()
+ }
+ }
}
- }
-
- /**
- * Adds a callback to the dialog which is called on positive button press
- *
- * @param callback called when positive button is pressed
- */
- @Composable
- override fun DialogCallback(callback: () -> Unit) {
- val callbackIndex = rememberSaveable { callbackCounter.getAndIncrement() }
-
- DisposableEffect(Unit) {
- callbacks[callbackIndex] = callback
- onDispose { callbacks[callbackIndex] = {} }
+
+ /**
+ * Adds a callback to the dialog which is called on positive button press
+ *
+ * @param callback called when positive button is pressed
+ */
+ @Composable
+ override fun DialogCallback(callback: () -> Unit) {
+ val callbackIndex = rememberSaveable { callbackCounter.getAndIncrement() }
+
+ DisposableEffect(Unit) {
+ callbacks[callbackIndex] = callback
+ onDispose { callbacks[callbackIndex] = {} }
+ }
}
- }
}
/**
@@ -156,39 +155,39 @@ internal class MaterialDialogScopeImpl(
* @param initialValue the initial showing state of the dialog
*/
class MaterialDialogState(initialValue: Boolean = false) {
- var showing by mutableStateOf(initialValue)
-
- /**
- * Dialog background color with elevation overlay
- */
- var dialogBackgroundColor by mutableStateOf(null)
-
- /**
- * Shows the dialog
- */
- fun show() {
- showing = true
- }
-
- /**
- * Clears focus with a given [FocusManager] and then hides the dialog
- *
- * @param focusManager the focus manager of the dialog view
- */
- fun hide(focusManager: FocusManager? = null) {
- focusManager?.clearFocus()
- showing = false
- }
-
- companion object {
+ var showing by mutableStateOf(initialValue)
+
+ /**
+ * Dialog background color with elevation overlay
+ */
+ var dialogBackgroundColor by mutableStateOf(null)
+
+ /**
+ * Shows the dialog
+ */
+ fun show() {
+ showing = true
+ }
+
/**
- * The default [Saver] implementation for [MaterialDialogState].
+ * Clears focus with a given [FocusManager] and then hides the dialog
+ *
+ * @param focusManager the focus manager of the dialog view
*/
- fun Saver(): Saver = Saver(
- save = { it.showing },
- restore = { MaterialDialogState(it) }
- )
- }
+ fun hide(focusManager: FocusManager? = null) {
+ focusManager?.clearFocus()
+ showing = false
+ }
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [MaterialDialogState].
+ */
+ fun Saver(): Saver = Saver(
+ save = { it.showing },
+ restore = { MaterialDialogState(it) },
+ )
+ }
}
/**
@@ -198,9 +197,9 @@ class MaterialDialogState(initialValue: Boolean = false) {
*/
@Composable
fun rememberMaterialDialogState(initialValue: Boolean = false): MaterialDialogState {
- return rememberSaveable(saver = MaterialDialogState.Saver()) {
- MaterialDialogState(initialValue)
- }
+ return rememberSaveable(saver = MaterialDialogState.Saver()) {
+ MaterialDialogState(initialValue)
+ }
}
/**
@@ -218,89 +217,89 @@ fun rememberMaterialDialogState(initialValue: Boolean = false): MaterialDialogSt
*/
@Composable
fun MaterialDialog(
- dialogState: MaterialDialogState = rememberMaterialDialogState(),
- properties: DialogProperties = DialogProperties(),
- backgroundColor: Color = MaterialTheme.colorScheme.surface,
- shape: Shape = MaterialTheme.shapes.medium,
- border: BorderStroke? = null,
- elevation: Dp = 0.dp,
- autoDismiss: Boolean = true,
- onCloseRequest: (MaterialDialogState) -> Unit = { it.hide() },
- buttons: @Composable MaterialDialogButtons.() -> Unit = {},
- content: @Composable MaterialDialogScope.() -> Unit
+ dialogState: MaterialDialogState = rememberMaterialDialogState(),
+ properties: DialogProperties = DialogProperties(),
+ backgroundColor: Color = MaterialTheme.colorScheme.surface,
+ shape: Shape = MaterialTheme.shapes.medium,
+ border: BorderStroke? = null,
+ elevation: Dp = 0.dp,
+ autoDismiss: Boolean = true,
+ onCloseRequest: (MaterialDialogState) -> Unit = { it.hide() },
+ buttons: @Composable MaterialDialogButtons.() -> Unit = {},
+ content: @Composable MaterialDialogScope.() -> Unit,
) {
- val dialogScope = remember { MaterialDialogScopeImpl(dialogState, autoDismiss) }
- DisposableEffect(dialogState.showing) {
- if (!dialogState.showing) dialogScope.reset()
- onDispose { }
- }
-
- BoxWithConstraints {
- val maxHeight = if (isLargeDevice()) {
- LocalConfiguration.current.screenHeightDp.dp - 96.dp
- } else {
- 560.dp
+ val dialogScope = remember { MaterialDialogScopeImpl(dialogState, autoDismiss) }
+ DisposableEffect(dialogState.showing) {
+ if (!dialogState.showing) dialogScope.reset()
+ onDispose { }
}
- val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx().toInt() }
- val isDialogFullWidth = LocalConfiguration.current.screenWidthDp.dp == maxWidth
- val padding = if (isDialogFullWidth) 16.dp else 0.dp
-
- if (dialogState.showing) {
- dialogState.dialogBackgroundColor = LocalElevationOverlay.current?.apply(
- color = backgroundColor,
- elevation = elevation
- ) ?: MaterialTheme.colorScheme.surface
-
- Dialog(
- properties = properties,
- onDismissRequest = { onCloseRequest(dialogState) }
- ) {
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .sizeIn(maxHeight = maxHeight, maxWidth = 560.dp)
- .padding(horizontal = padding)
- .clipToBounds()
- .wrapContentHeight()
- .testTag("dialog"),
- shape = shape,
- color = backgroundColor,
- border = border,
- tonalElevation = elevation
- ) {
- Layout(
- content = {
- dialogScope.DialogButtonsLayout(
- modifier = Modifier.layoutId("buttons"),
- content = buttons
- )
- Column(Modifier.layoutId("content")) { content(dialogScope) }
- }
- ) { measurables, constraints ->
- val buttonsHeight =
- measurables[0].minIntrinsicHeight(constraints.maxWidth)
- val buttonsPlaceable = measurables[0].measure(
- constraints.copy(maxHeight = buttonsHeight, minHeight = 0)
- )
-
- val contentPlaceable = measurables[1].measure(
- constraints.copy(
- maxHeight = maxHeightPx - buttonsPlaceable.height,
- minHeight = 0
- )
- )
-
- val height =
- min(maxHeightPx, buttonsPlaceable.height + contentPlaceable.height)
-
- return@Layout layout(constraints.maxWidth, height) {
- contentPlaceable.place(0, 0)
- buttonsPlaceable.place(0, height - buttonsPlaceable.height)
+ BoxWithConstraints {
+ val maxHeight = if (isLargeDevice()) {
+ LocalConfiguration.current.screenHeightDp.dp - 96.dp
+ } else {
+ 560.dp
+ }
+
+ val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx().toInt() }
+ val isDialogFullWidth = LocalConfiguration.current.screenWidthDp.dp == maxWidth
+ val padding = if (isDialogFullWidth) 16.dp else 0.dp
+
+ if (dialogState.showing) {
+ dialogState.dialogBackgroundColor = LocalElevationOverlay.current?.apply(
+ color = backgroundColor,
+ elevation = elevation,
+ ) ?: MaterialTheme.colorScheme.surface
+
+ Dialog(
+ properties = properties,
+ onDismissRequest = { onCloseRequest(dialogState) },
+ ) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .sizeIn(maxHeight = maxHeight, maxWidth = 560.dp)
+ .padding(horizontal = padding)
+ .clipToBounds()
+ .wrapContentHeight()
+ .testTag("dialog"),
+ shape = shape,
+ color = backgroundColor,
+ border = border,
+ tonalElevation = elevation,
+ ) {
+ Layout(
+ content = {
+ dialogScope.DialogButtonsLayout(
+ modifier = Modifier.layoutId("buttons"),
+ content = buttons,
+ )
+ Column(Modifier.layoutId("content")) { content(dialogScope) }
+ },
+ ) { measurables, constraints ->
+ val buttonsHeight =
+ measurables[0].minIntrinsicHeight(constraints.maxWidth)
+ val buttonsPlaceable = measurables[0].measure(
+ constraints.copy(maxHeight = buttonsHeight, minHeight = 0),
+ )
+
+ val contentPlaceable = measurables[1].measure(
+ constraints.copy(
+ maxHeight = maxHeightPx - buttonsPlaceable.height,
+ minHeight = 0,
+ ),
+ )
+
+ val height =
+ min(maxHeightPx, buttonsPlaceable.height + contentPlaceable.height)
+
+ return@Layout layout(constraints.maxWidth, height) {
+ contentPlaceable.place(0, 0)
+ buttonsPlaceable.place(0, height - buttonsPlaceable.height)
+ }
+ }
+ }
}
- }
}
- }
}
- }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialogButtons.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialogButtons.kt
index b23f50b..6f2b309 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialogButtons.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/MaterialDialogButtons.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.datetimedialog
-
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -27,15 +26,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import java.util.Locale
internal enum class MaterialDialogButtonTypes(val testTag: String) {
- Text("text"),
- Positive("positive"),
- Negative("negative"),
- Accessibility("accessibility")
+ Text("text"),
+ Positive("positive"),
+ Negative("negative"),
+ Accessibility("accessibility"),
}
/**
@@ -45,72 +43,72 @@ internal enum class MaterialDialogButtonTypes(val testTag: String) {
*/
@Composable
internal fun MaterialDialogScope.DialogButtonsLayout(
- modifier: Modifier = Modifier,
- content: @Composable MaterialDialogButtons.() -> Unit
+ modifier: Modifier = Modifier,
+ content: @Composable MaterialDialogButtons.() -> Unit,
) {
- val interButtonPadding = with(LocalDensity.current) { 12.dp.toPx().toInt() }
- val defaultBoxHeight = with(LocalDensity.current) { 52.dp.toPx().toInt() }
- val defaultButtonHeight = with(LocalDensity.current) { 36.dp.toPx().toInt() }
- val accessibilityPadding = with(LocalDensity.current) { 12.dp.toPx().toInt() }
- val verticalPadding = with(LocalDensity.current) { 8.dp.toPx().toInt() }
+ val interButtonPadding = with(LocalDensity.current) { 12.dp.toPx().toInt() }
+ val defaultBoxHeight = with(LocalDensity.current) { 52.dp.toPx().toInt() }
+ val defaultButtonHeight = with(LocalDensity.current) { 36.dp.toPx().toInt() }
+ val accessibilityPadding = with(LocalDensity.current) { 12.dp.toPx().toInt() }
+ val verticalPadding = with(LocalDensity.current) { 8.dp.toPx().toInt() }
- Layout(
- { content(dialogButtons) },
- modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp)
- .background(Color.Transparent),
- { measurables, constraints ->
+ Layout(
+ { content(dialogButtons) },
+ modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ .background(Color.Transparent),
+ { measurables, constraints ->
- if (measurables.isEmpty()) {
- return@Layout layout(0, 0) {}
- }
+ if (measurables.isEmpty()) {
+ return@Layout layout(0, 0) {}
+ }
- val placeables = measurables.map {
- (it.layoutId as MaterialDialogButtonTypes) to it.measure(
- constraints.copy(minWidth = 0, maxHeight = defaultButtonHeight)
- )
- }
- val totalWidth = placeables.sumOf { it.second.width }
- val column = totalWidth > 0.8 * constraints.maxWidth
+ val placeables = measurables.map {
+ (it.layoutId as MaterialDialogButtonTypes) to it.measure(
+ constraints.copy(minWidth = 0, maxHeight = defaultButtonHeight),
+ )
+ }
+ val totalWidth = placeables.sumOf { it.second.width }
+ val column = totalWidth > 0.8 * constraints.maxWidth
- val height =
- if (column) {
- val buttonHeight = placeables.sumOf { it.second.height }
- val heightPadding = (placeables.size - 1) * interButtonPadding
- buttonHeight + heightPadding + 2 * verticalPadding
- } else {
- defaultBoxHeight
- }
+ val height =
+ if (column) {
+ val buttonHeight = placeables.sumOf { it.second.height }
+ val heightPadding = (placeables.size - 1) * interButtonPadding
+ buttonHeight + heightPadding + 2 * verticalPadding
+ } else {
+ defaultBoxHeight
+ }
- layout(constraints.maxWidth, height) {
- var currX = constraints.maxWidth
- var currY = verticalPadding
+ layout(constraints.maxWidth, height) {
+ var currX = constraints.maxWidth
+ var currY = verticalPadding
- val posButtons = placeables.buttons(MaterialDialogButtonTypes.Positive)
- val negButtons = placeables.buttons(MaterialDialogButtonTypes.Negative)
- val textButtons = placeables.buttons(MaterialDialogButtonTypes.Text)
- val accButtons = placeables.buttons(MaterialDialogButtonTypes.Accessibility)
+ val posButtons = placeables.buttons(MaterialDialogButtonTypes.Positive)
+ val negButtons = placeables.buttons(MaterialDialogButtonTypes.Negative)
+ val textButtons = placeables.buttons(MaterialDialogButtonTypes.Text)
+ val accButtons = placeables.buttons(MaterialDialogButtonTypes.Accessibility)
- val buttonInOrder = posButtons + textButtons + negButtons
- buttonInOrder.forEach { button ->
- if (column) {
- button.place(currX - button.width, currY)
- currY += button.height + interButtonPadding
- } else {
- currX -= button.width
- button.place(currX, currY)
- }
- }
+ val buttonInOrder = posButtons + textButtons + negButtons
+ buttonInOrder.forEach { button ->
+ if (column) {
+ button.place(currX - button.width, currY)
+ currY += button.height + interButtonPadding
+ } else {
+ currX -= button.width
+ button.place(currX, currY)
+ }
+ }
- if (accButtons.isNotEmpty()) {
- /* There can only be one accessibility button so take first */
- val button = accButtons[0]
- button.place(accessibilityPadding, height - button.height)
- }
- }
- }
- )
+ if (accButtons.isNotEmpty()) {
+ /* There can only be one accessibility button so take first */
+ val button = accButtons[0]
+ button.place(accessibilityPadding, height - button.height)
+ }
+ }
+ },
+ )
}
/**
@@ -118,149 +116,149 @@ internal fun MaterialDialogScope.DialogButtonsLayout(
* with the [com.vanpra.composematerialdialogs.MaterialDialog.dialogButtons] function
*/
class MaterialDialogButtons(private val scope: MaterialDialogScope) {
- /**
- * Adds a button which is always enabled to the bottom of the dialog. This should
- * only be used for neutral actions.
- *
- * @param text the string literal text shown in the button
- * @param res the string resource text shown in the button
- * @param onClick a callback which is called when the button is pressed
- */
- @Composable
- fun button(
- text: String? = null,
- @StringRes res: Int? = null,
- colors: ButtonColors = ButtonDefaults.textButtonColors(),
- onClick: () -> Unit = {}
- ) {
- val buttonText = getString(res, text).uppercase(Locale.ROOT)
- TextButton(
- onClick = {
- onClick()
- },
- modifier = Modifier
- .layoutId(MaterialDialogButtonTypes.Text)
- .testTag(MaterialDialogButtonTypes.Text.testTag),
- colors = colors
+ /**
+ * Adds a button which is always enabled to the bottom of the dialog. This should
+ * only be used for neutral actions.
+ *
+ * @param text the string literal text shown in the button
+ * @param res the string resource text shown in the button
+ * @param onClick a callback which is called when the button is pressed
+ */
+ @Composable
+ fun Button(
+ text: String? = null,
+ @StringRes res: Int? = null,
+ colors: ButtonColors = ButtonDefaults.textButtonColors(),
+ onClick: () -> Unit = {},
) {
- Text(text = buttonText)
+ val buttonText = getString(res, text).uppercase(Locale.ROOT)
+ TextButton(
+ onClick = {
+ onClick()
+ },
+ modifier = Modifier
+ .layoutId(MaterialDialogButtonTypes.Text)
+ .testTag(MaterialDialogButtonTypes.Text.testTag),
+ colors = colors,
+ ) {
+ Text(text = buttonText)
+ }
}
- }
- /**
- * Adds a positive button to the dialog
- *
- * @param text the string literal text shown in the button
- * @param res the string resource text shown in the button
- * @param disableDismiss when true this will stop the dialog closing when the button is pressed
- * even if autoDismissing is disabled
- * @param onClick a callback which is called when the button is pressed
- */
- @Composable
- fun positiveButton(
- text: String? = null,
- @StringRes res: Int? = null,
- colors: ButtonColors = ButtonDefaults.textButtonColors(),
- disableDismiss: Boolean = false,
- onClick: () -> Unit = {}
- ) {
- val buttonText = getString(res, text).uppercase(Locale.ROOT)
- val buttonEnabled = scope.positiveButtonEnabled.values.all { it }
- val focusManager = LocalFocusManager.current
+ /**
+ * Adds a positive button to the dialog
+ *
+ * @param text the string literal text shown in the button
+ * @param res the string resource text shown in the button
+ * @param disableDismiss when true this will stop the dialog closing when the button is pressed
+ * even if autoDismissing is disabled
+ * @param onClick a callback which is called when the button is pressed
+ */
+ @Composable
+ fun PositiveButton(
+ text: String? = null,
+ @StringRes res: Int? = null,
+ colors: ButtonColors = ButtonDefaults.textButtonColors(),
+ disableDismiss: Boolean = false,
+ onClick: () -> Unit = {},
+ ) {
+ val buttonText = getString(res, text).uppercase(Locale.ROOT)
+ val buttonEnabled = scope.positiveButtonEnabled.values.all { it }
+ val focusManager = LocalFocusManager.current
- TextButton(
- onClick = {
- if (scope.autoDismiss && !disableDismiss) {
- scope.dialogState.hide(focusManager)
- }
+ TextButton(
+ onClick = {
+ if (scope.autoDismiss && !disableDismiss) {
+ scope.dialogState.hide(focusManager)
+ }
- scope.callbacks.values.forEach {
- it()
- }
+ scope.callbacks.values.forEach {
+ it()
+ }
- onClick()
- },
- modifier = Modifier.layoutId(MaterialDialogButtonTypes.Positive)
- .testTag(MaterialDialogButtonTypes.Positive.testTag),
- enabled = buttonEnabled,
- colors = colors
- ) {
- Text(text = buttonText)
+ onClick()
+ },
+ modifier = Modifier.layoutId(MaterialDialogButtonTypes.Positive)
+ .testTag(MaterialDialogButtonTypes.Positive.testTag),
+ enabled = buttonEnabled,
+ colors = colors,
+ ) {
+ Text(text = buttonText)
+ }
}
- }
- /**
- * Adds a negative button to the dialog
- *
- * @param text the string literal text shown in the button
- * @param res the string resource text shown in the button
- * even if autoDismissing is disabled
- * @param onClick a callback which is called when the button is pressed
- */
- @Composable
- fun negativeButton(
- text: String? = null,
- @StringRes res: Int? = null,
- colors: ButtonColors = ButtonDefaults.textButtonColors(),
- onClick: () -> Unit = {}
- ) {
- val buttonText = getString(res, text).uppercase(Locale.ROOT)
- val focusManager = LocalFocusManager.current
+ /**
+ * Adds a negative button to the dialog
+ *
+ * @param text the string literal text shown in the button
+ * @param res the string resource text shown in the button
+ * even if autoDismissing is disabled
+ * @param onClick a callback which is called when the button is pressed
+ */
+ @Composable
+ fun NegativeButton(
+ text: String? = null,
+ @StringRes res: Int? = null,
+ colors: ButtonColors = ButtonDefaults.textButtonColors(),
+ onClick: () -> Unit = {},
+ ) {
+ val buttonText = getString(res, text).uppercase(Locale.ROOT)
+ val focusManager = LocalFocusManager.current
- TextButton(
- onClick = {
- if (scope.autoDismiss) {
- scope.dialogState.hide(focusManager)
+ TextButton(
+ onClick = {
+ if (scope.autoDismiss) {
+ scope.dialogState.hide(focusManager)
+ }
+ onClick()
+ },
+ modifier = Modifier.layoutId(MaterialDialogButtonTypes.Negative)
+ .testTag(MaterialDialogButtonTypes.Negative.testTag),
+ colors = colors,
+ ) {
+ Text(text = buttonText)
}
- onClick()
- },
- modifier = Modifier.layoutId(MaterialDialogButtonTypes.Negative)
- .testTag(MaterialDialogButtonTypes.Negative.testTag),
- colors = colors
- ) {
- Text(text = buttonText)
}
- }
- /**
- * Adds a accessibility button to the bottom left of the dialog
- *
- * @param icon the icon to be shown on the button
- * @param onClick a callback which is called when the button is pressed
- */
- @Composable
- fun accessibilityButton(
- icon: ImageVector,
- colorFilter: ColorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
- onClick: () -> Unit
- ) {
- Box(
- Modifier
- .size(48.dp)
- .layoutId(MaterialDialogButtonTypes.Accessibility)
- .testTag(MaterialDialogButtonTypes.Accessibility.testTag)
- .clickable(onClick = onClick),
- contentAlignment = Alignment.Center
+ /**
+ * Adds a accessibility button to the bottom left of the dialog
+ *
+ * @param icon the icon to be shown on the button
+ * @param onClick a callback which is called when the button is pressed
+ */
+ @Composable
+ fun AccessibilityButton(
+ icon: ImageVector,
+ colorFilter: ColorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
+ onClick: () -> Unit,
) {
- Image(
- icon,
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- colorFilter = colorFilter
- )
+ Box(
+ Modifier
+ .size(48.dp)
+ .layoutId(MaterialDialogButtonTypes.Accessibility)
+ .testTag(MaterialDialogButtonTypes.Accessibility.testTag)
+ .clickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ icon,
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ colorFilter = colorFilter,
+ )
+ }
}
- }
}
@Composable
internal fun getString(@StringRes res: Int? = null, default: String? = null): String {
- return if (res != null) {
- LocalContext.current.getString(res)
- } else {
- default
- ?: throw IllegalArgumentException("Function must receive one non null string parameter")
- }
+ return if (res != null) {
+ LocalContext.current.getString(res)
+ } else {
+ default
+ ?: throw IllegalArgumentException("Function must receive one non null string parameter")
+ }
}
internal fun List>.buttons(type: MaterialDialogButtonTypes) =
- this.filter { it.first == type }.map { it.second }
\ No newline at end of file
+ this.filter { it.first == type }.map { it.second }
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePicker.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePicker.kt
index e45ac3f..4f1f51d 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePicker.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePicker.kt
@@ -9,7 +9,20 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.ContentAlpha
import androidx.compose.material3.MaterialTheme
@@ -17,6 +30,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -35,15 +49,28 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.example.android.january2022.ui.datetimedialog.*
+import com.example.android.january2022.ui.datetimedialog.MaterialDialogScope
+import com.example.android.january2022.ui.datetimedialog.getOffset
+import com.example.android.january2022.ui.datetimedialog.isAM
+import com.example.android.january2022.ui.datetimedialog.isSmallDevice
+import com.example.android.january2022.ui.datetimedialog.noSeconds
+import com.example.android.january2022.ui.datetimedialog.simpleHour
+import com.example.android.january2022.ui.datetimedialog.toAM
+import com.example.android.january2022.ui.datetimedialog.toPM
import java.time.LocalTime
-import kotlin.math.*
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.pow
+import kotlin.math.roundToInt
+import kotlin.math.sin
/* Offset of the clock line and selected circle */
private data class SelectedOffset(
- val lineOffset: Offset = Offset.Zero,
- val selectedOffset: Offset = Offset.Zero,
- val selectedRadius: Float = 0.0f
+ val lineOffset: Offset = Offset.Zero,
+ val selectedOffset: Offset = Offset.Zero,
+ val selectedRadius: Float = 0.0f,
)
/**
@@ -59,680 +86,680 @@ private data class SelectedOffset(
* @param onTimeChange callback with a LocalTime object when the user completes their input
*/
@Composable
-fun MaterialDialogScope.timepicker(
- initialTime: LocalTime = LocalTime.now().noSeconds(),
- title: String = "SELECT TIME",
- colors: TimePickerColors = TimePickerDefaults.colors(),
- waitForPositiveButton: Boolean = true,
- timeRange: ClosedRange = LocalTime.MIN..LocalTime.MAX,
- is24HourClock: Boolean = false,
- onTimeChange: (LocalTime) -> Unit = {}
+fun MaterialDialogScope.Timepicker(
+ initialTime: LocalTime = LocalTime.now().noSeconds(),
+ title: String = "SELECT TIME",
+ colors: TimePickerColors = TimePickerDefaults.colors(),
+ waitForPositiveButton: Boolean = true,
+ timeRange: ClosedRange = LocalTime.MIN..LocalTime.MAX,
+ is24HourClock: Boolean = false,
+ onTimeChange: (LocalTime) -> Unit = {},
) {
- val timePickerState = remember {
- TimePickerState(
- selectedTime = initialTime.coerceIn(timeRange),
- colors = colors,
- timeRange = timeRange,
- is24Hour = is24HourClock
- )
- }
-
- if (waitForPositiveButton) {
- DialogCallback { onTimeChange(timePickerState.selectedTime) }
- } else {
- DisposableEffect(timePickerState.selectedTime) {
- onTimeChange(timePickerState.selectedTime)
- onDispose { }
+ val timePickerState = remember {
+ TimePickerState(
+ selectedTime = initialTime.coerceIn(timeRange),
+ colors = colors,
+ timeRange = timeRange,
+ is24Hour = is24HourClock,
+ )
+ }
+
+ if (waitForPositiveButton) {
+ DialogCallback { onTimeChange(timePickerState.selectedTime) }
+ } else {
+ DisposableEffect(timePickerState.selectedTime) {
+ onTimeChange(timePickerState.selectedTime)
+ onDispose { }
+ }
}
- }
- TimePickerImpl(title = title, state = timePickerState)
+ TimePickerImpl(title = title, state = timePickerState)
}
@Composable
internal fun TimePickerExpandedImpl(
- modifier: Modifier = Modifier,
- title: String,
- state: TimePickerState
+ modifier: Modifier = Modifier,
+ title: String,
+ state: TimePickerState,
) {
- Column(modifier.padding(start = 24.dp, end = 24.dp)) {
- Box(Modifier.align(Alignment.Start)) {
- TimePickerTitle(Modifier.height(36.dp), title, state)
- }
+ Column(modifier.padding(start = 24.dp, end = 24.dp)) {
+ Box(Modifier.align(Alignment.Start)) {
+ TimePickerTitle(Modifier.height(36.dp), title, state)
+ }
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
- Column(
- Modifier
- .padding(top = 72.dp, bottom = 50.dp)
- .width(216.dp)
- ) {
- TimeLayout(state = state)
- Spacer(modifier = Modifier.height(12.dp))
- HorizontalPeriodPicker(state = state)
- }
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ Column(
+ Modifier
+ .padding(top = 72.dp, bottom = 50.dp)
+ .width(216.dp),
+ ) {
+ TimeLayout(state = state)
+ Spacer(modifier = Modifier.height(12.dp))
+ HorizontalPeriodPicker(state = state)
+ }
/* This isn't an exact match to the material spec as there is a contradiction it.
Dialogs should be limited to the size of 560 dp but given sizes for extended
time picker go over this limit */
- Spacer(modifier = Modifier.width(40.dp))
- Crossfade(state.currentScreen) {
- when (it) {
- ClockScreen.Hour -> if (state.is24Hour) {
- ExtendedClockHourLayout(state = state)
- } else {
- ClockHourLayout(state = state)
- }
- ClockScreen.Minute -> ClockMinuteLayout(state = state)
+ Spacer(modifier = Modifier.width(40.dp))
+ Crossfade(state.currentScreen) {
+ when (it) {
+ ClockScreen.Hour -> if (state.is24Hour) {
+ ExtendedClockHourLayout(state = state)
+ } else {
+ ClockHourLayout(state = state)
+ }
+ ClockScreen.Minute -> ClockMinuteLayout(state = state)
+ }
+ }
}
- }
}
- }
}
@Composable
internal fun TimePickerImpl(
- modifier: Modifier = Modifier,
- title: String,
- state: TimePickerState
+ modifier: Modifier = Modifier,
+ title: String,
+ state: TimePickerState,
) {
- Column(
- modifier.padding(start = 24.dp, end = 24.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- if (title != "") {
- Box(Modifier.align(Alignment.Start)) {
- TimePickerTitle(Modifier.height(52.dp), title, state)
- }
- }
+ Column(
+ modifier.padding(start = 24.dp, end = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ if (title != "") {
+ Box(Modifier.align(Alignment.Start)) {
+ TimePickerTitle(Modifier.height(52.dp), title, state)
+ }
+ }
- TimeLayout(state = state)
+ TimeLayout(state = state)
- Spacer(modifier = Modifier.height(if (isSmallDevice()) 24.dp else 36.dp))
- Crossfade(state.currentScreen) {
- when (it) {
- ClockScreen.Hour -> if (state.is24Hour) {
- ExtendedClockHourLayout(state = state)
- } else {
- ClockHourLayout(state = state)
+ Spacer(modifier = Modifier.height(if (isSmallDevice()) 24.dp else 36.dp))
+ Crossfade(state.currentScreen) {
+ when (it) {
+ ClockScreen.Hour -> if (state.is24Hour) {
+ ExtendedClockHourLayout(state = state)
+ } else {
+ ClockHourLayout(state = state)
+ }
+ ClockScreen.Minute -> ClockMinuteLayout(state = state)
+ }
}
- ClockScreen.Minute -> ClockMinuteLayout(state = state)
- }
- }
- Spacer(modifier = Modifier.height(24.dp))
- }
+ Spacer(modifier = Modifier.height(24.dp))
+ }
}
@Composable
private fun ExtendedClockHourLayout(state: TimePickerState) {
- fun adjustAnchor(anchor: Int): Int = when (anchor) {
- 0 -> 12
- 12 -> 0
- else -> anchor
- }
-
- val isEnabled: (Int) -> Boolean = remember(state.timeRange) {
- { index -> adjustAnchor(index) in state.hourRange() }
- }
-
- ClockLayout(
- anchorPoints = 12,
- innerAnchorPoints = 12,
- label = { index ->
- /* Swapping 12 and 00 as this is the standard layout */
- when (index) {
- 0 -> "12"
- 12 -> "00"
- else -> index.toString()
- }
- },
- onAnchorChange = { anchor ->
- /* Swapping 12 and 00 as this is the standard layout */
- state.selectedTime =
- state.selectedTime.withHour(adjustAnchor(anchor)).coerceIn(state.timeRange)
- },
- startAnchor = adjustAnchor(state.selectedTime.hour),
- onLift = { state.currentScreen = ClockScreen.Minute },
- colors = state.colors,
- isAnchorEnabled = isEnabled
- )
+ fun adjustAnchor(anchor: Int): Int = when (anchor) {
+ 0 -> 12
+ 12 -> 0
+ else -> anchor
+ }
+
+ val isEnabled: (Int) -> Boolean = remember(state.timeRange) {
+ { index -> adjustAnchor(index) in state.hourRange() }
+ }
+
+ ClockLayout(
+ anchorPoints = 12,
+ innerAnchorPoints = 12,
+ label = { index ->
+ /* Swapping 12 and 00 as this is the standard layout */
+ when (index) {
+ 0 -> "12"
+ 12 -> "00"
+ else -> index.toString()
+ }
+ },
+ onAnchorChange = { anchor ->
+ /* Swapping 12 and 00 as this is the standard layout */
+ state.selectedTime =
+ state.selectedTime.withHour(adjustAnchor(anchor)).coerceIn(state.timeRange)
+ },
+ startAnchor = adjustAnchor(state.selectedTime.hour),
+ onLift = { state.currentScreen = ClockScreen.Minute },
+ colors = state.colors,
+ isAnchorEnabled = isEnabled,
+ )
}
@Composable
private fun ClockHourLayout(state: TimePickerState) {
- fun adjustedHour(hour: Int): Int {
- return if (state.selectedTime.isAM || hour == 12) hour else hour + 12
- }
-
- val isEnabled: (Int) -> Boolean = remember(state.timeRange, state.selectedTime) {
- { index -> adjustedHour(index) in state.hourRange() }
- }
-
- ClockLayout(
- anchorPoints = 12,
- label = { index -> if (index == 0) "12" else index.toString() },
- onAnchorChange = { hours ->
- val adjustedHour = when (hours) {
- 12 -> if (state.selectedTime.isAM) 0 else 12
- else -> if (state.selectedTime.isAM) hours else hours + 12
- }
- state.selectedTime = state.selectedTime.withHour(adjustedHour).coerceIn(state.timeRange)
- },
- startAnchor = state.selectedTime.simpleHour % 12,
- onLift = { state.currentScreen = ClockScreen.Minute },
- colors = state.colors,
- isAnchorEnabled = isEnabled
- )
+ fun adjustedHour(hour: Int): Int {
+ return if (state.selectedTime.isAM || hour == 12) hour else hour + 12
+ }
+
+ val isEnabled: (Int) -> Boolean = remember(state.timeRange, state.selectedTime) {
+ { index -> adjustedHour(index) in state.hourRange() }
+ }
+
+ ClockLayout(
+ anchorPoints = 12,
+ label = { index -> if (index == 0) "12" else index.toString() },
+ onAnchorChange = { hours ->
+ val adjustedHour = when (hours) {
+ 12 -> if (state.selectedTime.isAM) 0 else 12
+ else -> if (state.selectedTime.isAM) hours else hours + 12
+ }
+ state.selectedTime = state.selectedTime.withHour(adjustedHour).coerceIn(state.timeRange)
+ },
+ startAnchor = state.selectedTime.simpleHour % 12,
+ onLift = { state.currentScreen = ClockScreen.Minute },
+ colors = state.colors,
+ isAnchorEnabled = isEnabled,
+ )
}
@Composable
private fun ClockMinuteLayout(state: TimePickerState) {
- val isEnabled: (Int) -> Boolean =
- remember(state.timeRange, state.selectedTime, state.selectedTime.isAM) {
- { index ->
- index in state.minuteRange(state.selectedTime.isAM, state.selectedTime.hour)
- }
- }
- ClockLayout(
- anchorPoints = 60,
- label = { index -> index.toString().padStart(2, '0') },
- onAnchorChange = { mins -> state.selectedTime = state.selectedTime.withMinute(mins) },
- startAnchor = state.selectedTime.minute,
- isNamedAnchor = { anchor -> anchor % 5 == 0 },
- colors = state.colors,
- isAnchorEnabled = isEnabled
- )
+ val isEnabled: (Int) -> Boolean =
+ remember(state.timeRange, state.selectedTime, state.selectedTime.isAM) {
+ { index ->
+ index in state.minuteRange(state.selectedTime.isAM, state.selectedTime.hour)
+ }
+ }
+ ClockLayout(
+ anchorPoints = 60,
+ label = { index -> index.toString().padStart(2, '0') },
+ onAnchorChange = { mins -> state.selectedTime = state.selectedTime.withMinute(mins) },
+ startAnchor = state.selectedTime.minute,
+ isNamedAnchor = { anchor -> anchor % 5 == 0 },
+ colors = state.colors,
+ isAnchorEnabled = isEnabled,
+ )
}
@Composable
internal fun TimePickerTitle(modifier: Modifier, text: String, state: TimePickerState) {
- Box(modifier) {
- Text(
- text,
- modifier = Modifier.paddingFromBaseline(top = 28.dp),
- style = TextStyle(color = state.colors.headerTextColor())
- )
- }
+ Box(modifier) {
+ Text(
+ text,
+ modifier = Modifier.paddingFromBaseline(top = 28.dp),
+ style = TextStyle(color = state.colors.headerTextColor()),
+ )
+ }
}
@Composable
internal fun ClockLabel(
- text: String,
- backgroundColor: Color,
- textColor: Color,
- onClick: () -> Unit
+ text: String,
+ backgroundColor: Color,
+ textColor: Color,
+ onClick: () -> Unit,
) {
- Surface(
- modifier = Modifier
- .width(if (isSmallDevice()) 80.dp else 96.dp)
- .fillMaxHeight(),
- shape = MaterialTheme.shapes.medium,
- color = backgroundColor
- ) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .clickable(onClick = onClick),
- contentAlignment = Alignment.Center
+ Surface(
+ modifier = Modifier
+ .width(if (isSmallDevice()) 80.dp else 96.dp)
+ .fillMaxHeight(),
+ shape = MaterialTheme.shapes.medium,
+ color = backgroundColor,
) {
- Text(
- text = text,
- style = TextStyle(
- fontSize = 50.sp,
- color = textColor
- )
- )
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = text,
+ style = TextStyle(
+ fontSize = 50.sp,
+ color = textColor,
+ ),
+ )
+ }
}
- }
}
@Composable
internal fun TimeLayout(modifier: Modifier = Modifier, state: TimePickerState) {
- val clockHour: String = remember(
- state.is24Hour,
- state.selectedTime,
- state.selectedTime.hour
- ) {
- if (state.is24Hour) {
- state.selectedTime.hour.toString().padStart(2, '0')
- } else {
- state.selectedTime.simpleHour.toString()
+ val clockHour: String = remember(
+ state.is24Hour,
+ state.selectedTime,
+ state.selectedTime.hour,
+ ) {
+ if (state.is24Hour) {
+ state.selectedTime.hour.toString().padStart(2, '0')
+ } else {
+ state.selectedTime.simpleHour.toString()
+ }
}
- }
-
- Row(
- horizontalArrangement = Arrangement.Center,
- modifier = modifier
- .height(80.dp)
- .fillMaxWidth()
- ) {
- ClockLabel(
- text = clockHour,
- backgroundColor = state.colors.backgroundColor(state.currentScreen.isHour()).value,
- textColor = state.colors.textColor(state.currentScreen.isHour()).value,
- onClick = { state.currentScreen = ClockScreen.Hour }
- )
- Box(
- Modifier
- .width(24.dp)
- .fillMaxHeight(),
- contentAlignment = Alignment.Center
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ modifier = modifier
+ .height(80.dp)
+ .fillMaxWidth(),
) {
- Text(
- text = ":",
- style = TextStyle(fontSize = 50.sp, color = MaterialTheme.colorScheme.onSurface)
- )
- }
+ ClockLabel(
+ text = clockHour,
+ backgroundColor = state.colors.backgroundColor(state.currentScreen.isHour()).value,
+ textColor = state.colors.textColor(state.currentScreen.isHour()).value,
+ onClick = { state.currentScreen = ClockScreen.Hour },
+ )
- ClockLabel(
- text = state.selectedTime.minute.toString().padStart(2, '0'),
- backgroundColor = state.colors.backgroundColor(state.currentScreen.isMinute()).value,
- textColor = state.colors.textColor(state.currentScreen.isMinute()).value,
- onClick = { state.currentScreen = ClockScreen.Minute }
+ Box(
+ Modifier
+ .width(24.dp)
+ .fillMaxHeight(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = ":",
+ style = TextStyle(fontSize = 50.sp, color = MaterialTheme.colorScheme.onSurface),
+ )
+ }
- )
+ ClockLabel(
+ text = state.selectedTime.minute.toString().padStart(2, '0'),
+ backgroundColor = state.colors.backgroundColor(state.currentScreen.isMinute()).value,
+ textColor = state.colors.textColor(state.currentScreen.isMinute()).value,
+ onClick = { state.currentScreen = ClockScreen.Minute },
+
+ )
- if (!state.is24Hour) {
- VerticalPeriodPicker(state = state)
+ if (!state.is24Hour) {
+ VerticalPeriodPicker(state = state)
+ }
}
- }
}
@Composable
private fun VerticalPeriodPicker(state: TimePickerState) {
- val topPeriodShape = MaterialTheme.shapes.medium.copy(
- bottomStart = CornerSize(0.dp),
- bottomEnd = CornerSize(0.dp)
- )
- val bottomPeriodShape =
- MaterialTheme.shapes.medium.copy(topStart = CornerSize(0.dp), topEnd = CornerSize(0.dp))
- val isAMEnabled = remember(state.timeRange) { state.timeRange.start.hour <= 12 }
- val isPMEnabled = remember(state.timeRange) { state.timeRange.endInclusive.hour >= 0 }
-
- Spacer(modifier = Modifier.width(12.dp))
-
- Column(
- Modifier
- .fillMaxHeight()
- .width(52.dp)
- .border(state.colors.border, MaterialTheme.shapes.medium)
- ) {
- Box(
- modifier = Modifier
- .size(height = 40.dp, width = 52.dp)
- .clip(topPeriodShape)
- .background(state.colors.periodBackgroundColor(state.selectedTime.isAM).value)
- .then(
- if (isAMEnabled) {
- Modifier.clickable {
- state.selectedTime = state.selectedTime
- .toAM()
- .coerceIn(state.timeRange)
- }
- } else {
- Modifier
- }
- ),
- contentAlignment = Alignment.Center
- ) {
- Text(
- "AM",
- style = TextStyle(
- state.colors.textColor(state.selectedTime.isAM).value.copy(alpha = if (isAMEnabled) ContentAlpha.high else ContentAlpha.disabled)
- )
- )
- }
-
- Spacer(
- Modifier
- .fillMaxWidth()
- .height(1.dp)
- .background(state.colors.border.brush)
+ val topPeriodShape = MaterialTheme.shapes.medium.copy(
+ bottomStart = CornerSize(0.dp),
+ bottomEnd = CornerSize(0.dp),
)
+ val bottomPeriodShape =
+ MaterialTheme.shapes.medium.copy(topStart = CornerSize(0.dp), topEnd = CornerSize(0.dp))
+ val isAMEnabled = remember(state.timeRange) { state.timeRange.start.hour <= 12 }
+ val isPMEnabled = remember(state.timeRange) { state.timeRange.endInclusive.hour >= 0 }
- Box(
- modifier = Modifier
- .size(height = 40.dp, width = 52.dp)
- .clip(bottomPeriodShape)
- .background(state.colors.periodBackgroundColor(!state.selectedTime.isAM).value)
- .then(
- if (isPMEnabled) {
- Modifier.clickable {
- state.selectedTime = state.selectedTime
- .toPM()
- .coerceIn(state.timeRange)
- }
- } else {
- Modifier
- }
- ),
- contentAlignment = Alignment.Center
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column(
+ Modifier
+ .fillMaxHeight()
+ .width(52.dp)
+ .border(state.colors.border, MaterialTheme.shapes.medium),
) {
- Text(
- "PM",
- style = TextStyle(
- state.colors.textColor(!state.selectedTime.isAM).value.copy(
- alpha = if (isPMEnabled) ContentAlpha.high else ContentAlpha.disabled
- )
+ Box(
+ modifier = Modifier
+ .size(height = 40.dp, width = 52.dp)
+ .clip(topPeriodShape)
+ .background(state.colors.periodBackgroundColor(state.selectedTime.isAM).value)
+ .then(
+ if (isAMEnabled) {
+ Modifier.clickable {
+ state.selectedTime = state.selectedTime
+ .toAM()
+ .coerceIn(state.timeRange)
+ }
+ } else {
+ Modifier
+ },
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ "AM",
+ style = TextStyle(
+ state.colors.textColor(state.selectedTime.isAM).value.copy(alpha = if (isAMEnabled) ContentAlpha.high else ContentAlpha.disabled),
+ ),
+ )
+ }
+
+ Spacer(
+ Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(state.colors.border.brush),
)
- )
+
+ Box(
+ modifier = Modifier
+ .size(height = 40.dp, width = 52.dp)
+ .clip(bottomPeriodShape)
+ .background(state.colors.periodBackgroundColor(!state.selectedTime.isAM).value)
+ .then(
+ if (isPMEnabled) {
+ Modifier.clickable {
+ state.selectedTime = state.selectedTime
+ .toPM()
+ .coerceIn(state.timeRange)
+ }
+ } else {
+ Modifier
+ },
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ "PM",
+ style = TextStyle(
+ state.colors.textColor(!state.selectedTime.isAM).value.copy(
+ alpha = if (isPMEnabled) ContentAlpha.high else ContentAlpha.disabled,
+ ),
+ ),
+ )
+ }
}
- }
}
@Composable
private fun HorizontalPeriodPicker(state: TimePickerState) {
- val leftPeriodShape = MaterialTheme.shapes.medium.copy(
- bottomEnd = CornerSize(0.dp),
- topEnd = CornerSize(0.dp)
- )
- val rightPeriodShape = MaterialTheme.shapes.medium.copy(
- topStart = CornerSize(0.dp),
- bottomStart = CornerSize(0.dp)
- )
- val isAMEnabled = remember(state.timeRange) { state.timeRange.start.hour <= 12 }
- val isPMEnabled = remember(state.timeRange) { state.timeRange.endInclusive.hour >= 0 }
-
- Spacer(modifier = Modifier.width(12.dp))
-
- Row(
- Modifier
- .fillMaxWidth()
- .height(height = 40.dp)
- .border(state.colors.border, MaterialTheme.shapes.medium)
- ) {
- Box(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(0.5f)
- .clip(leftPeriodShape)
- .background(state.colors.periodBackgroundColor(state.selectedTime.isAM).value)
- .then(
- if (isAMEnabled) {
- Modifier.clickable {
- state.selectedTime = state.selectedTime
- .toAM()
- .coerceIn(state.timeRange)
- }
- } else {
- Modifier
- }
- ),
- contentAlignment = Alignment.Center
- ) {
- Text(
- "AM",
- style = TextStyle(
- state.colors.textColor(state.selectedTime.isAM).value.copy(alpha = if (isAMEnabled) ContentAlpha.high else ContentAlpha.disabled)
- )
- )
- }
-
- Spacer(
- Modifier
- .fillMaxHeight()
- .width(1.dp)
- .background(state.colors.border.brush)
+ val leftPeriodShape = MaterialTheme.shapes.medium.copy(
+ bottomEnd = CornerSize(0.dp),
+ topEnd = CornerSize(0.dp),
)
+ val rightPeriodShape = MaterialTheme.shapes.medium.copy(
+ topStart = CornerSize(0.dp),
+ bottomStart = CornerSize(0.dp),
+ )
+ val isAMEnabled = remember(state.timeRange) { state.timeRange.start.hour <= 12 }
+ val isPMEnabled = remember(state.timeRange) { state.timeRange.endInclusive.hour >= 0 }
- Box(
- modifier = Modifier
- .fillMaxSize()
- .clip(rightPeriodShape)
- .background(state.colors.periodBackgroundColor(!state.selectedTime.isAM).value)
- .then(
- if (isPMEnabled) {
- Modifier.clickable {
- state.selectedTime = state.selectedTime
- .toPM()
- .coerceIn(state.timeRange)
- }
- } else {
- Modifier
- }
- ),
- contentAlignment = Alignment.Center
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .height(height = 40.dp)
+ .border(state.colors.border, MaterialTheme.shapes.medium),
) {
- Text(
- "PM",
- style = TextStyle(
- state.colors.textColor(!state.selectedTime.isAM).value.copy(
- alpha = if (isPMEnabled) ContentAlpha.high else ContentAlpha.disabled
- )
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .fillMaxWidth(0.5f)
+ .clip(leftPeriodShape)
+ .background(state.colors.periodBackgroundColor(state.selectedTime.isAM).value)
+ .then(
+ if (isAMEnabled) {
+ Modifier.clickable {
+ state.selectedTime = state.selectedTime
+ .toAM()
+ .coerceIn(state.timeRange)
+ }
+ } else {
+ Modifier
+ },
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ "AM",
+ style = TextStyle(
+ state.colors.textColor(state.selectedTime.isAM).value.copy(alpha = if (isAMEnabled) ContentAlpha.high else ContentAlpha.disabled),
+ ),
+ )
+ }
+
+ Spacer(
+ Modifier
+ .fillMaxHeight()
+ .width(1.dp)
+ .background(state.colors.border.brush),
)
- )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(rightPeriodShape)
+ .background(state.colors.periodBackgroundColor(!state.selectedTime.isAM).value)
+ .then(
+ if (isPMEnabled) {
+ Modifier.clickable {
+ state.selectedTime = state.selectedTime
+ .toPM()
+ .coerceIn(state.timeRange)
+ }
+ } else {
+ Modifier
+ },
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ "PM",
+ style = TextStyle(
+ state.colors.textColor(!state.selectedTime.isAM).value.copy(
+ alpha = if (isPMEnabled) ContentAlpha.high else ContentAlpha.disabled,
+ ),
+ ),
+ )
+ }
}
- }
}
@Composable
private fun ClockLayout(
- isNamedAnchor: (Int) -> Boolean = { true },
- anchorPoints: Int,
- innerAnchorPoints: Int = 0,
- label: (Int) -> String,
- startAnchor: Int,
- colors: TimePickerColors,
- isAnchorEnabled: (Int) -> Boolean,
- onAnchorChange: (Int) -> Unit = {},
- onLift: () -> Unit = {}
+ isNamedAnchor: (Int) -> Boolean = { true },
+ anchorPoints: Int,
+ innerAnchorPoints: Int = 0,
+ label: (Int) -> String,
+ startAnchor: Int,
+ colors: TimePickerColors,
+ isAnchorEnabled: (Int) -> Boolean,
+ onAnchorChange: (Int) -> Unit = {},
+ onLift: () -> Unit = {},
) {
- BoxWithConstraints {
- val faceDiameter = min(maxHeight.value, maxWidth.value).coerceAtMost(256f).dp
- val faceDiameterPx = with(LocalDensity.current) { faceDiameter.toPx() }
+ BoxWithConstraints {
+ val faceDiameter = min(maxHeight.value, maxWidth.value).coerceAtMost(256f).dp
+ val faceDiameterPx = with(LocalDensity.current) { faceDiameter.toPx() }
- val faceRadiusPx = faceDiameterPx / 2f
+ val faceRadiusPx = faceDiameterPx / 2f
- val outerRadiusPx = faceRadiusPx * 0.8f
- val innerRadiusPx = remember(outerRadiusPx) { outerRadiusPx * 0.6f }
+ val outerRadiusPx = faceRadiusPx * 0.8f
+ val innerRadiusPx = remember(outerRadiusPx) { outerRadiusPx * 0.6f }
- val textSizePx = with(LocalDensity.current) { 18.sp.toPx() }
- val innerTextSizePx = remember(textSizePx) { textSizePx * 0.8f }
+ val textSizePx = with(LocalDensity.current) { 18.sp.toPx() }
+ val innerTextSizePx = remember(textSizePx) { textSizePx * 0.8f }
- val selectedRadius = remember(outerRadiusPx) { outerRadiusPx * 0.2f }
- val selectedInnerDotRadius = remember(selectedRadius) { selectedRadius * 0.2f }
- val innerSelectedRadius = remember(innerRadiusPx) { innerRadiusPx * 0.3f }
+ val selectedRadius = remember(outerRadiusPx) { outerRadiusPx * 0.2f }
+ val selectedInnerDotRadius = remember(selectedRadius) { selectedRadius * 0.2f }
+ val innerSelectedRadius = remember(innerRadiusPx) { innerRadiusPx * 0.3f }
- val centerCircleRadius = remember(selectedRadius) { selectedRadius * 0.4f }
- val selectedLineWidth = remember(centerCircleRadius) { centerCircleRadius * 0.5f }
+ val centerCircleRadius = remember(selectedRadius) { selectedRadius * 0.4f }
+ val selectedLineWidth = remember(centerCircleRadius) { centerCircleRadius * 0.5f }
- val center = remember { Offset(faceRadiusPx, faceRadiusPx) }
+ val center = remember { Offset(faceRadiusPx, faceRadiusPx) }
- val namedAnchor = remember(isNamedAnchor) { mutableStateOf(isNamedAnchor(startAnchor)) }
- val selectedAnchor = remember { mutableStateOf(startAnchor) }
+ val namedAnchor = remember(isNamedAnchor) { mutableStateOf(isNamedAnchor(startAnchor)) }
+ val selectedAnchor = remember { mutableIntStateOf(startAnchor) }
- val anchors = remember(anchorPoints, innerAnchorPoints) {
- val anchors = mutableListOf()
- for (x in 0 until anchorPoints) {
- val angle = (2 * PI / anchorPoints) * (x - 15)
- val selectedOuterOffset = outerRadiusPx.getOffset(angle)
- val lineOuterOffset = (outerRadiusPx - selectedRadius).getOffset(angle)
+ val anchors = remember(anchorPoints, innerAnchorPoints) {
+ val anchors = mutableListOf()
+ for (x in 0 until anchorPoints) {
+ val angle = (2 * PI / anchorPoints) * (x - 15)
+ val selectedOuterOffset = outerRadiusPx.getOffset(angle)
+ val lineOuterOffset = (outerRadiusPx - selectedRadius).getOffset(angle)
- anchors.add(
- SelectedOffset(
- lineOuterOffset,
- selectedOuterOffset,
- selectedRadius
- )
- )
- }
- for (x in 0 until innerAnchorPoints) {
- val angle = (2 * PI / innerAnchorPoints) * (x - 15)
- val selectedOuterOffset = innerRadiusPx.getOffset(angle)
- val lineOuterOffset = (innerRadiusPx - innerSelectedRadius).getOffset(angle)
-
- anchors.add(
- SelectedOffset(
- lineOuterOffset,
- selectedOuterOffset,
- innerSelectedRadius
- )
- )
- }
- anchors
- }
-
- val anchoredOffset = remember(anchors, startAnchor) { mutableStateOf(anchors[startAnchor]) }
+ anchors.add(
+ SelectedOffset(
+ lineOuterOffset,
+ selectedOuterOffset,
+ selectedRadius,
+ ),
+ )
+ }
+ for (x in 0 until innerAnchorPoints) {
+ val angle = (2 * PI / innerAnchorPoints) * (x - 15)
+ val selectedOuterOffset = innerRadiusPx.getOffset(angle)
+ val lineOuterOffset = (innerRadiusPx - innerSelectedRadius).getOffset(angle)
+
+ anchors.add(
+ SelectedOffset(
+ lineOuterOffset,
+ selectedOuterOffset,
+ innerSelectedRadius,
+ ),
+ )
+ }
+ anchors
+ }
- val updateAnchor: (Offset) -> Boolean = remember(anchors, isAnchorEnabled) {
- { newOffset ->
- val absDiff = anchors.map {
- val diff = it.selectedOffset - newOffset + center
- diff.x.pow(2) + diff.y.pow(2)
+ val anchoredOffset = remember(anchors, startAnchor) { mutableStateOf(anchors[startAnchor]) }
+
+ val updateAnchor: (Offset) -> Boolean = remember(anchors, isAnchorEnabled) {
+ { newOffset ->
+ val absDiff = anchors.map {
+ val diff = it.selectedOffset - newOffset + center
+ diff.x.pow(2) + diff.y.pow(2)
+ }
+
+ val minAnchor = absDiff.withIndex().minByOrNull { (_, f) -> f }!!.index
+ if (isAnchorEnabled(minAnchor)) {
+ if (anchoredOffset.value.selectedOffset != anchors[minAnchor].selectedOffset) {
+ onAnchorChange(minAnchor)
+
+ anchoredOffset.value = anchors[minAnchor]
+ namedAnchor.value = isNamedAnchor(minAnchor)
+ selectedAnchor.value = minAnchor
+ }
+ true
+ } else {
+ false
+ }
+ }
}
- val minAnchor = absDiff.withIndex().minByOrNull { (_, f) -> f }!!.index
- if (isAnchorEnabled(minAnchor)) {
- if (anchoredOffset.value.selectedOffset != anchors[minAnchor].selectedOffset) {
- onAnchorChange(minAnchor)
+ val dragSuccess = remember { mutableStateOf(false) }
- anchoredOffset.value = anchors[minAnchor]
- namedAnchor.value = isNamedAnchor(minAnchor)
- selectedAnchor.value = minAnchor
- }
- true
- } else {
- false
+ val dragObserver: suspend PointerInputScope.() -> Unit = {
+ detectDragGestures(
+ onDragStart = { dragSuccess.value = true },
+ onDragCancel = { dragSuccess.value = false },
+ onDragEnd = { if (dragSuccess.value) onLift() },
+ ) { change, _ ->
+ dragSuccess.value = updateAnchor(change.position)
+ if (change.positionChange() != Offset.Zero) change.consume()
+ }
}
- }
- }
-
- val dragSuccess = remember { mutableStateOf(false) }
-
- val dragObserver: suspend PointerInputScope.() -> Unit = {
- detectDragGestures(
- onDragStart = { dragSuccess.value = true },
- onDragCancel = { dragSuccess.value = false },
- onDragEnd = { if (dragSuccess.value) onLift() }
- ) { change, _ ->
- dragSuccess.value = updateAnchor(change.position)
- if (change.positionChange() != Offset.Zero) change.consume()
- }
- }
- val tapObserver: suspend PointerInputScope.() -> Unit = {
- detectTapGestures(onPress = {
- val anchorsChanged = updateAnchor(it)
- val success = tryAwaitRelease()
+ val tapObserver: suspend PointerInputScope.() -> Unit = {
+ detectTapGestures(onPress = {
+ val anchorsChanged = updateAnchor(it)
+ val success = tryAwaitRelease()
- if ((success || !dragSuccess.value) && anchorsChanged) {
- onLift()
+ if ((success || !dragSuccess.value) && anchorsChanged) {
+ onLift()
+ }
+ })
}
- })
- }
- val inactiveTextColor = colors.textColor(false).value.toArgb()
- val clockBackgroundColor = colors.backgroundColor(false).value
- val selectorColor = remember { colors.selectorColor() }
- val selectorTextColor = remember { colors.selectorTextColor().toArgb() }
+ val inactiveTextColor = colors.textColor(false).value.toArgb()
+ val clockBackgroundColor = colors.backgroundColor(false).value
+ val selectorColor = remember { colors.selectorColor() }
+ val selectorTextColor = remember { colors.selectorTextColor().toArgb() }
- val enabledAlpha = ContentAlpha.high
- val disabledAlpha = ContentAlpha.disabled
+ val enabledAlpha = ContentAlpha.high
+ val disabledAlpha = ContentAlpha.disabled
- Canvas(
- modifier = Modifier
- .size(faceDiameter)
- .pointerInput(null, dragObserver)
- .pointerInput(null, tapObserver)
- ) {
- drawCircle(clockBackgroundColor, radius = faceRadiusPx, center = center)
- drawCircle(selectorColor, radius = centerCircleRadius, center = center)
- drawLine(
- color = selectorColor,
- start = center,
- end = center + anchoredOffset.value.lineOffset,
- strokeWidth = selectedLineWidth,
- alpha = 0.8f
- )
-
- drawCircle(
- selectorColor,
- center = center + anchoredOffset.value.selectedOffset,
- radius = anchoredOffset.value.selectedRadius,
- alpha = 0.7f
- )
-
- if (!namedAnchor.value) {
- drawCircle(
- Color.White,
- center = center + anchoredOffset.value.selectedOffset,
- radius = selectedInnerDotRadius,
- alpha = 0.8f
- )
- }
-
- drawIntoCanvas { canvas ->
- fun drawAnchorText(
- anchor: Int,
- textSize: Float,
- radius: Float,
- angle: Double,
- alpha: Int = 255
+ Canvas(
+ modifier = Modifier
+ .size(faceDiameter)
+ .pointerInput(null, dragObserver)
+ .pointerInput(null, tapObserver),
) {
- val textOuter = label(anchor)
- val textColor = if (selectedAnchor.value == anchor) {
- selectorTextColor
- } else {
- inactiveTextColor
- }
-
- val contentAlpha = if (isAnchorEnabled(anchor)) enabledAlpha else disabledAlpha
-
- drawText(
- textSize,
- textOuter,
- center,
- angle.toFloat(),
- canvas,
- radius,
- alpha = (255f * contentAlpha).roundToInt().coerceAtMost(alpha),
- color = textColor
- )
- }
+ drawCircle(clockBackgroundColor, radius = faceRadiusPx, center = center)
+ drawCircle(selectorColor, radius = centerCircleRadius, center = center)
+ drawLine(
+ color = selectorColor,
+ start = center,
+ end = center + anchoredOffset.value.lineOffset,
+ strokeWidth = selectedLineWidth,
+ alpha = 0.8f,
+ )
- for (x in 0 until 12) {
- val angle = (2 * PI / 12) * (x - 15)
- drawAnchorText(x * anchorPoints / 12, textSizePx, outerRadiusPx, angle)
-
- if (innerAnchorPoints > 0) {
- drawAnchorText(
- x * innerAnchorPoints / 12 + anchorPoints,
- innerTextSizePx,
- innerRadiusPx,
- angle,
- alpha = (255 * 0.8f).toInt()
+ drawCircle(
+ selectorColor,
+ center = center + anchoredOffset.value.selectedOffset,
+ radius = anchoredOffset.value.selectedRadius,
+ alpha = 0.7f,
)
- }
+
+ if (!namedAnchor.value) {
+ drawCircle(
+ Color.White,
+ center = center + anchoredOffset.value.selectedOffset,
+ radius = selectedInnerDotRadius,
+ alpha = 0.8f,
+ )
+ }
+
+ drawIntoCanvas { canvas ->
+ fun drawAnchorText(
+ anchor: Int,
+ textSize: Float,
+ radius: Float,
+ angle: Double,
+ alpha: Int = 255,
+ ) {
+ val textOuter = label(anchor)
+ val textColor = if (selectedAnchor.value == anchor) {
+ selectorTextColor
+ } else {
+ inactiveTextColor
+ }
+
+ val contentAlpha = if (isAnchorEnabled(anchor)) enabledAlpha else disabledAlpha
+
+ drawText(
+ textSize,
+ textOuter,
+ center,
+ angle.toFloat(),
+ canvas,
+ radius,
+ alpha = (255f * contentAlpha).roundToInt().coerceAtMost(alpha),
+ color = textColor,
+ )
+ }
+
+ for (x in 0 until 12) {
+ val angle = (2 * PI / 12) * (x - 15)
+ drawAnchorText(x * anchorPoints / 12, textSizePx, outerRadiusPx, angle)
+
+ if (innerAnchorPoints > 0) {
+ drawAnchorText(
+ x * innerAnchorPoints / 12 + anchorPoints,
+ innerTextSizePx,
+ innerRadiusPx,
+ angle,
+ alpha = (255 * 0.8f).toInt(),
+ )
+ }
+ }
+ }
}
- }
}
- }
}
private fun drawText(
- textSize: Float,
- text: String,
- center: Offset,
- angle: Float,
- canvas: Canvas,
- radius: Float,
- alpha: Int = 255,
- color: Int = android.graphics.Color.WHITE
+ textSize: Float,
+ text: String,
+ center: Offset,
+ angle: Float,
+ canvas: Canvas,
+ radius: Float,
+ alpha: Int = 255,
+ color: Int = android.graphics.Color.WHITE,
) {
- val outerText = Paint()
- outerText.color = color
- outerText.textSize = textSize
- outerText.textAlign = Paint.Align.CENTER
- outerText.alpha = alpha
-
- val r = Rect()
- outerText.getTextBounds(text, 0, text.length, r)
-
- canvas.nativeCanvas.drawText(
- text,
- center.x + (radius * cos(angle)),
- center.y + (radius * sin(angle)) + (abs(r.height())) / 2,
- outerText
- )
+ val outerText = Paint()
+ outerText.color = color
+ outerText.textSize = textSize
+ outerText.textAlign = Paint.Align.CENTER
+ outerText.alpha = alpha
+
+ val r = Rect()
+ outerText.getTextBounds(text, 0, text.length, r)
+
+ canvas.nativeCanvas.drawText(
+ text,
+ center.x + (radius * cos(angle)),
+ center.y + (radius * sin(angle)) + (abs(r.height())) / 2,
+ outerText,
+ )
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerColors.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerColors.kt
index dea7e31..35a9cc3 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerColors.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerColors.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.datetimedialog.time
-
import androidx.compose.foundation.BorderStroke
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -9,83 +8,83 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
/**
- * Represents the colors used by a [timepicker] and its parts in different states
+ * Represents the colors used by a [Timepicker] and its parts in different states
*
* See [TimePickerDefaults.colors] for the default implementation
*/
interface TimePickerColors {
- val border: BorderStroke
+ val border: BorderStroke
- /**
- * Gets the background color dependant on if the item is active or not
- *
- * @param active true if the component/item is selected and false otherwise
- * @return background color as a State
- */
- @Composable
- fun backgroundColor(active: Boolean): State
+ /**
+ * Gets the background color dependant on if the item is active or not
+ *
+ * @param active true if the component/item is selected and false otherwise
+ * @return background color as a State
+ */
+ @Composable
+ fun backgroundColor(active: Boolean): State
- /**
- * Gets the text color dependant on if the item is active or not
- *
- * @param active true if the component/item is selected and false otherwise
- * @return text color as a State
- */
- @Composable
- fun textColor(active: Boolean): State
+ /**
+ * Gets the text color dependant on if the item is active or not
+ *
+ * @param active true if the component/item is selected and false otherwise
+ * @return text color as a State
+ */
+ @Composable
+ fun textColor(active: Boolean): State
- /**
- * Get the color of clock hand and color of text in clock hand
- */
- fun selectorColor(): Color
- fun selectorTextColor(): Color
+ /**
+ * Get the color of clock hand and color of text in clock hand
+ */
+ fun selectorColor(): Color
+ fun selectorTextColor(): Color
- /**
- * Get color of title text
- */
- fun headerTextColor(): Color
+ /**
+ * Get color of title text
+ */
+ fun headerTextColor(): Color
- @Composable
- fun periodBackgroundColor(active: Boolean): State
+ @Composable
+ fun periodBackgroundColor(active: Boolean): State
}
internal class DefaultTimePickerColors(
- private val activeBackgroundColor: Color,
- private val inactiveBackgroundColor: Color,
- private val activeTextColor: Color,
- private val inactiveTextColor: Color,
- private val inactivePeriodBackground: Color,
- private val selectorColor: Color,
- private val selectorTextColor: Color,
- private val headerTextColor: Color,
- borderColor: Color
+ private val activeBackgroundColor: Color,
+ private val inactiveBackgroundColor: Color,
+ private val activeTextColor: Color,
+ private val inactiveTextColor: Color,
+ private val inactivePeriodBackground: Color,
+ private val selectorColor: Color,
+ private val selectorTextColor: Color,
+ private val headerTextColor: Color,
+ borderColor: Color,
) : TimePickerColors {
- override val border = BorderStroke(1.dp, borderColor)
+ override val border = BorderStroke(1.dp, borderColor)
- @Composable
- override fun backgroundColor(active: Boolean): State {
- return rememberUpdatedState(if (active) activeBackgroundColor else inactiveBackgroundColor)
- }
+ @Composable
+ override fun backgroundColor(active: Boolean): State {
+ return rememberUpdatedState(if (active) activeBackgroundColor else inactiveBackgroundColor)
+ }
- @Composable
- override fun textColor(active: Boolean): State {
- return rememberUpdatedState(if (active) activeTextColor else inactiveTextColor)
- }
+ @Composable
+ override fun textColor(active: Boolean): State {
+ return rememberUpdatedState(if (active) activeTextColor else inactiveTextColor)
+ }
- override fun selectorColor(): Color {
- return selectorColor
- }
+ override fun selectorColor(): Color {
+ return selectorColor
+ }
- override fun selectorTextColor(): Color {
- return selectorTextColor
- }
+ override fun selectorTextColor(): Color {
+ return selectorTextColor
+ }
- override fun headerTextColor(): Color {
- return headerTextColor
- }
+ override fun headerTextColor(): Color {
+ return headerTextColor
+ }
- @Composable
- override fun periodBackgroundColor(active: Boolean): State {
- return rememberUpdatedState(if (active) activeBackgroundColor else inactivePeriodBackground)
- }
+ @Composable
+ override fun periodBackgroundColor(active: Boolean): State {
+ return rememberUpdatedState(if (active) activeBackgroundColor else inactivePeriodBackground)
+ }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerDefaults.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerDefaults.kt
index 8bcca60..a1109b9 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerDefaults.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerDefaults.kt
@@ -1,51 +1,50 @@
package com.example.android.january2022.ui.datetimedialog.time
-
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
- * Object to hold default values used by [timepicker]
+ * Object to hold default values used by [Timepicker]
*/
object TimePickerDefaults {
- /**
- * Initialises a [TimePickerColors] object which represents the different colors used by
- * the [timepicker] composable
- *
- * @param activeBackgroundColor background color of selected time unit or period (AM/PM)
- * @param inactiveBackgroundColor background color of inactive items in the dialog including
- * the clock face
- * @param activeTextColor color of text on the activeBackgroundColor
- * @param inactiveTextColor color of text on the inactiveBackgroundColor
- * @param inactivePeriodBackground background color of the inactive period (AM/PM) selector
- * @param selectorColor color of clock hand/selector
- * @param selectorTextColor color of text on selectedColor
- * @param headerTextColor Get color of title text
- * @param borderColor border color of the period (AM/PM) selector
- */
- @Composable
- fun colors(
- activeBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(0.6f),
- inactiveBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(0.3f),
- activeTextColor: Color = MaterialTheme.colorScheme.primary,
- inactiveTextColor: Color = MaterialTheme.colorScheme.onBackground,
- inactivePeriodBackground: Color = Color.Transparent,
- selectorColor: Color = MaterialTheme.colorScheme.primary,
- selectorTextColor: Color = MaterialTheme.colorScheme.onPrimary,
- headerTextColor: Color = MaterialTheme.colorScheme.onBackground,
- borderColor: Color = MaterialTheme.colorScheme.onBackground
- ): TimePickerColors {
- return DefaultTimePickerColors(
- activeBackgroundColor = activeBackgroundColor,
- inactiveBackgroundColor = inactiveBackgroundColor,
- activeTextColor = activeTextColor,
- inactiveTextColor = inactiveTextColor,
- inactivePeriodBackground = inactivePeriodBackground,
- selectorColor = selectorColor,
- selectorTextColor = selectorTextColor,
- headerTextColor = headerTextColor,
- borderColor = borderColor
- )
- }
+ /**
+ * Initialises a [TimePickerColors] object which represents the different colors used by
+ * the [Timepicker] composable
+ *
+ * @param activeBackgroundColor background color of selected time unit or period (AM/PM)
+ * @param inactiveBackgroundColor background color of inactive items in the dialog including
+ * the clock face
+ * @param activeTextColor color of text on the activeBackgroundColor
+ * @param inactiveTextColor color of text on the inactiveBackgroundColor
+ * @param inactivePeriodBackground background color of the inactive period (AM/PM) selector
+ * @param selectorColor color of clock hand/selector
+ * @param selectorTextColor color of text on selectedColor
+ * @param headerTextColor Get color of title text
+ * @param borderColor border color of the period (AM/PM) selector
+ */
+ @Composable
+ fun colors(
+ activeBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(0.6f),
+ inactiveBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(0.3f),
+ activeTextColor: Color = MaterialTheme.colorScheme.primary,
+ inactiveTextColor: Color = MaterialTheme.colorScheme.onBackground,
+ inactivePeriodBackground: Color = Color.Transparent,
+ selectorColor: Color = MaterialTheme.colorScheme.primary,
+ selectorTextColor: Color = MaterialTheme.colorScheme.onPrimary,
+ headerTextColor: Color = MaterialTheme.colorScheme.onBackground,
+ borderColor: Color = MaterialTheme.colorScheme.onBackground,
+ ): TimePickerColors {
+ return DefaultTimePickerColors(
+ activeBackgroundColor = activeBackgroundColor,
+ inactiveBackgroundColor = inactiveBackgroundColor,
+ activeTextColor = activeTextColor,
+ inactiveTextColor = inactiveTextColor,
+ inactivePeriodBackground = inactivePeriodBackground,
+ selectorColor = selectorColor,
+ selectorTextColor = selectorTextColor,
+ headerTextColor = headerTextColor,
+ borderColor = borderColor,
+ )
+ }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerState.kt b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerState.kt
index 3b957a7..a142fde 100644
--- a/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerState.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/datetimedialog/time/TimePickerState.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.datetimedialog.time
-
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -8,54 +7,55 @@ import com.example.android.january2022.ui.datetimedialog.isAM
import java.time.LocalTime
internal enum class ClockScreen {
- Hour,
- Minute;
+ Hour,
+ Minute,
+ ;
- fun isHour() = this == Hour
- fun isMinute() = this == Minute
+ fun isHour() = this == Hour
+ fun isMinute() = this == Minute
}
internal class TimePickerState(
- val colors: TimePickerColors,
- selectedTime: LocalTime,
- currentScreen: ClockScreen = ClockScreen.Hour,
- clockInput: Boolean = true,
- timeRange: ClosedRange,
- is24Hour: Boolean
+ val colors: TimePickerColors,
+ selectedTime: LocalTime,
+ currentScreen: ClockScreen = ClockScreen.Hour,
+ clockInput: Boolean = true,
+ timeRange: ClosedRange,
+ is24Hour: Boolean,
) {
- var selectedTime by mutableStateOf(selectedTime)
- var timeRange by mutableStateOf(timeRange)
- var is24Hour by mutableStateOf(is24Hour)
- var currentScreen by mutableStateOf(currentScreen)
- var clockInput by mutableStateOf(clockInput)
-
- private fun minimumMinute(isAM: Boolean, hour: Int): Int {
- return when {
- isAM == timeRange.start.isAM ->
- if (timeRange.start.hour == hour) {
- timeRange.start.minute
- } else {
- 0
+ var selectedTime by mutableStateOf(selectedTime)
+ var timeRange by mutableStateOf(timeRange)
+ var is24Hour by mutableStateOf(is24Hour)
+ var currentScreen by mutableStateOf(currentScreen)
+ var clockInput by mutableStateOf(clockInput)
+
+ private fun minimumMinute(isAM: Boolean, hour: Int): Int {
+ return when {
+ isAM == timeRange.start.isAM ->
+ if (timeRange.start.hour == hour) {
+ timeRange.start.minute
+ } else {
+ 0
+ }
+ isAM -> 61
+ else -> 0
}
- isAM -> 61
- else -> 0
}
- }
-
- private fun maximumMinute(isAM: Boolean, hour: Int): Int {
- return when {
- isAM == timeRange.endInclusive.isAM ->
- if (timeRange.endInclusive.hour == hour) {
- timeRange.endInclusive.minute
- } else {
- 60
+
+ private fun maximumMinute(isAM: Boolean, hour: Int): Int {
+ return when {
+ isAM == timeRange.endInclusive.isAM ->
+ if (timeRange.endInclusive.hour == hour) {
+ timeRange.endInclusive.minute
+ } else {
+ 60
+ }
+ isAM -> 60
+ else -> 0
}
- isAM -> 60
- else -> 0
}
- }
- fun hourRange() = timeRange.start.hour..timeRange.endInclusive.hour
+ fun hourRange() = timeRange.start.hour..timeRange.endInclusive.hour
- fun minuteRange(isAM: Boolean, hour: Int) = minimumMinute(isAM, hour)..maximumMinute(isAM, hour)
+ fun minuteRange(isAM: Boolean, hour: Int) = minimumMinute(isAM, hour)..maximumMinute(isAM, hour)
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/ExercisePickerScreen.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/ExercisePickerScreen.kt
index 84c8495..5868a4f 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/ExercisePickerScreen.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/ExercisePickerScreen.kt
@@ -1,7 +1,21 @@
package com.example.android.january2022.ui.exercisepicker
-import androidx.compose.animation.*
-import androidx.compose.foundation.layout.*
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CutCornerShape
@@ -10,8 +24,25 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccessibilityNew
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.FitnessCenter
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -36,216 +67,220 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun ExercisePickerScreen(
- navController: NavController,
- viewModel: PickerViewModel = hiltViewModel()
+ navController: NavController,
+ viewModel: PickerViewModel = hiltViewModel(),
) {
- val exercises by viewModel.filteredExercises.collectAsState(initial = emptyList())
- val selectedExercises by viewModel.selectedExercises.collectAsState()
- val muscleFilter by viewModel.muscleFilter.collectAsState()
- val equipmentFilter by viewModel.equipmentFilter.collectAsState()
- val filterSelected by viewModel.filterSelected.collectAsState()
- val filterUsed by viewModel.filterUsed.collectAsState()
- val searchText by viewModel.searchText.collectAsState()
+ val exercises by viewModel.filteredExercises.collectAsState(initial = emptyList())
+ val selectedExercises by viewModel.selectedExercises.collectAsState()
+ val muscleFilter by viewModel.muscleFilter.collectAsState()
+ val equipmentFilter by viewModel.equipmentFilter.collectAsState()
+ val filterSelected by viewModel.filterSelected.collectAsState()
+ val filterUsed by viewModel.filterUsed.collectAsState()
+ val searchText by viewModel.searchText.collectAsState()
- val controller = LocalSoftwareKeyboardController.current
- val uriHandler = LocalUriHandler.current
+ val controller = LocalSoftwareKeyboardController.current
+ val uriHandler = LocalUriHandler.current
- LaunchedEffect(true) {
- viewModel.uiEvent.collect { event ->
- when (event) {
- is UiEvent.OpenWebsite -> {
- uriHandler.openUri(event.url)
+ LaunchedEffect(true) {
+ viewModel.uiEvent.collect { event ->
+ when (event) {
+ is UiEvent.OpenWebsite -> {
+ uriHandler.openUri(event.url)
+ }
+ else -> Unit
+ }
}
- else -> Unit
- }
}
- }
- val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
- val coroutineScope = rememberCoroutineScope()
- val equipmentBottomsheet = remember { mutableStateOf(false) }
- val filterColors = FilterChipDefaults.filterChipColors(
- selectedContainerColor = MaterialTheme.colorScheme.primary,
- selectedLabelColor = MaterialTheme.colorScheme.onPrimary,
- selectedTrailingIconColor = MaterialTheme.colorScheme.onPrimary,
- labelColor = MaterialTheme.colorScheme.onSurfaceVariant,
- iconColor = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
+ val coroutineScope = rememberCoroutineScope()
+ val equipmentBottomsheet = remember { mutableStateOf(false) }
+ val filterColors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = MaterialTheme.colorScheme.primary,
+ selectedLabelColor = MaterialTheme.colorScheme.onPrimary,
+ selectedTrailingIconColor = MaterialTheme.colorScheme.onPrimary,
+ labelColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
- ModalBottomSheetLayout(
- sheetContent = {
- if (equipmentBottomsheet.value) {
- EquipmentSheet(equipmentFilter, viewModel::onEvent)
- } else {
- MuscleSheet(muscleFilter, viewModel::onEvent)
- }
- },
- sheetState = sheetState,
- sheetShape = MaterialTheme.shapes.large.onlyTop()
- ) {
- Scaffold(
- floatingActionButton = {
- Box(
- modifier = Modifier
- .height(64.dp)
- .width(80.dp)
- ) {
- AnimatedVisibility(
- visible = selectedExercises.isNotEmpty(),
- enter = scaleIn() + fadeIn(),
- exit = scaleOut() + fadeOut()
- ) {
- FloatingActionButton(
- onClick = {
- viewModel.onEvent(PickerEvent.AddExercises)
- navController.popBackStack()
- },
- containerColor = MaterialTheme.colorScheme.primary,
- modifier = Modifier.align(Alignment.BottomEnd)
- ) {
- Text(
- text = "ADD ${selectedExercises.size}",
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 10.dp)
- .fillMaxWidth(),
- style = MaterialTheme.typography.labelLarge,
- textAlign = TextAlign.Center
- )
+ ModalBottomSheetLayout(
+ sheetContent = {
+ if (equipmentBottomsheet.value) {
+ EquipmentSheet(equipmentFilter, viewModel::onEvent)
+ } else {
+ MuscleSheet(muscleFilter, viewModel::onEvent)
}
- }
- }
- },
- topBar = {
- Surface(
- shape = CutCornerShape(0.dp),
- tonalElevation = 2.dp
- ) {
- Column {
- Spacer(Modifier.height(40.dp))
- TextField(
- value = searchText,
- label = {
- Text(
- text = "search for exercise",
- textAlign = TextAlign.Center,
- modifier = Modifier.fillMaxWidth()
- )
- },
- onValueChange = { viewModel.onEvent(PickerEvent.SearchChanged(it)) },
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 4.dp, start = 8.dp, end = 8.dp)
- .clearFocusOnKeyboardDismiss()
- .align(Alignment.CenterHorizontally),
- colors = TextFieldDefaults.textFieldColors(
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent
- ),
- shape = RoundedCornerShape(8.dp),
- textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)
- )
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(end = 8.dp, bottom = 0.dp),
- horizontalArrangement = Arrangement.End
- ) {
- FilterChip(
- selected = filterSelected,
- onClick = { viewModel.onEvent(PickerEvent.FilterSelected) },
- label = {
- Text(text = "Selected")
- },
- colors = filterColors
- )
- Spacer(Modifier.width(8.dp))
- FilterChip(
- selected = filterUsed,
- onClick = { viewModel.onEvent(PickerEvent.FilterUsed) },
- label = {
- Text(text = "Used")
- },
- colors = filterColors
- )
- Spacer(Modifier.width(8.dp))
- FilterChip(
- selected = muscleFilter.isNotEmpty(),
- onClick = {
- equipmentBottomsheet.value = false
- coroutineScope.launch {
- if (sheetState.isVisible) sheetState.hide() else {
- controller?.hide()
- sheetState.expand()
+ },
+ sheetState = sheetState,
+ sheetShape = MaterialTheme.shapes.large.onlyTop(),
+ ) {
+ Scaffold(
+ floatingActionButton = {
+ Box(
+ modifier = Modifier
+ .height(64.dp)
+ .width(80.dp),
+ ) {
+ AnimatedVisibility(
+ visible = selectedExercises.isNotEmpty(),
+ enter = scaleIn() + fadeIn(),
+ exit = scaleOut() + fadeOut(),
+ ) {
+ FloatingActionButton(
+ onClick = {
+ viewModel.onEvent(PickerEvent.AddExercises)
+ navController.popBackStack()
+ },
+ containerColor = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.align(Alignment.BottomEnd),
+ ) {
+ Text(
+ text = "ADD ${selectedExercises.size}",
+ modifier = Modifier
+ .padding(vertical = 4.dp, horizontal = 10.dp)
+ .fillMaxWidth(),
+ style = MaterialTheme.typography.labelLarge,
+ textAlign = TextAlign.Center,
+ )
+ }
}
- }
- },
- label = {
- Icon(
- imageVector = Icons.Default.AccessibilityNew,
- contentDescription = "Equipment",
- modifier = Modifier.size(18.dp)
- )
- },
- trailingIcon = {
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = "Dropdown",
- modifier = Modifier.size(22.dp)
- )
- },
- colors = filterColors
- )
- Spacer(Modifier.width(8.dp))
- FilterChip(
- selected = equipmentFilter.isNotEmpty(),
- onClick = {
- equipmentBottomsheet.value = true
- coroutineScope.launch {
- if (sheetState.isVisible) sheetState.hide() else {
- controller?.hide()
- sheetState.show()
+ }
+ },
+ topBar = {
+ Surface(
+ shape = CutCornerShape(0.dp),
+ tonalElevation = 2.dp,
+ ) {
+ Column {
+ Spacer(Modifier.height(40.dp))
+ TextField(
+ value = searchText,
+ label = {
+ Text(
+ text = "search for exercise",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ },
+ onValueChange = { viewModel.onEvent(PickerEvent.SearchChanged(it)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp, start = 8.dp, end = 8.dp)
+ .clearFocusOnKeyboardDismiss()
+ .align(Alignment.CenterHorizontally),
+ colors = TextFieldDefaults.colors(
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ ),
+ shape = RoundedCornerShape(8.dp),
+ textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp, bottom = 0.dp),
+ horizontalArrangement = Arrangement.End,
+ ) {
+ FilterChip(
+ selected = filterSelected,
+ onClick = { viewModel.onEvent(PickerEvent.FilterSelected) },
+ label = {
+ Text(text = "Selected")
+ },
+ colors = filterColors,
+ )
+ Spacer(Modifier.width(8.dp))
+ FilterChip(
+ selected = filterUsed,
+ onClick = { viewModel.onEvent(PickerEvent.FilterUsed) },
+ label = {
+ Text(text = "Used")
+ },
+ colors = filterColors,
+ )
+ Spacer(Modifier.width(8.dp))
+ FilterChip(
+ selected = muscleFilter.isNotEmpty(),
+ onClick = {
+ equipmentBottomsheet.value = false
+ coroutineScope.launch {
+ if (sheetState.isVisible) {
+ sheetState.hide()
+ } else {
+ controller?.hide()
+ sheetState.expand()
+ }
+ }
+ },
+ label = {
+ Icon(
+ imageVector = Icons.Default.AccessibilityNew,
+ contentDescription = "Equipment",
+ modifier = Modifier.size(18.dp),
+ )
+ },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = "Dropdown",
+ modifier = Modifier.size(22.dp),
+ )
+ },
+ colors = filterColors,
+ )
+ Spacer(Modifier.width(8.dp))
+ FilterChip(
+ selected = equipmentFilter.isNotEmpty(),
+ onClick = {
+ equipmentBottomsheet.value = true
+ coroutineScope.launch {
+ if (sheetState.isVisible) {
+ sheetState.hide()
+ } else {
+ controller?.hide()
+ sheetState.show()
+ }
+ }
+ },
+ label = {
+ Icon(
+ imageVector = Icons.Default.FitnessCenter,
+ contentDescription = "Equipment",
+ modifier = Modifier.size(18.dp),
+ )
+ },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = "Dropdown",
+ modifier = Modifier.size(22.dp),
+ )
+ },
+ colors = filterColors,
+ )
+ }
}
- }
- },
- label = {
- Icon(
- imageVector = Icons.Default.FitnessCenter,
- contentDescription = "Equipment",
- modifier = Modifier.size(18.dp)
- )
- },
- trailingIcon = {
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = "Dropdown",
- modifier = Modifier.size(22.dp)
- )
- },
- colors = filterColors
- )
+ }
+ },
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp),
+ ) {
+ item {
+ Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 8.dp))
+ }
+ items(exercises) { exercise ->
+ ExerciseCard(
+ exercise = exercise,
+ selected = selectedExercises.contains(exercise),
+ onEvent = viewModel::onEvent,
+ ) {
+ viewModel.onEvent(PickerEvent.ExerciseSelected(exercise))
+ }
+ }
}
- }
- }
- },
- ) { paddingValues ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 8.dp)
- ) {
- item {
- Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 8.dp))
- }
- items(exercises) { exercise ->
- ExerciseCard(
- exercise = exercise,
- selected = selectedExercises.contains(exercise),
- onEvent = viewModel::onEvent
- ) {
- viewModel.onEvent(PickerEvent.ExerciseSelected(exercise))
- }
}
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerEvent.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerEvent.kt
index e9471e5..104efd9 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerEvent.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerEvent.kt
@@ -4,14 +4,14 @@ import com.example.android.january2022.db.entities.Exercise
import com.example.android.january2022.utils.Event
sealed class PickerEvent : Event {
- data class ExerciseSelected(val exercise: Exercise) : PickerEvent()
- data class OpenGuide(val exercise: Exercise) : PickerEvent()
- object FilterSelected : PickerEvent()
- object FilterUsed : PickerEvent()
- data class SelectMuscle(val muscle: String) : PickerEvent()
- object DeselectMuscles : PickerEvent()
- data class SelectEquipment(val equipment: String) : PickerEvent()
- object DeselectEquipment : PickerEvent()
- object AddExercises : PickerEvent()
- data class SearchChanged(val text: String) : PickerEvent()
+ data class ExerciseSelected(val exercise: Exercise) : PickerEvent()
+ data class OpenGuide(val exercise: Exercise) : PickerEvent()
+ object FilterSelected : PickerEvent()
+ object FilterUsed : PickerEvent()
+ data class SelectMuscle(val muscle: String) : PickerEvent()
+ object DeselectMuscles : PickerEvent()
+ data class SelectEquipment(val equipment: String) : PickerEvent()
+ object DeselectEquipment : PickerEvent()
+ object AddExercises : PickerEvent()
+ data class SearchChanged(val text: String) : PickerEvent()
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerViewModel.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerViewModel.kt
index 652fcfd..4e0e2b3 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerViewModel.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/PickerViewModel.kt
@@ -10,161 +10,165 @@ import com.example.android.january2022.utils.Event
import com.example.android.january2022.utils.UiEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PickerViewModel @Inject constructor(
- private val repo: GymRepository,
- private val savedStateHandle: SavedStateHandle
+ private val repo: GymRepository,
+ private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
- private val _selectedExercises = MutableStateFlow>(emptyList())
- val selectedExercises = _selectedExercises.asStateFlow()
+ private val _selectedExercises = MutableStateFlow>(emptyList())
+ val selectedExercises = _selectedExercises.asStateFlow()
- private val _equipmentFilter = MutableStateFlow>(emptyList())
- val equipmentFilter = _equipmentFilter.asStateFlow()
+ private val _equipmentFilter = MutableStateFlow>(emptyList())
+ val equipmentFilter = _equipmentFilter.asStateFlow()
- private val _muscleFilter = MutableStateFlow>(emptyList())
- val muscleFilter = _muscleFilter.asStateFlow()
+ private val _muscleFilter = MutableStateFlow>(emptyList())
+ val muscleFilter = _muscleFilter.asStateFlow()
- private val _filterSelected = MutableStateFlow(false)
- val filterSelected = _filterSelected.asStateFlow()
+ private val _filterSelected = MutableStateFlow(false)
+ val filterSelected = _filterSelected.asStateFlow()
- private val _filterUsed = MutableStateFlow(false)
- val filterUsed = _filterUsed.asStateFlow()
+ private val _filterUsed = MutableStateFlow(false)
+ val filterUsed = _filterUsed.asStateFlow()
- private val _searchText = MutableStateFlow("")
- val searchText = _searchText.asStateFlow()
+ private val _searchText = MutableStateFlow("")
+ val searchText = _searchText.asStateFlow()
- val filteredExercises: Flow> = combine(
- repo.getAllExercises(),
- selectedExercises,
- equipmentFilter,
- muscleFilter,
- filterSelected,
- filterUsed,
- searchText
- ) { exercises, selectedExercises, equipmentFilter, muscleFilter, selected, used, text ->
- exercises.filter { exercise ->
- val muscleCondition =
- (muscleFilter.isEmpty() || exercise.getMuscleGroups().any { muscleFilter.contains(it) })
- val equipmentCondition =
- (equipmentFilter.isEmpty() || exercise.equipment.any { equipmentFilter.contains(it) })
- val selectedCondition = (!selected || selectedExercises.contains(exercise))
+ val filteredExercises: Flow> = combine(
+ repo.getAllExercises(),
+ selectedExercises,
+ equipmentFilter,
+ muscleFilter,
+ filterSelected,
+ filterUsed,
+ searchText,
+ ) { exercises, selectedExercises, equipmentFilter, muscleFilter, selected, _, text ->
+ exercises.filter { exercise ->
+ val muscleCondition =
+ (muscleFilter.isEmpty() || exercise.getMuscleGroups().any { muscleFilter.contains(it) })
+ val equipmentCondition =
+ (equipmentFilter.isEmpty() || exercise.equipment.any { equipmentFilter.contains(it) })
+ val selectedCondition = (!selected || selectedExercises.contains(exercise))
- muscleCondition && equipmentCondition && selectedCondition && exercise.getStringMatch(text)
- }.sortedBy { exercise ->
- if (text.isNotBlank()) {
- exercise.title.length
- } else {
- exercise.title.first().code
- }
+ muscleCondition && equipmentCondition && selectedCondition && exercise.getStringMatch(text)
+ }.sortedBy { exercise ->
+ if (text.isNotBlank()) {
+ exercise.title.length
+ } else {
+ exercise.title.first().code
+ }
+ }
}
- }
- fun onEvent(event: Event) {
- when (event) {
- is PickerEvent.OpenGuide -> openGuide(event.exercise)
- is PickerEvent.ExerciseSelected -> {
- _selectedExercises.value = buildList {
- if (_selectedExercises.value.contains(event.exercise)) {
- addAll(_selectedExercises.value.minusElement(event.exercise))
- } else {
- addAll(_selectedExercises.value)
- add(event.exercise)
- }
- }
- }
- is PickerEvent.FilterSelected -> {
- _filterSelected.value = !_filterSelected.value
- }
- is PickerEvent.FilterUsed -> {
- _filterUsed.value = !_filterUsed.value
- }
- is PickerEvent.SelectMuscle -> {
- _muscleFilter.value = if (_muscleFilter.value.contains(event.muscle)) {
- _muscleFilter.value.minus(event.muscle)
- } else {
- _muscleFilter.value.plus(event.muscle)
- }
- }
- is PickerEvent.DeselectMuscles -> {
- _muscleFilter.value = emptyList()
- }
- is PickerEvent.SelectEquipment -> {
- _equipmentFilter.value = if (_equipmentFilter.value.contains(event.equipment)) {
- _equipmentFilter.value.minus(event.equipment)
- } else {
- _equipmentFilter.value.plus(event.equipment)
- }
- }
- is PickerEvent.DeselectEquipment -> {
- _equipmentFilter.value = emptyList()
- }
- is PickerEvent.AddExercises -> {
- viewModelScope.launch {
- _selectedExercises.value.forEach { exercise ->
- savedStateHandle.get("session_id")?.let { sessionId ->
- repo.insertSessionExercise(
- SessionExercise(
- parentSessionId = sessionId,
- parentExerciseId = exercise.id
- )
- )
+ fun onEvent(event: Event) {
+ when (event) {
+ is PickerEvent.OpenGuide -> openGuide(event.exercise)
+ is PickerEvent.ExerciseSelected -> {
+ _selectedExercises.value = buildList {
+ if (_selectedExercises.value.contains(event.exercise)) {
+ addAll(_selectedExercises.value.minusElement(event.exercise))
+ } else {
+ addAll(_selectedExercises.value)
+ add(event.exercise)
+ }
+ }
+ }
+ is PickerEvent.FilterSelected -> {
+ _filterSelected.value = !_filterSelected.value
+ }
+ is PickerEvent.FilterUsed -> {
+ _filterUsed.value = !_filterUsed.value
+ }
+ is PickerEvent.SelectMuscle -> {
+ _muscleFilter.value = if (_muscleFilter.value.contains(event.muscle)) {
+ _muscleFilter.value.minus(event.muscle)
+ } else {
+ _muscleFilter.value.plus(event.muscle)
+ }
+ }
+ is PickerEvent.DeselectMuscles -> {
+ _muscleFilter.value = emptyList()
+ }
+ is PickerEvent.SelectEquipment -> {
+ _equipmentFilter.value = if (_equipmentFilter.value.contains(event.equipment)) {
+ _equipmentFilter.value.minus(event.equipment)
+ } else {
+ _equipmentFilter.value.plus(event.equipment)
+ }
+ }
+ is PickerEvent.DeselectEquipment -> {
+ _equipmentFilter.value = emptyList()
+ }
+ is PickerEvent.AddExercises -> {
+ viewModelScope.launch {
+ _selectedExercises.value.forEach { exercise ->
+ savedStateHandle.get("session_id")?.let { sessionId ->
+ repo.insertSessionExercise(
+ SessionExercise(
+ parentSessionId = sessionId,
+ parentExerciseId = exercise.id,
+ ),
+ )
+ }
+ }
+ }
+ }
+ is PickerEvent.SearchChanged -> {
+ _searchText.value = event.text
}
- }
}
- }
- is PickerEvent.SearchChanged -> {
- _searchText.value = event.text
- }
}
- }
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
+ private val _uiEvent = Channel()
+ val uiEvent = _uiEvent.receiveAsFlow()
- private fun openGuide(exercise: Exercise) {
- sendUiEvent(UiEvent.OpenWebsite(url = "https://duckduckgo.com/?q=! exrx ${exercise.title}"))
- }
+ private fun openGuide(exercise: Exercise) {
+ sendUiEvent(UiEvent.OpenWebsite(url = "https://duckduckgo.com/?q=! exrx ${exercise.title}"))
+ }
- private fun sendUiEvent(event: UiEvent) {
- viewModelScope.launch {
- _uiEvent.send(event)
+ private fun sendUiEvent(event: UiEvent) {
+ viewModelScope.launch {
+ _uiEvent.send(event)
+ }
}
- }
}
-inline fun combine(
- flow: Flow,
- flow2: Flow,
- flow3: Flow,
- flow4: Flow,
- flow5: Flow,
- flow6: Flow,
- flow7: Flow,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+inline fun combine(
+ flow: Flow,
+ flow2: Flow,
+ flow3: Flow,
+ flow4: Flow,
+ flow5: Flow,
+ flow6: Flow,
+ flow7: Flow,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R,
): Flow {
- return combine(
- flow,
- flow2,
- flow3,
- flow4,
- flow5,
- flow6,
- flow7
- ) { args: Array<*> ->
- @Suppress("UNCHECKED_CAST")
- transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6,
- args[6] as T7,
- )
- }
-}
\ No newline at end of file
+ return combine(
+ flow,
+ flow2,
+ flow3,
+ flow4,
+ flow5,
+ flow6,
+ flow7,
+ ) { args: Array<*> ->
+ @Suppress("UNCHECKED_CAST")
+ transform(
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ args[6] as T7,
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/EquipmentSheet.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/EquipmentSheet.kt
index 7dd7b20..c2ee420 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/EquipmentSheet.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/EquipmentSheet.kt
@@ -7,15 +7,15 @@ import com.example.android.january2022.utils.Event
@Composable
fun EquipmentSheet(
- selectedEquipment: List,
- onEvent: (Event) -> Unit
+ selectedEquipment: List,
+ onEvent: (Event) -> Unit,
) {
- Sheet(
- items = Equipment.getAllEquipment().sorted(),
- selectedItems = selectedEquipment,
- title = "Filter by Equipment",
- onSelect = { onEvent(PickerEvent.SelectEquipment(it)) }
- ) {
- onEvent(PickerEvent.DeselectEquipment)
- }
-}
\ No newline at end of file
+ Sheet(
+ items = Equipment.getAllEquipment().sorted(),
+ selectedItems = selectedEquipment,
+ title = "Filter by Equipment",
+ onSelect = { onEvent(PickerEvent.SelectEquipment(it)) },
+ ) {
+ onEvent(PickerEvent.DeselectEquipment)
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/ExerciseCard.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/ExerciseCard.kt
index 17538d6..b8029d2 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/ExerciseCard.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/ExerciseCard.kt
@@ -2,12 +2,25 @@ package com.example.android.january2022.ui.exercisepicker.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -24,91 +37,90 @@ import com.example.android.january2022.utils.Event
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExerciseCard(
- exercise: Exercise,
- selected: Boolean,
- onEvent: (Event) -> Unit,
- onClick: () -> Unit
+ exercise: Exercise,
+ selected: Boolean,
+ onEvent: (Event) -> Unit,
+ onClick: () -> Unit,
) {
+ val targets = exercise.getMuscleGroups()
+ val equipment = exercise.equipment
+ val tonalElevation by animateDpAsState(targetValue = if (selected) 2.dp else 0.dp)
+ val indicatorColor by
+ animateColorAsState(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent)
- val targets = exercise.getMuscleGroups()
- val equipment = exercise.equipment
- val tonalElevation by animateDpAsState(targetValue = if (selected) 2.dp else 0.dp)
- val indicatorColor by
- animateColorAsState(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent)
+ val localDensity = LocalDensity.current
+ var rowHeightDp by remember { mutableStateOf(0.dp) }
- val localDensity = LocalDensity.current
- var rowHeightDp by remember { mutableStateOf(0.dp) }
+ val indicatorHeight by
+ animateDpAsState(targetValue = if (selected) rowHeightDp else 0.dp)
- val indicatorHeight by
- animateDpAsState(targetValue = if (selected) rowHeightDp else 0.dp)
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 8.dp)
- .onGloballyPositioned { coordinates ->
- // Set column height using the LayoutCoordinates
- rowHeightDp = with(localDensity) {
- coordinates.size.height
- .minus(95)
- .toDp()
- }
- },
- verticalAlignment = Alignment.CenterVertically
- ) {
- Surface(
- color = indicatorColor,
- shape = MaterialTheme.shapes.small,
- modifier = Modifier
- .width(3.dp)
- .height(indicatorHeight)
- ) {}
- Spacer(modifier = Modifier.width(4.dp))
- Surface(
- onClick = onClick,
- modifier = Modifier
- .fillMaxWidth()
- .defaultMinSize(minHeight = 80.dp),
- tonalElevation = tonalElevation,
- shape = MaterialTheme.shapes.medium
- ) {
- Row(
+ Row(
modifier = Modifier
- .padding(start = 14.dp, top = 4.dp, bottom = 4.dp, end = 4.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(
- modifier = Modifier
- .padding(top = 8.dp),
- verticalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- text = exercise.title,
+ .fillMaxWidth()
+ .padding(bottom = 8.dp)
+ .onGloballyPositioned { coordinates ->
+ // Set column height using the LayoutCoordinates
+ rowHeightDp = with(localDensity) {
+ coordinates.size.height
+ .minus(95)
+ .toDp()
+ }
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Surface(
+ color = indicatorColor,
+ shape = MaterialTheme.shapes.small,
modifier = Modifier
- .padding(bottom = 8.dp)
- .fillMaxWidth(0.65f),
- style = MaterialTheme.typography.titleMedium
- )
- Row(
- modifier = Modifier.padding(bottom = 4.dp)
- ) {
- targets.forEach { target ->
- SmallPill(text = target, modifier = Modifier.padding(end = 4.dp))
- }
- equipment.forEach { eq ->
- SmallPill(text = eq)
- }
- }
- }
- Row(
- modifier = Modifier.fillMaxHeight(),
- verticalAlignment = Alignment.CenterVertically
+ .width(3.dp)
+ .height(indicatorHeight),
+ ) {}
+ Spacer(modifier = Modifier.width(4.dp))
+ Surface(
+ onClick = onClick,
+ modifier = Modifier
+ .fillMaxWidth()
+ .defaultMinSize(minHeight = 80.dp),
+ tonalElevation = tonalElevation,
+ shape = MaterialTheme.shapes.medium,
) {
- OpenStatsAction {}
- OpenInNewAction { onEvent(PickerEvent.OpenGuide(exercise)) }
+ Row(
+ modifier = Modifier
+ .padding(start = 14.dp, top = 4.dp, bottom = 4.dp, end = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(top = 8.dp),
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = exercise.title,
+ modifier = Modifier
+ .padding(bottom = 8.dp)
+ .fillMaxWidth(0.65f),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ Row(
+ modifier = Modifier.padding(bottom = 4.dp),
+ ) {
+ targets.forEach { target ->
+ SmallPill(text = target, modifier = Modifier.padding(end = 4.dp))
+ }
+ equipment.forEach { eq ->
+ SmallPill(text = eq)
+ }
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxHeight(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ OpenStatsAction {}
+ OpenInNewAction { onEvent(PickerEvent.OpenGuide(exercise)) }
+ }
+ }
}
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleButton.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleButton.kt
index 8adb5e8..1c722c1 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleButton.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleButton.kt
@@ -1,8 +1,12 @@
package com.example.android.january2022.ui.exercisepicker.components
import androidx.compose.animation.animateColorAsState
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.*
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -12,24 +16,24 @@ import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MuscleButton(
- muscle: String,
- selected: Boolean,
- onClick: () -> Unit
+ muscle: String,
+ selected: Boolean,
+ onClick: () -> Unit,
) {
- val containerColor by animateColorAsState(
- targetValue = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.23f)
- )
-
- Surface(
- onClick = onClick,
- color = containerColor,
- shape = MaterialTheme.shapes.medium,
- modifier = Modifier.padding(vertical = 2.dp, horizontal = 12.dp)
- ) {
- Text(
- text = muscle.uppercase(),
- modifier = Modifier.padding(8.dp).fillMaxWidth(),
- textAlign = TextAlign.Center
+ val containerColor by animateColorAsState(
+ targetValue = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.23f),
)
- }
-}
\ No newline at end of file
+
+ Surface(
+ onClick = onClick,
+ color = containerColor,
+ shape = MaterialTheme.shapes.medium,
+ modifier = Modifier.padding(vertical = 2.dp, horizontal = 12.dp),
+ ) {
+ Text(
+ text = muscle.uppercase(),
+ modifier = Modifier.padding(8.dp).fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleSheet.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleSheet.kt
index 88d836f..68ec55d 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleSheet.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/MuscleSheet.kt
@@ -7,15 +7,15 @@ import com.example.android.january2022.utils.Event
@Composable
fun MuscleSheet(
- selectedMusclegroups: List,
- onEvent: (Event) -> Unit
+ selectedMusclegroups: List,
+ onEvent: (Event) -> Unit,
) {
- Sheet(
- items = MuscleGroup.getAllMuscleGroups().sorted(),
- selectedItems = selectedMusclegroups,
- title = "Filter by Body-part",
- onSelect = { onEvent(PickerEvent.SelectMuscle(it)) }
- ) {
- onEvent(PickerEvent.DeselectMuscles)
- }
-}
\ No newline at end of file
+ Sheet(
+ items = MuscleGroup.getAllMuscleGroups().sorted(),
+ selectedItems = selectedMusclegroups,
+ title = "Filter by Body-part",
+ onSelect = { onEvent(PickerEvent.SelectMuscle(it)) },
+ ) {
+ onEvent(PickerEvent.DeselectMuscles)
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/Sheet.kt b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/Sheet.kt
index a773c63..0c76de6 100644
--- a/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/Sheet.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/exercisepicker/components/Sheet.kt
@@ -18,43 +18,43 @@ import androidx.compose.ui.unit.dp
@Composable
fun Sheet(
- items: List,
- title: String,
- selectedItems: List,
- onSelect: (String) -> Unit,
- onDeselectAll: () -> Unit
+ items: List,
+ title: String,
+ selectedItems: List,
+ onSelect: (String) -> Unit,
+ onDeselectAll: () -> Unit,
) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp, bottom = 20.dp)
- .padding(horizontal = 16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- text = title,
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 12.dp)
- )
- LazyVerticalGrid(
- columns = GridCells.Adaptive(120.dp),
- horizontalArrangement = Arrangement.Center
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp, bottom = 20.dp)
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
- items(items) { item ->
- MuscleButton(
- muscle = item,
- selected = selectedItems.contains(item)
- ) { onSelect(item) }
- }
+ Text(
+ text = title,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ )
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(120.dp),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ items(items) { item ->
+ MuscleButton(
+ muscle = item,
+ selected = selectedItems.contains(item),
+ ) { onSelect(item) }
+ }
+ }
+ TextButton(onClick = { onDeselectAll() }) {
+ Text(
+ text = "Deselect All".uppercase(),
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
}
- TextButton(onClick = { onDeselectAll() }) {
- Text(
- text = "Deselect All".uppercase(),
- style = MaterialTheme.typography.labelLarge
- )
- }
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/home/HomeEvent.kt b/app/src/main/java/com/example/android/january2022/ui/home/HomeEvent.kt
index 96ddcaf..c0f118e 100644
--- a/app/src/main/java/com/example/android/january2022/ui/home/HomeEvent.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/home/HomeEvent.kt
@@ -4,7 +4,7 @@ import com.example.android.january2022.ui.SessionWrapper
import com.example.android.january2022.utils.Event
sealed class HomeEvent : Event {
- data class SessionClicked(val sessionWrapper: SessionWrapper) : HomeEvent()
- object NewSession : HomeEvent()
- object OpenSettings : HomeEvent()
-}
\ No newline at end of file
+ data class SessionClicked(val sessionWrapper: SessionWrapper) : HomeEvent()
+ object NewSession : HomeEvent()
+ object OpenSettings : HomeEvent()
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/home/HomeScreen.kt b/app/src/main/java/com/example/android/january2022/ui/home/HomeScreen.kt
index 622bb9e..74b1c74 100644
--- a/app/src/main/java/com/example/android/january2022/ui/home/HomeScreen.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/home/HomeScreen.kt
@@ -20,38 +20,38 @@ import com.example.android.january2022.utils.UiEvent
@Composable
fun HomeScreen(
- onNavigate: (UiEvent.Navigate) -> Unit,
- viewModel: HomeViewModel = hiltViewModel()
+ onNavigate: (UiEvent.Navigate) -> Unit,
+ viewModel: HomeViewModel = hiltViewModel(),
) {
- val sessions by viewModel.sessions.collectAsState(initial = emptyList())
+ val sessions by viewModel.sessions.collectAsState(initial = emptyList())
- LaunchedEffect(true) {
- viewModel.uiEvent.collect { event ->
- when (event) {
- is UiEvent.Navigate -> onNavigate(event)
- else -> Unit
- }
+ LaunchedEffect(true) {
+ viewModel.uiEvent.collect { event ->
+ when (event) {
+ is UiEvent.Navigate -> onNavigate(event)
+ else -> Unit
+ }
+ }
}
- }
- Scaffold(
- bottomBar = {
- HomeAppBar(onEvent = viewModel::onEvent)
- }
- ) { paddingValues ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 8.dp, vertical = 2.dp)
- ) {
- item {
- Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding()))
- }
- items(items = sessions, key = { it.session.sessionId }) { session ->
- SessionCard(sessionWrapper = session) {
- viewModel.onEvent(HomeEvent.SessionClicked(session))
+ Scaffold(
+ bottomBar = {
+ HomeAppBar(onEvent = viewModel::onEvent)
+ },
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp, vertical = 2.dp),
+ ) {
+ item {
+ Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding()))
+ }
+ items(items = sessions, key = { it.session.sessionId }) { session ->
+ SessionCard(sessionWrapper = session) {
+ viewModel.onEvent(HomeEvent.SessionClicked(session))
+ }
+ }
}
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/android/january2022/ui/home/HomeViewModel.kt
index 39d3090..b3418d7 100644
--- a/app/src/main/java/com/example/android/january2022/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/home/HomeViewModel.kt
@@ -5,7 +5,10 @@ import androidx.lifecycle.viewModelScope
import com.example.android.january2022.db.GymRepository
import com.example.android.january2022.db.entities.Session
import com.example.android.january2022.ui.SessionWrapper
-import com.example.android.january2022.utils.*
+import com.example.android.january2022.utils.Event
+import com.example.android.january2022.utils.Routes
+import com.example.android.january2022.utils.UiEvent
+import com.example.android.january2022.utils.sortedListOfMuscleGroups
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
@@ -17,44 +20,44 @@ import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
- private val repo: GymRepository
+ private val repo: GymRepository,
) : ViewModel() {
- val sessions = combine(repo.getAllSessionExercises(), repo.getAllSessions()) { sewes, sessions ->
- sessions.map { session ->
- val muscleGroups = sewes.filter { it.sessionExercise.parentSessionId == session.sessionId }
- .sortedListOfMuscleGroups()
- SessionWrapper(session, muscleGroups)
+ val sessions = combine(repo.getAllSessionExercises(), repo.getAllSessions()) { sewes, sessions ->
+ sessions.map { session ->
+ val muscleGroups = sewes.filter { it.sessionExercise.parentSessionId == session.sessionId }
+ .sortedListOfMuscleGroups()
+ SessionWrapper(session, muscleGroups)
+ }
}
- }
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
+ private val _uiEvent = Channel()
+ val uiEvent = _uiEvent.receiveAsFlow()
- fun onEvent(event: Event) {
- when (event) {
- is HomeEvent.SessionClicked -> {
- sendUiEvent(UiEvent.Navigate("${Routes.SESSION}/${event.sessionWrapper.session.sessionId}"))
- }
- is HomeEvent.OpenSettings -> {
- sendUiEvent(UiEvent.Navigate(Routes.SETTINGS))
- }
- is HomeEvent.NewSession -> {
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- repo.insertSession(Session())
- val session = repo.getLastSession()
- sendUiEvent(UiEvent.Navigate("${Routes.SESSION}/${session.sessionId}"))
- }
+ fun onEvent(event: Event) {
+ when (event) {
+ is HomeEvent.SessionClicked -> {
+ sendUiEvent(UiEvent.Navigate("${Routes.SESSION}/${event.sessionWrapper.session.sessionId}"))
+ }
+ is HomeEvent.OpenSettings -> {
+ sendUiEvent(UiEvent.Navigate(Routes.SETTINGS))
+ }
+ is HomeEvent.NewSession -> {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ repo.insertSession(Session())
+ val session = repo.getLastSession()
+ sendUiEvent(UiEvent.Navigate("${Routes.SESSION}/${session.sessionId}"))
+ }
+ }
+ }
+ else -> Unit
}
- }
- else -> Unit
}
- }
- private fun sendUiEvent(event: UiEvent) {
- viewModelScope.launch {
- _uiEvent.send(event)
+ private fun sendUiEvent(event: UiEvent) {
+ viewModelScope.launch {
+ _uiEvent.send(event)
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/home/components/HomeAppBar.kt b/app/src/main/java/com/example/android/january2022/ui/home/components/HomeAppBar.kt
index 0169c94..6ce0e89 100644
--- a/app/src/main/java/com/example/android/january2022/ui/home/components/HomeAppBar.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/home/components/HomeAppBar.kt
@@ -4,7 +4,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.*
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import com.example.android.january2022.ui.home.HomeEvent
import com.example.android.january2022.ui.session.actions.ActionSpacer
@@ -13,26 +17,26 @@ import com.example.android.january2022.utils.Event
@Composable
fun HomeAppBar(
- onEvent: (Event) -> Unit
+ onEvent: (Event) -> Unit,
) {
- BottomAppBar(
- actions = {
- ActionSpacerStart()
- IconButton(onClick = { /*TODO*/ }) {
- Icon(imageVector = Icons.Default.MoreVert, contentDescription = "Options")
- }
- ActionSpacer()
- IconButton(onClick = { onEvent(HomeEvent.OpenSettings) }) {
- Icon(imageVector = Icons.Default.Settings, contentDescription = "Settings")
- }
- },
- floatingActionButton = {
- FloatingActionButton(
- onClick = { onEvent(HomeEvent.NewSession) },
- containerColor = MaterialTheme.colorScheme.primary
- ) {
- Icon(Icons.Default.Add, "Add Session")
- }
- }
- )
-}
\ No newline at end of file
+ BottomAppBar(
+ actions = {
+ ActionSpacerStart()
+ IconButton(onClick = { /*TODO*/ }) {
+ Icon(imageVector = Icons.Default.MoreVert, contentDescription = "Options")
+ }
+ ActionSpacer()
+ IconButton(onClick = { onEvent(HomeEvent.OpenSettings) }) {
+ Icon(imageVector = Icons.Default.Settings, contentDescription = "Settings")
+ }
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = { onEvent(HomeEvent.NewSession) },
+ containerColor = MaterialTheme.colorScheme.primary,
+ ) {
+ Icon(Icons.Default.Add, "Add Session")
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/home/components/SessionCard.kt b/app/src/main/java/com/example/android/january2022/ui/home/components/SessionCard.kt
index b40ad17..38cd03e 100644
--- a/app/src/main/java/com/example/android/january2022/ui/home/components/SessionCard.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/home/components/SessionCard.kt
@@ -1,6 +1,11 @@
package com.example.android.january2022.ui.home.components
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -16,52 +21,52 @@ import com.example.android.january2022.ui.SessionWrapper
@Composable
fun SessionCard(
- sessionWrapper: SessionWrapper,
- onClick: () -> Unit
+ sessionWrapper: SessionWrapper,
+ onClick: () -> Unit,
) {
- val session = sessionWrapper.session
- val muscleGroups = sessionWrapper.muscleGroups
- val muscleTitle by remember {
- derivedStateOf {
- if (muscleGroups.isNotEmpty()) muscleGroups[0].uppercase() else ""
+ val session = sessionWrapper.session
+ val muscleGroups = sessionWrapper.muscleGroups
+ val muscleTitle by remember {
+ derivedStateOf {
+ if (muscleGroups.isNotEmpty()) muscleGroups[0].uppercase() else ""
+ }
}
- }
- val muscleSubtitle by remember {
- derivedStateOf {
- muscleGroups.drop(1).take(3).toString().drop(1).dropLast(1).uppercase()
+ val muscleSubtitle by remember {
+ derivedStateOf {
+ muscleGroups.drop(1).take(3).toString().drop(1).dropLast(1).uppercase()
+ }
}
- }
- Surface(
- onClick = { onClick() },
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 8.dp)
- .requiredHeight(75.dp),
- shape = MaterialTheme.shapes.medium
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(12.dp),
- verticalAlignment = Alignment.CenterVertically
+ Surface(
+ onClick = { onClick() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp)
+ .requiredHeight(75.dp),
+ shape = MaterialTheme.shapes.medium,
) {
- SessionDate(session, Modifier.padding(start = 4.dp, end = 14.dp))
- Column(verticalArrangement = Arrangement.Center) {
- Text(
- text = muscleTitle,
- style = MaterialTheme.typography.headlineSmall
- )
- Row {
- if (muscleSubtitle.isNotEmpty()) {
- Text(
- text = muscleSubtitle,
- style = MaterialTheme.typography.bodySmall,
- color = LocalContentColor.current.copy(alpha = 0.7f)
- )
- }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ SessionDate(session, Modifier.padding(start = 4.dp, end = 14.dp))
+ Column(verticalArrangement = Arrangement.Center) {
+ Text(
+ text = muscleTitle,
+ style = MaterialTheme.typography.headlineSmall,
+ )
+ Row {
+ if (muscleSubtitle.isNotEmpty()) {
+ Text(
+ text = muscleSubtitle,
+ style = MaterialTheme.typography.bodySmall,
+ color = LocalContentColor.current.copy(alpha = 0.7f),
+ )
+ }
+ }
+ }
}
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/home/components/SessionDate.kt b/app/src/main/java/com/example/android/january2022/ui/home/components/SessionDate.kt
index 8a8863a..157f0af 100644
--- a/app/src/main/java/com/example/android/january2022/ui/home/components/SessionDate.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/home/components/SessionDate.kt
@@ -1,7 +1,6 @@
package com.example.android.january2022.ui.home.components
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.LocalContentColor
+import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -10,9 +9,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.BlurEffect
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight
import com.example.android.january2022.db.entities.Session
import java.time.format.TextStyle
@@ -20,30 +16,30 @@ import java.util.*
@Composable
fun SessionDate(
- session: Session,
- modifier: Modifier = Modifier
+ session: Session,
+ modifier: Modifier = Modifier,
) {
- val month by remember {
- derivedStateOf {
- session.start.month.getDisplayName(TextStyle.SHORT, Locale.ENGLISH)
+ val month by remember {
+ derivedStateOf {
+ session.start.month.getDisplayName(TextStyle.SHORT, Locale.ENGLISH)
+ }
}
- }
- val day by remember { derivedStateOf { session.start.dayOfMonth.toString() } }
+ val day by remember { derivedStateOf { session.start.dayOfMonth.toString() } }
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = modifier
- ) {
- DateText(text = month)
- DateText(text = day)
- }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier,
+ ) {
+ DateText(text = month)
+ DateText(text = day)
+ }
}
@Composable
fun DateText(text: String) {
- Text(
- text = text,
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold
- )
-}
\ No newline at end of file
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.SemiBold,
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/ModalBottomSheet.kt b/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/ModalBottomSheet.kt
index f2f5ce7..8c3cd62 100644
--- a/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/ModalBottomSheet.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/ModalBottomSheet.kt
@@ -22,12 +22,23 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
@@ -37,7 +48,12 @@ import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.semantics.*
+import androidx.compose.ui.semantics.collapse
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -52,21 +68,21 @@ import kotlin.math.roundToInt
*/
@ExperimentalMaterial3Api
enum class ModalBottomSheetValue {
- /**
- * The bottom sheet is not visible.
- */
- Hidden,
+ /**
+ * The bottom sheet is not visible.
+ */
+ Hidden,
- /**
- * The bottom sheet is visible at full height.
- */
- Expanded,
+ /**
+ * The bottom sheet is visible at full height.
+ */
+ Expanded,
- /**
- * The bottom sheet is partially visible at 50% of the screen height. This state is only
- * enabled if the height of the bottom sheet is more than 50% of the screen height.
- */
- HalfExpanded
+ /**
+ * The bottom sheet is partially visible at 50% of the screen height. This state is only
+ * enabled if the height of the bottom sheet is more than 50% of the screen height.
+ */
+ HalfExpanded,
}
/**
@@ -78,85 +94,88 @@ enum class ModalBottomSheetValue {
*/
@ExperimentalMaterial3Api
class ModalBottomSheetState(
- initialValue: ModalBottomSheetValue,
- animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
- confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true },
) : SwipeableState(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange,
) {
- /**
- * Whether the bottom sheet is visible.
- */
- val isVisible: Boolean
- get() = currentValue != ModalBottomSheetValue.Hidden
-
- internal val isHalfExpandedEnabled: Boolean
- get() = anchors.values.contains(ModalBottomSheetValue.HalfExpanded)
+ /**
+ * Whether the bottom sheet is visible.
+ */
+ val isVisible: Boolean
+ get() = currentValue != ModalBottomSheetValue.Hidden
- /**
- * Show the bottom sheet with animation and suspend until it's shown. If half expand is
- * enabled, the bottom sheet will be half expanded. Otherwise it will be fully expanded.
- *
- * @throws [CancellationException] if the animation is interrupted
- */
- suspend fun show() {
- val targetValue =
- if (isHalfExpandedEnabled) ModalBottomSheetValue.HalfExpanded
- else ModalBottomSheetValue.Expanded
- animateTo(targetValue = targetValue)
- }
+ internal val isHalfExpandedEnabled: Boolean
+ get() = anchors.values.contains(ModalBottomSheetValue.HalfExpanded)
- /**
- * Half expand the bottom sheet if half expand is enabled with animation and suspend until it
- * animation is complete or cancelled
- *
- * @throws [CancellationException] if the animation is interrupted
- */
- internal suspend fun halfExpand() {
- if (!isHalfExpandedEnabled) {
- return
+ /**
+ * Show the bottom sheet with animation and suspend until it's shown. If half expand is
+ * enabled, the bottom sheet will be half expanded. Otherwise it will be fully expanded.
+ *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ suspend fun show() {
+ val targetValue =
+ if (isHalfExpandedEnabled) {
+ ModalBottomSheetValue.HalfExpanded
+ } else {
+ ModalBottomSheetValue.Expanded
+ }
+ animateTo(targetValue = targetValue)
}
- animateTo(ModalBottomSheetValue.HalfExpanded)
- }
- /**
- * Fully expand the bottom sheet with animation and suspend until it if fully expanded or
- * animation has been cancelled.
- * *
- * @throws [CancellationException] if the animation is interrupted
- */
- internal suspend fun expand() = animateTo(ModalBottomSheetValue.Expanded)
-
- /**
- * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has
- * been cancelled.
- *
- * @throws [CancellationException] if the animation is interrupted
- */
- suspend fun hide() = animateTo(ModalBottomSheetValue.Hidden)
+ /**
+ * Half expand the bottom sheet if half expand is enabled with animation and suspend until it
+ * animation is complete or cancelled
+ *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ internal suspend fun halfExpand() {
+ if (!isHalfExpandedEnabled) {
+ return
+ }
+ animateTo(ModalBottomSheetValue.HalfExpanded)
+ }
- internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+ /**
+ * Fully expand the bottom sheet with animation and suspend until it if fully expanded or
+ * animation has been cancelled.
+ * *
+ * @throws [CancellationException] if the animation is interrupted
+ */
+ internal suspend fun expand() = animateTo(ModalBottomSheetValue.Expanded)
- companion object {
/**
- * The default [Saver] implementation for [ModalBottomSheetState].
+ * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has
+ * been cancelled.
+ *
+ * @throws [CancellationException] if the animation is interrupted
*/
- fun Saver(
- animationSpec: AnimationSpec,
- confirmStateChange: (ModalBottomSheetValue) -> Boolean
- ): Saver = Saver(
- save = { it.currentValue },
- restore = {
- ModalBottomSheetState(
- initialValue = it,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
+ suspend fun hide() = animateTo(ModalBottomSheetValue.Hidden)
+
+ internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [ModalBottomSheetState].
+ */
+ fun Saver(
+ animationSpec: AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean,
+ ): Saver = Saver(
+ save = { it.currentValue },
+ restore = {
+ ModalBottomSheetState(
+ initialValue = it,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange,
+ )
+ },
)
- }
- )
- }
+ }
}
/**
@@ -169,22 +188,22 @@ class ModalBottomSheetState(
@Composable
@ExperimentalMaterial3Api
fun rememberModalBottomSheetState(
- initialValue: ModalBottomSheetValue,
- animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
- confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true },
): ModalBottomSheetState {
- return rememberSaveable(
- saver = ModalBottomSheetState.Saver(
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- ) {
- ModalBottomSheetState(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- }
+ return rememberSaveable(
+ saver = ModalBottomSheetState.Saver(
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange,
+ ),
+ ) {
+ ModalBottomSheetState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange,
+ )
+ }
}
/**
@@ -218,152 +237,152 @@ fun rememberModalBottomSheetState(
@Composable
@ExperimentalMaterial3Api
fun ModalBottomSheetLayout(
- sheetContent: @Composable ColumnScope.() -> Unit,
- modifier: Modifier = Modifier,
- sheetState: ModalBottomSheetState =
- rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
- sheetShape: Shape = MaterialTheme.shapes.large,
- sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
- sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
- sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
- scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
- content: @Composable () -> Unit
+ sheetContent: @Composable ColumnScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ sheetState: ModalBottomSheetState =
+ rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
+ sheetShape: Shape = MaterialTheme.shapes.large,
+ sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
+ sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
+ sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+ scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
+ content: @Composable () -> Unit,
) {
- val scope = rememberCoroutineScope()
- BoxWithConstraints(modifier) {
- val fullHeight = constraints.maxHeight.toFloat()
- val sheetHeightState = remember { mutableStateOf(null) }
- Box(Modifier.fillMaxSize()) {
- content()
- Scrim(
- color = scrimColor,
- onDismiss = {
- if (sheetState.confirmStateChange(ModalBottomSheetValue.Hidden)) {
- scope.launch { sheetState.hide() }
- }
- },
- visible = sheetState.targetValue != ModalBottomSheetValue.Hidden
- )
- }
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .nestedScroll(sheetState.nestedScrollConnection)
- .offset {
- val y = if (sheetState.anchors.isEmpty()) {
- // if we don't know our anchors yet, render the sheet as hidden
- fullHeight.roundToInt()
- } else {
- // if we do know our anchors, respect them
- sheetState.offset.value.roundToInt()
- }
- IntOffset(0, y)
- }
- .bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState)
- .onGloballyPositioned {
- sheetHeightState.value = it.size.height.toFloat()
+ val scope = rememberCoroutineScope()
+ BoxWithConstraints(modifier) {
+ val fullHeight = constraints.maxHeight.toFloat()
+ val sheetHeightState = remember { mutableStateOf(null) }
+ Box(Modifier.fillMaxSize()) {
+ content()
+ Scrim(
+ color = scrimColor,
+ onDismiss = {
+ if (sheetState.confirmStateChange(ModalBottomSheetValue.Hidden)) {
+ scope.launch { sheetState.hide() }
+ }
+ },
+ visible = sheetState.targetValue != ModalBottomSheetValue.Hidden,
+ )
}
- .semantics {
- if (sheetState.isVisible) {
- dismiss {
- if (sheetState.confirmStateChange(ModalBottomSheetValue.Hidden)) {
- scope.launch { sheetState.hide() }
- }
- true
- }
- if (sheetState.currentValue == ModalBottomSheetValue.HalfExpanded) {
- expand {
- if (sheetState.confirmStateChange(ModalBottomSheetValue.Expanded)) {
- scope.launch { sheetState.expand() }
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .nestedScroll(sheetState.nestedScrollConnection)
+ .offset {
+ val y = if (sheetState.anchors.isEmpty()) {
+ // if we don't know our anchors yet, render the sheet as hidden
+ fullHeight.roundToInt()
+ } else {
+ // if we do know our anchors, respect them
+ sheetState.offset.value.roundToInt()
+ }
+ IntOffset(0, y)
}
- true
- }
- } else if (sheetState.isHalfExpandedEnabled) {
- collapse {
- if (sheetState.confirmStateChange(ModalBottomSheetValue.HalfExpanded)) {
- scope.launch { sheetState.halfExpand() }
+ .bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState)
+ .onGloballyPositioned {
+ sheetHeightState.value = it.size.height.toFloat()
}
- true
- }
- }
- }
- },
- shape = sheetShape,
- tonalElevation = sheetElevation,
- color = sheetBackgroundColor,
- contentColor = sheetContentColor
- ) {
- Column(content = sheetContent)
+ .semantics {
+ if (sheetState.isVisible) {
+ dismiss {
+ if (sheetState.confirmStateChange(ModalBottomSheetValue.Hidden)) {
+ scope.launch { sheetState.hide() }
+ }
+ true
+ }
+ if (sheetState.currentValue == ModalBottomSheetValue.HalfExpanded) {
+ expand {
+ if (sheetState.confirmStateChange(ModalBottomSheetValue.Expanded)) {
+ scope.launch { sheetState.expand() }
+ }
+ true
+ }
+ } else if (sheetState.isHalfExpandedEnabled) {
+ collapse {
+ if (sheetState.confirmStateChange(ModalBottomSheetValue.HalfExpanded)) {
+ scope.launch { sheetState.halfExpand() }
+ }
+ true
+ }
+ }
+ }
+ },
+ shape = sheetShape,
+ tonalElevation = sheetElevation,
+ color = sheetBackgroundColor,
+ contentColor = sheetContentColor,
+ ) {
+ Column(content = sheetContent)
+ }
}
- }
}
@Suppress("ModifierInspectorInfo")
@OptIn(ExperimentalMaterial3Api::class)
private fun Modifier.bottomSheetSwipeable(
- sheetState: ModalBottomSheetState,
- fullHeight: Float,
- sheetHeightState: State
+ sheetState: ModalBottomSheetState,
+ fullHeight: Float,
+ sheetHeightState: State,
): Modifier {
- val sheetHeight = sheetHeightState.value
- val modifier = if (sheetHeight != null) {
- val anchors = if (sheetHeight < fullHeight / 2) {
- mapOf(
- fullHeight to ModalBottomSheetValue.Hidden,
- fullHeight - sheetHeight to ModalBottomSheetValue.Expanded
- )
+ val sheetHeight = sheetHeightState.value
+ val modifier = if (sheetHeight != null) {
+ val anchors = if (sheetHeight < fullHeight / 2) {
+ mapOf(
+ fullHeight to ModalBottomSheetValue.Hidden,
+ fullHeight - sheetHeight to ModalBottomSheetValue.Expanded,
+ )
+ } else {
+ mapOf(
+ fullHeight to ModalBottomSheetValue.Hidden,
+ fullHeight / 2 to ModalBottomSheetValue.HalfExpanded,
+ max(0f, fullHeight - sheetHeight) to ModalBottomSheetValue.Expanded,
+ )
+ }
+ Modifier.swipeable(
+ state = sheetState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = sheetState.currentValue != ModalBottomSheetValue.Hidden,
+ resistance = null,
+ )
} else {
- mapOf(
- fullHeight to ModalBottomSheetValue.Hidden,
- fullHeight / 2 to ModalBottomSheetValue.HalfExpanded,
- max(0f, fullHeight - sheetHeight) to ModalBottomSheetValue.Expanded
- )
+ Modifier
}
- Modifier.swipeable(
- state = sheetState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = sheetState.currentValue != ModalBottomSheetValue.Hidden,
- resistance = null
- )
- } else {
- Modifier
- }
- return this.then(modifier)
+ return this.then(modifier)
}
@Composable
private fun Scrim(
- color: Color,
- onDismiss: () -> Unit,
- visible: Boolean
+ color: Color,
+ onDismiss: () -> Unit,
+ visible: Boolean,
) {
- if (color.isSpecified) {
- val alpha by animateFloatAsState(
- targetValue = if (visible) 1f else 0f,
- animationSpec = TweenSpec()
- )
- val closeSheet = getString(CloseSheet)
- val dismissModifier = if (visible) {
- Modifier
- .pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
- .semantics(mergeDescendants = true) {
- contentDescription = closeSheet
- onClick { onDismiss(); true }
+ if (color.isSpecified) {
+ val alpha by animateFloatAsState(
+ targetValue = if (visible) 1f else 0f,
+ animationSpec = TweenSpec(),
+ )
+ val closeSheet = getString(CloseSheet)
+ val dismissModifier = if (visible) {
+ Modifier
+ .pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
+ .semantics(mergeDescendants = true) {
+ contentDescription = closeSheet
+ onClick { onDismiss(); true }
+ }
+ } else {
+ Modifier
}
- } else {
- Modifier
- }
- Canvas(
- Modifier
- .fillMaxSize()
- .then(dismissModifier)
- ) {
- drawRect(color = color, alpha = alpha)
+ Canvas(
+ Modifier
+ .fillMaxSize()
+ .then(dismissModifier),
+ ) {
+ drawRect(color = color, alpha = alpha)
+ }
}
- }
}
/**
@@ -371,15 +390,15 @@ private fun Scrim(
*/
object ModalBottomSheetDefaults {
- /**
- * The default elevation used by [ModalBottomSheetLayout].
- */
- val Elevation = 2.dp
+ /**
+ * The default elevation used by [ModalBottomSheetLayout].
+ */
+ val Elevation = 2.dp
- /**
- * The default scrim color used by [ModalBottomSheetLayout].
- */
- val scrimColor: Color
- @Composable
- get() = MaterialTheme.colorScheme.scrim.copy(alpha = 0.4f)
-}
\ No newline at end of file
+ /**
+ * The default scrim color used by [ModalBottomSheetLayout].
+ */
+ val scrimColor: Color
+ @Composable
+ get() = MaterialTheme.colorScheme.scrim.copy(alpha = 0.4f)
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Strings.kt b/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Strings.kt
index 1e83f58..8cfaca6 100644
--- a/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Strings.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Strings.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.modalbottomsheet
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -26,29 +25,29 @@ import androidx.compose.ui.platform.LocalContext
@Immutable
@kotlin.jvm.JvmInline
value class Strings private constructor(@Suppress("unused") private val value: Int) {
- companion object {
- val NavigationMenu = Strings(0)
- val CloseDrawer = Strings(1)
- val CloseSheet = Strings(2)
- val DefaultErrorMessage = Strings(3)
- val ExposedDropdownMenu = Strings(4)
- val SliderRangeStart = Strings(5)
- val SliderRangeEnd = Strings(6)
- }
+ companion object {
+ val NavigationMenu = Strings(0)
+ val CloseDrawer = Strings(1)
+ val CloseSheet = Strings(2)
+ val DefaultErrorMessage = Strings(3)
+ val ExposedDropdownMenu = Strings(4)
+ val SliderRangeStart = Strings(5)
+ val SliderRangeEnd = Strings(6)
+ }
}
@Composable
fun getString(string: Strings): String {
- LocalConfiguration.current
- val resources = LocalContext.current.resources
- return when (string) {
- Strings.NavigationMenu -> resources.getString(R.string.navigation_menu)
- Strings.CloseDrawer -> resources.getString(R.string.close_drawer)
- Strings.CloseSheet -> resources.getString(R.string.close_sheet)
- Strings.DefaultErrorMessage -> resources.getString(R.string.default_error_message)
- Strings.ExposedDropdownMenu -> resources.getString(R.string.dropdown_menu)
- Strings.SliderRangeStart -> resources.getString(R.string.range_start)
- Strings.SliderRangeEnd -> resources.getString(R.string.range_end)
- else -> ""
- }
-}
\ No newline at end of file
+ LocalConfiguration.current
+ val resources = LocalContext.current.resources
+ return when (string) {
+ Strings.NavigationMenu -> resources.getString(R.string.navigation_menu)
+ Strings.CloseDrawer -> resources.getString(R.string.close_drawer)
+ Strings.CloseSheet -> resources.getString(R.string.close_sheet)
+ Strings.DefaultErrorMessage -> resources.getString(R.string.default_error_message)
+ Strings.ExposedDropdownMenu -> resources.getString(R.string.dropdown_menu)
+ Strings.SliderRangeStart -> resources.getString(R.string.range_start)
+ Strings.SliderRangeEnd -> resources.getString(R.string.range_end)
+ else -> ""
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Swipeable.kt b/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Swipeable.kt
index 37aa94d..b8be359 100644
--- a/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Swipeable.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/modalbottomsheet/Swipeable.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.modalbottomsheet
-
/*
* Copyright 2020 The Android Open Source Project
*
@@ -25,9 +24,18 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
@@ -67,342 +75,345 @@ import kotlin.math.sin
@Stable
@ExperimentalMaterial3Api
open class SwipeableState(
- initialValue: T,
- internal val animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true }
+ initialValue: T,
+ internal val animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ internal val confirmStateChange: (newValue: T) -> Boolean = { true },
) {
- /**
- * The current value of the state.
- *
- * If no swipe or animation is in progress, this corresponds to the anchor at which the
- * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
- * the last anchor at which the [swipeable] was settled before the swipe or animation started.
- */
- var currentValue: T by mutableStateOf(initialValue)
- private set
-
- /**
- * Whether the state is currently animating.
- */
- var isAnimationRunning: Boolean by mutableStateOf(false)
- private set
-
- /**
- * The current position (in pixels) of the [swipeable].
- *
- * You should use this state to offset your content accordingly. The recommended way is to
- * use `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
- */
- val offset: State get() = offsetState
-
- /**
- * The amount by which the [swipeable] has been swiped past its bounds.
- */
- val overflow: State get() = overflowState
-
- // Use `Float.NaN` as a placeholder while the state is uninitialised.
- private val offsetState = mutableStateOf(0f)
- private val overflowState = mutableStateOf(0f)
-
- // the source of truth for the "real"(non ui) position
- // basically position in bounds + overflow
- private val absoluteOffset = mutableStateOf(0f)
-
- // current animation target, if animating, otherwise null
- private val animationTarget = mutableStateOf(null)
-
- internal var anchors by mutableStateOf(emptyMap())
-
- private val latestNonEmptyAnchorsFlow: Flow> =
- snapshotFlow { anchors }
- .filter { it.isNotEmpty() }
- .take(1)
-
- internal var minBound = Float.NEGATIVE_INFINITY
- internal var maxBound = Float.POSITIVE_INFINITY
-
- internal fun ensureInit(newAnchors: Map) {
- if (anchors.isEmpty()) {
- // need to do initial synchronization synchronously :(
- val initialOffset = newAnchors.getOffset(currentValue)
- requireNotNull(initialOffset) {
- "The initial value must have an associated anchor."
- }
- offsetState.value = initialOffset
- absoluteOffset.value = initialOffset
- }
- }
-
- internal suspend fun processNewAnchors(
- oldAnchors: Map,
- newAnchors: Map
- ) {
- if (oldAnchors.isEmpty()) {
- // If this is the first time that we receive anchors, then we need to initialise
- // the state so we snap to the offset associated to the initial value.
- minBound = newAnchors.keys.minOrNull()!!
- maxBound = newAnchors.keys.maxOrNull()!!
- val initialOffset = newAnchors.getOffset(currentValue)
- requireNotNull(initialOffset) {
- "The initial value must have an associated anchor."
- }
- snapInternalToOffset(initialOffset)
- } else if (newAnchors != oldAnchors) {
- // If we have received new anchors, then the offset of the current value might
- // have changed, so we need to animate to the new offset. If the current value
- // has been removed from the anchors then we animate to the closest anchor
- // instead. Note that this stops any ongoing animation.
- minBound = Float.NEGATIVE_INFINITY
- maxBound = Float.POSITIVE_INFINITY
- val animationTargetValue = animationTarget.value
- // if we're in the animation already, let's find it a new home
- val targetOffset = if (animationTargetValue != null) {
- // first, try to map old state to the new state
- val oldState = oldAnchors[animationTargetValue]
- val newState = newAnchors.getOffset(oldState)
- // return new state if exists, or find the closes one among new anchors
- newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
- } else {
- // we're not animating, proceed by finding the new anchors for an old value
- val actualOldValue = oldAnchors[offset.value]
- val value = if (actualOldValue == currentValue) currentValue else actualOldValue
- newAnchors.getOffset(value) ?: newAnchors
- .keys.minByOrNull { abs(it - offset.value) }!!
- }
- try {
- animateInternalToOffset(targetOffset, animationSpec)
- } catch (c: CancellationException) {
- // If the animation was interrupted for any reason, snap as a last resort.
- snapInternalToOffset(targetOffset)
- } finally {
- currentValue = newAnchors.getValue(targetOffset)
- minBound = newAnchors.keys.minOrNull()!!
- maxBound = newAnchors.keys.maxOrNull()!!
- }
+ /**
+ * The current value of the state.
+ *
+ * If no swipe or animation is in progress, this corresponds to the anchor at which the
+ * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
+ * the last anchor at which the [swipeable] was settled before the swipe or animation started.
+ */
+ var currentValue: T by mutableStateOf(initialValue)
+ private set
+
+ /**
+ * Whether the state is currently animating.
+ */
+ var isAnimationRunning: Boolean by mutableStateOf(false)
+ private set
+
+ /**
+ * The current position (in pixels) of the [swipeable].
+ *
+ * You should use this state to offset your content accordingly. The recommended way is to
+ * use `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
+ */
+ val offset: State get() = offsetState
+
+ /**
+ * The amount by which the [swipeable] has been swiped past its bounds.
+ */
+ val overflow: State get() = overflowState
+
+ // Use `Float.NaN` as a placeholder while the state is uninitialised.
+ private val offsetState = mutableFloatStateOf(0f)
+ private val overflowState = mutableFloatStateOf(0f)
+
+ // the source of truth for the "real"(non ui) position
+ // basically position in bounds + overflow
+ private val absoluteOffset = mutableFloatStateOf(0f)
+
+ // current animation target, if animating, otherwise null
+ private val animationTarget = mutableStateOf(null)
+
+ internal var anchors by mutableStateOf(emptyMap())
+
+ private val latestNonEmptyAnchorsFlow: Flow> =
+ snapshotFlow { anchors }
+ .filter { it.isNotEmpty() }
+ .take(1)
+
+ internal var minBound = Float.NEGATIVE_INFINITY
+ internal var maxBound = Float.POSITIVE_INFINITY
+
+ internal fun ensureInit(newAnchors: Map) {
+ if (anchors.isEmpty()) {
+ // need to do initial synchronization synchronously :(
+ val initialOffset = newAnchors.getOffset(currentValue)
+ requireNotNull(initialOffset) {
+ "The initial value must have an associated anchor."
+ }
+ offsetState.value = initialOffset
+ absoluteOffset.value = initialOffset
+ }
}
- }
- internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
+ internal suspend fun processNewAnchors(
+ oldAnchors: Map,
+ newAnchors: Map,
+ ) {
+ if (oldAnchors.isEmpty()) {
+ // If this is the first time that we receive anchors, then we need to initialise
+ // the state so we snap to the offset associated to the initial value.
+ minBound = newAnchors.keys.minOrNull()!!
+ maxBound = newAnchors.keys.maxOrNull()!!
+ val initialOffset = newAnchors.getOffset(currentValue)
+ requireNotNull(initialOffset) {
+ "The initial value must have an associated anchor."
+ }
+ snapInternalToOffset(initialOffset)
+ } else if (newAnchors != oldAnchors) {
+ // If we have received new anchors, then the offset of the current value might
+ // have changed, so we need to animate to the new offset. If the current value
+ // has been removed from the anchors then we animate to the closest anchor
+ // instead. Note that this stops any ongoing animation.
+ minBound = Float.NEGATIVE_INFINITY
+ maxBound = Float.POSITIVE_INFINITY
+ val animationTargetValue = animationTarget.value
+ // if we're in the animation already, let's find it a new home
+ val targetOffset = if (animationTargetValue != null) {
+ // first, try to map old state to the new state
+ val oldState = oldAnchors[animationTargetValue]
+ val newState = newAnchors.getOffset(oldState)
+ // return new state if exists, or find the closes one among new anchors
+ newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
+ } else {
+ // we're not animating, proceed by finding the new anchors for an old value
+ val actualOldValue = oldAnchors[offset.value]
+ val value = if (actualOldValue == currentValue) currentValue else actualOldValue
+ newAnchors.getOffset(value) ?: newAnchors
+ .keys.minByOrNull { abs(it - offset.value) }!!
+ }
+ try {
+ animateInternalToOffset(targetOffset, animationSpec)
+ } catch (c: CancellationException) {
+ // If the animation was interrupted for any reason, snap as a last resort.
+ snapInternalToOffset(targetOffset)
+ } finally {
+ currentValue = newAnchors.getValue(targetOffset)
+ minBound = newAnchors.keys.minOrNull()!!
+ maxBound = newAnchors.keys.maxOrNull()!!
+ }
+ }
+ }
- internal var velocityThreshold by mutableStateOf(0f)
+ internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
- internal var resistance: ResistanceConfig? by mutableStateOf(null)
+ internal var velocityThreshold by mutableFloatStateOf(0f)
- internal val draggableState = DraggableState {
- val newAbsolute = absoluteOffset.value + it
- val clamped = newAbsolute.coerceIn(minBound, maxBound)
- val overflow = newAbsolute - clamped
- val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
- offsetState.value = clamped + resistanceDelta
- overflowState.value = overflow
- absoluteOffset.value = newAbsolute
- }
+ internal var resistance: ResistanceConfig? by mutableStateOf(null)
- private suspend fun snapInternalToOffset(target: Float) {
- draggableState.drag {
- dragBy(target - absoluteOffset.value)
+ internal val draggableState = DraggableState {
+ val newAbsolute = absoluteOffset.value + it
+ val clamped = newAbsolute.coerceIn(minBound, maxBound)
+ val overflow = newAbsolute - clamped
+ val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
+ offsetState.value = clamped + resistanceDelta
+ overflowState.value = overflow
+ absoluteOffset.value = newAbsolute
}
- }
-
- private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec) {
- draggableState.drag {
- var prevValue = absoluteOffset.value
- animationTarget.value = target
- isAnimationRunning = true
- try {
- Animatable(prevValue).animateTo(target, spec) {
- dragBy(this.value - prevValue)
- prevValue = this.value
+
+ private suspend fun snapInternalToOffset(target: Float) {
+ draggableState.drag {
+ dragBy(target - absoluteOffset.value)
}
- } finally {
- animationTarget.value = null
- isAnimationRunning = false
- }
}
- }
-
- /**
- * The target value of the state.
- *
- * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
- * swipe finished. If an animation is running, this is the target value of that animation.
- * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
- */
- @ExperimentalMaterial3Api
- val targetValue: T
- get() {
- // TODO(calintat): Track current velocity (b/149549482) and use that here.
- val target = animationTarget.value ?: computeTarget(
- offset = offset.value,
- lastValue = anchors.getOffset(currentValue) ?: offset.value,
- anchors = anchors.keys,
- thresholds = thresholds,
- velocity = 0f,
- velocityThreshold = Float.POSITIVE_INFINITY
- )
- return anchors[target] ?: currentValue
+
+ private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec) {
+ draggableState.drag {
+ var prevValue = absoluteOffset.value
+ animationTarget.value = target
+ isAnimationRunning = true
+ try {
+ Animatable(prevValue).animateTo(target, spec) {
+ dragBy(this.value - prevValue)
+ prevValue = this.value
+ }
+ } finally {
+ animationTarget.value = null
+ isAnimationRunning = false
+ }
+ }
}
- /**
- * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
- *
- * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
- */
- @ExperimentalMaterial3Api
- val progress: SwipeProgress
- get() {
- val bounds = findBounds(offset.value, anchors.keys)
- val from: T
- val to: T
- val fraction: Float
- when (bounds.size) {
- 0 -> {
- from = currentValue
- to = currentValue
- fraction = 1f
+ /**
+ * The target value of the state.
+ *
+ * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
+ * swipe finished. If an animation is running, this is the target value of that animation.
+ * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+ */
+ @ExperimentalMaterial3Api
+ val targetValue: T
+ get() {
+ // TODO(calintat): Track current velocity (b/149549482) and use that here.
+ val target = animationTarget.value ?: computeTarget(
+ offset = offset.value,
+ lastValue = anchors.getOffset(currentValue) ?: offset.value,
+ anchors = anchors.keys,
+ thresholds = thresholds,
+ velocity = 0f,
+ velocityThreshold = Float.POSITIVE_INFINITY,
+ )
+ return anchors[target] ?: currentValue
}
- 1 -> {
- from = anchors.getValue(bounds[0])
- to = anchors.getValue(bounds[0])
- fraction = 1f
+
+ /**
+ * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
+ *
+ * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
+ */
+ @ExperimentalMaterial3Api
+ val progress: SwipeProgress
+ get() {
+ val bounds = findBounds(offset.value, anchors.keys)
+ val from: T
+ val to: T
+ val fraction: Float
+ when (bounds.size) {
+ 0 -> {
+ from = currentValue
+ to = currentValue
+ fraction = 1f
+ }
+ 1 -> {
+ from = anchors.getValue(bounds[0])
+ to = anchors.getValue(bounds[0])
+ fraction = 1f
+ }
+ else -> {
+ val (a, b) =
+ if (direction > 0f) {
+ bounds[0] to bounds[1]
+ } else {
+ bounds[1] to bounds[0]
+ }
+ from = anchors.getValue(a)
+ to = anchors.getValue(b)
+ fraction = (offset.value - a) / (b - a)
+ }
+ }
+ return SwipeProgress(from, to, fraction)
}
- else -> {
- val (a, b) =
- if (direction > 0f) {
- bounds[0] to bounds[1]
- } else {
- bounds[1] to bounds[0]
+
+ /**
+ * The direction in which the [swipeable] is moving, relative to the current [currentValue].
+ *
+ * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
+ * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
+ */
+ @ExperimentalMaterial3Api
+ val direction: Float
+ get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
+
+ /**
+ * Set the state without any animation and suspend until it's set
+ *
+ * @param targetValue The new target value to set [currentValue] to.
+ */
+ @ExperimentalMaterial3Api
+ suspend fun snapTo(targetValue: T) {
+ latestNonEmptyAnchorsFlow.collect { anchors ->
+ val targetOffset = anchors.getOffset(targetValue)
+ requireNotNull(targetOffset) {
+ "The target value must have an associated anchor."
}
- from = anchors.getValue(a)
- to = anchors.getValue(b)
- fraction = (offset.value - a) / (b - a)
+ snapInternalToOffset(targetOffset)
+ currentValue = targetValue
}
- }
- return SwipeProgress(from, to, fraction)
}
- /**
- * The direction in which the [swipeable] is moving, relative to the current [currentValue].
- *
- * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
- * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
- */
- @ExperimentalMaterial3Api
- val direction: Float
- get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
-
- /**
- * Set the state without any animation and suspend until it's set
- *
- * @param targetValue The new target value to set [currentValue] to.
- */
- @ExperimentalMaterial3Api
- suspend fun snapTo(targetValue: T) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- val targetOffset = anchors.getOffset(targetValue)
- requireNotNull(targetOffset) {
- "The target value must have an associated anchor."
- }
- snapInternalToOffset(targetOffset)
- currentValue = targetValue
- }
- }
-
- /**
- * Set the state to the target value by starting an animation.
- *
- * @param targetValue The new value to animate to.
- * @param anim The animation that will be used to animate to the new value.
- */
- @ExperimentalMaterial3Api
- suspend fun animateTo(targetValue: T, anim: AnimationSpec = animationSpec) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- try {
- val targetOffset = anchors.getOffset(targetValue)
- requireNotNull(targetOffset) {
- "The target value must have an associated anchor."
+ /**
+ * Set the state to the target value by starting an animation.
+ *
+ * @param targetValue The new value to animate to.
+ * @param anim The animation that will be used to animate to the new value.
+ */
+ @ExperimentalMaterial3Api
+ suspend fun animateTo(targetValue: T, anim: AnimationSpec = animationSpec) {
+ latestNonEmptyAnchorsFlow.collect { anchors ->
+ try {
+ val targetOffset = anchors.getOffset(targetValue)
+ requireNotNull(targetOffset) {
+ "The target value must have an associated anchor."
+ }
+ animateInternalToOffset(targetOffset, anim)
+ } finally {
+ val endOffset = absoluteOffset.value
+ val endValue = anchors
+ // fighting rounding error once again, anchor should be as close as 0.5 pixels
+ .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
+ .values.firstOrNull() ?: currentValue
+ currentValue = endValue
+ }
}
- animateInternalToOffset(targetOffset, anim)
- } finally {
- val endOffset = absoluteOffset.value
- val endValue = anchors
- // fighting rounding error once again, anchor should be as close as 0.5 pixels
- .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
- .values.firstOrNull() ?: currentValue
- currentValue = endValue
- }
- }
- }
-
- /**
- * Perform fling with settling to one of the anchors which is determined by the given
- * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
- * since it will settle at the anchor.
- *
- * In general cases, [swipeable] flings by itself when being swiped. This method is to be
- * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
- * want to trigger settling fling when the child scroll container reaches the bound.
- *
- * @param velocity velocity to fling and settle with
- *
- * @return the reason fling ended
- */
- suspend fun performFling(velocity: Float) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- val lastAnchor = anchors.getOffset(currentValue)!!
- val targetValue = computeTarget(
- offset = offset.value,
- lastValue = lastAnchor,
- anchors = anchors.keys,
- thresholds = thresholds,
- velocity = velocity,
- velocityThreshold = velocityThreshold
- )
- val targetState = anchors[targetValue]
- if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
- // If the user vetoed the state change, rollback to the previous state.
- else animateInternalToOffset(lastAnchor, animationSpec)
}
- }
-
- /**
- * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
- * gesture flow.
- *
- * Note: This method performs generic drag and it won't settle to any particular anchor, *
- * leaving swipeable in between anchors. When done dragging, [performFling] must be
- * called as well to ensure swipeable will settle at the anchor.
- *
- * In general cases, [swipeable] drags by itself when being swiped. This method is to be
- * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
- * want to force drag when the child scroll container reaches the bound.
- *
- * @param delta delta in pixels to drag by
- *
- * @return the amount of [delta] consumed
- */
- fun performDrag(delta: Float): Float {
- val potentiallyConsumed = absoluteOffset.value + delta
- val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
- val deltaToConsume = clamped - absoluteOffset.value
- if (abs(deltaToConsume) > 0) {
- draggableState.dispatchRawDelta(deltaToConsume)
+
+ /**
+ * Perform fling with settling to one of the anchors which is determined by the given
+ * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
+ * since it will settle at the anchor.
+ *
+ * In general cases, [swipeable] flings by itself when being swiped. This method is to be
+ * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+ * want to trigger settling fling when the child scroll container reaches the bound.
+ *
+ * @param velocity velocity to fling and settle with
+ *
+ * @return the reason fling ended
+ */
+ suspend fun performFling(velocity: Float) {
+ latestNonEmptyAnchorsFlow.collect { anchors ->
+ val lastAnchor = anchors.getOffset(currentValue)!!
+ val targetValue = computeTarget(
+ offset = offset.value,
+ lastValue = lastAnchor,
+ anchors = anchors.keys,
+ thresholds = thresholds,
+ velocity = velocity,
+ velocityThreshold = velocityThreshold,
+ )
+ val targetState = anchors[targetValue]
+ if (targetState != null && confirmStateChange(targetState)) {
+ animateTo(targetState)
+ } // If the user vetoed the state change, rollback to the previous state.
+ else {
+ animateInternalToOffset(lastAnchor, animationSpec)
+ }
+ }
}
- return deltaToConsume
- }
- companion object {
/**
- * The default [Saver] implementation for [SwipeableState].
+ * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
+ * gesture flow.
+ *
+ * Note: This method performs generic drag and it won't settle to any particular anchor, *
+ * leaving swipeable in between anchors. When done dragging, [performFling] must be
+ * called as well to ensure swipeable will settle at the anchor.
+ *
+ * In general cases, [swipeable] drags by itself when being swiped. This method is to be
+ * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+ * want to force drag when the child scroll container reaches the bound.
+ *
+ * @param delta delta in pixels to drag by
+ *
+ * @return the amount of [delta] consumed
*/
- fun Saver(
- animationSpec: AnimationSpec,
- confirmStateChange: (T) -> Boolean
- ) = Saver, T>(
- save = { it.currentValue },
- restore = { SwipeableState(it, animationSpec, confirmStateChange) }
- )
- }
+ fun performDrag(delta: Float): Float {
+ val potentiallyConsumed = absoluteOffset.value + delta
+ val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+ val deltaToConsume = clamped - absoluteOffset.value
+ if (abs(deltaToConsume) > 0) {
+ draggableState.dispatchRawDelta(deltaToConsume)
+ }
+ return deltaToConsume
+ }
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [SwipeableState].
+ */
+ fun Saver(
+ animationSpec: AnimationSpec,
+ confirmStateChange: (T) -> Boolean,
+ ) = Saver, T>(
+ save = { it.currentValue },
+ restore = { SwipeableState(it, animationSpec, confirmStateChange) },
+ )
+ }
}
/**
@@ -418,32 +429,32 @@ open class SwipeableState(
@Immutable
@ExperimentalMaterial3Api
class SwipeProgress(
- val from: T,
- val to: T,
- /*@FloatRange(from = 0.0, to = 1.0)*/
- val fraction: Float
+ val from: T,
+ val to: T,
+ /*@FloatRange(from = 0.0, to = 1.0)*/
+ val fraction: Float,
) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is SwipeProgress<*>) return false
-
- if (from != other.from) return false
- if (to != other.to) return false
- if (fraction != other.fraction) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = from?.hashCode() ?: 0
- result = 31 * result + (to?.hashCode() ?: 0)
- result = 31 * result + fraction.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
- }
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SwipeProgress<*>) return false
+
+ if (from != other.from) return false
+ if (to != other.to) return false
+ if (fraction != other.fraction) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = from?.hashCode() ?: 0
+ result = 31 * result + (to?.hashCode() ?: 0)
+ result = 31 * result + fraction.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
+ }
}
/**
@@ -456,60 +467,22 @@ class SwipeProgress(
@Composable
@ExperimentalMaterial3Api
fun rememberSwipeableState(
- initialValue: T,
- animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
- confirmStateChange: (newValue: T) -> Boolean = { true }
+ initialValue: T,
+ animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (newValue: T) -> Boolean = { true },
): SwipeableState {
- return rememberSaveable(
- saver = SwipeableState.Saver(
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- ) {
- SwipeableState(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- }
-}
-
-/**
- * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
- * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
- * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
- * [value] will be notified to update their state to the new value of the [SwipeableState] by
- * invoking [onValueChange]. If the owner does not update their state to the provided value for
- * some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
- */
-@Composable
-@ExperimentalMaterial3Api
-internal fun rememberSwipeableStateFor(
- value: T,
- onValueChange: (T) -> Unit,
- animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec
-): SwipeableState {
- val swipeableState = remember {
- SwipeableState(
- initialValue = value,
- animationSpec = animationSpec,
- confirmStateChange = { true }
- )
- }
- val forceAnimationCheck = remember { mutableStateOf(false) }
- LaunchedEffect(value, forceAnimationCheck.value) {
- if (value != swipeableState.currentValue) {
- swipeableState.animateTo(value)
- }
- }
- DisposableEffect(swipeableState.currentValue) {
- if (value != swipeableState.currentValue) {
- onValueChange(swipeableState.currentValue)
- forceAnimationCheck.value = !forceAnimationCheck.value
+ return rememberSaveable(
+ saver = SwipeableState.Saver(
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange,
+ ),
+ ) {
+ SwipeableState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange,
+ )
}
- onDispose { }
- }
- return swipeableState
}
/**
@@ -551,62 +524,64 @@ internal fun rememberSwipeableStateFor(
*/
@ExperimentalMaterial3Api
fun Modifier.swipeable(
- state: SwipeableState,
- anchors: Map,
- orientation: Orientation,
- enabled: Boolean = true,
- reverseDirection: Boolean = false,
- interactionSource: MutableInteractionSource? = null,
- thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
- resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
- velocityThreshold: Dp = VelocityThreshold
-) = composed(
- inspectorInfo = debugInspectorInfo {
- name = "swipeable"
- properties["state"] = state
- properties["anchors"] = anchors
- properties["orientation"] = orientation
- properties["enabled"] = enabled
- properties["reverseDirection"] = reverseDirection
- properties["interactionSource"] = interactionSource
- properties["thresholds"] = thresholds
- properties["resistance"] = resistance
- properties["velocityThreshold"] = velocityThreshold
- }
-) {
- require(anchors.isNotEmpty()) {
- "You must have at least one anchor."
- }
- require(anchors.values.distinct().count() == anchors.size) {
- "You cannot have two anchors mapped to the same state."
- }
- val density = LocalDensity.current
- state.ensureInit(anchors)
- LaunchedEffect(anchors, state) {
- val oldAnchors = state.anchors
- state.anchors = anchors
- state.resistance = resistance
- state.thresholds = { a, b ->
- val from = anchors.getValue(a)
- val to = anchors.getValue(b)
- with(thresholds(from, to)) { density.computeThreshold(a, b) }
- }
- with(density) {
- state.velocityThreshold = velocityThreshold.toPx()
- }
- state.processNewAnchors(oldAnchors, anchors)
- }
-
- Modifier.draggable(
- orientation = orientation,
- enabled = enabled,
- reverseDirection = reverseDirection,
- interactionSource = interactionSource,
- startDragImmediately = state.isAnimationRunning,
- onDragStopped = { velocity -> launch { state.performFling(velocity) } },
- state = state.draggableState
- )
-}
+ state: SwipeableState,
+ anchors: Map,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false,
+ interactionSource: MutableInteractionSource? = null,
+ thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
+ resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
+ velocityThreshold: Dp = VelocityThreshold,
+) = this.then(
+ composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "swipeable"
+ properties["state"] = state
+ properties["anchors"] = anchors
+ properties["orientation"] = orientation
+ properties["enabled"] = enabled
+ properties["reverseDirection"] = reverseDirection
+ properties["interactionSource"] = interactionSource
+ properties["thresholds"] = thresholds
+ properties["resistance"] = resistance
+ properties["velocityThreshold"] = velocityThreshold
+ },
+ ) {
+ require(anchors.isNotEmpty()) {
+ "You must have at least one anchor."
+ }
+ require(anchors.values.distinct().count() == anchors.size) {
+ "You cannot have two anchors mapped to the same state."
+ }
+ val density = LocalDensity.current
+ state.ensureInit(anchors)
+ LaunchedEffect(anchors, state) {
+ val oldAnchors = state.anchors
+ state.anchors = anchors
+ state.resistance = resistance
+ state.thresholds = { a, b ->
+ val from = anchors.getValue(a)
+ val to = anchors.getValue(b)
+ with(thresholds(from, to)) { density.computeThreshold(a, b) }
+ }
+ with(density) {
+ state.velocityThreshold = velocityThreshold.toPx()
+ }
+ state.processNewAnchors(oldAnchors, anchors)
+ }
+
+ Modifier.draggable(
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ startDragImmediately = state.isAnimationRunning,
+ onDragStopped = { velocity -> launch { state.performFling(velocity) } },
+ state = state.draggableState,
+ )
+ },
+)
/**
* Interface to compute a threshold between two anchors/states in a [swipeable].
@@ -616,10 +591,10 @@ fun Modifier.swipeable(
@Stable
@ExperimentalMaterial3Api
interface ThresholdConfig {
- /**
- * Compute the value of the threshold (in pixels), once the values of the anchors are known.
- */
- fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
+ /**
+ * Compute the value of the threshold (in pixels), once the values of the anchors are known.
+ */
+ fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
}
/**
@@ -630,9 +605,9 @@ interface ThresholdConfig {
@Immutable
@ExperimentalMaterial3Api
data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
- override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
- return fromValue + offset.toPx() * sign(toValue - fromValue)
- }
+ override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+ return fromValue + offset.toPx() * sign(toValue - fromValue)
+ }
}
/**
@@ -643,12 +618,12 @@ data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
@Immutable
@ExperimentalMaterial3Api
data class FractionalThreshold(
- /*@FloatRange(from = 0.0, to = 1.0)*/
- private val fraction: Float
+ /*@FloatRange(from = 0.0, to = 1.0)*/
+ private val fraction: Float,
) : ThresholdConfig {
- override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
- return lerp(fromValue, toValue, fraction)
- }
+ override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+ return lerp(fromValue, toValue, fraction)
+ }
}
/**
@@ -675,41 +650,41 @@ data class FractionalThreshold(
*/
@Immutable
class ResistanceConfig(
- /*@FloatRange(from = 0.0, fromInclusive = false)*/
- val basis: Float,
- /*@FloatRange(from = 0.0)*/
- val factorAtMin: Float = StandardResistanceFactor,
- /*@FloatRange(from = 0.0)*/
- val factorAtMax: Float = StandardResistanceFactor
+ /*@FloatRange(from = 0.0, fromInclusive = false)*/
+ val basis: Float,
+ /*@FloatRange(from = 0.0)*/
+ val factorAtMin: Float = StandardResistanceFactor,
+ /*@FloatRange(from = 0.0)*/
+ val factorAtMax: Float = StandardResistanceFactor,
) {
- fun computeResistance(overflow: Float): Float {
- val factor = if (overflow < 0) factorAtMin else factorAtMax
- if (factor == 0f) return 0f
- val progress = (overflow / basis).coerceIn(-1f, 1f)
- return basis / factor * sin(progress * PI.toFloat() / 2)
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ResistanceConfig) return false
-
- if (basis != other.basis) return false
- if (factorAtMin != other.factorAtMin) return false
- if (factorAtMax != other.factorAtMax) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = basis.hashCode()
- result = 31 * result + factorAtMin.hashCode()
- result = 31 * result + factorAtMax.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
- }
+ fun computeResistance(overflow: Float): Float {
+ val factor = if (overflow < 0) factorAtMin else factorAtMax
+ if (factor == 0f) return 0f
+ val progress = (overflow / basis).coerceIn(-1f, 1f)
+ return basis / factor * sin(progress * PI.toFloat() / 2)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ResistanceConfig) return false
+
+ if (basis != other.basis) return false
+ if (factorAtMin != other.factorAtMin) return false
+ if (factorAtMax != other.factorAtMax) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = basis.hashCode()
+ result = 31 * result + factorAtMin.hashCode()
+ result = 31 * result + factorAtMax.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
+ }
}
/**
@@ -722,158 +697,158 @@ class ResistanceConfig(
* 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
*/
private fun findBounds(
- offset: Float,
- anchors: Set
+ offset: Float,
+ anchors: Set,
): List {
- // Find the anchors the target lies between with a little bit of rounding error.
- val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
- val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
-
- return when {
- a == null ->
- // case 1 or 3
- listOfNotNull(b)
- b == null ->
- // case 4
- listOf(a)
- a == b ->
- // case 2
- // Can't return offset itself here since it might not be exactly equal
- // to the anchor, despite being considered an exact match.
- listOf(a)
- else ->
- // case 5
- listOf(a, b)
- }
+ // Find the anchors the target lies between with a little bit of rounding error.
+ val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
+ val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
+
+ return when {
+ a == null ->
+ // case 1 or 3
+ listOfNotNull(b)
+ b == null ->
+ // case 4
+ listOf(a)
+ a == b ->
+ // case 2
+ // Can't return offset itself here since it might not be exactly equal
+ // to the anchor, despite being considered an exact match.
+ listOf(a)
+ else ->
+ // case 5
+ listOf(a, b)
+ }
}
private fun computeTarget(
- offset: Float,
- lastValue: Float,
- anchors: Set,
- thresholds: (Float, Float) -> Float,
- velocity: Float,
- velocityThreshold: Float
+ offset: Float,
+ lastValue: Float,
+ anchors: Set,
+ thresholds: (Float, Float) -> Float,
+ velocity: Float,
+ velocityThreshold: Float,
): Float {
- val bounds = findBounds(offset, anchors)
- return when (bounds.size) {
- 0 -> lastValue
- 1 -> bounds[0]
- else -> {
- val lower = bounds[0]
- val upper = bounds[1]
- if (lastValue <= offset) {
- // Swiping from lower to upper (positive).
- if (velocity >= velocityThreshold) {
- return upper
- } else {
- val threshold = thresholds(lower, upper)
- if (offset < threshold) lower else upper
- }
- } else {
- // Swiping from upper to lower (negative).
- if (velocity <= -velocityThreshold) {
- return lower
- } else {
- val threshold = thresholds(upper, lower)
- if (offset > threshold) upper else lower
+ val bounds = findBounds(offset, anchors)
+ return when (bounds.size) {
+ 0 -> lastValue
+ 1 -> bounds[0]
+ else -> {
+ val lower = bounds[0]
+ val upper = bounds[1]
+ if (lastValue <= offset) {
+ // Swiping from lower to upper (positive).
+ if (velocity >= velocityThreshold) {
+ return upper
+ } else {
+ val threshold = thresholds(lower, upper)
+ if (offset < threshold) lower else upper
+ }
+ } else {
+ // Swiping from upper to lower (negative).
+ if (velocity <= -velocityThreshold) {
+ return lower
+ } else {
+ val threshold = thresholds(upper, lower)
+ if (offset > threshold) upper else lower
+ }
+ }
}
- }
}
- }
}
private fun Map.getOffset(state: T): Float? {
- return entries.firstOrNull { it.value == state }?.key
+ return entries.firstOrNull { it.value == state }?.key
}
/**
* Contains useful defaults for [swipeable] and [SwipeableState].
*/
object SwipeableDefaults {
- /**
- * The default animation used by [SwipeableState].
- */
- val AnimationSpec = SpringSpec()
-
- /**
- * The default velocity threshold (1.8 dp per millisecond) used by [swipeable].
- */
- val VelocityThreshold = 125.dp
-
- /**
- * A stiff resistance factor which indicates that swiping isn't available right now.
- */
- const val StiffResistanceFactor = 20f
-
- /**
- * A standard resistance factor which indicates that the user has run out of things to see.
- */
- const val StandardResistanceFactor = 10f
-
- /**
- * The default resistance config used by [swipeable].
- *
- * This returns `null` if there is one anchor. If there are at least two anchors, it returns
- * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
- */
- fun resistanceConfig(
- anchors: Set,
- factorAtMin: Float = StandardResistanceFactor,
- factorAtMax: Float = StandardResistanceFactor
- ): ResistanceConfig? {
- return if (anchors.size <= 1) {
- null
- } else {
- val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
- ResistanceConfig(basis, factorAtMin, factorAtMax)
+ /**
+ * The default animation used by [SwipeableState].
+ */
+ val AnimationSpec = SpringSpec()
+
+ /**
+ * The default velocity threshold (1.8 dp per millisecond) used by [swipeable].
+ */
+ val VelocityThreshold = 125.dp
+
+ /**
+ * A stiff resistance factor which indicates that swiping isn't available right now.
+ */
+ const val StiffResistanceFactor = 20f
+
+ /**
+ * A standard resistance factor which indicates that the user has run out of things to see.
+ */
+ const val StandardResistanceFactor = 10f
+
+ /**
+ * The default resistance config used by [swipeable].
+ *
+ * This returns `null` if there is one anchor. If there are at least two anchors, it returns
+ * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
+ */
+ fun resistanceConfig(
+ anchors: Set,
+ factorAtMin: Float = StandardResistanceFactor,
+ factorAtMax: Float = StandardResistanceFactor,
+ ): ResistanceConfig? {
+ return if (anchors.size <= 1) {
+ null
+ } else {
+ val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
+ ResistanceConfig(basis, factorAtMin, factorAtMax)
+ }
}
- }
}
// temp default nested scroll connection for swipeables which desire as an opt in
// revisit in b/174756744 as all types will have their own specific connection probably
@ExperimentalMaterial3Api
internal val SwipeableState.PreUpPostDownNestedScrollConnection: NestedScrollConnection
- get() = object : NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
- performDrag(delta).toOffset()
- } else {
- Offset.Zero
- }
- }
+ get() = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ performDrag(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- return if (source == NestedScrollSource.Drag) {
- performDrag(available.toFloat()).toOffset()
- } else {
- Offset.Zero
- }
- }
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ return if (source == NestedScrollSource.Drag) {
+ performDrag(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
- override suspend fun onPreFling(available: Velocity): Velocity {
- val toFling = Offset(available.x, available.y).toFloat()
- return if (toFling < 0 && offset.value > minBound) {
- performFling(velocity = toFling)
- // since we go to the anchor with tween settling, consume all for the best UX
- available
- } else {
- Velocity.Zero
- }
- }
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ val toFling = Offset(available.x, available.y).toFloat()
+ return if (toFling < 0 && offset.value > minBound) {
+ performFling(velocity = toFling)
+ // since we go to the anchor with tween settling, consume all for the best UX
+ available
+ } else {
+ Velocity.Zero
+ }
+ }
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- performFling(velocity = Offset(available.x, available.y).toFloat())
- return available
- }
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ performFling(velocity = Offset(available.x, available.y).toFloat())
+ return available
+ }
- private fun Float.toOffset(): Offset = Offset(0f, this)
+ private fun Float.toOffset(): Offset = Offset(0f, this)
- private fun Offset.toFloat(): Float = this.y
- }
+ private fun Offset.toFloat(): Float = this.y
+ }
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/SessionEvent.kt b/app/src/main/java/com/example/android/january2022/ui/session/SessionEvent.kt
index a1e4fb6..02a51e3 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/SessionEvent.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/SessionEvent.kt
@@ -6,24 +6,24 @@ import com.example.android.january2022.utils.Event
import java.time.LocalTime
sealed class SessionEvent : Event {
- data class ExerciseExpanded(val exercise: ExerciseWrapper) : SessionEvent()
- data class ExerciseSelected(val exercise: ExerciseWrapper) : SessionEvent()
- data class SetChanged(val updatedSet: GymSet) : SessionEvent()
- data class SetCreated(val sessionExercise: ExerciseWrapper) : SessionEvent()
- data class SetDeleted(val set: GymSet) : SessionEvent()
+ data class ExerciseExpanded(val exercise: ExerciseWrapper) : SessionEvent()
+ data class ExerciseSelected(val exercise: ExerciseWrapper) : SessionEvent()
+ data class SetChanged(val updatedSet: GymSet) : SessionEvent()
+ data class SetCreated(val sessionExercise: ExerciseWrapper) : SessionEvent()
+ data class SetDeleted(val set: GymSet) : SessionEvent()
- object RemoveSelectedExercises : SessionEvent()
- object RemoveSession : SessionEvent()
- object DeselectExercises : SessionEvent()
+ object RemoveSelectedExercises : SessionEvent()
+ object RemoveSession : SessionEvent()
+ object DeselectExercises : SessionEvent()
- object TimerToggled : SessionEvent()
- object TimerReset : SessionEvent()
- object TimerIncreased : SessionEvent()
- object TimerDecreased : SessionEvent()
+ object TimerToggled : SessionEvent()
+ object TimerReset : SessionEvent()
+ object TimerIncreased : SessionEvent()
+ object TimerDecreased : SessionEvent()
- object OpenGuide : SessionEvent()
- object AddExercise : SessionEvent()
+ object OpenGuide : SessionEvent()
+ object AddExercise : SessionEvent()
- data class StartTimeChanged(val newTime: LocalTime) : SessionEvent()
- data class EndTimeChanged(val newTime: LocalTime) : SessionEvent()
-}
\ No newline at end of file
+ data class StartTimeChanged(val newTime: LocalTime) : SessionEvent()
+ data class EndTimeChanged(val newTime: LocalTime) : SessionEvent()
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/SessionScreen.kt b/app/src/main/java/com/example/android/january2022/ui/session/SessionScreen.kt
index f4d3e76..caf69c2 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/SessionScreen.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/SessionScreen.kt
@@ -4,16 +4,33 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import androidx.compose.animation.*
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.layout.*
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -29,11 +46,17 @@ import com.example.android.january2022.ui.SessionWrapper
import com.example.android.january2022.ui.TimerState
import com.example.android.january2022.ui.datetimedialog.MaterialDialog
import com.example.android.january2022.ui.datetimedialog.rememberMaterialDialogState
-import com.example.android.january2022.ui.datetimedialog.time.timepicker
+import com.example.android.january2022.ui.datetimedialog.time.Timepicker
import com.example.android.january2022.ui.modalbottomsheet.ModalBottomSheetLayout
import com.example.android.january2022.ui.modalbottomsheet.ModalBottomSheetValue
import com.example.android.january2022.ui.modalbottomsheet.rememberModalBottomSheetState
-import com.example.android.january2022.ui.session.components.*
+import com.example.android.january2022.ui.session.components.DeletionAlertDialog
+import com.example.android.january2022.ui.session.components.ExerciseCard
+import com.example.android.january2022.ui.session.components.SessionAppBar
+import com.example.android.january2022.ui.session.components.SessionAppBarExpanded
+import com.example.android.january2022.ui.session.components.SessionAppBarSelected
+import com.example.android.january2022.ui.session.components.SessionHeader
+import com.example.android.january2022.ui.session.components.TimerBar
import com.example.android.january2022.ui.theme.onlyTop
import com.example.android.january2022.utils.UiEvent
import kotlinx.coroutines.launch
@@ -45,285 +68,285 @@ import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SessionScreen(
- onNavigate: (UiEvent.Navigate) -> Unit,
- viewModel: SessionViewModel = hiltViewModel()
+ onNavigate: (UiEvent.Navigate) -> Unit,
+ viewModel: SessionViewModel = hiltViewModel(),
) {
- val uriHandler = LocalUriHandler.current
- val context = LocalContext.current
+ val uriHandler = LocalUriHandler.current
+ val context = LocalContext.current
- LaunchedEffect(true) {
- viewModel.uiEvent.collect { event ->
- Timber.d("UiEvent Received: $event")
- when (event) {
- is UiEvent.OpenWebsite -> {
- uriHandler.openUri(event.url)
+ LaunchedEffect(true) {
+ viewModel.uiEvent.collect { event ->
+ Timber.d("UiEvent Received: $event")
+ when (event) {
+ is UiEvent.OpenWebsite -> {
+ uriHandler.openUri(event.url)
+ }
+ is UiEvent.Navigate -> onNavigate(event)
+ is UiEvent.ToggleTimer -> context.sendTimerAction(TimerService.Actions.TOGGLE)
+ is UiEvent.ResetTimer -> context.sendTimerAction(TimerService.Actions.RESET)
+ is UiEvent.IncrementTimer -> context.sendTimerAction(TimerService.Actions.INCREMENT)
+ is UiEvent.DecrementTimer -> context.sendTimerAction(TimerService.Actions.DECREMENT)
+ else -> Unit
+ }
}
- is UiEvent.Navigate -> onNavigate(event)
- is UiEvent.ToggleTimer -> context.sendTimerAction(TimerService.Actions.TOGGLE)
- is UiEvent.ResetTimer -> context.sendTimerAction(TimerService.Actions.RESET)
- is UiEvent.IncrementTimer -> context.sendTimerAction(TimerService.Actions.INCREMENT)
- is UiEvent.DecrementTimer -> context.sendTimerAction(TimerService.Actions.DECREMENT)
- else -> Unit
- }
}
- }
- val session by viewModel.session.collectAsState(SessionWrapper(Session(), emptyList()))
- val exercises by viewModel.exercises.collectAsState(initial = emptyList())
- val expandedExercise by viewModel.expandedExercise.collectAsState()
- val selectedExercises by viewModel.selectedExercises.collectAsState()
- val muscleGroups by viewModel.muscleGroups.collectAsState(emptyList())
+ val session by viewModel.session.collectAsState(SessionWrapper(Session(), emptyList()))
+ val exercises by viewModel.exercises.collectAsState(initial = emptyList())
+ val expandedExercise by viewModel.expandedExercise.collectAsState()
+ val selectedExercises by viewModel.selectedExercises.collectAsState()
+ val muscleGroups by viewModel.muscleGroups.collectAsState(emptyList())
- var timerState by remember { mutableStateOf(TimerState(0L, false, 0L)) }
- val receiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- intent?.let {
- timerState = TimerState(
- time = it.getLongExtra(TimerService.Intents.Extras.TIME.toString(), 0L),
- running = it.getBooleanExtra(TimerService.Intents.Extras.IS_RUNNING.toString(), false),
- maxTime = it.getLongExtra(TimerService.Intents.Extras.MAX_TIME.toString(), 0L)
- )
- }
+ var timerState by remember { mutableStateOf(TimerState(0L, false, 0L)) }
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent?.let {
+ timerState = TimerState(
+ time = it.getLongExtra(TimerService.Intents.Extras.TIME.toString(), 0L),
+ running = it.getBooleanExtra(TimerService.Intents.Extras.IS_RUNNING.toString(), false),
+ maxTime = it.getLongExtra(TimerService.Intents.Extras.MAX_TIME.toString(), 0L),
+ )
+ }
+ }
+ }
+ // Register local broadcast manager to update Timer State when Intents are received
+ ContextCompat.registerReceiver(
+ context,
+ receiver,
+ IntentFilter(TimerService.Intents.STATUS.toString()),
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ ).also {
+ context.sendTimerAction(TimerService.Actions.QUERY)
}
- }
- // Register local broadcast manager to update Timer State when Intents are received
- ContextCompat.registerReceiver(
- context,
- receiver,
- IntentFilter(TimerService.Intents.STATUS.toString()),
- ContextCompat.RECEIVER_NOT_EXPORTED
- ).also {
- context.sendTimerAction(TimerService.Actions.QUERY)
- }
- val scrollState = rememberLazyListState()
- val headerHeight = 240.dp
- val coroutineScope = rememberCoroutineScope()
- val timerVisible = remember { mutableStateOf(false) }
+ val scrollState = rememberLazyListState()
+ val headerHeight = 240.dp
+ val coroutineScope = rememberCoroutineScope()
+ val timerVisible = remember { mutableStateOf(false) }
- val deleteExerciseDialog = remember { mutableStateOf(false) }
- val deleteSessionDialog = remember { mutableStateOf(false) }
- val deleteSetDialog = remember { mutableStateOf(null) }
+ val deleteExerciseDialog = remember { mutableStateOf(false) }
+ val deleteSessionDialog = remember { mutableStateOf(false) }
+ val deleteSetDialog = remember { mutableStateOf(null) }
- if (deleteExerciseDialog.value) {
- DeletionAlertDialog(
- onDismiss = { deleteExerciseDialog.value = false },
- onDelete = {
- viewModel.onEvent(SessionEvent.RemoveSelectedExercises)
- deleteExerciseDialog.value = false
- },
- title = {
- Text(text = "Remove ${selectedExercises.size} Exercise${if (selectedExercises.size > 1) "s" else ""}?")
- },
- text = {
- Text(text = "Are you sure you want to remove the selected exercises from this session? This action can not be undone.")
- }
- )
- }
- if (deleteSessionDialog.value) {
- DeletionAlertDialog(
- onDismiss = { deleteSessionDialog.value = false },
- onDelete = {
- viewModel.onEvent(SessionEvent.RemoveSession)
- deleteSessionDialog.value = false
- },
- title = {
- Text(text = "Delete Session?")
- },
- text = {
- Text(text = "Are you sure you want to delete this session and all of its contents? This action can not be undone.")
- }
- )
- }
- if (deleteSetDialog.value != null) {
- DeletionAlertDialog(
- onDismiss = { deleteSetDialog.value = null },
- onDelete = {
- deleteSetDialog.value?.let { viewModel.onEvent(SessionEvent.SetDeleted(it)) }
- deleteSetDialog.value = null
- },
- title = {
- Text(text = "Delete Set?")
- },
- text = {
- Text(text = "Are you sure you want to delete this set? This action can not be undone.")
- }
- )
- }
- val startTimeDialogState = rememberMaterialDialogState()
- MaterialDialog(
- dialogState = startTimeDialogState,
- buttons = {
- positiveButton("Ok")
- negativeButton("Cancel")
+ if (deleteExerciseDialog.value) {
+ DeletionAlertDialog(
+ onDismiss = { deleteExerciseDialog.value = false },
+ onDelete = {
+ viewModel.onEvent(SessionEvent.RemoveSelectedExercises)
+ deleteExerciseDialog.value = false
+ },
+ title = {
+ Text(text = "Remove ${selectedExercises.size} Exercise${if (selectedExercises.size > 1) "s" else ""}?")
+ },
+ text = {
+ Text(text = "Are you sure you want to remove the selected exercises from this session? This action can not be undone.")
+ },
+ )
+ }
+ if (deleteSessionDialog.value) {
+ DeletionAlertDialog(
+ onDismiss = { deleteSessionDialog.value = false },
+ onDelete = {
+ viewModel.onEvent(SessionEvent.RemoveSession)
+ deleteSessionDialog.value = false
+ },
+ title = {
+ Text(text = "Delete Session?")
+ },
+ text = {
+ Text(text = "Are you sure you want to delete this session and all of its contents? This action can not be undone.")
+ },
+ )
}
- ) {
- timepicker(
- initialTime = session.session.start.toLocalTime(),
- is24HourClock = true,
- waitForPositiveButton = true,
- title = "Select end-time"
- ) { time ->
- viewModel.onEvent(SessionEvent.StartTimeChanged(time))
+ if (deleteSetDialog.value != null) {
+ DeletionAlertDialog(
+ onDismiss = { deleteSetDialog.value = null },
+ onDelete = {
+ deleteSetDialog.value?.let { viewModel.onEvent(SessionEvent.SetDeleted(it)) }
+ deleteSetDialog.value = null
+ },
+ title = {
+ Text(text = "Delete Set?")
+ },
+ text = {
+ Text(text = "Are you sure you want to delete this set? This action can not be undone.")
+ },
+ )
}
- }
- val endTimeDialogState = rememberMaterialDialogState()
- MaterialDialog(
- dialogState = endTimeDialogState,
- buttons = {
- positiveButton("Ok")
- negativeButton("Cancel")
+ val startTimeDialogState = rememberMaterialDialogState()
+ MaterialDialog(
+ dialogState = startTimeDialogState,
+ buttons = {
+ PositiveButton("Ok")
+ NegativeButton("Cancel")
+ },
+ ) {
+ Timepicker(
+ initialTime = session.session.start.toLocalTime(),
+ is24HourClock = true,
+ waitForPositiveButton = true,
+ title = "Select end-time",
+ ) { time ->
+ viewModel.onEvent(SessionEvent.StartTimeChanged(time))
+ }
}
- ) {
- timepicker(
- initialTime = LocalTime.now(),
- is24HourClock = true,
- waitForPositiveButton = true,
- title = "Select end-time"
- ) { time ->
- viewModel.onEvent(SessionEvent.EndTimeChanged(time))
+ val endTimeDialogState = rememberMaterialDialogState()
+ MaterialDialog(
+ dialogState = endTimeDialogState,
+ buttons = {
+ PositiveButton("Ok")
+ NegativeButton("Cancel")
+ },
+ ) {
+ Timepicker(
+ initialTime = LocalTime.now(),
+ is24HourClock = true,
+ waitForPositiveButton = true,
+ title = "Select end-time",
+ ) { time ->
+ viewModel.onEvent(SessionEvent.EndTimeChanged(time))
+ }
}
- }
- val sessionInfoSheetState =
- rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
+ val sessionInfoSheetState =
+ rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
- ModalBottomSheetLayout(
- sheetContent = {
- Text("SESSION INFO", modifier = Modifier.height(400.dp))
- },
- sheetState = sessionInfoSheetState,
- sheetShape = MaterialTheme.shapes.large.onlyTop()
- ) {
- Scaffold(
- bottomBar = {
- Box(
- contentAlignment = Alignment.BottomCenter
- ) {
- BottomAppBar {}
- AnimatedVisibility(
- visible = timerVisible.value,
- exit = slideOutVertically(
- animationSpec = tween(250),
- targetOffsetY = { height ->
- height / 2
- }
- ),
- enter = slideInVertically(
- animationSpec = tween(250),
- initialOffsetY = { height ->
- height / 2
- }
- )
- ) {
- Column {
- TimerBar(timerState, viewModel::onEvent)
- BottomAppBar {}
- }
- }
- AnimatedVisibility(
- visible = expandedExercise == null,
- exit = fadeOut(tween(500)),
- enter = fadeIn(tween(500))
- ) {
- SessionAppBar(
- onDeleteSession = { deleteSessionDialog.value = true },
- timerVisible = timerVisible.value,
- timerState = timerState,
- onTimerPress = {
- timerVisible.value = !timerVisible.value
- }
+ ModalBottomSheetLayout(
+ sheetContent = {
+ Text("SESSION INFO", modifier = Modifier.height(400.dp))
+ },
+ sheetState = sessionInfoSheetState,
+ sheetShape = MaterialTheme.shapes.large.onlyTop(),
+ ) {
+ Scaffold(
+ bottomBar = {
+ Box(
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ BottomAppBar {}
+ AnimatedVisibility(
+ visible = timerVisible.value,
+ exit = slideOutVertically(
+ animationSpec = tween(250),
+ targetOffsetY = { height ->
+ height / 2
+ },
+ ),
+ enter = slideInVertically(
+ animationSpec = tween(250),
+ initialOffsetY = { height ->
+ height / 2
+ },
+ ),
+ ) {
+ Column {
+ TimerBar(timerState, viewModel::onEvent)
+ BottomAppBar {}
+ }
+ }
+ AnimatedVisibility(
+ visible = expandedExercise == null,
+ exit = fadeOut(tween(500)),
+ enter = fadeIn(tween(500)),
+ ) {
+ SessionAppBar(
+ onDeleteSession = { deleteSessionDialog.value = true },
+ timerVisible = timerVisible.value,
+ timerState = timerState,
+ onTimerPress = {
+ timerVisible.value = !timerVisible.value
+ },
+ ) {
+ viewModel.onEvent(SessionEvent.AddExercise)
+ }
+ }
+ AnimatedVisibility(
+ visible = expandedExercise != null,
+ exit = fadeOut(tween(500)),
+ enter = fadeIn(tween(500)),
+ ) {
+ SessionAppBarExpanded(
+ timerVisible = timerVisible.value,
+ timerState = timerState,
+ onTimerPress = {
+ timerVisible.value = !timerVisible.value
+ },
+ onDeleteSession = { deleteSessionDialog.value = true },
+ onEvent = viewModel::onEvent,
+ )
+ }
+ AnimatedVisibility(
+ visible = selectedExercises.isNotEmpty(),
+ exit = fadeOut(tween(500)),
+ enter = fadeIn(tween(500)),
+ ) {
+ SessionAppBarSelected(
+ timerVisible = timerVisible.value,
+ timerState = timerState,
+ onTimerPress = {
+ timerVisible.value = !timerVisible.value
+ },
+ onDeleteExercise = { deleteExerciseDialog.value = true },
+ onDeleteSession = { deleteSessionDialog.value = true },
+ onEvent = viewModel::onEvent,
+ )
+ }
+ }
+ },
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize(),
+ state = scrollState,
) {
- viewModel.onEvent(SessionEvent.AddExercise)
+ item {
+ SessionHeader(
+ sessionWrapper = session,
+ muscleGroups = muscleGroups,
+ scrollState = scrollState,
+ height = headerHeight,
+ topPadding = paddingValues.calculateTopPadding(),
+ onEndTime = { endTimeDialogState.show() },
+ onStartTime = { startTimeDialogState.show() },
+ )
+ }
+ itemsIndexed(
+ items = exercises,
+ key = { _, exercise ->
+ exercise.sessionExercise.sessionExerciseId
+ },
+ ) { index, exercise ->
+ val expanded =
+ exercise.sessionExercise.sessionExerciseId == expandedExercise?.sessionExercise?.sessionExerciseId
+ val selected = selectedExercises.contains(exercise)
+ ExerciseCard(
+ exerciseWrapper = exercise,
+ expanded = expanded,
+ selected = selected,
+ onEvent = viewModel::onEvent,
+ onLongClick = { viewModel.onEvent(SessionEvent.ExerciseSelected(exercise)) },
+ onSetDeleted = { deleteSetDialog.value = it },
+ ) {
+ viewModel.onEvent(SessionEvent.ExerciseExpanded(exercise))
+ if (!expanded) {
+ coroutineScope.launch {
+ scrollState.animateScrollToItem(index = (index - 1).coerceAtLeast(0))
+ }
+ }
+ }
+ }
+ item {
+ Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding()))
+ }
}
- }
- AnimatedVisibility(
- visible = expandedExercise != null,
- exit = fadeOut(tween(500)),
- enter = fadeIn(tween(500))
- ) {
- SessionAppBarExpanded(
- timerVisible = timerVisible.value,
- timerState = timerState,
- onTimerPress = {
- timerVisible.value = !timerVisible.value
- },
- onDeleteSession = { deleteSessionDialog.value = true },
- onEvent = viewModel::onEvent
- )
- }
- AnimatedVisibility(
- visible = selectedExercises.isNotEmpty(),
- exit = fadeOut(tween(500)),
- enter = fadeIn(tween(500))
- ) {
- SessionAppBarSelected(
- timerVisible = timerVisible.value,
- timerState = timerState,
- onTimerPress = {
- timerVisible.value = !timerVisible.value
- },
- onDeleteExercise = { deleteExerciseDialog.value = true },
- onDeleteSession = { deleteSessionDialog.value = true },
- onEvent = viewModel::onEvent
- )
- }
}
- }
- ) { paddingValues ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize(),
- state = scrollState
- ) {
- item {
- SessionHeader(
- sessionWrapper = session,
- muscleGroups = muscleGroups,
- scrollState = scrollState,
- height = headerHeight,
- topPadding = paddingValues.calculateTopPadding(),
- onEndTime = { endTimeDialogState.show() },
- onStartTime = { startTimeDialogState.show() }
- )
- }
- itemsIndexed(
- items = exercises,
- key = { _, exercise ->
- exercise.sessionExercise.sessionExerciseId
- }
- ) { index, exercise ->
- val expanded =
- exercise.sessionExercise.sessionExerciseId == expandedExercise?.sessionExercise?.sessionExerciseId
- val selected = selectedExercises.contains(exercise)
- ExerciseCard(
- exerciseWrapper = exercise,
- expanded = expanded,
- selected = selected,
- onEvent = viewModel::onEvent,
- onLongClick = { viewModel.onEvent(SessionEvent.ExerciseSelected(exercise)) },
- onSetDeleted = { deleteSetDialog.value = it }
- ) {
- viewModel.onEvent(SessionEvent.ExerciseExpanded(exercise))
- if (!expanded) {
- coroutineScope.launch {
- scrollState.animateScrollToItem(index = (index - 1).coerceAtLeast(0))
- }
- }
- }
- }
- item {
- Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding()))
- }
- }
}
- }
}
fun Session.toSessionTitle(): String {
- return try {
- DateTimeFormatter.ofPattern("MMM d yyyy").format(this.start)
- } catch (e: Exception) {
- "no date"
- }
-}
\ No newline at end of file
+ return try {
+ DateTimeFormatter.ofPattern("MMM d yyyy").format(this.start)
+ } catch (e: Exception) {
+ "no date"
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/SessionViewModel.kt b/app/src/main/java/com/example/android/january2022/ui/session/SessionViewModel.kt
index 7333552..28c03bb 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/SessionViewModel.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/SessionViewModel.kt
@@ -15,7 +15,11 @@ import com.example.android.january2022.utils.sortedListOfMuscleGroups
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -24,167 +28,167 @@ import javax.inject.Inject
@HiltViewModel
class SessionViewModel @Inject constructor(
- private val repo: GymRepository,
- savedStateHandle: SavedStateHandle
+ private val repo: GymRepository,
+ savedStateHandle: SavedStateHandle,
) : ViewModel() {
- private val _session = MutableStateFlow(Session())
- val session = _session.asStateFlow().map {
- SessionWrapper(it, emptyList())
- }
+ private val _session = MutableStateFlow(Session())
+ val session = _session.asStateFlow().map {
+ SessionWrapper(it, emptyList())
+ }
- private val _expandedExercise = MutableStateFlow(null)
- val expandedExercise = _expandedExercise.asStateFlow()
+ private val _expandedExercise = MutableStateFlow(null)
+ val expandedExercise = _expandedExercise.asStateFlow()
- private val _selectedExercises = MutableStateFlow>(emptyList())
- val selectedExercises = _selectedExercises.asStateFlow()
+ private val _selectedExercises = MutableStateFlow>(emptyList())
+ val selectedExercises = _selectedExercises.asStateFlow()
- val exercises = combine(
- repo.getExercisesForSession(_session),
- repo.getAllSets()
- ) { exercises, sets ->
- exercises.map { sewe ->
- ExerciseWrapper(
- sessionExercise = sewe.sessionExercise,
- exercise = sewe.exercise,
- sets = sets.filter { set ->
- set.parentSessionExerciseId == sewe.sessionExercise.sessionExerciseId
+ val exercises = combine(
+ repo.getExercisesForSession(_session),
+ repo.getAllSets(),
+ ) { exercises, sets ->
+ exercises.map { sewe ->
+ ExerciseWrapper(
+ sessionExercise = sewe.sessionExercise,
+ exercise = sewe.exercise,
+ sets = sets.filter { set ->
+ set.parentSessionExerciseId == sewe.sessionExercise.sessionExerciseId
+ },
+ )
}
- )
}
- }
- val muscleGroups = exercises.map { exercises ->
- exercises.map { it.exercise }.sortedListOfMuscleGroups()
- }
+ val muscleGroups = exercises.map { exercises ->
+ exercises.map { it.exercise }.sortedListOfMuscleGroups()
+ }
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
+ private val _uiEvent = Channel()
+ val uiEvent = _uiEvent.receiveAsFlow()
- init {
- savedStateHandle.get("session_id")?.let { sessionId ->
- Timber.d("Session ID: $sessionId")
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- _session.value = repo.getSessionById(sessionId)
+ init {
+ savedStateHandle.get("session_id")?.let { sessionId ->
+ Timber.d("Session ID: $sessionId")
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ _session.value = repo.getSessionById(sessionId)
+ }
+ }
}
- }
}
- }
- fun onEvent(event: Event) {
- when (event) {
- is SessionEvent.ExerciseExpanded -> {
- event.exercise.let { se ->
- if (se.sessionExercise.sessionExerciseId == _expandedExercise.value?.sessionExercise?.sessionExerciseId) {
- _expandedExercise.value = null
- } else {
- _expandedExercise.value = se
- }
- _selectedExercises.value = emptyList()
- }
- }
- is SessionEvent.ExerciseSelected -> {
- _selectedExercises.value = buildList {
- if (_selectedExercises.value.contains(event.exercise)) {
- addAll(_selectedExercises.value.minusElement(event.exercise))
- } else {
- addAll(_selectedExercises.value)
- add(event.exercise)
- }
- }
- _expandedExercise.value = null
- }
- is SessionEvent.SetChanged -> {
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- repo.updateSet(event.updatedSet)
- }
+ fun onEvent(event: Event) {
+ when (event) {
+ is SessionEvent.ExerciseExpanded -> {
+ event.exercise.let { se ->
+ if (se.sessionExercise.sessionExerciseId == _expandedExercise.value?.sessionExercise?.sessionExerciseId) {
+ _expandedExercise.value = null
+ } else {
+ _expandedExercise.value = se
+ }
+ _selectedExercises.value = emptyList()
+ }
+ }
+ is SessionEvent.ExerciseSelected -> {
+ _selectedExercises.value = buildList {
+ if (_selectedExercises.value.contains(event.exercise)) {
+ addAll(_selectedExercises.value.minusElement(event.exercise))
+ } else {
+ addAll(_selectedExercises.value)
+ add(event.exercise)
+ }
+ }
+ _expandedExercise.value = null
+ }
+ is SessionEvent.SetChanged -> {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ repo.updateSet(event.updatedSet)
+ }
+ }
+ }
+ is SessionEvent.SetCreated -> {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ repo.createSet(event.sessionExercise.sessionExercise)
+ }
+ }
+ }
+ is SessionEvent.SetDeleted -> {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ repo.deleteSet(event.set)
+ }
+ }
+ }
+ is SessionEvent.TimerToggled -> sendUiEvent(UiEvent.ToggleTimer)
+ is SessionEvent.TimerReset -> sendUiEvent(UiEvent.ResetTimer)
+ is SessionEvent.TimerIncreased -> sendUiEvent(UiEvent.IncrementTimer)
+ is SessionEvent.TimerDecreased -> sendUiEvent(UiEvent.DecrementTimer)
+ is SessionEvent.OpenGuide -> {
+ expandedExercise.value?.exercise?.let { openGuide(it) }
+ }
+ is SessionEvent.AddExercise -> {
+ _session.value.sessionId.let { id ->
+ sendUiEvent(UiEvent.Navigate("${Routes.EXERCISE_PICKER}/$id"))
+ }
+ }
+ is SessionEvent.RemoveSelectedExercises -> {
+ viewModelScope.launch {
+ _selectedExercises.value.forEach {
+ repo.removeSessionExercise(it.sessionExercise)
+ }
+ _selectedExercises.value = emptyList()
+ }
+ }
+ is SessionEvent.RemoveSession -> {
+ sendUiEvent(UiEvent.Navigate(Routes.HOME, popBackStack = true))
+ viewModelScope.launch {
+ repo.removeSession(_session.value)
+ }
+ }
+ is SessionEvent.DeselectExercises -> {
+ _selectedExercises.value = emptyList()
+ }
+ is SessionEvent.EndTimeChanged -> {
+ var session = _session.value
+ val date = session.end?.toLocalDate() ?: session.start.toLocalDate()
+ val newEndTime = LocalDateTime.of(date, event.newTime)
+ viewModelScope.launch {
+ repo.updateSession(
+ session.copy(
+ end = newEndTime,
+ ).also { session = it },
+ )
+ withContext(Dispatchers.IO) {
+ _session.value = repo.getSessionById(_session.value.sessionId)
+ }
+ }
+ }
+ is SessionEvent.StartTimeChanged -> {
+ val session = _session.value
+ val newStartTime = LocalDateTime.of(session.start.toLocalDate(), event.newTime)
+ viewModelScope.launch {
+ repo.updateSession(
+ session.copy(
+ start = newStartTime,
+ ),
+ )
+ withContext(Dispatchers.IO) {
+ _session.value = repo.getSessionById(_session.value.sessionId)
+ }
+ }
+ }
+ else -> Unit
}
- }
- is SessionEvent.SetCreated -> {
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- repo.createSet(event.sessionExercise.sessionExercise)
- }
- }
- }
- is SessionEvent.SetDeleted -> {
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- repo.deleteSet(event.set)
- }
- }
- }
- is SessionEvent.TimerToggled -> sendUiEvent(UiEvent.ToggleTimer)
- is SessionEvent.TimerReset -> sendUiEvent(UiEvent.ResetTimer)
- is SessionEvent.TimerIncreased -> sendUiEvent(UiEvent.IncrementTimer)
- is SessionEvent.TimerDecreased -> sendUiEvent(UiEvent.DecrementTimer)
- is SessionEvent.OpenGuide -> {
- expandedExercise.value?.exercise?.let { openGuide(it) }
- }
- is SessionEvent.AddExercise -> {
- _session.value.sessionId.let { id ->
- sendUiEvent(UiEvent.Navigate("${Routes.EXERCISE_PICKER}/$id"))
- }
- }
- is SessionEvent.RemoveSelectedExercises -> {
- viewModelScope.launch {
- _selectedExercises.value.forEach {
- repo.removeSessionExercise(it.sessionExercise)
- }
- _selectedExercises.value = emptyList()
- }
- }
- is SessionEvent.RemoveSession -> {
- sendUiEvent(UiEvent.Navigate(Routes.HOME, popBackStack = true))
- viewModelScope.launch {
- repo.removeSession(_session.value)
- }
- }
- is SessionEvent.DeselectExercises -> {
- _selectedExercises.value = emptyList()
- }
- is SessionEvent.EndTimeChanged -> {
- var session = _session.value
- val date = session.end?.toLocalDate() ?: session.start.toLocalDate()
- val newEndTime = LocalDateTime.of(date, event.newTime)
- viewModelScope.launch {
- repo.updateSession(
- session.copy(
- end = newEndTime
- ).also { session = it }
- )
- withContext(Dispatchers.IO) {
- _session.value = repo.getSessionById(_session.value.sessionId)
- }
- }
- }
- is SessionEvent.StartTimeChanged -> {
- val session = _session.value
- val newStartTime = LocalDateTime.of(session.start.toLocalDate(), event.newTime)
- viewModelScope.launch {
- repo.updateSession(
- session.copy(
- start = newStartTime
- )
- )
- withContext(Dispatchers.IO) {
- _session.value = repo.getSessionById(_session.value.sessionId)
- }
- }
- }
- else -> Unit
}
- }
- private fun openGuide(exercise: Exercise) {
- sendUiEvent(UiEvent.OpenWebsite(url = "https://duckduckgo.com/?q=! exrx ${exercise.title}"))
- }
+ private fun openGuide(exercise: Exercise) {
+ sendUiEvent(UiEvent.OpenWebsite(url = "https://duckduckgo.com/?q=! exrx ${exercise.title}"))
+ }
- private fun sendUiEvent(event: UiEvent) {
- viewModelScope.launch {
- _uiEvent.send(event)
+ private fun sendUiEvent(event: UiEvent) {
+ viewModelScope.launch {
+ _uiEvent.send(event)
+ }
}
- }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/actions/MenuAction.kt b/app/src/main/java/com/example/android/january2022/ui/session/actions/MenuAction.kt
index 3c3a4fd..b3e7411 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/actions/MenuAction.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/actions/MenuAction.kt
@@ -2,27 +2,31 @@ package com.example.android.january2022.ui.session.actions
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.material.DropdownMenu
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material3.*
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
fun MenuAction(
- onDelete: () -> Unit,
+ onDelete: () -> Unit,
) {
- val expanded = remember { mutableStateOf(false) }
+ val expanded = remember { mutableStateOf(false) }
- Column {
- Box {
- DropdownMenu(expanded = expanded.value, onDismissRequest = { expanded.value = false }) {
- DropdownMenuItem(text = { Text(text = "Delete Session") }, onClick = onDelete )
- }
+ Column {
+ Box {
+ DropdownMenu(expanded = expanded.value, onDismissRequest = { expanded.value = false }) {
+ DropdownMenuItem(text = { Text(text = "Delete Session") }, onClick = onDelete)
+ }
+ }
+ IconButton(onClick = { expanded.value = true }) {
+ Icon(imageVector = Icons.Default.MoreVert, contentDescription = "Options")
+ }
}
- IconButton(onClick = { expanded.value = true }) {
- Icon(imageVector = Icons.Default.MoreVert, contentDescription = "Options")
- }
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenInNewAction.kt b/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenInNewAction.kt
index 7a3a668..49aca3e 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenInNewAction.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenInNewAction.kt
@@ -8,9 +8,9 @@ import androidx.compose.runtime.Composable
@Composable
fun OpenInNewAction(
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
- IconButton(onClick = onClick) {
- Icon(imageVector = Icons.Outlined.OpenInNew, contentDescription = "Open exercise guide.")
- }
-}
\ No newline at end of file
+ IconButton(onClick = onClick) {
+ Icon(imageVector = Icons.Outlined.OpenInNew, contentDescription = "Open exercise guide.")
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenStatsAction.kt b/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenStatsAction.kt
index d191d7c..62ef6a5 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenStatsAction.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/actions/OpenStatsAction.kt
@@ -8,9 +8,9 @@ import androidx.compose.runtime.Composable
@Composable
fun OpenStatsAction(
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
- IconButton(onClick = onClick) {
- Icon(imageVector = Icons.Outlined.Analytics, contentDescription = "Open exercise guide.")
- }
-}
\ No newline at end of file
+ IconButton(onClick = onClick) {
+ Icon(imageVector = Icons.Outlined.Analytics, contentDescription = "Open exercise guide.")
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/actions/Spacers.kt b/app/src/main/java/com/example/android/january2022/ui/session/actions/Spacers.kt
index 04795c7..3bcb524 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/actions/Spacers.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/actions/Spacers.kt
@@ -8,10 +8,10 @@ import androidx.compose.ui.unit.dp
@Composable
fun ActionSpacer() {
- Spacer(modifier = Modifier.width(8.dp))
+ Spacer(modifier = Modifier.width(8.dp))
}
@Composable
fun ActionSpacerStart() {
- Spacer(modifier = Modifier.width(4.dp))
-}
\ No newline at end of file
+ Spacer(modifier = Modifier.width(4.dp))
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/actions/TimerAction.kt b/app/src/main/java/com/example/android/january2022/ui/session/actions/TimerAction.kt
index bd67fef..6497df6 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/actions/TimerAction.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/actions/TimerAction.kt
@@ -1,7 +1,6 @@
package com.example.android.january2022.ui.session.actions
import androidx.compose.animation.animateColor
-import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
@@ -18,27 +17,27 @@ import com.example.android.january2022.ui.TimerState
@Composable
fun TimerAction(
- timerState: TimerState,
- timerVisible: Boolean,
- onClick: () -> Unit
+ timerState: TimerState,
+ timerVisible: Boolean,
+ onClick: () -> Unit,
) {
- val infiniteTransition = rememberInfiniteTransition()
- val activeColor by infiniteTransition.animateColor(
- initialValue = LocalContentColor.current,
- targetValue = MaterialTheme.colorScheme.primary,
- animationSpec = infiniteRepeatable(
- animation = tween(durationMillis = 1500),
- repeatMode = RepeatMode.Reverse
+ val infiniteTransition = rememberInfiniteTransition()
+ val activeColor by infiniteTransition.animateColor(
+ initialValue = LocalContentColor.current,
+ targetValue = MaterialTheme.colorScheme.primary,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 1500),
+ repeatMode = RepeatMode.Reverse,
+ ),
)
- )
- IconButton(onClick = {
- onClick()
- }) {
- Icon(
- imageVector = Icons.Outlined.Timer,
- contentDescription = "Timer",
- tint = if (!timerVisible && timerState.running) activeColor else LocalContentColor.current
- )
- }
-}
\ No newline at end of file
+ IconButton(onClick = {
+ onClick()
+ }) {
+ Icon(
+ imageVector = Icons.Outlined.Timer,
+ contentDescription = "Timer",
+ tint = if (!timerVisible && timerState.running) activeColor else LocalContentColor.current,
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/CompactSetCard.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/CompactSetCard.kt
index 2661dea..74869e6 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/CompactSetCard.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/CompactSetCard.kt
@@ -1,6 +1,12 @@
package com.example.android.january2022.ui.session.components
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.width
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -16,45 +22,46 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.android.january2022.db.entities.GymSet
-
@Composable
fun CompactSetCard(
- set: GymSet
+ set: GymSet,
) {
- val reps = set.reps
- val weight = set.weight
- val repsText by remember { derivedStateOf { reps?.toString() ?: "0" } }
- val weightText by remember { derivedStateOf { weight?.toString() ?: "0" } }
+ val reps = set.reps
+ val weight = set.weight
+ val repsText by remember { derivedStateOf { reps?.toString() ?: "0" } }
+ val weightText by remember { derivedStateOf { weight?.toString() ?: "0" } }
- Row(
- Modifier
- .padding(horizontal = 8.dp, vertical = 4.dp)
- .requiredHeight(48.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- Surface(
- modifier = Modifier
- .fillMaxHeight(0.7f)
- .padding(top = 1.dp)
- .width(2.dp),
- color = setTypeColor(set.setType, MaterialTheme.colorScheme)
- ) {}
- Column(Modifier.padding(start = 4.dp)) {
- Row {
- Text(text = repsText, fontWeight = FontWeight.Bold)
- Text(
- text = "reps",
- fontSize = 10.sp,
- color = LocalContentColor.current.copy(alpha = 0.85f)
- )
- }
- Row {
- Text(text = weightText, fontWeight = FontWeight.Bold)
- Text(
- text = "kg", fontSize = 10.sp, color = LocalContentColor.current.copy(alpha = 0.85f)
- )
- }
+ Row(
+ Modifier
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ .requiredHeight(48.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ Surface(
+ modifier = Modifier
+ .fillMaxHeight(0.7f)
+ .padding(top = 1.dp)
+ .width(2.dp),
+ color = setTypeColor(set.setType, MaterialTheme.colorScheme),
+ ) {}
+ Column(Modifier.padding(start = 4.dp)) {
+ Row {
+ Text(text = repsText, fontWeight = FontWeight.Bold)
+ Text(
+ text = "reps",
+ fontSize = 10.sp,
+ color = LocalContentColor.current.copy(alpha = 0.85f),
+ )
+ }
+ Row {
+ Text(text = weightText, fontWeight = FontWeight.Bold)
+ Text(
+ text = "kg",
+ fontSize = 10.sp,
+ color = LocalContentColor.current.copy(alpha = 0.85f),
+ )
+ }
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/DeletionAlertDialog.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/DeletionAlertDialog.kt
index de2bd24..b71bc4a 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/DeletionAlertDialog.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/DeletionAlertDialog.kt
@@ -8,24 +8,24 @@ import androidx.compose.runtime.Composable
@Composable
fun DeletionAlertDialog(
- onDismiss: () -> Unit,
- onDelete: () -> Unit,
- title: @Composable () -> Unit,
- text: @Composable () -> Unit
+ onDismiss: () -> Unit,
+ onDelete: () -> Unit,
+ title: @Composable () -> Unit,
+ text: @Composable () -> Unit,
) {
- AlertDialog(
- onDismissRequest = onDismiss,
- confirmButton = {
- Button(onClick = onDelete) {
- Text(text = "Delete")
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(text = "Cancel")
- }
- },
- title = title,
- text = text
- )
-}
\ No newline at end of file
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ Button(onClick = onDelete) {
+ Text(text = "Delete")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = "Cancel")
+ }
+ },
+ title = title,
+ text = text,
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/ExerciseCard.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/ExerciseCard.kt
index ef9749c..a8df042 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/ExerciseCard.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/ExerciseCard.kt
@@ -1,20 +1,39 @@
package com.example.android.january2022.ui.session.components
-import androidx.compose.animation.*
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -34,130 +53,130 @@ import com.example.android.january2022.utils.Event
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExerciseCard(
- exerciseWrapper: ExerciseWrapper,
- expanded: Boolean = false,
- selected: Boolean = false,
- onEvent: (Event) -> Unit,
- onLongClick: () -> Unit,
- onSetDeleted: (GymSet) -> Unit,
- onClick: () -> Unit
+ exerciseWrapper: ExerciseWrapper,
+ expanded: Boolean = false,
+ selected: Boolean = false,
+ onEvent: (Event) -> Unit,
+ onLongClick: () -> Unit,
+ onSetDeleted: (GymSet) -> Unit,
+ onClick: () -> Unit,
) {
- val exercise = exerciseWrapper.exercise
- val sets = exerciseWrapper.sets
- val tonalElevation by animateDpAsState(targetValue = if (selected) 2.dp else 0.dp)
- val localHaptic = LocalHapticFeedback.current
+ val exercise = exerciseWrapper.exercise
+ val sets = exerciseWrapper.sets
+ val tonalElevation by animateDpAsState(targetValue = if (selected) 2.dp else 0.dp)
+ val localHaptic = LocalHapticFeedback.current
- Surface(
- tonalElevation = tonalElevation,
- shape = MaterialTheme.shapes.medium,
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp, horizontal = 8.dp)
- .clip(MaterialTheme.shapes.medium)
- .combinedClickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(bounded = true),
- onLongClick = {
- localHaptic.performHapticFeedback(HapticFeedbackType.LongPress)
- onLongClick()
- },
- onClick = onClick
- )
- ) {
- Column(
- Modifier
- .padding(top = 12.dp, bottom = 6.dp)
- .fillMaxWidth()
+ Surface(
+ tonalElevation = tonalElevation,
+ shape = MaterialTheme.shapes.medium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp, horizontal = 8.dp)
+ .clip(MaterialTheme.shapes.medium)
+ .combinedClickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = true),
+ onLongClick = {
+ localHaptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onLongClick()
+ },
+ onClick = onClick,
+ ),
) {
- Text(
- text = exercise.title,
- modifier = Modifier.padding(horizontal = 12.dp),
- style = MaterialTheme.typography.titleMedium
- )
- Spacer(Modifier.height(4.dp))
- AnimatedVisibility(expanded) {
- ExpandedExerciseContent(
- sets = sets,
- onEvent = onEvent,
- onSetCreated = {
- onEvent(SessionEvent.SetCreated(exerciseWrapper))
- },
- onSetDeleted = onSetDeleted
- )
- }
- AnimatedVisibility(!expanded) {
- val listState = rememberLazyListState()
- val width by remember {
- derivedStateOf { listState.layoutInfo.viewportSize.width }
- }
- val startWidth = 50f
- val endWidth by animateFloatAsState(
- targetValue = width.toFloat() - if (listState.canScrollForward) 225f else startWidth
- )
- Box(
- contentAlignment = Alignment.CenterEnd
+ Column(
+ Modifier
+ .padding(top = 12.dp, bottom = 6.dp)
+ .fillMaxWidth(),
) {
- LazyRow(
- modifier = Modifier
- .fillMaxWidth()
- .graphicsLayer { alpha = 0.99f }
- .drawWithContent {
- val colors = listOf(Color.Black, Color.Transparent)
- drawContent()
- drawRect(
- brush = Brush.horizontalGradient(
- colors = colors,
- startX = endWidth
- ),
- blendMode = BlendMode.DstIn
+ Text(
+ text = exercise.title,
+ modifier = Modifier.padding(horizontal = 12.dp),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ Spacer(Modifier.height(4.dp))
+ AnimatedVisibility(expanded) {
+ ExpandedExerciseContent(
+ sets = sets,
+ onEvent = onEvent,
+ onSetCreated = {
+ onEvent(SessionEvent.SetCreated(exerciseWrapper))
+ },
+ onSetDeleted = onSetDeleted,
)
- drawRect(
- brush = Brush.horizontalGradient(
- colors = colors.reversed(),
- endX = startWidth
- ),
- blendMode = BlendMode.DstIn
- )
- },
- state = listState
- ) {
- item {
- Spacer(Modifier.width(12.dp))
}
- items(sets) { set ->
- CompactSetCard(set)
+ AnimatedVisibility(!expanded) {
+ val listState = rememberLazyListState()
+ val width by remember {
+ derivedStateOf { listState.layoutInfo.viewportSize.width }
+ }
+ val startWidth = 50f
+ val endWidth by animateFloatAsState(
+ targetValue = width.toFloat() - if (listState.canScrollForward) 225f else startWidth,
+ )
+ Box(
+ contentAlignment = Alignment.CenterEnd,
+ ) {
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .graphicsLayer { alpha = 0.99f }
+ .drawWithContent {
+ val colors = listOf(Color.Black, Color.Transparent)
+ drawContent()
+ drawRect(
+ brush = Brush.horizontalGradient(
+ colors = colors,
+ startX = endWidth,
+ ),
+ blendMode = BlendMode.DstIn,
+ )
+ drawRect(
+ brush = Brush.horizontalGradient(
+ colors = colors.reversed(),
+ endX = startWidth,
+ ),
+ blendMode = BlendMode.DstIn,
+ )
+ },
+ state = listState,
+ ) {
+ item {
+ Spacer(Modifier.width(12.dp))
+ }
+ items(sets) { set ->
+ CompactSetCard(set)
+ }
+ }
+ Column {
+ AnimatedVisibility(
+ visible = listState.canScrollForward,
+ enter = slideInHorizontally(initialOffsetX = { it / 2 }) + fadeIn(),
+ exit = slideOutHorizontally(targetOffsetX = { it / 2 }) + fadeOut(),
+ ) {
+ Icon(
+ imageVector = Icons.Default.ChevronRight,
+ contentDescription = "More sets in list.",
+ modifier = Modifier.padding(end = 8.dp),
+ tint = LocalContentColor.current.copy(alpha = 0.5f),
+ )
+ }
+ }
+ }
}
- }
- Column {
- AnimatedVisibility(
- visible = listState.canScrollForward,
- enter = slideInHorizontally(initialOffsetX = { it / 2 }) + fadeIn(),
- exit = slideOutHorizontally(targetOffsetX = { it / 2 }) + fadeOut()
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start,
) {
- Icon(
- imageVector = Icons.Default.ChevronRight,
- contentDescription = "More sets in list.",
- modifier = Modifier.padding(end = 8.dp),
- tint = LocalContentColor.current.copy(alpha = 0.5f)
- )
+ exercise.getMuscleGroups().forEach {
+ SmallPill(text = it, modifier = Modifier.padding(end = 4.dp))
+ }
+ exercise.equipment.forEach { eq ->
+ SmallPill(text = eq, modifier = Modifier.padding(end = 4.dp))
+ }
}
- }
- }
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 4.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- exercise.getMuscleGroups().forEach {
- SmallPill(text = it, modifier = Modifier.padding(end = 4.dp))
- }
- exercise.equipment.forEach { eq ->
- SmallPill(text = eq, modifier = Modifier.padding(end = 4.dp))
}
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/ExpandedExerciseContent.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/ExpandedExerciseContent.kt
index 9fb3036..7bb9468 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/ExpandedExerciseContent.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/ExpandedExerciseContent.kt
@@ -1,13 +1,24 @@
package com.example.android.january2022.ui.session.components
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
-import androidx.compose.material3.*
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -25,117 +36,117 @@ import com.example.android.january2022.utils.Event
@Composable
fun ExpandedExerciseContent(
- sets: List,
- onEvent: (Event) -> Unit,
- onSetDeleted: (GymSet) -> Unit,
- onSetCreated: () -> Unit
+ sets: List,
+ onEvent: (Event) -> Unit,
+ onSetDeleted: (GymSet) -> Unit,
+ onSetCreated: () -> Unit,
) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- sets.forEach { set ->
- val localFocusManager = LocalFocusManager.current
- val reps = set.reps ?: ""
- val weight = set.weight ?: ""
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ Column(
modifier = Modifier
- .padding(bottom = 4.dp, start = 6.dp, end = 8.dp)
- .fillMaxWidth()
- .clickable { }
- ) {
- IconButton(
- onClick = { onSetDeleted(set) },
- modifier = Modifier.padding(end = 8.dp)
- ) {
- Icon(
- imageVector = Icons.Default.Close,
- contentDescription = "Delete Set",
- tint = LocalContentColor.current.copy(alpha = 0.75f)
- )
- }
- InputField(
- label = "reps",
- initialValue = reps.toString(),
- onValueChange = {
- val tfv = it.text.trim().toIntOrNull()
- if (tfv != null) {
- onEvent(SessionEvent.SetChanged(set.copy(reps = tfv)))
- true
- } else {
- false
- }
- },
- keyboardActions = KeyboardActions(
- onNext = { localFocusManager.moveFocus(FocusDirection.Next) }
- ),
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Number,
- imeAction = ImeAction.Next
- ),
- autoRequestFocus = true
- )
- InputField(
- label = "kg",
- initialValue = weight.toString(),
- onValueChange = {
- val tfv = it.text.trim().toFloatOrNull()
- if (tfv != null) {
- onEvent(SessionEvent.SetChanged(set.copy(weight = tfv)))
- true
- } else {
- false
- }
- },
- keyboardActions = KeyboardActions(
- onDone = {
- localFocusManager.moveFocus(FocusDirection.Next)
- localFocusManager.clearFocus()
+ .fillMaxWidth()
+ .padding(top = 8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ sets.forEach { set ->
+ val localFocusManager = LocalFocusManager.current
+ val reps = set.reps ?: ""
+ val weight = set.weight ?: ""
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .padding(bottom = 4.dp, start = 6.dp, end = 8.dp)
+ .fillMaxWidth()
+ .clickable { },
+ ) {
+ IconButton(
+ onClick = { onSetDeleted(set) },
+ modifier = Modifier.padding(end = 8.dp),
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Delete Set",
+ tint = LocalContentColor.current.copy(alpha = 0.75f),
+ )
+ }
+ InputField(
+ label = "reps",
+ initialValue = reps.toString(),
+ onValueChange = {
+ val tfv = it.text.trim().toIntOrNull()
+ if (tfv != null) {
+ onEvent(SessionEvent.SetChanged(set.copy(reps = tfv)))
+ true
+ } else {
+ false
+ }
+ },
+ keyboardActions = KeyboardActions(
+ onNext = { localFocusManager.moveFocus(FocusDirection.Next) },
+ ),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Next,
+ ),
+ autoRequestFocus = true,
+ )
+ InputField(
+ label = "kg",
+ initialValue = weight.toString(),
+ onValueChange = {
+ val tfv = it.text.trim().toFloatOrNull()
+ if (tfv != null) {
+ onEvent(SessionEvent.SetChanged(set.copy(weight = tfv)))
+ true
+ } else {
+ false
+ }
+ },
+ keyboardActions = KeyboardActions(
+ onDone = {
+ localFocusManager.moveFocus(FocusDirection.Next)
+ localFocusManager.clearFocus()
+ },
+ ),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Done,
+ ),
+ )
+ Surface(
+ onClick = {
+ onEvent(SessionEvent.SetChanged(set.copy(setType = SetType.next(set.setType))))
+ },
+ color = setTypeColor(set.setType, MaterialTheme.colorScheme),
+ shape = MaterialTheme.shapes.small,
+ modifier = Modifier
+ .defaultMinSize(minWidth = 100.dp)
+ .padding(start = 12.dp),
+ ) {
+ Text(
+ text = set.setType,
+ modifier = Modifier.padding(6.dp),
+ textAlign = TextAlign.Center,
+ )
+ }
}
- ),
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Number,
- imeAction = ImeAction.Done
- )
- )
- Surface(
- onClick = {
- onEvent(SessionEvent.SetChanged(set.copy(setType = SetType.next(set.setType))))
- },
- color = setTypeColor(set.setType, MaterialTheme.colorScheme),
- shape = MaterialTheme.shapes.small,
- modifier = Modifier
- .defaultMinSize(minWidth = 100.dp)
- .padding(start = 12.dp)
+ }
+ IconButton(
+ onClick = { onSetCreated() },
) {
- Text(
- text = set.setType,
- modifier = Modifier.padding(6.dp),
- textAlign = TextAlign.Center
- )
+ Icon(imageVector = Icons.Default.Add, contentDescription = "Add new set")
}
- }
}
- IconButton(
- onClick = { onSetCreated() }
- ) {
- Icon(imageVector = Icons.Default.Add, contentDescription = "Add new set")
- }
- }
}
fun setTypeColor(setType: String, colorScheme: ColorScheme): Color {
- return when (setType) {
- SetType.WARMUP -> Color(0xFF7A7272)
- SetType.EASY -> Color(0xFF6A9E44)
- SetType.NORMAL -> colorScheme.primary
- SetType.HARD -> Color(0xFFB84733)
- SetType.DROP -> Color(0xFFAD49A8)
- else -> Color.White
- }
+ return when (setType) {
+ SetType.WARMUP -> Color(0xFF7A7272)
+ SetType.EASY -> Color(0xFF6A9E44)
+ SetType.NORMAL -> colorScheme.primary
+ SetType.HARD -> Color(0xFFB84733)
+ SetType.DROP -> Color(0xFFAD49A8)
+ else -> Color.White
+ }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/InputField.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/InputField.kt
index 33daea5..cbeff59 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/InputField.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/InputField.kt
@@ -2,12 +2,22 @@ package com.example.android.january2022.ui.session.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -24,67 +34,67 @@ import androidx.compose.ui.unit.sp
@Composable
fun InputField(
- label: String,
- initialValue: String,
- onValueChange: (TextFieldValue) -> Boolean,
- keyboardActions: KeyboardActions,
- keyboardOptions: KeyboardOptions,
- autoRequestFocus: Boolean = false
+ label: String,
+ initialValue: String,
+ onValueChange: (TextFieldValue) -> Boolean,
+ keyboardActions: KeyboardActions,
+ keyboardOptions: KeyboardOptions,
+ autoRequestFocus: Boolean = false,
) {
- val initialText = initialValue.toFloatOrNull().let {
- if (it == null || it < 0) "" else initialValue
- }
- val requester = remember { FocusRequester() }
- var textValidation by remember { mutableStateOf(true) }
- val selection = remember { mutableStateOf(TextRange(100)) }
- var textFieldValue by remember {
- val tfv = TextFieldValue(text = initialText, selection = selection.value)
- mutableStateOf(tfv)
- }
- val textColor by animateColorAsState(
- targetValue = if (textValidation) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error
- )
- DisposableEffect(Unit) {
- if (autoRequestFocus && initialText.isEmpty()) requester.requestFocus()
- onDispose { }
- }
-
- Row(
- modifier = Modifier
- .clickable {
- requester.requestFocus()
- }
- .height(40.dp)
- .defaultMinSize(minWidth = 60.dp)
- .padding(start = 8.dp, end = 6.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- BasicTextField(
- value = textFieldValue,
- onValueChange = {
- textFieldValue = it
- textValidation = onValueChange(textFieldValue)
- },
- textStyle = TextStyle(
- color = textColor,
- fontSize = 21.sp,
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.End
- ),
- keyboardOptions = keyboardOptions,
- keyboardActions = keyboardActions,
- cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
- modifier = Modifier
- .width(60.dp)
- .focusRequester(requester)
- .onFocusChanged {
- // reset cursor position when receiving focus
- if (it.hasFocus || it.isFocused) {
- textFieldValue = TextFieldValue(text = textFieldValue.text, selection = selection.value)
- }
- }
+ val initialText = initialValue.toFloatOrNull().let {
+ if (it == null || it < 0) "" else initialValue
+ }
+ val requester = remember { FocusRequester() }
+ var textValidation by remember { mutableStateOf(true) }
+ val selection = remember { mutableStateOf(TextRange(100)) }
+ var textFieldValue by remember {
+ val tfv = TextFieldValue(text = initialText, selection = selection.value)
+ mutableStateOf(tfv)
+ }
+ val textColor by animateColorAsState(
+ targetValue = if (textValidation) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error,
)
- InputLabel(text = label)
- }
-}
\ No newline at end of file
+ DisposableEffect(Unit) {
+ if (autoRequestFocus && initialText.isEmpty()) requester.requestFocus()
+ onDispose { }
+ }
+
+ Row(
+ modifier = Modifier
+ .clickable {
+ requester.requestFocus()
+ }
+ .height(40.dp)
+ .defaultMinSize(minWidth = 60.dp)
+ .padding(start = 8.dp, end = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ BasicTextField(
+ value = textFieldValue,
+ onValueChange = {
+ textFieldValue = it
+ textValidation = onValueChange(textFieldValue)
+ },
+ textStyle = TextStyle(
+ color = textColor,
+ fontSize = 21.sp,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.End,
+ ),
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
+ modifier = Modifier
+ .width(60.dp)
+ .focusRequester(requester)
+ .onFocusChanged {
+ // reset cursor position when receiving focus
+ if (it.hasFocus || it.isFocused) {
+ textFieldValue = TextFieldValue(text = textFieldValue.text, selection = selection.value)
+ }
+ },
+ )
+ InputLabel(text = label)
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/InputLabel.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/InputLabel.kt
index cef3b4d..940f55f 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/InputLabel.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/InputLabel.kt
@@ -11,12 +11,12 @@ import androidx.compose.ui.unit.dp
@Composable
fun InputLabel(text: String) {
- Text(
- text = text,
- color = LocalContentColor.current.copy(alpha = 0.8f),
- style = MaterialTheme.typography.labelSmall,
- modifier = Modifier
- .padding(start = 4.dp, top = 6.dp)
- .fillMaxHeight()
- )
-}
\ No newline at end of file
+ Text(
+ text = text,
+ color = LocalContentColor.current.copy(alpha = 0.8f),
+ style = MaterialTheme.typography.labelSmall,
+ modifier = Modifier
+ .padding(start = 4.dp, top = 6.dp)
+ .fillMaxHeight(),
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBar.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBar.kt
index 46e7bee..4f19825 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBar.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBar.kt
@@ -7,38 +7,36 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
import com.example.android.january2022.ui.TimerState
import com.example.android.january2022.ui.session.actions.ActionSpacer
import com.example.android.january2022.ui.session.actions.ActionSpacerStart
import com.example.android.january2022.ui.session.actions.MenuAction
import com.example.android.january2022.ui.session.actions.TimerAction
-import com.example.android.january2022.utils.Event
@Composable
fun SessionAppBar(
- onDeleteSession: () -> Unit,
- timerState: TimerState,
- timerVisible: Boolean,
- onTimerPress: () -> Unit,
- onFAB: () -> Unit
+ onDeleteSession: () -> Unit,
+ timerState: TimerState,
+ timerVisible: Boolean,
+ onTimerPress: () -> Unit,
+ onFAB: () -> Unit,
) {
- BottomAppBar(
- actions = {
- ActionSpacerStart()
- MenuAction(
- onDelete = onDeleteSession,
- )
- ActionSpacer()
- TimerAction(timerState, timerVisible, onTimerPress)
- },
- floatingActionButton = {
- FloatingActionButton(
- onClick = { onFAB() },
- containerColor = MaterialTheme.colorScheme.primary
- ) {
- Icon(imageVector = Icons.Default.Add, contentDescription = "Add Exercise")
- }
- }
- )
-}
\ No newline at end of file
+ BottomAppBar(
+ actions = {
+ ActionSpacerStart()
+ MenuAction(
+ onDelete = onDeleteSession,
+ )
+ ActionSpacer()
+ TimerAction(timerState, timerVisible, onTimerPress)
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = { onFAB() },
+ containerColor = MaterialTheme.colorScheme.primary,
+ ) {
+ Icon(imageVector = Icons.Default.Add, contentDescription = "Add Exercise")
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarExpanded.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarExpanded.kt
index d73f9b8..6ab0dd5 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarExpanded.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarExpanded.kt
@@ -4,29 +4,33 @@ import androidx.compose.material3.BottomAppBar
import androidx.compose.runtime.Composable
import com.example.android.january2022.ui.TimerState
import com.example.android.january2022.ui.session.SessionEvent
-import com.example.android.january2022.ui.session.actions.*
-import com.example.android.january2022.utils.Event
+import com.example.android.january2022.ui.session.actions.ActionSpacer
+import com.example.android.january2022.ui.session.actions.ActionSpacerStart
+import com.example.android.january2022.ui.session.actions.MenuAction
+import com.example.android.january2022.ui.session.actions.OpenInNewAction
+import com.example.android.january2022.ui.session.actions.OpenStatsAction
+import com.example.android.january2022.ui.session.actions.TimerAction
@Composable
fun SessionAppBarExpanded(
- onEvent: (SessionEvent) -> Unit,
- onDeleteSession: () -> Unit,
- timerState: TimerState,
- timerVisible: Boolean,
- onTimerPress: () -> Unit,
+ onEvent: (SessionEvent) -> Unit,
+ onDeleteSession: () -> Unit,
+ timerState: TimerState,
+ timerVisible: Boolean,
+ onTimerPress: () -> Unit,
) {
- BottomAppBar(
- actions = {
- ActionSpacerStart()
- MenuAction(
- onDelete = onDeleteSession,
- )
- ActionSpacer()
- TimerAction(timerState = timerState, timerVisible = timerVisible) { onTimerPress() }
- ActionSpacer()
- OpenInNewAction { onEvent(SessionEvent.OpenGuide) }
- ActionSpacer()
- OpenStatsAction { /* TODO */ }
- }
- )
-}
\ No newline at end of file
+ BottomAppBar(
+ actions = {
+ ActionSpacerStart()
+ MenuAction(
+ onDelete = onDeleteSession,
+ )
+ ActionSpacer()
+ TimerAction(timerState = timerState, timerVisible = timerVisible) { onTimerPress() }
+ ActionSpacer()
+ OpenInNewAction { onEvent(SessionEvent.OpenGuide) }
+ ActionSpacer()
+ OpenStatsAction { /* TODO */ }
+ },
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarSelected.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarSelected.kt
index 44badcb..a630ee2 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarSelected.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionAppBarSelected.kt
@@ -1,6 +1,5 @@
package com.example.android.january2022.ui.session.components
-
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Deselect
import androidx.compose.material.icons.outlined.Delete
@@ -15,34 +14,33 @@ import com.example.android.january2022.ui.session.actions.ActionSpacer
import com.example.android.january2022.ui.session.actions.ActionSpacerStart
import com.example.android.january2022.ui.session.actions.MenuAction
import com.example.android.january2022.ui.session.actions.TimerAction
-import com.example.android.january2022.utils.Event
@Composable
fun SessionAppBarSelected(
- onEvent: (SessionEvent) -> Unit,
- onDeleteSession: () -> Unit,
- onDeleteExercise: () -> Unit,
- timerState: TimerState,
- timerVisible: Boolean,
- onTimerPress: () -> Unit,
+ onEvent: (SessionEvent) -> Unit,
+ onDeleteSession: () -> Unit,
+ onDeleteExercise: () -> Unit,
+ timerState: TimerState,
+ timerVisible: Boolean,
+ onTimerPress: () -> Unit,
) {
- BottomAppBar(
- actions = {
- ActionSpacerStart()
- MenuAction(
- onDelete = onDeleteSession,
- )
- ActionSpacer()
- TimerAction(timerState = timerState, timerVisible = timerVisible) { onTimerPress() }
- ActionSpacer()
- IconButton(onClick = onDeleteExercise) {
- Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Toggle set deletion mode.")
- }
- },
- floatingActionButton = {
- FloatingActionButton(onClick = { onEvent(SessionEvent.DeselectExercises) }) {
- Icon(imageVector = Icons.Default.Deselect, contentDescription = "Deselect selected exercises.")
- }
- }
- )
-}
\ No newline at end of file
+ BottomAppBar(
+ actions = {
+ ActionSpacerStart()
+ MenuAction(
+ onDelete = onDeleteSession,
+ )
+ ActionSpacer()
+ TimerAction(timerState = timerState, timerVisible = timerVisible) { onTimerPress() }
+ ActionSpacer()
+ IconButton(onClick = onDeleteExercise) {
+ Icon(imageVector = Icons.Outlined.Delete, contentDescription = "Toggle set deletion mode.")
+ }
+ },
+ floatingActionButton = {
+ FloatingActionButton(onClick = { onEvent(SessionEvent.DeselectExercises) }) {
+ Icon(imageVector = Icons.Default.Deselect, contentDescription = "Deselect selected exercises.")
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionHeader.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionHeader.kt
index e789a8f..0083fd6 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/SessionHeader.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/SessionHeader.kt
@@ -1,7 +1,14 @@
package com.example.android.january2022.ui.session.components
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -19,92 +26,92 @@ import java.time.format.DateTimeFormatter
@Composable
fun SessionHeader(
- sessionWrapper: SessionWrapper,
- muscleGroups: List,
- scrollState: LazyListState,
- height: Dp,
- topPadding: Dp,
- onEndTime: () -> Unit,
- onStartTime: () -> Unit
+ sessionWrapper: SessionWrapper,
+ muscleGroups: List,
+ scrollState: LazyListState,
+ height: Dp,
+ topPadding: Dp,
+ onEndTime: () -> Unit,
+ onStartTime: () -> Unit,
) {
- val session = sessionWrapper.session
- val startTime = DateTimeFormatter.ofPattern("HH:mm").format(session.start)
- val endTime = session.end?.let { DateTimeFormatter.ofPattern("HH:mm").format(it) } ?: "ongoing"
+ val session = sessionWrapper.session
+ val startTime = DateTimeFormatter.ofPattern("HH:mm").format(session.start)
+ val endTime = session.end?.let { DateTimeFormatter.ofPattern("HH:mm").format(it) } ?: "ongoing"
- Box(
- modifier = Modifier
- .padding(
- start = 12.dp,
- top = topPadding,
- end = 12.dp
- )
- .height(height)
- .fillMaxWidth()
- .graphicsLayer {
- val scroll = if(scrollState.layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0) {
- scrollState.firstVisibleItemScrollOffset.toFloat()
- } else {
- 10000f
- }
- translationY = scroll / 3f // Parallax effect
- alpha = 1 - scroll / 250f // Fade out text
- scaleX = 1 - scroll / 3000f
- scaleY = 1 - scroll / 3000f
- }
- ) {
- Column(
- modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Bottom
- ) {
- Text(
- text = session.toSessionTitle(),
- style = MaterialTheme.typography.headlineLarge
- )
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
+ Box(
modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(0.5f)
- ) {
- Text(
- text = startTime,
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.headlineSmall,
- modifier = Modifier
- .padding(start = 4.dp)
- .clickable {
- onStartTime()
- }
- )
- Text(
- text = "-",
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.headlineSmall,
- modifier = Modifier.padding(start = 4.dp)
- )
- Text(
- text = endTime,
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.headlineSmall,
- modifier = Modifier
- .padding(start = 4.dp)
- .clickable {
- onEndTime()
- }
- )
- }
- FlowRow(
- mainAxisAlignment = FlowMainAxisAlignment.Center
+ .padding(
+ start = 12.dp,
+ top = topPadding,
+ end = 12.dp,
+ )
+ .height(height)
+ .fillMaxWidth()
+ .graphicsLayer {
+ val scroll = if (scrollState.layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0) {
+ scrollState.firstVisibleItemScrollOffset.toFloat()
+ } else {
+ 10000f
+ }
+ translationY = scroll / 3f // Parallax effect
+ alpha = 1 - scroll / 250f // Fade out text
+ scaleX = 1 - scroll / 3000f
+ scaleY = 1 - scroll / 3000f
+ },
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Bottom,
) {
- muscleGroups.forEach { muscle ->
- SmallPill(text = muscle, modifier = Modifier.padding(4.dp))
- }
+ Text(
+ text = session.toSessionTitle(),
+ style = MaterialTheme.typography.headlineLarge,
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(0.5f),
+ ) {
+ Text(
+ text = startTime,
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .clickable {
+ onStartTime()
+ },
+ )
+ Text(
+ text = "-",
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier.padding(start = 4.dp),
+ )
+ Text(
+ text = endTime,
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .clickable {
+ onEndTime()
+ },
+ )
+ }
+ FlowRow(
+ mainAxisAlignment = FlowMainAxisAlignment.Center,
+ ) {
+ muscleGroups.forEach { muscle ->
+ SmallPill(text = muscle, modifier = Modifier.padding(4.dp))
+ }
+ }
+ }
}
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/SmallPill.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/SmallPill.kt
index bf9ac90..d0e6147 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/SmallPill.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/SmallPill.kt
@@ -12,15 +12,15 @@ import com.example.android.january2022.ui.theme.Shapes
@Composable
fun SmallPill(text: String, modifier: Modifier = Modifier) {
- Surface(
- shape = Shapes.small,
- tonalElevation = LocalAbsoluteTonalElevation.current + 1.dp,
- modifier = modifier
- ) {
- Text(
- text = text.uppercase(),
- style = MaterialTheme.typography.labelSmall,
- modifier = Modifier.padding(4.dp)
- )
- }
-}
\ No newline at end of file
+ Surface(
+ shape = Shapes.small,
+ tonalElevation = LocalAbsoluteTonalElevation.current + 1.dp,
+ modifier = modifier,
+ ) {
+ Text(
+ text = text.uppercase(),
+ style = MaterialTheme.typography.labelSmall,
+ modifier = Modifier.padding(4.dp),
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/session/components/TimerBar.kt b/app/src/main/java/com/example/android/january2022/ui/session/components/TimerBar.kt
index 29ec346..3da9938 100644
--- a/app/src/main/java/com/example/android/january2022/ui/session/components/TimerBar.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/session/components/TimerBar.kt
@@ -1,9 +1,20 @@
package com.example.android.january2022.ui.session.components
import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Pause
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
@@ -22,65 +33,65 @@ import com.example.android.january2022.utils.Event
@Composable
fun TimerBar(
- timerState: TimerState,
- onEvent: (Event) -> Unit
+ timerState: TimerState,
+ onEvent: (Event) -> Unit,
) {
- val timerTime = timerState.time
- val timerRunning = timerState.running
- val timerMaxTime = timerState.maxTime
- val maxWidth = LocalConfiguration.current.screenWidthDp
- val timerWidth = maxWidth.times(timerTime.toFloat() / timerMaxTime).toInt().dp
- val timerToggleIcon = if (timerRunning) Icons.Default.Pause else Icons.Default.PlayArrow
- val timerTimeText =
- if (timerTime > 0L) timerTime.toTimerString() else timerMaxTime.toTimerString()
- val timerTonalElevation by animateDpAsState(targetValue = if (timerRunning) 140.dp else 14.dp)
+ val timerTime = timerState.time
+ val timerRunning = timerState.running
+ val timerMaxTime = timerState.maxTime
+ val maxWidth = LocalConfiguration.current.screenWidthDp
+ val timerWidth = maxWidth.times(timerTime.toFloat() / timerMaxTime).toInt().dp
+ val timerToggleIcon = if (timerRunning) Icons.Default.Pause else Icons.Default.PlayArrow
+ val timerTimeText =
+ if (timerTime > 0L) timerTime.toTimerString() else timerMaxTime.toTimerString()
+ val timerTonalElevation by animateDpAsState(targetValue = if (timerRunning) 140.dp else 14.dp)
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .height(50.dp),
- tonalElevation = 8.dp
- ) {
- Box {
- Surface(
+ Surface(
modifier = Modifier
- .width(timerWidth)
- .height(50.dp),
- tonalElevation = timerTonalElevation
- ) {}
- Row(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(onClick = { onEvent(SessionEvent.TimerDecreased) }) {
- Icon(Icons.Default.Remove, "Decrease time")
- }
- Text(
- text = timerTimeText,
- textAlign = TextAlign.Center,
- modifier = Modifier.width(50.dp)
- )
- IconButton(onClick = { onEvent(SessionEvent.TimerIncreased) }) {
- Icon(Icons.Default.Add, "Increase time")
- }
- }
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(onClick = { onEvent(SessionEvent.TimerReset) }) {
- Icon(Icons.Default.Refresh, "Reset Timer")
- }
- IconButton(onClick = { onEvent(SessionEvent.TimerToggled) }) {
- Icon(timerToggleIcon, "Toggle Timer")
- }
+ .fillMaxWidth()
+ .height(50.dp),
+ tonalElevation = 8.dp,
+ ) {
+ Box {
+ Surface(
+ modifier = Modifier
+ .width(timerWidth)
+ .height(50.dp),
+ tonalElevation = timerTonalElevation,
+ ) {}
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconButton(onClick = { onEvent(SessionEvent.TimerDecreased) }) {
+ Icon(Icons.Default.Remove, "Decrease time")
+ }
+ Text(
+ text = timerTimeText,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.width(50.dp),
+ )
+ IconButton(onClick = { onEvent(SessionEvent.TimerIncreased) }) {
+ Icon(Icons.Default.Add, "Increase time")
+ }
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconButton(onClick = { onEvent(SessionEvent.TimerReset) }) {
+ Icon(Icons.Default.Refresh, "Reset Timer")
+ }
+ IconButton(onClick = { onEvent(SessionEvent.TimerToggled) }) {
+ Icon(timerToggleIcon, "Toggle Timer")
+ }
+ }
+ }
}
- }
}
- }
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/settings/SettingsEvent.kt b/app/src/main/java/com/example/android/january2022/ui/settings/SettingsEvent.kt
index 7de6737..539c3cc 100644
--- a/app/src/main/java/com/example/android/january2022/ui/settings/SettingsEvent.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/settings/SettingsEvent.kt
@@ -5,8 +5,8 @@ import android.net.Uri
import com.example.android.january2022.utils.Event
sealed class SettingsEvent : Event {
- data class ExportDatabase(val context: Context, val uri: Uri): SettingsEvent()
- data class ImportDatabase(val context: Context, val uri: Uri): SettingsEvent()
- object CreateFile: SettingsEvent()
- object ClearDatabase: SettingsEvent()
+ data class ExportDatabase(val context: Context, val uri: Uri) : SettingsEvent()
+ data class ImportDatabase(val context: Context, val uri: Uri) : SettingsEvent()
+ object CreateFile : SettingsEvent()
+ object ClearDatabase : SettingsEvent()
}
diff --git a/app/src/main/java/com/example/android/january2022/ui/settings/SettingsScreen.kt b/app/src/main/java/com/example/android/january2022/ui/settings/SettingsScreen.kt
index 67ac58d..4eda2cd 100644
--- a/app/src/main/java/com/example/android/january2022/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/settings/SettingsScreen.kt
@@ -20,61 +20,61 @@ import com.example.android.january2022.utils.UiEvent
@Composable
fun SettingsScreen(
- viewModel: SettingsViewModel = hiltViewModel()
+ viewModel: SettingsViewModel = hiltViewModel(),
) {
- val mContext = LocalContext.current
- val exportLauncher = rememberLauncherForActivityResult(
- contract = CreateDocument("application/json"),
- onResult = { uri ->
- uri?.let {
- viewModel.onEvent(SettingsEvent.ExportDatabase(mContext, it))
- }
- }
- )
- val importLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
- onResult = { uri ->
- uri?.let {
- viewModel.onEvent(SettingsEvent.ImportDatabase(mContext, it))
- }
- }
- )
+ val mContext = LocalContext.current
+ val exportLauncher = rememberLauncherForActivityResult(
+ contract = CreateDocument("application/json"),
+ onResult = { uri ->
+ uri?.let {
+ viewModel.onEvent(SettingsEvent.ExportDatabase(mContext, it))
+ }
+ },
+ )
+ val importLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.GetContent(),
+ onResult = { uri ->
+ uri?.let {
+ viewModel.onEvent(SettingsEvent.ImportDatabase(mContext, it))
+ }
+ },
+ )
- LaunchedEffect(key1 = true) {
- viewModel.uiEvent.collect { event ->
- when (event) {
- is UiEvent.FileCreated -> {
- exportLauncher.launch(event.fileName)
+ LaunchedEffect(key1 = true) {
+ viewModel.uiEvent.collect { event ->
+ when (event) {
+ is UiEvent.FileCreated -> {
+ exportLauncher.launch(event.fileName)
+ }
+ else -> Unit
+ }
}
- else -> Unit
- }
}
- }
- Scaffold { padding ->
- Column(
- Modifier
- .fillMaxSize()
- .padding(padding),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text("Settings")
- FilledTonalButton(onClick = {
- viewModel.onEvent(SettingsEvent.CreateFile)
- }) {
- Text("Export Database")
- }
- FilledTonalButton(onClick = {
- importLauncher.launch("application/json")
- }) {
- Text("Import Database")
- }
- FilledTonalButton(onClick = {
- viewModel.onEvent(SettingsEvent.ClearDatabase)
- }) {
- Text("Delete Database")
- }
+ Scaffold { padding ->
+ Column(
+ Modifier
+ .fillMaxSize()
+ .padding(padding),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text("Settings")
+ FilledTonalButton(onClick = {
+ viewModel.onEvent(SettingsEvent.CreateFile)
+ }) {
+ Text("Export Database")
+ }
+ FilledTonalButton(onClick = {
+ importLauncher.launch("application/json")
+ }) {
+ Text("Import Database")
+ }
+ FilledTonalButton(onClick = {
+ viewModel.onEvent(SettingsEvent.ClearDatabase)
+ }) {
+ Text("Delete Database")
+ }
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/theme/Color.kt b/app/src/main/java/com/example/android/january2022/ui/theme/Color.kt
index ebf7906..b96a278 100644
--- a/app/src/main/java/com/example/android/january2022/ui/theme/Color.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/theme/Color.kt
@@ -64,7 +64,6 @@ val md_theme_dark_surfaceTint = Color(0xFFFFB957)
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
val md_theme_dark_scrim = Color(0xFF000000)
-
val seed4 = Color(0xFFFFC881)
val seed3 = Color(0xFFFDCA6C)
val seed2 = Color(0xFFF6CF00)
diff --git a/app/src/main/java/com/example/android/january2022/ui/theme/Shape.kt b/app/src/main/java/com/example/android/january2022/ui/theme/Shape.kt
index 1aece24..10361a1 100644
--- a/app/src/main/java/com/example/android/january2022/ui/theme/Shape.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/theme/Shape.kt
@@ -9,12 +9,12 @@ import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(5.dp),
medium = RoundedCornerShape(10.dp),
- large = RoundedCornerShape(20.dp)
+ large = RoundedCornerShape(20.dp),
)
-fun CornerBasedShape.onlyTop() : CornerBasedShape {
+fun CornerBasedShape.onlyTop(): CornerBasedShape {
return copy(
bottomStart = CornerSize(0.dp),
- bottomEnd = CornerSize(0.dp)
+ bottomEnd = CornerSize(0.dp),
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/theme/Theme.kt b/app/src/main/java/com/example/android/january2022/ui/theme/Theme.kt
index 65d1074..6e555d4 100644
--- a/app/src/main/java/com/example/android/january2022/ui/theme/Theme.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/theme/Theme.kt
@@ -1,82 +1,82 @@
package com.example.android.january2022.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.*
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import com.google.accompanist.systemuicontroller.rememberSystemUiController
-
private val LightColors = lightColorScheme(
- primary = md_theme_light_primary,
- onPrimary = md_theme_light_onPrimary,
- primaryContainer = md_theme_light_primaryContainer,
- onPrimaryContainer = md_theme_light_onPrimaryContainer,
- secondary = md_theme_light_secondary,
- onSecondary = md_theme_light_onSecondary,
- secondaryContainer = md_theme_light_secondaryContainer,
- onSecondaryContainer = md_theme_light_onSecondaryContainer,
- tertiary = md_theme_light_tertiary,
- onTertiary = md_theme_light_onTertiary,
- tertiaryContainer = md_theme_light_tertiaryContainer,
- onTertiaryContainer = md_theme_light_onTertiaryContainer,
- error = md_theme_light_error,
- errorContainer = md_theme_light_errorContainer,
- onError = md_theme_light_onError,
- onErrorContainer = md_theme_light_onErrorContainer,
- background = md_theme_light_background,
- onBackground = md_theme_light_onBackground,
- surface = md_theme_light_surface,
- onSurface = md_theme_light_onSurface,
- surfaceVariant = md_theme_light_surfaceVariant,
- onSurfaceVariant = md_theme_light_onSurfaceVariant,
- outline = md_theme_light_outline,
- inverseOnSurface = md_theme_light_inverseOnSurface,
- inverseSurface = md_theme_light_inverseSurface,
- inversePrimary = md_theme_light_inversePrimary,
- surfaceTint = md_theme_light_surfaceTint,
- outlineVariant = md_theme_light_outlineVariant,
- scrim = md_theme_light_scrim,
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
)
-
private val DarkColors = darkColorScheme(
- primary = md_theme_dark_primary,
- onPrimary = md_theme_dark_onPrimary,
- primaryContainer = md_theme_dark_primaryContainer,
- onPrimaryContainer = md_theme_dark_onPrimaryContainer,
- secondary = md_theme_dark_secondary,
- onSecondary = md_theme_dark_onSecondary,
- secondaryContainer = md_theme_dark_secondaryContainer,
- onSecondaryContainer = md_theme_dark_onSecondaryContainer,
- tertiary = md_theme_dark_tertiary,
- onTertiary = md_theme_dark_onTertiary,
- tertiaryContainer = md_theme_dark_tertiaryContainer,
- onTertiaryContainer = md_theme_dark_onTertiaryContainer,
- error = md_theme_dark_error,
- errorContainer = md_theme_dark_errorContainer,
- onError = md_theme_dark_onError,
- onErrorContainer = md_theme_dark_onErrorContainer,
- background = md_theme_dark_background,
- onBackground = md_theme_dark_onBackground,
- surface = md_theme_dark_surface,
- onSurface = md_theme_dark_onSurface,
- surfaceVariant = md_theme_dark_surfaceVariant,
- onSurfaceVariant = md_theme_dark_onSurfaceVariant,
- outline = md_theme_dark_outline,
- inverseOnSurface = md_theme_dark_inverseOnSurface,
- inverseSurface = md_theme_dark_inverseSurface,
- inversePrimary = md_theme_dark_inversePrimary,
- surfaceTint = md_theme_dark_surfaceTint,
- outlineVariant = md_theme_dark_outlineVariant,
- scrim = md_theme_dark_scrim,
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ outlineVariant = md_theme_dark_outlineVariant,
+ scrim = md_theme_dark_scrim,
)
@Composable
fun WorkoutTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
- content: @Composable() () -> Unit
+ content: @Composable () -> Unit,
) {
val colorScheme = if (!useDarkTheme) {
LightColors
@@ -84,21 +84,21 @@ fun WorkoutTheme(
DarkColors
}
- // Remember a SystemUiController
- val systemUiController = rememberSystemUiController()
- val useDarkIcons = !useDarkTheme
+ // Remember a SystemUiController
+ val systemUiController = rememberSystemUiController()
+ val useDarkIcons = !useDarkTheme
- SideEffect {
- // Update all of the system bar colors to be transparent, and use
- // dark icons if we're in light theme
- systemUiController.setSystemBarsColor(
- color = Color.Transparent,
- darkIcons = useDarkIcons
+ SideEffect {
+ // Update all of the system bar colors to be transparent, and use
+ // dark icons if we're in light theme
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = useDarkIcons,
+ )
+ // setStatusBarColor() and setNavigationBarColor() also exist
+ }
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = content,
)
- // setStatusBarColor() and setNavigationBarColor() also exist
- }
- MaterialTheme(
- colorScheme = colorScheme,
- content = content
- )
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/ui/theme/Type.kt b/app/src/main/java/com/example/android/january2022/ui/theme/Type.kt
index eb48a9c..b7a5318 100644
--- a/app/src/main/java/com/example/android/january2022/ui/theme/Type.kt
+++ b/app/src/main/java/com/example/android/january2022/ui/theme/Type.kt
@@ -2,118 +2,116 @@ package com.example.android.january2022.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-//Replace with your font locations
+// Replace with your font locations
val Roboto = FontFamily.Default
val AppTypography = Typography(
- displayLarge = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 57.sp,
- lineHeight = 64.sp,
- letterSpacing = -0.25.sp,
- ),
- displayMedium = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 45.sp,
- lineHeight = 52.sp,
- letterSpacing = 0.sp,
- ),
- displaySmall = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 36.sp,
- lineHeight = 44.sp,
- letterSpacing = 0.sp,
- ),
- headlineLarge = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 32.sp,
- lineHeight = 40.sp,
- letterSpacing = 0.sp,
- ),
- headlineMedium = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 28.sp,
- lineHeight = 36.sp,
- letterSpacing = 0.sp,
- ),
- headlineSmall = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 24.sp,
- lineHeight = 32.sp,
- letterSpacing = 0.sp,
- ),
- titleLarge = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp,
- ),
- titleMedium = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.Medium,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.1.sp,
- ),
- titleSmall = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.Medium,
- fontSize = 14.sp,
- lineHeight = 20.sp,
- letterSpacing = 0.1.sp,
- ),
- labelLarge = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.Medium,
- fontSize = 14.sp,
- lineHeight = 20.sp,
- letterSpacing = 0.1.sp,
- ),
- bodyLarge = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp,
- ),
- bodyMedium = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 14.sp,
- lineHeight = 20.sp,
- letterSpacing = 0.25.sp,
- ),
- bodySmall = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.W400,
- fontSize = 12.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.4.sp,
- ),
- labelMedium = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.Medium,
- fontSize = 12.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp,
- ),
- labelSmall = TextStyle(
- fontFamily = Roboto,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp,
- ),
+ displayLarge = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 57.sp,
+ lineHeight = 64.sp,
+ letterSpacing = (-0.25).sp,
+ ),
+ displayMedium = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 45.sp,
+ lineHeight = 52.sp,
+ letterSpacing = 0.sp,
+ ),
+ displaySmall = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 36.sp,
+ lineHeight = 44.sp,
+ letterSpacing = 0.sp,
+ ),
+ headlineLarge = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp,
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp,
+ ),
+ headlineSmall = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp,
+ ),
+ titleLarge = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp,
+ ),
+ titleMedium = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.1.sp,
+ ),
+ titleSmall = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp,
+ ),
+ labelLarge = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp,
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp,
+ ),
+ bodySmall = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.W400,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.4.sp,
+ ),
+ labelMedium = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ labelSmall = TextStyle(
+ fontFamily = Roboto,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp,
+ ),
)
diff --git a/app/src/main/java/com/example/android/january2022/utils/BackPressHandler.kt b/app/src/main/java/com/example/android/january2022/utils/BackPressHandler.kt
deleted file mode 100644
index 37e7ba3..0000000
--- a/app/src/main/java/com/example/android/january2022/utils/BackPressHandler.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.example.android.january2022.utils
-
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.OnBackPressedDispatcher
-import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
-import androidx.compose.runtime.*
-
-@Composable
-fun BackPressHandler(
- backPressedDispatcher: OnBackPressedDispatcher? =
- LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher,
- onBackPressed: () -> Unit
-) {
- val currentOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
-
- val backCallback = remember {
- object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- currentOnBackPressed()
- }
- }
- }
-
- DisposableEffect(key1 = backPressedDispatcher) {
- backPressedDispatcher?.addCallback(backCallback)
-
- onDispose {
- backCallback.remove()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/january2022/utils/Event.kt b/app/src/main/java/com/example/android/january2022/utils/Event.kt
index b034aa5..13b6e49 100644
--- a/app/src/main/java/com/example/android/january2022/utils/Event.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/Event.kt
@@ -1,4 +1,3 @@
package com.example.android.january2022.utils
-interface Event {
-}
\ No newline at end of file
+interface Event
diff --git a/app/src/main/java/com/example/android/january2022/utils/FuzzySearch.kt b/app/src/main/java/com/example/android/january2022/utils/FuzzySearch.kt
index 14534d8..65b4882 100644
--- a/app/src/main/java/com/example/android/january2022/utils/FuzzySearch.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/FuzzySearch.kt
@@ -2,13 +2,13 @@ package com.example.android.january2022.utils
class FuzzySearch {
- companion object {
- fun regexMatch(query: String, text: String): Boolean {
- val q = query.lowercase().filter { it.isLetterOrDigit() || it.isWhitespace() }
- val t = text.lowercase().filter { it.isLetterOrDigit() || it.isWhitespace() }
- val regexp =
- q.split(" ").joinToString(separator = "", postfix = ".*") { "(?=.*$it)" }.toRegex()
- return regexp.matches(t)
+ companion object {
+ fun regexMatch(query: String, text: String): Boolean {
+ val q = query.lowercase().filter { it.isLetterOrDigit() || it.isWhitespace() }
+ val t = text.lowercase().filter { it.isLetterOrDigit() || it.isWhitespace() }
+ val regexp =
+ q.split(" ").joinToString(separator = "", postfix = ".*") { "(?=.*$it)" }.toRegex()
+ return regexp.matches(t)
+ }
}
- }
}
diff --git a/app/src/main/java/com/example/android/january2022/utils/Modifiers.kt b/app/src/main/java/com/example/android/january2022/utils/Modifiers.kt
index 3ad8b5e..8ddfe40 100644
--- a/app/src/main/java/com/example/android/january2022/utils/Modifiers.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/Modifiers.kt
@@ -3,7 +3,11 @@ package com.example.android.january2022.utils
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.isImeVisible
-import androidx.compose.runtime.*
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus.onFocusEvent
@@ -11,25 +15,25 @@ import androidx.compose.ui.platform.LocalFocusManager
@OptIn(ExperimentalLayoutApi::class)
fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed {
- var isFocused by remember { mutableStateOf(false) }
- var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) }
- if (isFocused) {
- val imeIsVisible = WindowInsets.isImeVisible
- val focusManager = LocalFocusManager.current
- LaunchedEffect(imeIsVisible) {
- if (imeIsVisible) {
- keyboardAppearedSinceLastFocused = true
- } else if (keyboardAppearedSinceLastFocused) {
- focusManager.clearFocus()
- }
+ var isFocused by remember { mutableStateOf(false) }
+ var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) }
+ if (isFocused) {
+ val imeIsVisible = WindowInsets.isImeVisible
+ val focusManager = LocalFocusManager.current
+ LaunchedEffect(imeIsVisible) {
+ if (imeIsVisible) {
+ keyboardAppearedSinceLastFocused = true
+ } else if (keyboardAppearedSinceLastFocused) {
+ focusManager.clearFocus()
+ }
+ }
}
- }
- onFocusEvent {
- if (isFocused != it.isFocused) {
- isFocused = it.isFocused
- if (isFocused) {
- keyboardAppearedSinceLastFocused = false
- }
+ onFocusEvent {
+ if (isFocused != it.isFocused) {
+ isFocused = it.isFocused
+ if (isFocused) {
+ keyboardAppearedSinceLastFocused = false
+ }
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/utils/MuscleConverters.kt b/app/src/main/java/com/example/android/january2022/utils/MuscleConverters.kt
index f8489ed..6c176c5 100644
--- a/app/src/main/java/com/example/android/january2022/utils/MuscleConverters.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/MuscleConverters.kt
@@ -6,84 +6,84 @@ import com.example.android.january2022.db.entities.SessionExerciseWithExercise
import timber.log.Timber
fun turnTargetIntoMuscleGroups(targets: List): List {
- return turnTargetIntoMuscleGroups(targets.joinToString("|"))
+ return turnTargetIntoMuscleGroups(targets.joinToString("|"))
}
fun turnTargetIntoMuscleGroups(targets: String): List {
- return targets.split("|").map { target ->
- when (target) {
- "Adductor Magnus, ischial fibers" -> MuscleGroup.HIPS
- "Adductors, Hip" -> MuscleGroup.HIPS
- "Back, General" -> MuscleGroup.BACK
- "Biceps Brachii" -> MuscleGroup.BICEPS
- "Brachialis" -> MuscleGroup.BICEPS
- "Brachioradialis" -> MuscleGroup.BICEPS
- "Deltoid, Anterior" -> MuscleGroup.SHOULDERS
- "Deltoid, Lateral" -> MuscleGroup.SHOULDERS
- "Deltoid, Posterior" -> MuscleGroup.SHOULDERS
- "Erector Spinae" -> MuscleGroup.BACK
- "Extensor Carpi Radialis" -> MuscleGroup.FOREARMS
- "Extensor Carpi Ulnaris" -> MuscleGroup.FOREARMS
- "External Hip Rotators" -> MuscleGroup.HIPS
- "Flexor Carpi Radialis" -> MuscleGroup.FOREARMS
- "Flexor Carpi Ulnaris" -> MuscleGroup.FOREARMS
- "Gastrocnemius" -> MuscleGroup.CALVES
- "Gluteus Maximus" -> MuscleGroup.GLUTES
- "Gluteus Medius" -> MuscleGroup.GLUTES
- "Gluteus Minimus" -> MuscleGroup.GLUTES
- "Gluteus Minimus, Anterior Fibers" -> MuscleGroup.GLUTES
- "Hamstrings" -> MuscleGroup.HAMSTRINGS
- "Hip Abductors" -> MuscleGroup.HIPS
- "Hip External Rotators" -> MuscleGroup.HIPS
- "Hip Internal Rotators" -> MuscleGroup.HIPS
- "Iliopsoas" -> MuscleGroup.HIPS
- "Infraspinatus" -> MuscleGroup.SHOULDERS
- "Latissimus Dorsi" -> MuscleGroup.BACK
- "Longus colli" -> MuscleGroup.BACK
- "Obliques" -> MuscleGroup.CORE
- "Pectoralis Major, Clavicular" -> MuscleGroup.CHEST
- "Pectoralis Major, Sternal" -> MuscleGroup.CHEST
- "Pectoralis Minor" -> MuscleGroup.CHEST
- "Piriformis" -> MuscleGroup.HIPS
- "Pronators" -> MuscleGroup.FOREARMS
- "Quadratus Femoris" -> MuscleGroup.QUADRICEPS
- "Quadriceps" -> MuscleGroup.QUADRICEPS
- "Rectus Abdominis" -> MuscleGroup.CORE
- "Rectus Femoris" -> MuscleGroup.QUADRICEPS
- "Rhomboids" -> MuscleGroup.BACK
- "Serratus Anterior" -> MuscleGroup.BACK
- "Soleus" -> MuscleGroup.CALVES
- "Splenius" -> MuscleGroup.BACK
- "Sternocleidomastoid" -> MuscleGroup.NECK
- "Subscapularis" -> MuscleGroup.SHOULDERS
- "Supinator" -> MuscleGroup.FOREARMS
- "Supraspinatus" -> MuscleGroup.SHOULDERS
- "Tensor Fasciae Latae" -> MuscleGroup.GLUTES
- "Teres Major" -> MuscleGroup.SHOULDERS
- "Teres Minor" -> MuscleGroup.SHOULDERS
- "Tibialis Anterior" -> MuscleGroup.CALVES
- "Transverse Abdominis" -> MuscleGroup.CORE
- "Trapezius, Lower" -> MuscleGroup.BACK
- "Trapezius, Middle" -> MuscleGroup.BACK
- "Trapezius, Upper" -> MuscleGroup.BACK
- "Triceps Brachii" -> MuscleGroup.TRICEPS
- "Triceps Brachii, Long Head" -> MuscleGroup.TRICEPS
- "Wrist Extensors" -> MuscleGroup.FOREARMS
- "Wrist Flexors" -> MuscleGroup.FOREARMS
- else -> "FAILURE".also { Timber.d("Failed with: $targets") }
- }
- }.distinct().filterNot { it == "FAILURE" }
+ return targets.split("|").map { target ->
+ when (target) {
+ "Adductor Magnus, ischial fibers" -> MuscleGroup.HIPS
+ "Adductors, Hip" -> MuscleGroup.HIPS
+ "Back, General" -> MuscleGroup.BACK
+ "Biceps Brachii" -> MuscleGroup.BICEPS
+ "Brachialis" -> MuscleGroup.BICEPS
+ "Brachioradialis" -> MuscleGroup.BICEPS
+ "Deltoid, Anterior" -> MuscleGroup.SHOULDERS
+ "Deltoid, Lateral" -> MuscleGroup.SHOULDERS
+ "Deltoid, Posterior" -> MuscleGroup.SHOULDERS
+ "Erector Spinae" -> MuscleGroup.BACK
+ "Extensor Carpi Radialis" -> MuscleGroup.FOREARMS
+ "Extensor Carpi Ulnaris" -> MuscleGroup.FOREARMS
+ "External Hip Rotators" -> MuscleGroup.HIPS
+ "Flexor Carpi Radialis" -> MuscleGroup.FOREARMS
+ "Flexor Carpi Ulnaris" -> MuscleGroup.FOREARMS
+ "Gastrocnemius" -> MuscleGroup.CALVES
+ "Gluteus Maximus" -> MuscleGroup.GLUTES
+ "Gluteus Medius" -> MuscleGroup.GLUTES
+ "Gluteus Minimus" -> MuscleGroup.GLUTES
+ "Gluteus Minimus, Anterior Fibers" -> MuscleGroup.GLUTES
+ "Hamstrings" -> MuscleGroup.HAMSTRINGS
+ "Hip Abductors" -> MuscleGroup.HIPS
+ "Hip External Rotators" -> MuscleGroup.HIPS
+ "Hip Internal Rotators" -> MuscleGroup.HIPS
+ "Iliopsoas" -> MuscleGroup.HIPS
+ "Infraspinatus" -> MuscleGroup.SHOULDERS
+ "Latissimus Dorsi" -> MuscleGroup.BACK
+ "Longus colli" -> MuscleGroup.BACK
+ "Obliques" -> MuscleGroup.CORE
+ "Pectoralis Major, Clavicular" -> MuscleGroup.CHEST
+ "Pectoralis Major, Sternal" -> MuscleGroup.CHEST
+ "Pectoralis Minor" -> MuscleGroup.CHEST
+ "Piriformis" -> MuscleGroup.HIPS
+ "Pronators" -> MuscleGroup.FOREARMS
+ "Quadratus Femoris" -> MuscleGroup.QUADRICEPS
+ "Quadriceps" -> MuscleGroup.QUADRICEPS
+ "Rectus Abdominis" -> MuscleGroup.CORE
+ "Rectus Femoris" -> MuscleGroup.QUADRICEPS
+ "Rhomboids" -> MuscleGroup.BACK
+ "Serratus Anterior" -> MuscleGroup.BACK
+ "Soleus" -> MuscleGroup.CALVES
+ "Splenius" -> MuscleGroup.BACK
+ "Sternocleidomastoid" -> MuscleGroup.NECK
+ "Subscapularis" -> MuscleGroup.SHOULDERS
+ "Supinator" -> MuscleGroup.FOREARMS
+ "Supraspinatus" -> MuscleGroup.SHOULDERS
+ "Tensor Fasciae Latae" -> MuscleGroup.GLUTES
+ "Teres Major" -> MuscleGroup.SHOULDERS
+ "Teres Minor" -> MuscleGroup.SHOULDERS
+ "Tibialis Anterior" -> MuscleGroup.CALVES
+ "Transverse Abdominis" -> MuscleGroup.CORE
+ "Trapezius, Lower" -> MuscleGroup.BACK
+ "Trapezius, Middle" -> MuscleGroup.BACK
+ "Trapezius, Upper" -> MuscleGroup.BACK
+ "Triceps Brachii" -> MuscleGroup.TRICEPS
+ "Triceps Brachii, Long Head" -> MuscleGroup.TRICEPS
+ "Wrist Extensors" -> MuscleGroup.FOREARMS
+ "Wrist Flexors" -> MuscleGroup.FOREARMS
+ else -> "FAILURE".also { Timber.d("Failed with: $targets") }
+ }
+ }.distinct().filterNot { it == "FAILURE" }
}
@JvmName("sortedListOfMuscleGroupsForSessionExercises")
fun List.sortedListOfMuscleGroups(): List {
- return this.map { it.exercise }.sortedListOfMuscleGroups()
+ return this.map { it.exercise }.sortedListOfMuscleGroups()
}
@JvmName("sortedListOfMuscleGroupsForExercises")
fun List.sortedListOfMuscleGroups(): List {
- return this.map { turnTargetIntoMuscleGroups(it.targets) }.flatten()
- .groupingBy { it }.eachCount().toList()
- .sortedByDescending { it.second }
- .map { it.first }
-}
\ No newline at end of file
+ return this.map { turnTargetIntoMuscleGroups(it.targets) }.flatten()
+ .groupingBy { it }.eachCount().toList()
+ .sortedByDescending { it.second }
+ .map { it.first }
+}
diff --git a/app/src/main/java/com/example/android/january2022/utils/Routes.kt b/app/src/main/java/com/example/android/january2022/utils/Routes.kt
index ef21973..daf8180 100644
--- a/app/src/main/java/com/example/android/january2022/utils/Routes.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/Routes.kt
@@ -5,4 +5,4 @@ object Routes {
const val SESSION = "session"
const val EXERCISE_PICKER = "exercisePicker"
const val SETTINGS = "settings"
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/utils/TypeConverters.kt b/app/src/main/java/com/example/android/january2022/utils/TypeConverters.kt
index 5dee762..4ca35e0 100644
--- a/app/src/main/java/com/example/android/january2022/utils/TypeConverters.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/TypeConverters.kt
@@ -1,33 +1,31 @@
package com.example.android.january2022.utils
import androidx.room.TypeConverter
-import timber.log.Timber
import java.time.LocalDateTime
-import javax.xml.transform.Source
class Converters {
- @TypeConverter
- fun fromString(source: String): List {
- return source.split("|")
- }
+ @TypeConverter
+ fun fromString(source: String): List {
+ return source.split("|")
+ }
- @TypeConverter
- fun fromList(source: List): String {
- return source.joinToString("|")
- }
+ @TypeConverter
+ fun fromList(source: List): String {
+ return source.joinToString("|")
+ }
- @TypeConverter
- fun fromDateTime(source: LocalDateTime?): String {
- return source?.toString() ?: ""
- }
+ @TypeConverter
+ fun fromDateTime(source: LocalDateTime?): String {
+ return source?.toString() ?: ""
+ }
- @TypeConverter
- fun toDateTime(source: String?): LocalDateTime? {
- return try {
- LocalDateTime.parse(source)
- } catch (e: Exception) {
- null
+ @TypeConverter
+ fun toDateTime(source: String?): LocalDateTime? {
+ return try {
+ LocalDateTime.parse(source)
+ } catch (e: Exception) {
+ null
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/android/january2022/utils/UiEvent.kt b/app/src/main/java/com/example/android/january2022/utils/UiEvent.kt
index c60af23..4e1d065 100644
--- a/app/src/main/java/com/example/android/january2022/utils/UiEvent.kt
+++ b/app/src/main/java/com/example/android/january2022/utils/UiEvent.kt
@@ -2,11 +2,11 @@ package com.example.android.january2022.utils
sealed class UiEvent {
data class OpenWebsite(val url: String) : UiEvent()
- data class Navigate(val route: String, val popBackStack: Boolean = false): UiEvent()
+ data class Navigate(val route: String, val popBackStack: Boolean = false) : UiEvent()
data class FileCreated(val fileName: String) : UiEvent()
- object ToggleTimer: UiEvent()
- object ResetTimer: UiEvent()
- object IncrementTimer: UiEvent()
- object DecrementTimer: UiEvent()
+ object ToggleTimer : UiEvent()
+ object ResetTimer : UiEvent()
+ object IncrementTimer : UiEvent()
+ object DecrementTimer : UiEvent()
}
diff --git a/app/src/test/java/com/example/android/january2022/ExampleUnitTest.kt b/app/src/test/java/com/example/android/january2022/ExampleUnitTest.kt
index 851becf..444096d 100644
--- a/app/src/test/java/com/example/android/january2022/ExampleUnitTest.kt
+++ b/app/src/test/java/com/example/android/january2022/ExampleUnitTest.kt
@@ -1,9 +1,8 @@
package com.example.android.january2022
+import junit.framework.TestCase.assertEquals
import org.junit.Test
-import org.junit.Assert.*
-
/**
* Example local unit test, which will execute on the development machine (host).
*
@@ -14,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
-}
\ No newline at end of file
+}