Skip to content

Commit 0f382ac

Browse files
committed
feat: Implement circular progress indicator and enhance UI/UX
This commit introduces a circular progress indicator for the timer, refactors UI components, and improves the overall user experience with refined animations and layout adjustments. Specific changes: - Added `TimerDisplay.kt` component with a circular progress indicator option. - Modified `Timer.kt`: - Renamed to `Timer.kt` (previously `Timer.kt` under `handlers` package). - `isTimerRunning` is now a `mutableStateOf<Boolean>`. - Added `timerProgress` to track progress as a float. - `formattedTime` is now a `mutableStateOf<String>`. - Added `getTimerProgress()` method. - Updated `PomoDoro.kt`: - Renamed from `PomoDoro.kt` (previously `PomoDoro.kt` under `handlers` package). - Renamed `routineSettings` to `appSettings`. - `PomoDoroSettings` properties are now `mutableStateOf` or `mutableFloatStateOf`. - Added `enableProgressIndicator` to `PomoDoroSettings`. - `currentTimer` is now `timerInstance`. - `workSessionsCompleted` is now a `mutableStateOf<Int>`. - Updated `TimerScreen.kt`: - Renamed from `Timer.kt`. - Integrated `TimerDisplay` component. - Adjusted spacing based on `enableProgressIndicator`. - Display work sessions completed count. - Updated `SettingsScreen.kt`: - Renamed from `Settings.kt`. - Added vertical scrollbar. - Added a toggle option for `enableProgressIndicator`. - Updated `App.kt`: - Reduced screen transition animation duration from 400ms to 300ms. - Updated `main.kt`: - Adjusted minimum window dimensions. - Updated `ThemeManager.kt` and theme files: - Added `mantle`, `surface`, `surface100`, `surface200` color properties. - Renamed `textLight` to `text100` and `textDark` to `text200`. - Updated `README.md`: - Changed main icon from PNG to SVG. - Added a GIF showcasing the app. - Updated screenshot. - Updated `index.html` (website): - Added app GIF. - Updated hero section layout and styling. - Updated download links to version `1.1.0`. - Updated `build.gradle.kts`: - Incremented `packageVersion` to `1.1.0`. - Updated `.github/workflows/build.yml`: - Changed release name suffix from Alpha to Beta. - Added `pomolin.gif` to `docs/static` and `gitAssets`.
1 parent 67fb133 commit 0f382ac

19 files changed

Lines changed: 590 additions & 301 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ jobs:
247247
- name: Create Draft Release
248248
uses: softprops/action-gh-release@v2
249249
with:
250-
name: Pomolin v${{ steps.get_version.outputs.version }} Alpha
250+
name: Pomolin v${{ steps.get_version.outputs.version }} Beta
251251
draft: true
252252
tag_name: v${{ steps.get_version.outputs.version }}
253253
body: |

README.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="composeApp/src/desktopMain/composeResources/drawable/Pomolin.png" alt="Pomolin app icon" width="200"/>
2+
<img src="gitAssets/Pomolin.svg" alt="Pomolin app icon" width="200"/>
33
</p>
44

55

@@ -14,7 +14,9 @@
1414
<h1 align="center">Pomolin</h1>
1515

1616
<p align="center">
17-
<img src="gitAssets/pomolin.avif" alt="Pomolin app icon" width="450"/>
17+
<img src="gitAssets/pomolin.avif" width="45%" />
18+
&nbsp;&nbsp;
19+
<img src="gitAssets/pomolin.gif" width="45%" />
1820
</p>
1921

2022
A simple, beautiful, and minimalist Pomodoro timer for your desktop. Designed to help you stay
@@ -28,8 +30,8 @@ intervals.
2830
* **Animated Timer:** The timer digits animate smoothly, offering a dynamic and visually
2931
pleasing experience.
3032
* **Interactive Controls:**
31-
* The Play/Pause button animates to clearly indicate the timer's state.
32-
* A satisfying rotation animation plays on the reset button when clicked.
33+
* The Play/Pause button animates to clearly indicate the timer's state.
34+
* A satisfying rotation animation plays on the reset button when clicked.
3335
* **Routine Management:** Switch between **Pomodoro (25 min)**, **Short Break (5
3436
min)**, and **Long Break (20 min)** routines.
3537
* **Audio Notifications:** The app plays a sound when the timer completes.
@@ -53,7 +55,8 @@ You can download the latest version of Pomolin from the
5355
1. Download the `.dmg` or `.pkg` file.
5456
2. Open the file and drag the `Pomolin.app` to your `Applications` folder.
5557

56-
> NOTE: For macOS, it will not allow you to run the .pkg or the app installed using .dmg, to do so you will have to go
58+
> NOTE: For macOS, it will not allow you to run the .pkg or the app installed using .dmg, to do so
59+
> you will have to go
5760
> to privacy and security in settings, and then allow the execution of the app.
5861
5962
### Linux
@@ -63,7 +66,8 @@ You can download the latest version of Pomolin from the
6366
```bash
6467
sudo apt install ./pomolin_1.0.2_amd64.deb
6568
```
66-
or Install `AppImage` using `AppImageLauncher` or equivalent. You can run the AppImage just by double-clicking on it
69+
or Install `AppImage` using `AppImageLauncher` or equivalent. You can run the AppImage just by
70+
double-clicking on it
6771
too, no need to install it.
6872

6973
> NOTE: AppImage is the preferred way to run the app on Linux-based distributions.
@@ -93,18 +97,18 @@ To build Pomolin from the source code, you'll need to have **JDK** and **Git** i
9397

9498
2. **Build the application using Gradle:**
9599

96-
* On **Linux**:
97-
```bash
98-
./gradlew packageReleaseDeb
99-
```
100-
* On **macOS**:
101-
```bash
102-
./gradlew packageReleaseDmg
103-
```
104-
* On **Windows**:
105-
```bash
106-
.\gradlew.bat packageReleaseMsi
107-
```
100+
* On **Linux**:
101+
```bash
102+
./gradlew packageReleaseDeb
103+
```
104+
* On **macOS**:
105+
```bash
106+
./gradlew packageReleaseDmg
107+
```
108+
* On **Windows**:
109+
```bash
110+
.\gradlew.bat packageReleaseMsi
111+
```
108112

109113
3. The compiled application will be available in the `composeApp/build/compose/binaries/` directory.
110114

composeApp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ compose.desktop {
4949
TargetFormat.Rpm
5050
)
5151
packageName = "pomolin"
52-
packageVersion = "1.0.4"
52+
packageVersion = "1.1.0"
5353
description = "A simple Pomodoro App written in Kotlin. Focus on what matters! "
5454
vendor = "RedddFoxxyy"
5555
licenseFile.set(project.file("../LICENSE"))

composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/App.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.safeContentPadding
1010
import androidx.compose.material3.MaterialTheme
1111
import androidx.compose.material3.Scaffold
12-
import androidx.compose.runtime.*
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.runtime.mutableStateOf
15+
import androidx.compose.runtime.remember
16+
import androidx.compose.runtime.setValue
1317
import androidx.compose.ui.Modifier
14-
import com.redddfoxxyy.pomolin.handlers.PomoDoro
18+
import com.redddfoxxyy.pomolin.data.PomoDoro
1519
import com.redddfoxxyy.pomolin.ui.screens.SettingsScreen
1620
import com.redddfoxxyy.pomolin.ui.screens.TimerScreen
1721
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -33,21 +37,20 @@ fun App() {
3337
transitionSpec = {
3438
if (targetState == Screen.Settings) {
3539
slideInHorizontally(
36-
animationSpec = tween(400),
40+
animationSpec = tween(300),
3741
initialOffsetX = { fullWidth -> -fullWidth }
3842
) togetherWith
39-
4043
slideOutHorizontally(
41-
animationSpec = tween(400),
44+
animationSpec = tween(300),
4245
targetOffsetX = { fullWidth -> fullWidth }
4346
)
4447
} else {
4548
slideInHorizontally(
46-
animationSpec = tween(400),
49+
animationSpec = tween(300),
4750
initialOffsetX = { fullWidth -> fullWidth }
4851
) togetherWith
4952
slideOutHorizontally(
50-
animationSpec = tween(400),
53+
animationSpec = tween(300),
5154
targetOffsetX = { fullWidth -> -fullWidth }
5255
)
5356
}

composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/handlers/Audio.kt renamed to composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/data/Audio.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.redddfoxxyy.pomolin.handlers
1+
package com.redddfoxxyy.pomolin.data
22

33
import javazoom.jlgui.basicplayer.BasicPlayer
44
import pomolin.composeapp.generated.resources.Res

composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/handlers/PomoDoro.kt renamed to composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/data/PomoDoro.kt

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
package com.redddfoxxyy.pomolin.handlers
1+
package com.redddfoxxyy.pomolin.data
22

33
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableFloatStateOf
45
import androidx.compose.runtime.mutableStateOf
56
import androidx.compose.runtime.setValue
67

@@ -10,74 +11,75 @@ enum class PomoDoroRoutines(val displayName: String) {
1011
LongBreak("Long Break")
1112
}
1213

13-
internal data class PomoDoroSettings(
14-
var workingDuration: Float = 25f,
15-
var shortBreakDuration: Float = 5f,
16-
var longBreakDuration: Float = 20f,
17-
var workSessionDuration: Int = 2
18-
)
14+
internal class PomoDoroSettings {
15+
var workingDuration by mutableFloatStateOf(25f)
16+
var shortBreakDuration by mutableFloatStateOf(5f)
17+
var longBreakDuration by mutableFloatStateOf(20f)
18+
var workSessionDuration by mutableStateOf(4)
19+
var enableProgressIndicator by mutableStateOf(true)
20+
}
1921

2022

2123
internal class PomoDoro {
2224
internal val appAudioManager = Audio()
23-
internal val currentTimer by mutableStateOf(Timer(25f, appAudioManager, ::progressToNextRoutine))
25+
internal val timerInstance = Timer(25f, appAudioManager, ::progressToNextRoutine)
2426

2527
internal val routineList = PomoDoroRoutines.entries.toList()
26-
internal var routineSettings by mutableStateOf(PomoDoroSettings())
28+
internal var appSettings = PomoDoroSettings()
2729
internal var currentRoutine by mutableStateOf(PomoDoroRoutines.Working)
28-
private var workSessionsCompleted = 0
30+
internal var workSessionsCompleted by mutableStateOf(0)
2931

30-
internal fun startTimer() = currentTimer.startTimer()
31-
internal fun pauseTimer() = currentTimer.pause()
32+
internal fun startTimer() = timerInstance.startTimer()
33+
internal fun pauseTimer() = timerInstance.pause()
3234
internal fun resetTimer() {
33-
currentTimer.reset()
35+
timerInstance.reset()
3436
}
3537

3638
private fun getDurationFor(routine: PomoDoroRoutines): Float = when (routine) {
37-
PomoDoroRoutines.Working -> routineSettings.workingDuration
38-
PomoDoroRoutines.ShortBreak -> routineSettings.shortBreakDuration
39-
PomoDoroRoutines.LongBreak -> routineSettings.longBreakDuration
39+
PomoDoroRoutines.Working -> appSettings.workingDuration
40+
PomoDoroRoutines.ShortBreak -> appSettings.shortBreakDuration
41+
PomoDoroRoutines.LongBreak -> appSettings.longBreakDuration
4042
}
4143

4244
internal fun setRoutine(routine: PomoDoroRoutines) {
43-
currentTimer.updateDuration(getDurationFor(routine))
45+
timerInstance.updateDuration(getDurationFor(routine))
4446
currentRoutine = routine
4547
}
4648

4749
internal fun changeWorkingDuration(duration: Float) {
4850
val durationStepped = duration.toInt().toFloat()
49-
routineSettings = routineSettings.copy(workingDuration = durationStepped)
51+
appSettings.workingDuration = durationStepped
5052
if (currentRoutine == PomoDoroRoutines.Working) {
51-
currentTimer.updateDuration(durationStepped)
53+
timerInstance.updateDuration(durationStepped)
5254
}
5355
}
5456

5557
internal fun changeShortBreakDuration(duration: Float) {
5658
val durationStepped = duration.toInt().toFloat()
57-
routineSettings = routineSettings.copy(shortBreakDuration = durationStepped)
59+
appSettings.shortBreakDuration = durationStepped
5860
if (currentRoutine == PomoDoroRoutines.ShortBreak) {
59-
currentTimer.updateDuration(durationStepped)
61+
timerInstance.updateDuration(durationStepped)
6062
}
6163
}
6264

6365
internal fun changeLongBreakDuration(duration: Float) {
6466
val durationStepped = duration.toInt().toFloat()
65-
routineSettings = routineSettings.copy(longBreakDuration = durationStepped)
67+
appSettings.longBreakDuration = durationStepped
6668
if (currentRoutine == PomoDoroRoutines.LongBreak) {
67-
currentTimer.updateDuration(durationStepped)
69+
timerInstance.updateDuration(durationStepped)
6870
}
6971
}
7072

7173
internal fun changeWorkSessionDuration(duration: Float) {
7274
val durationStepped = duration.toInt()
73-
routineSettings = routineSettings.copy(workSessionDuration = durationStepped)
75+
appSettings.workSessionDuration = durationStepped
7476
}
7577

7678
private fun progressToNextRoutine() {
7779
when (currentRoutine) {
7880
PomoDoroRoutines.Working -> {
7981
workSessionsCompleted++
80-
if (workSessionsCompleted >= routineSettings.workSessionDuration) {
82+
if (workSessionsCompleted >= appSettings.workSessionDuration) {
8183
setRoutine(PomoDoroRoutines.LongBreak)
8284
workSessionsCompleted = 0
8385
} else {

composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/handlers/Timer.kt renamed to composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/data/Timer.kt

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
1-
package com.redddfoxxyy.pomolin.handlers
1+
package com.redddfoxxyy.pomolin.data
22

3+
import androidx.compose.runtime.getValue
34
import androidx.compose.runtime.mutableStateOf
4-
import kotlinx.coroutines.*
5-
import kotlinx.coroutines.flow.MutableStateFlow
5+
import androidx.compose.runtime.setValue
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.cancel
9+
import kotlinx.coroutines.delay
10+
import kotlinx.coroutines.launch
611

712
internal class Timer(durationMinutes: Float, val audio: Audio, private val onFinished: () -> Unit) {
8-
private var coroutineScope = CoroutineScope(Dispatchers.Main)
913

10-
// Timer Handlers
14+
internal var isTimerRunning by mutableStateOf(false)
1115
private var initialTimeMillis = (durationMinutes * 60 * 1000L).toLong()
1216
private var remainingTimeMillis = initialTimeMillis
13-
internal val formatedTime = mutableStateOf(formatTime(initialTimeMillis))
17+
private var timerProgress = 1.0f
18+
internal var formatedTime by mutableStateOf(formatTime(initialTimeMillis))
1419
private var lastUpdateTime = 0L
15-
internal val isTimerRunning = MutableStateFlow(false)
20+
21+
private var coroutineScope = CoroutineScope(Dispatchers.Main)
22+
1623
private var completionAudioPlayed = false
1724

1825
internal fun startTimer() {
19-
if (isTimerRunning.value || remainingTimeMillis <= 0L) return
26+
if (isTimerRunning || remainingTimeMillis <= 0L) return
2027

2128
coroutineScope.launch {
2229
lastUpdateTime = System.currentTimeMillis()
23-
isTimerRunning.value = true
30+
isTimerRunning = true
2431

25-
while (isTimerRunning.value && remainingTimeMillis > 0) {
32+
while (isTimerRunning && remainingTimeMillis > 0) {
2633
delay(50L)
2734
val elapsed = System.currentTimeMillis() - lastUpdateTime
2835
remainingTimeMillis -= elapsed
2936
lastUpdateTime = System.currentTimeMillis()
37+
timerProgress = remainingTimeMillis.toFloat() / initialTimeMillis.toFloat()
3038

3139
if (remainingTimeMillis < 0L) {
3240
remainingTimeMillis = 0L
@@ -37,26 +45,26 @@ internal class Timer(durationMinutes: Float, val audio: Audio, private val onFin
3745
completionAudioPlayed = true
3846
}
3947

40-
formatedTime.value = formatTime(remainingTimeMillis)
48+
formatedTime = formatTime(remainingTimeMillis)
4149
}
4250

4351
if (remainingTimeMillis <= 0L) {
44-
isTimerRunning.value = false
52+
isTimerRunning = false
4553
completionAudioPlayed = false
4654
onFinished()
4755
}
4856
}
4957
}
5058

5159
internal fun pause() {
52-
isTimerRunning.value = false
60+
isTimerRunning = false
5361
}
5462

5563
internal fun reset() {
56-
isTimerRunning.value = false
64+
isTimerRunning = false
5765
remainingTimeMillis = initialTimeMillis
5866
lastUpdateTime = 0L
59-
formatedTime.value = formatTime(initialTimeMillis)
67+
formatedTime = formatTime(initialTimeMillis)
6068
completionAudioPlayed = false
6169
coroutineScope.cancel()
6270
coroutineScope = CoroutineScope(Dispatchers.Main)
@@ -66,7 +74,11 @@ internal class Timer(durationMinutes: Float, val audio: Audio, private val onFin
6674
reset()
6775
initialTimeMillis = (durationMinutes * 60 * 1000L).toLong()
6876
remainingTimeMillis = initialTimeMillis
69-
formatedTime.value = formatTime(initialTimeMillis)
77+
formatedTime = formatTime(initialTimeMillis)
78+
}
79+
80+
internal fun getTimerProgress(): Float {
81+
return timerProgress
7082
}
7183

7284
private fun formatTime(millis: Long): String {

composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/main.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import javax.swing.SwingUtilities
1212

1313
fun main() = application {
1414
val icon = painterResource(Res.drawable.Pomolin)
15-
val minWindowDimensions = Pair(400, 570)
15+
val minWindowDimensions = Pair(380, 530)
1616
Window(
1717
onCloseRequest = ::exitApplication,
1818
title = "Pomolin",
@@ -28,7 +28,7 @@ fun main() = application {
2828
minimumSize = Dimension(minWindowDimensions.first, minWindowDimensions.second)
2929
}
3030
// WindowDraggableArea {
31-
// Box(Modifier.fillMaxWidth().height(20.dp).background(Color.DarkGray))
31+
// Box(Modifier.fillMaxWidth().height(50.dp).background(Color.White))
3232
// }
3333
App()
3434
}

composeApp/src/desktopMain/kotlin/com/redddfoxxyy/pomolin/ui/ThemeManager.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ interface Theme {
3131
}
3232

3333
interface ThemeColors {
34-
val base: Color
34+
3535
val crust: Color
36+
val mantle: Color
37+
val base: Color
38+
val surface: Color
39+
val surface100: Color
40+
val surface200: Color
3641
val text: Color
37-
val textLight: Color
38-
val textDark: Color
42+
val text100: Color
43+
val text200: Color
3944
val green: Color
4045
val red: Color
4146
val blue: Color

0 commit comments

Comments
 (0)