Skip to content

Refactor app from novel reader to Pomodoro timer#1

Open
AngKS wants to merge 1 commit intoeven-realities:mainfrom
AngKS:claude/explain-codebase-mlm41alwmny827bt-34Mf0
Open

Refactor app from novel reader to Pomodoro timer#1
AngKS wants to merge 1 commit intoeven-realities:mainfrom
AngKS:claude/explain-codebase-mlm41alwmny827bt-34Mf0

Conversation

@AngKS
Copy link
Copy Markdown

@AngKS AngKS commented Feb 14, 2026

Summary

This PR completely refactors the application from an e-book reader (InNovel) to a Pomodoro timer application. The core business logic, UI components, and data models have been replaced to support timer functionality instead of book reading.

Key Changes

Core State Management

  • Replaced book-related state (currentReadingBookId, readingFragments, etc.) with timer state (TimerState enum, remainingSeconds, selectedPreset)
  • Simplified AppUiState to focus on timer display: removed books, isFullScreenReading, audioEventDisplayLines
  • Added TimerPreset enum with predefined durations (5, 15, 25, 45 minutes)

Timer Logic

  • Implemented startTimer(), pauseTimer(), and resetTimer() functions with coroutine-based tick mechanism
  • Added selectPreset() to allow users to choose timer duration
  • Integrated timer display updates to glasses via pushTimerToGlasses() and updateTimerDisplay()
  • Timer state machine: IDLE → RUNNING → PAUSED/FINISHED

UI Components

  • Removed: BookItem, TextImageView, TextAudioView, ReadingDialog components
  • Added: TimerDisplay, TimerControls, PresetSelector, TimerHeaderCard (renamed from UserBookshelfCard)
  • Updated AppScreen to display timer controls and preset selection instead of book list
  • Simplified header to show user info and device status only

Data Models

  • Replaced BookModel and BookChapterModel with TimerPreset enum
  • Removed book-related event handlers (handleListItemEvent, handleTextItemEvent, handleSysItemEvent)
  • Removed audio event display logic

SDK Integration

  • Removed book loading from JSON (loadBooksFromJson(), defaultBooks())
  • Removed chapter reading functions (startReadingBook(), fullScreenReading(), updateChapterInfo())
  • Kept device status and EvenHubEvent observers but removed book-specific event handling
  • Added timer push to glasses display on initialization

Project Configuration

  • Updated app.json: package ID and app name changed to Pomodoro Timer
  • Updated settings.gradle.kts: root project name changed to PomodoroTimer
  • Updated HTML title and resources references
  • Added GitHub Actions workflow for building JS and Wasm bundles

Implementation Details

  • Timer uses Job and delay(1000) for 1-second ticks
  • Preset selector allows changing duration only when timer is IDLE
  • Timer state persists across pause/resume cycles
  • Display updates to glasses synchronized with timer state changes

https://claude.ai/code/session_01MTm41KGmtgFhjhGCWdRDjU

- Add 5/15/30 min timer presets selectable via ring scroll (ListContainer)
- Timer countdown displayed on glasses via TextContainer with 1s updates
- Double-click toggles start/pause/reset via SysItemEvent handling
- Compose UI: TimerDisplay, PresetSelector, TimerControls, TimerHeaderCard
- GitHub Actions workflow for JS and Wasm production builds
- Remove novel reader models, views, and books.json data

https://claude.ai/code/session_01MTm41KGmtgFhjhGCWdRDjU
Copilot AI review requested due to automatic review settings February 14, 2026 18:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR represents a complete application refactor from an e-book reader (InNovel) to a Pomodoro timer application. The transformation replaces all book-related functionality with timer-specific features while maintaining the core SDK integration for Even Realities glasses.

Changes:

  • Replaced book management (BookModel, chapters) with timer presets (5/15/30 minutes) and timer state machine (IDLE, RUNNING, PAUSED, FINISHED)
  • Refactored UI from book list/reading views to timer display with preset selector and control buttons
  • Updated glasses SDK integration to display timer and presets instead of books and chapters, with double-click gestures controlling timer state transitions

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
settings.gradle.kts Updated root project name from "InNovel" to "PomodoroTimer"
composeApp/src/webMain/resources/index.html Updated page title and resource paths to reflect Pomodoro Timer branding
composeApp/src/webMain/resources/books.json Removed JSON file containing book content (no longer needed)
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/TimerHeaderCard.kt Renamed from UserBookshelfCard, updated to show "Pomodoro Timer" header instead of user's bookshelf
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/TimerDisplay.kt New component displaying timer countdown, state label, and selected preset
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/TimerControls.kt New component with state-dependent controls (Start/Pause/Resume/Reset)
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/TextImageView.kt Removed test component for image upload functionality
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/TextAudioView.kt Removed test component for audio control functionality
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/ReadingDialog.kt Removed reading dialog component (no longer applicable)
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/PresetSelector.kt New component for selecting timer duration presets
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/BookItem.kt Removed book list item component
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/views/AppScreen.kt Completely refactored to show timer UI instead of book list
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/models/BookModel.kt Replaced BookModel/BookChapterModel with TimerPreset enum
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/models/AppState.kt Complete rewrite: replaced book reading logic with timer state machine, coroutine-based tick mechanism, and glasses display integration
composeApp/src/webMain/kotlin/com/fzfstudio/eh/innovel/App.kt Updated to use timer callbacks instead of book reading callbacks
composeApp/src/wasmJsMain/kotlin/com/fzfstudio/eh/innovel/App.wasmJs.kt Updated EvenHubEvent serialization to use jsonData field
composeApp/src/jsMain/kotlin/com/fzfstudio/eh/innovel/App.js.kt Removed unused EvenHubEvent import
app.json Updated package ID, name, tagline, and description to reflect Pomodoro Timer application
.github/workflows/build.yml Added new CI workflow for building JS and Wasm bundles

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +231 to +237
private fun handleListItemEvent(event: ListItemEvent) {
val index = event.currentSelectItemIndex ?: return
val presets = TimerPreset.values()
if (index in presets.indices) {
selectPreset(presets[index])
}
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a list item event is received (user selects a preset from the glasses), the selectPreset function is called which will silently fail if the timer is RUNNING. This could confuse users who try to change presets via the glasses while the timer is running. Consider handling this case explicitly to provide feedback or prevent the selection in the UI.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to 84
fun selectPreset(preset: TimerPreset) {
if (uiState.timerState == TimerState.RUNNING) return
uiState = uiState.copy(
audioEventDisplayLines = (uiState.audioEventDisplayLines + line).takeLast(maxAudioEventDisplayLines)
selectedPreset = preset,
remainingSeconds = preset.seconds,
timerState = TimerState.IDLE,
)
coroutineScope.launch { pushTimerToGlasses() }
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selectPreset function only blocks changes when the timer is RUNNING but allows changes when PAUSED. However, when PAUSED, changing the preset will reset the timer to IDLE and reset remainingSeconds, losing any progress. This behavior may be unexpected for users. Consider either blocking preset changes when PAUSED or clearly documenting this behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 10
package com.fzfstudio.eh.innovel.models

/**
* 图书信息
* Timer duration presets available to the user.
*/
data class BookModel(
/** 图书唯一标识 */
val id: String,
/** 书名 */
val title: String,
/** 作者 */
val author: String,
/** 图书类型 */
val type: String,
/** 图书内容 */
val chapters: List<BookChapterModel> = emptyList(),
) {

/** 总章节数 */
val totalChapters: Int
get() = chapters.size

/** 已阅读章节数 */
var readChapters: Int = 0;
}

/**
* 图书 - 章节
*/
data class BookChapterModel(
/** 所属图书Id */
val bookId: String,
/** 章节索引 */
val index: Int,
/** 章节标题 */
val title: String,
/** 章节内容 */
val content: String,
) {
/** 展示内容 */
val displayContent: String
get() = if (content.length > 25) content.take(25) + "..." else content
/** 是否已经读完 */
var hadRead: Boolean = false;
enum class TimerPreset(val label: String, val seconds: Int) {
FIVE_MIN("5 min", 5 * 60),
FIFTEEN_MIN("15 min", 15 * 60),
THIRTY_MIN("30 min", 30 * 60);
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is renamed from BookModel.kt but still has that filename. It should be renamed to TimerPreset.kt or TimerModels.kt to accurately reflect its new purpose and content.

Copilot uses AI. Check for mistakes.
Comment on lines +275 to 281
fun dispose() {
tickJob?.cancel()
unsubscribeDeviceStatus?.invoke()
unsubscribeDeviceStatus = null
unsubscribeEvenHubEvent?.invoke()
unsubscribeEvenHubEvent = null
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dispose() function is defined but never called. This means that when the app exits, the observers (unsubscribeDeviceStatus and unsubscribeEvenHubEvent) are not cleaned up, potentially causing memory leaks. Consider calling dispose() in a DisposableEffect or OnDispose callback in the App composable, or ensure it's called when exitTimer() is invoked.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to 161
private suspend fun pushTimerToGlasses() {
val presets = TimerPreset.values().toList()

val presetList = ListContainerProperty(
containerID = 1,
containerName = "presets",
xPosition = 0,
yPosition = 0,
width = 530,
height = 30,
width = 130,
height = 235,
borderWidth = 1,
borderColor = 13,
borderRdaius = 6,
paddingLength = 0,
content = "《${book.title}》--作者:${book.author}",
)
// - 1.2、章节列表
val bookChapters = listOf(
ListContainerProperty(
containerID = 1,
containerName = "chapters",
xPosition = 0,
yPosition = 35,
width = 110,
height = 200,
borderWidth = 1,
borderColor = 13,
borderRdaius = 6,
paddingLength = 5,
isEventCapture = 1,
itemContainer = ListItemContainerProperty(
itemCount = book.totalChapters,
itemWidth = 100,
isItemSelectBorderEn = 1,
itemName = book.chapters.map { "第${it.index}章" }
)
)
paddingLength = 5,
isEventCapture = 1,
itemContainer = ListItemContainerProperty(
itemCount = presets.size,
itemWidth = 120,
isItemSelectBorderEn = 1,
itemName = presets.map { it.label },
),
)
// - 1.3、章节概要(使用当前章节索引)
val currentChapter = if (currentChapterIndex < book.chapters.size) {
book.chapters[currentChapterIndex]
} else {
book.chapters[0] // 如果索引无效,使用第一章
}
val chapterInfo = TextContainerProperty(
containerID = 3,
containerName = "content",
xPosition = 115,
yPosition = 35,
width = 415,
height = 200,

val timerText = TextContainerProperty(
containerID = 2,
containerName = "timer",
xPosition = 140,
yPosition = 0,
width = 390,
height = 235,
borderWidth = 1,
borderColor = 13,
borderRdaius = 6,
paddingLength = 12,
content = "${currentChapter.title}\n\n${currentChapter.displayContent}\n\n双击全屏阅读>>",
isEventCapture = 1,
content = buildTimerContent(),
)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic numbers are used throughout the glasses display layout (e.g., width: 130, height: 235, xPosition: 140, borderColor: 13, etc.). These values lack context and make the code harder to understand and maintain. Consider using named constants with descriptive names to explain what these dimensions and colors represent.

Copilot uses AI. Check for mistakes.
enum class TimerPreset(val label: String, val seconds: Int) {
FIVE_MIN("5 min", 5 * 60),
FIFTEEN_MIN("15 min", 15 * 60),
THIRTY_MIN("30 min", 30 * 60);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions presets of 5, 15, 25, and 45 minutes, but the actual implementation only includes 5, 15, and 30 minutes. This discrepancy should be addressed - either update the code to match the description, or update the description to match the implementation.

Copilot uses AI. Check for mistakes.
@@ -1,11 +1,11 @@
{
"package_id": "com.fzfstudio.",
"package_id": "com.fzfstudio.pomodoro",
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package name remains "com.fzfstudio.eh.innovel" throughout the codebase but the application has been completely refactored from a novel reader to a Pomodoro timer. While this is outside the diff, it creates inconsistency - the package_id in app.json is correctly updated to "com.fzfstudio.pomodoro", but the source code package names don't match. Consider renaming the package structure to match the new application purpose.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to 84
fun selectPreset(preset: TimerPreset) {
if (uiState.timerState == TimerState.RUNNING) return
uiState = uiState.copy(
audioEventDisplayLines = (uiState.audioEventDisplayLines + line).takeLast(maxAudioEventDisplayLines)
selectedPreset = preset,
remainingSeconds = preset.seconds,
timerState = TimerState.IDLE,
)
coroutineScope.launch { pushTimerToGlasses() }
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selectPreset function changes the timer state to IDLE even when it's already IDLE. While harmless, this triggers unnecessary state updates and coroutine launches. Consider only updating state if the preset actually changed.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +183
val presetList = ListContainerProperty(
containerID = 1,
containerName = "presets",
xPosition = 0,
yPosition = 0,
width = 530,
height = 30,
width = 130,
height = 235,
borderWidth = 1,
borderColor = 13,
borderRdaius = 6,
paddingLength = 0,
content = "《${book.title}》--作者:${book.author}",
)
// - 1.2、章节列表
val bookChapters = listOf(
ListContainerProperty(
containerID = 1,
containerName = "chapters",
xPosition = 0,
yPosition = 35,
width = 110,
height = 200,
borderWidth = 1,
borderColor = 13,
borderRdaius = 6,
paddingLength = 5,
isEventCapture = 1,
itemContainer = ListItemContainerProperty(
itemCount = book.totalChapters,
itemWidth = 100,
isItemSelectBorderEn = 1,
itemName = book.chapters.map { "第${it.index}章" }
)
)
paddingLength = 5,
isEventCapture = 1,
itemContainer = ListItemContainerProperty(
itemCount = presets.size,
itemWidth = 120,
isItemSelectBorderEn = 1,
itemName = presets.map { it.label },
),
)
// - 1.3、章节概要(使用当前章节索引)
val currentChapter = if (currentChapterIndex < book.chapters.size) {
book.chapters[currentChapterIndex]
} else {
book.chapters[0] // 如果索引无效,使用第一章
}
val chapterInfo = TextContainerProperty(
containerID = 3,
containerName = "content",
xPosition = 115,
yPosition = 35,
width = 415,
height = 200,

val timerText = TextContainerProperty(
containerID = 2,
containerName = "timer",
xPosition = 140,
yPosition = 0,
width = 390,
height = 235,
borderWidth = 1,
borderColor = 13,
borderRdaius = 6,
paddingLength = 12,
content = "${currentChapter.title}\n\n${currentChapter.displayContent}\n\n双击全屏阅读>>",
isEventCapture = 1,
content = buildTimerContent(),
)

runCatching {
if (isRebuild) {
rebuildPageContainer(RebuildPageContainer(
containerTotalNum = 3,
listObject = bookChapters,
textObject = listOf(
bookInfo,
chapterInfo
)
))
} else {
createStartUpPageContainer(CreateStartUpPageContainer(
containerTotalNum = 3,
listObject = bookChapters,
textObject = listOf(
bookInfo,
chapterInfo
)
))
}
createStartUpPageContainer(
CreateStartUpPageContainer(
containerTotalNum = 2,
listObject = listOf(presetList),
textObject = listOf(timerText),
)
)
}.onFailure { error ->
uiState = uiState.copy(errorMessage = "Failed to create book view: ${error.message}")
uiState = uiState.copy(errorMessage = "Failed to create timer view: ${error.message}")
}
}

/**
* 更新章节概要
*/
suspend fun updateChapterInfo(chapter: BookChapterModel) {
// 1、初始化更新的对象
val update = TextContainerUpgrade(
containerID = 3,
containerName = "content",
content = "${chapter.title}\n\n${chapter.displayContent}\n\n双击全屏阅读>>"
)
private suspend fun updateTimerDisplay() {
runCatching {
textContainerUpgrade(update)
textContainerUpgrade(
TextContainerUpgrade(
containerID = 2,
containerName = "timer",
content = buildTimerContent(),
)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The containerID values (1 and 2) and container names ("presets", "timer") are hardcoded in multiple locations. If these need to change, updates must be made in pushTimerToGlasses() and updateTimerDisplay(). Consider defining these as constants at the class level to improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +98
tickJob = coroutineScope.launch {
while (uiState.remainingSeconds > 0 && uiState.timerState == TimerState.RUNNING) {
delay(1000)
if (uiState.timerState != TimerState.RUNNING) break
uiState = uiState.copy(remainingSeconds = uiState.remainingSeconds - 1)
updateTimerDisplay()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateTimerDisplay() function is called on every tick (every second) during timer operation. This creates SDK calls every second, which may impact performance and battery life on the glasses. Consider batching updates or only updating at key intervals, unless per-second updates are a hard requirement for the glasses display.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants