Skip to content
Merged

#15 #76

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum class PomodoroMode(
) {
CLASSIC(
displayName = "Classic Pomodoro",
emoji = "🍅",
emoji = "\uD83C\uDFAF",
sessionMinutes = 25,
breakMinutes = 5,
sessionsPerRound = 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class PomodoroTimerService(private val project: Project) {
private var job: Job? = null

private var settings = PomodoroMode.CLASSIC.toSettings()
private var currentPhase = TimerPhase.WORK
private var internalPhase = TimerPhase.WORK
private var remainingTimeMs: Long = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong())

private val _timeLeft = MutableStateFlow(formatTime(remainingTimeMs))
Expand All @@ -43,6 +43,9 @@ class PomodoroTimerService(private val project: Project) {
private val _currentSession = MutableStateFlow(1)
val currentSession: StateFlow<Int> = _currentSession

private val _currentPhase = MutableStateFlow(TimerPhase.WORK)
val currentPhase: StateFlow<TimerPhase> = _currentPhase

private val _settings = MutableStateFlow(settings)
val settingsFlow: StateFlow<PomodoroSettings> = _settings

Expand Down Expand Up @@ -70,7 +73,7 @@ class PomodoroTimerService(private val project: Project) {
val currentSessionNum = _currentSession.value
val totalSessions = settings.sessionsPerRound

if (currentPhase == TimerPhase.WORK) {
if (internalPhase == TimerPhase.WORK) {
// Work session complete
if (currentSessionNum >= totalSessions) {
// Last session complete - all done!
Expand All @@ -84,23 +87,27 @@ class PomodoroTimerService(private val project: Project) {
.notify(project)

// Reset to initial state
currentPhase = TimerPhase.WORK
internalPhase = TimerPhase.WORK
_currentPhase.value = TimerPhase.WORK
_currentSession.value = 1
remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong())
_timeLeft.value = formatTime(remainingTimeMs)
} else {
// Work session complete - start break
_currentSession.value = currentSessionNum + 1

NotificationGroupManager.getInstance()
.getNotificationGroup(NOTIFICATION_GROUP_ID)
.createNotification(
"\uD83C\uDF45 Session Complete!",
" Session $currentSessionNum Complete!",
"Great work! Starting ${settings.breakMinutes}-minute break ☕.",
NotificationType.INFORMATION
)
.notify(project)

// Start break timer
currentPhase = TimerPhase.BREAK
internalPhase = TimerPhase.BREAK
_currentPhase.value = TimerPhase.BREAK
remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.breakMinutes.toLong())
_timeLeft.value = formatTime(remainingTimeMs)
start()
Expand All @@ -113,13 +120,14 @@ class PomodoroTimerService(private val project: Project) {
.getNotificationGroup(NOTIFICATION_GROUP_ID)
.createNotification(
"☕ Break Complete!",
"Starting session ${currentSessionNum + 1} of $totalSessions.",
"Starting session $currentSessionNum of $totalSessions.",
NotificationType.INFORMATION
)
.notify(project)

// Start next work session
currentPhase = TimerPhase.WORK
internalPhase = TimerPhase.WORK
_currentPhase.value = TimerPhase.WORK
_currentSession.value = currentSessionNum + 1
remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong())
_timeLeft.value = formatTime(remainingTimeMs)
Expand All @@ -141,7 +149,8 @@ class PomodoroTimerService(private val project: Project) {
job = null

// Reset to initial state
currentPhase = TimerPhase.WORK
internalPhase = TimerPhase.WORK
_currentPhase.value = TimerPhase.WORK
remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong())
_timeLeft.value = formatTime(remainingTimeMs)
_currentSession.value = 1
Expand All @@ -162,7 +171,8 @@ class PomodoroTimerService(private val project: Project) {

settings = newSettings
_settings.value = newSettings
currentPhase = TimerPhase.WORK
internalPhase = TimerPhase.WORK
_currentPhase.value = TimerPhase.WORK
remainingTimeMs = TimeUnit.MINUTES.toMillis(newSettings.sessionMinutes.toLong())
_timeLeft.value = formatTime(remainingTimeMs)
_currentSession.value = 1
Expand All @@ -176,7 +186,7 @@ class PomodoroTimerService(private val project: Project) {
fun getSettings(): PomodoroSettings = settings

fun getProgress(): Float {
val totalMs = if (currentPhase == TimerPhase.WORK) {
val totalMs = if (internalPhase == TimerPhase.WORK) {
TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong())
} else {
TimeUnit.MINUTES.toMillis(settings.breakMinutes.toLong())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class DevFocusToolWindowFactory : ToolWindowFactory {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val panel = PomodoroToolWindowPanel(project)
val content = ContentFactory.getInstance().createContent(panel, "", false)
content.setDisposer(panel)
toolWindow.contentManager.addContent(content)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@ import com.github.akshayashokcode.devfocus.services.pomodoro.PomodoroTimerServic
import com.github.akshayashokcode.devfocus.ui.components.CircularTimerPanel
import com.github.akshayashokcode.devfocus.ui.components.SessionIndicatorPanel
import com.github.akshayashokcode.devfocus.ui.settings.PomodoroSettingsPanel
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.ui.components.JBPanel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import java.awt.*
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Font
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.*

class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>>(BorderLayout()) {
class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>>(BorderLayout()), Disposable {

private val timerService = project.getService(PomodoroTimerService::class.java) ?: error("PomodoroTimerService not available")

// Layout orientation tracking
private var isHorizontalLayout = false

// Mode selector
private val modeComboBox = JComboBox(PomodoroMode.entries.toTypedArray()).apply {
selectedItem = PomodoroMode.CLASSIC
Expand All @@ -25,7 +34,12 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
// Info label showing current mode settings
private val infoLabel = JLabel("📊 25 min work • 5 min break").apply {
horizontalAlignment = SwingConstants.CENTER
font = font.deriveFont(Font.PLAIN, 12f)
font = font.deriveFont(Font.BOLD, 12f)
}

private val sessionTextLabel = JLabel("Session 1 of 4").apply {
horizontalAlignment = SwingConstants.CENTER
font = font.deriveFont(Font.BOLD, 14f)
}

// Circular timer display
Expand All @@ -35,9 +49,18 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
private val sessionIndicator = SessionIndicatorPanel()

// Control buttons
private val startButton = JButton("Start")
private val pauseButton = JButton("Pause")
private val resetButton = JButton("Reset")
private val startButton = JButton("Start").apply {
preferredSize = Dimension(80, 32)
// Make it a prominent primary button
putClientProperty("JButton.buttonType", "default")
font = font.deriveFont(Font.BOLD)
}
private val pauseButton = JButton("Pause").apply {
preferredSize = Dimension(80, 32)
}
private val resetButton = JButton("Reset").apply {
preferredSize = Dimension(80, 32)
}

// Custom settings panel (only visible when Custom mode selected)
private val settingsPanel = PomodoroSettingsPanel { session, breakTime, sessions ->
Expand All @@ -50,40 +73,55 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
private var stateJob: Job? = null
private var timeJob: Job? = null
private var sessionJob: Job? = null
private var phaseJob: Job? = null

init {
buildUI()
setupListeners()
observeTimer()
updateSettingsPanelVisibility()
setupLayoutListener()
}

private fun buildUI() {
if(isHorizontalLayout) {
buildHorizontalLayout()
} else {
buildVerticalLayout()
}
}

private fun buildVerticalLayout() {
// Top panel with mode selector
val topPanel = JPanel(BorderLayout(5, 5)).apply {
border = BorderFactory.createEmptyBorder(10, 10, 5, 10)
add(modeComboBox, BorderLayout.CENTER)
}

// Info panel
val infoPanel = JPanel(FlowLayout(FlowLayout.CENTER)).apply {
val infoPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 5)).apply {
add(infoLabel)
}

// Timer panel
val timerPanel = JPanel(BorderLayout()).apply {
border = BorderFactory.createEmptyBorder(20, 10, 20, 10)
border = BorderFactory.createEmptyBorder(15, 10, 10, 10)
add(circularTimer, BorderLayout.CENTER)
}

// Session text label panel
val sessionPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 5)).apply {
add(sessionTextLabel)
}

// Progress panel
val progressPanel = JPanel(BorderLayout(5, 5)).apply {
border = BorderFactory.createEmptyBorder(0, 20, 10, 20)
border = BorderFactory.createEmptyBorder(5, 20, 10, 20)
add(sessionIndicator, BorderLayout.CENTER)
}

// Button panel
val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 10, 5)).apply {
val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 8, 5)).apply {
add(startButton)
add(pauseButton)
add(resetButton)
Expand All @@ -94,6 +132,7 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
layout = BoxLayout(this, BoxLayout.Y_AXIS)
add(infoPanel)
add(timerPanel)
add(sessionPanel)
add(progressPanel)
add(buttonPanel)
}
Expand All @@ -103,6 +142,10 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
add(settingsPanel, BorderLayout.SOUTH)
}

private fun buildHorizontalLayout() {
buildVerticalLayout()
}

private fun setupListeners() {
startButton.addActionListener { timerService.start() }
pauseButton.addActionListener { timerService.pause() }
Expand Down Expand Up @@ -131,15 +174,58 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
}

private fun updateProgressBar(totalSessions: Int) {
sessionIndicator.updateSessions(timerService.currentSession.value, totalSessions)
val currentSession = timerService.currentSession.value
sessionIndicator.updateSessions(currentSession, totalSessions)
sessionTextLabel.text = "Session $currentSession of $totalSessions"
}

private fun setupLayoutListener() {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) {
checkAndUpdateLayout()
}
})
}

private fun checkAndUpdateLayout() {
val width = width
val height = height

// Determine if we should use horizontal layout (width > height * 1.5)
val shouldBeHorizontal = width > height * 1.5

// Only rebuild if layout orientation changed
if (shouldBeHorizontal != isHorizontalLayout) {
isHorizontalLayout = shouldBeHorizontal
rebuildLayout()
}
}

private fun rebuildLayout() {
// Remove all components
removeAll()

// Rebuild UI with new layout
buildUI()

// Reconnect listeners (buttons are recreated, need new listeners)
setupListeners()

// Update setting panel visibility
updateSettingsPanelVisibility()

// Refresh the panel
revalidate()
repaint()
}

private fun observeTimer() {
timeJob = scope.launch {
timerService.timeLeft.collectLatest { time ->
SwingUtilities.invokeLater {
val progress = timerService.getProgress()
circularTimer.updateTimer(time, progress, false)
val isBreak = timerService.currentPhase.value == PomodoroTimerService.TimerPhase.BREAK
circularTimer.updateTimer(time, progress, isBreak)
}
}
}
Expand All @@ -150,8 +236,24 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
startButton.isEnabled = it != PomodoroTimerService.TimerState.RUNNING
pauseButton.isEnabled = it == PomodoroTimerService.TimerState.RUNNING
resetButton.isEnabled = it != PomodoroTimerService.TimerState.IDLE
// Disable mode selector when timer is active (running or paused)
modeComboBox.isEnabled = it == PomodoroTimerService.TimerState.IDLE

// Check if we're truly idle (session and work phase) or just transitioning
val currentSession = timerService.currentSession.value
val currentPhase = timerService.currentPhase.value
val isTrulyIdle = it == PomodoroTimerService.TimerState.IDLE &&
currentSession == 1 &&
currentPhase == PomodoroTimerService.TimerPhase.WORK

modeComboBox.isEnabled = isTrulyIdle

// Hide custom settings panel when timer is active
if (!isTrulyIdle && modeComboBox.selectedItem == PomodoroMode.CUSTOM) {
settingsPanel.isVisible = false
revalidate()
repaint()
} else if (isTrulyIdle){
updateSettingsPanelVisibility()
}
}
}
}
Expand All @@ -160,16 +262,30 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
timerService.currentSession.collectLatest { session ->
SwingUtilities.invokeLater {
val settings = timerService.getSettings()
sessionIndicator.updateSessions(session, settings.sessionsPerRound)
val isBreak = timerService.currentPhase.value == PomodoroTimerService.TimerPhase.BREAK
sessionIndicator.updateSessions(session, settings.sessionsPerRound, isBreak)
sessionTextLabel.text = "Session $session of ${settings.sessionsPerRound}"
}
}
}

phaseJob = scope.launch {
timerService.currentPhase.collectLatest { phase ->
SwingUtilities.invokeLater {
val settings = timerService.getSettings()
val session = timerService.currentSession.value
val isBreak = phase == PomodoroTimerService.TimerPhase.BREAK
sessionIndicator.updateSessions(session, settings.sessionsPerRound, isBreak)
}
}
}
}

fun dispose() {
override fun dispose() {
stateJob?.cancel()
timeJob?.cancel()
sessionJob?.cancel()
phaseJob?.cancel()
scope.cancel()
}
}
Loading
Loading