Repository navigation
Task detail screen #129
Description
Activity
/oc
Task Detail Screen Implementation Plan
Issue target
Implement issue
#129as a new read-onlyTask Detailscreen in the KMP app.The screen must:
- Open when the user taps a task row.
- Show task detail fields.
- Show overdue/completed status.
- Open task URLs in the platform browser.
- Let the user mark the task as completed.
- Let the user delete the task.
- Let the user share the task as plain text.
- Support back navigation.
- Use string resources for all user-visible text.
- Receive an explicit design review before the ticket is considered done.
Current codebase status
Todayis the only task-list screen currently implemented. There is noUpcomingscreen in the repository yet.TaskRepositoryonly exposesfindTodayTasks()andsave(task).TaskDaoonly exposessave,findAll, andfindTodayTasks.- There is no existing task detail route, screen, or ViewModel.
- There is no existing outbound share abstraction.
- There is no existing outbound browser abstraction, but
LocalUriHandlercan likely cover URL opening in common Compose UI. - Task rows are rendered from
TodayScreen.ktusingFoliaryTaskCard, and row taps are not wired today.
Important scope notes
- The ticket mentions navigation from
TodayorUpcoming, but onlyTodayexists right now. - The ticket description says "all fields of a task", but
location mapis explicitly out of scope and the current entity has a nullablelocationfield. - The app targets
android,ios, andjvm. Android and iOS have native share sheets. JVM desktop does not have a standard OS share sheet equivalent, so desktop behavior needs an explicit implementation decision.
Recommended implementation shape
Keep the implementation minimal and consistent with the existing screen pattern:
- Add a dedicated
feature/taskdetail/package. - Add the standard three feature files:
TaskDetailViewModel.kt,TaskDetailScreen.kt, andTaskDetailNavigation.kt. - Extend the existing task data layer only where the detail screen needs it.
- Reuse the existing
save(task)API to mark a task as completed instead of adding a separate complete endpoint. - Use
LocalUriHandlerfor URL opening. - Add a small platform share abstraction for plain-text sharing.
Files to add
foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailViewModel.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailScreen.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailNavigation.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.ktfoliary/src/androidMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.android.ktfoliary/src/iosMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.ios.ktfoliary/src/jvmMain/kotlin/dev/appoutlet/foliary/core/share/TaskShareLauncher.jvm.ktfoliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailViewModelTest.ktfoliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/taskdetail/TaskDetailScreenTest.kt
Files to update
foliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/TaskRepository.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/TaskRepositoryImpl.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/data/task/database/TaskDao.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/today/TodayViewModel.ktfoliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/today/TodayScreen.ktfoliary/src/commonMain/composeResources/values/strings.xmlfoliary/src/commonMain/composeResources/values-en/strings.xmlfoliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/task/TaskRepositoryImplTest.ktfoliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/task/database/TaskDaoTest.ktfoliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/today/TodayViewModelTest.ktfoliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/today/TodayScreenTest.kt
Data-layer changes
TaskRepositoryAdd only the methods the detail flow actually needs:
interface TaskRepository { fun findTodayTasks(): Flow<List<Task>> fun findById(id: Uuid): Flow<Task?> suspend fun save(task: Task) suspend fun delete(id: Uuid) }
Rationale:
findById(id)lets the detail screen react to edits and deletion.save(task)already supports replace/upsert and is enough for mark-as-completed.delete(id)is the only missing write operation needed for this ticket.
TaskDaoAdd:
@Query("SELECT * FROM Task WHERE id = :id") fun findById(id: Uuid): Flow<Task?> @Query("DELETE FROM Task WHERE id = :id") suspend fun delete(id: Uuid)
Keep
save(vararg task)unchanged and reuse it for completion.TaskRepositoryImplImplement the new methods as thin DAO pass-throughs.
No new business logic is needed here beyond delegating to the DAO.
Navigation changes
New nav key
Add a dedicated key in
TaskDetailNavigation.kt:@Serializable data class TaskDetailNavKey(val taskId: String) : NavKey
Use
Stringin the nav key because that matches the currentFoliaryTaskCardViewData.idshape and avoids custom nav serialization work forUuid.Route setup
Follow the same pattern used by
CreateTaskNavigation:- Register
scope.entry<TaskDetailNavKey> { ... }. - Create the ViewModel with
koinViewModel<TaskDetailViewModel>(). - Wrap the screen in
Screen(screenName = "TaskDetailScreen", ...). - Pass the
taskIdfrom theNavKeydown into the composable.
Why pass
taskIdinto the screen instead of the ViewModel constructorThe current codebase does not show a parameterized
koinViewModelpattern. The least risky approach is:- Create a normal
TaskDetailViewModelwith injected dependencies only. - Pass
taskIdintoTaskDetailScreen(taskId, viewData, onEvent). - Trigger an initial
LoadTask(taskId)event from aLaunchedEffect(taskId)inside the screen.
This avoids adding a new dependency-injection pattern just for this feature.
Today integration
TodayViewModel.ktAdd:
sealed interface TodayEvent { data object OnAddTaskClick : TodayEvent data class OnTaskClick(val taskId: String) : TodayEvent } sealed interface TodayAction : Action { data object NavigateToCreateTask : TodayAction data object NavigateToSignIn : TodayAction data class NavigateToTaskDetail(val taskId: String) : TodayAction }
Handle
OnTaskClickby postingNavigateToTaskDetail(taskId).TodayScreen.ktWire row taps in the
items(viewData.tasks)block.Recommended minimal approach:
- Wrap the existing
FoliaryTaskCardcall in a clickable container. - Emit
TodayEvent.OnTaskClick(task.id)when that container is tapped. - Handle the new action in
onActionwithnavigator.navigate(TaskDetailNavKey(taskId)).
This is smaller than changing
FoliaryTaskCardglobally.UpcomingDo not invent an
Upcomingscreen as part of this ticket unless the user asks for it.Document this in the PR or implementation notes:
"Task Detail navigation is wired from
Todaynow.Upcomingcan use the sameTaskDetailNavKeyonce that feature exists."ViewModel design
Dependencies
TaskDetailViewModelshould depend on:TaskRepositoryTimeProvider
No browser or share dependency is needed in the ViewModel if URL opening is handled in the composable and sharing is emitted as a side effect.
Suggested state model
sealed interface TaskDetailViewData { data object Idle : TaskDetailViewData data object Loading : TaskDetailViewData data class Loaded( val task: Task, val isOverdue: Boolean, ) : TaskDetailViewData }
This keeps the state minimal and lets the screen render directly from the entity plus one derived flag.
Suggested events
sealed interface TaskDetailEvent { data class LoadTask(val taskId: String) : TaskDetailEvent data object BackClicked : TaskDetailEvent data object MarkCompletedClicked : TaskDetailEvent data object DeleteClicked : TaskDetailEvent data object ShareClicked : TaskDetailEvent }
Suggested actions
sealed interface TaskDetailAction : Action { data object NavigateBack : TaskDetailAction data class ShareTask(val text: String) : TaskDetailAction }
OpenUrldoes not need to be a ViewModel action if the URL is opened directly from the composable withLocalUriHandler.ViewModel behavior
Load flow
- On first
LoadTask(taskId), parse theUuid. - Start collecting
taskRepository.findById(id). - Reduce to
Loadingbefore collection starts. - When a task is emitted, reduce to
Loaded(task, isOverdue). - If the DAO emits
nullbefore any task is loaded, surface an error state or navigate back.
Recommended behavior for
null:- If the screen never successfully loaded a task, emit an
ErrorStatewith explicit task-detail strings. - If the task was already loaded and later becomes
nullbecause of deletion, navigate back.
Overdue calculation
Use the same logic already present in
FoliaryTaskCardViewDataMapper:- Completed tasks are never overdue.
- Null
dueDateis not overdue. - Otherwise
dueDate < timeProvider.now()is overdue.
This logic can stay private in the ViewModel to avoid introducing a new shared mapper unless reuse becomes necessary.
Mark as completed
Implementation:
val loaded = state as? TaskDetailViewData.Loaded ?: return val updatedTask = loaded.task.copy(completionDate = loaded.task.completionDate ?: timeProvider.now()) taskRepository.save(updatedTask)
Recommendations:
- Keep the button visible to satisfy the ticket.
- Disable it when
completionDate != nullto avoid duplicate writes. - Let the repository flow refresh the UI after save.
Delete
Implementation:
- Read the loaded task id.
- Call
taskRepository.delete(id). - Post
NavigateBackimmediately after delete succeeds.
This is simpler than waiting for the observed task flow to emit
null.Share
Implementation:
- Build a plain-text payload from the currently loaded task.
- Post
TaskDetailAction.ShareTask(text). - Let the route-level action handler call the platform share launcher.
Screen UI design
Layout structure
Use
CreateTaskScreenas the closest full-screen layout reference.Recommended structure:
ScaffoldTopAppBarwithFoliaryBackIconButton- Scrollable detail content constrained with
Modifier.widthInCompact() - Bottom action area with the primary and secondary actions
Top bar
Use:
- Back arrow on the left
- Title from string resources, for example
Task details
Content sections
Render the task content as stacked labeled sections in this order:
- Status indicator, if needed
- Title
- Description
- Due date
- Priority
- URL
- Creation date
- Completion date, only when completed
Optional-field rules
Use these rules to keep behavior consistent with the ticket and current nullable model:
URL: hide the entire field whenurl == nullor blank. This is explicitly required by the issue.Completion date: hide whencompletionDate == null.Description: show a fallback string such asNo descriptionwhen null or blank.Due date: show a fallback string such asNo due datewhen null.Priority: show a fallback string such asNo prioritywhen null.Location: do not render in this ticket.
The fallback approach is recommended because tasks created today default to null
description,dueDate,priority, andurl.Status indicator
Rules:
- Overdue and not completed: show red indicator.
- Completed: show grey indicator.
- Unfinished and on-track: render nothing.
Recommended implementation:
- A small inline status row near the top of the content.
- Use color plus label so the meaning is not color-only.
- Add test tags for the overdue and completed indicators.
Possible resource-backed labels:
OverdueCompleted
URL behavior
If
urlis present:- Render it as visually interactive text.
- Use
LocalUriHandler.current.openUri(url)on tap. - Style it distinctly from plain body text.
This avoids adding a dedicated browser abstraction.
Buttons
Render three buttons:
Mark as completedDeleteShare
Recommended arrangement:
Mark as completedas the primary button.Shareas secondary or outlined.Deleteas outlined or low-emphasis destructive action.
Because delete confirmation is explicitly out of scope, the delete button can act immediately.
Date formatting
No shared date formatter currently exists in common code.
Recommended minimal implementation:
- Add small private formatter helpers inside
TaskDetailScreen.ktorTaskDetailViewModel.kt. - Convert
Instantto local date/time withTimeZone.currentSystemDefault(). - Format consistently for due date, creation date, and completion date.
Keep the formatter local unless a second screen needs the same output.
Priority formatting
Map the enum values to resource-backed labels:
LOWESTLOWMEDIUMHIGHHIGHESTBLOCKER
Do not display raw enum names in the UI.
Share implementation
Common API
Add a small interface in common code:
interface TaskShareLauncher { fun share(text: String) }
Inject it in
TaskDetailNavigation.ktand call it from theonActionhandler whenShareTask(text)is emitted.Android
Use
Intent.ACTION_SENDwith MIME typetext/plainand launch a chooser.iOS
Use
UIActivityViewControllerwith the plain-text payload.JVM desktop
This requires a product/engineering decision before implementation is complete.
Recommended options, in order of honesty:
- Hide the share button on JVM if true share-sheet parity is required.
- Provide a JVM-specific fallback such as copy-to-clipboard, but only if the ticket is explicitly relaxed for desktop.
- Attempt a best-effort desktop share integration if a supported OS-level API is available, but do not claim parity without verification.
Because the issue explicitly says "platform share sheet", mobile support is straightforward, but JVM needs explicit agreement.
Share text format
Recommended payload:
<Task title> Description: <description or fallback> Due date: <formatted due date or fallback> Priority: <localized priority or fallback> URL: <url, only when present> Created: <formatted creation date> Completed: <formatted completion date, only when present>Recommendations:
- Keep it plain text only.
- Reuse the same labels and fallback strings as the UI where practical.
- Omit URL and completion lines when those values are absent.
String resources to add
At minimum add new entries to
values/strings.xmlandvalues-en/strings.xml.Suggested names:
task_detail_titletask_detail_task_title_labeltask_detail_description_labeltask_detail_due_date_labeltask_detail_priority_labeltask_detail_url_labeltask_detail_creation_date_labeltask_detail_completion_date_labeltask_detail_mark_completedtask_detail_deletetask_detail_sharetask_detail_status_overduetask_detail_status_completedtask_detail_no_descriptiontask_detail_no_due_datetask_detail_no_prioritytask_detail_not_found_titletask_detail_not_found_message
If the share button or URL row needs accessibility text beyond visible labels, add those too.
Testing plan
Before implementing tests, load the
testingskill as required byAGENTS.md.DAO tests
Update
TaskDaoTest.ktto cover:findByIdreturns the saved task.delete(id)removes the task.findByIdemitsnullafter deletion if the test shape supports flow observation.
Repository tests
Update
TaskRepositoryImplTest.ktto cover:findByIddelegates to the DAO.delete(id)delegates to the DAO.
Today tests
Update
TodayViewModelTest.ktto cover:OnTaskClick(taskId)postsNavigateToTaskDetail(taskId).
Update
TodayScreenTest.ktto cover:- Tapping a rendered task row emits
OnTaskClick(taskId).
This may require adding a stable test tag to the clickable row wrapper.
Task Detail ViewModel tests
Add
TaskDetailViewModelTest.ktfor:- Loading a task by id.
- Reducing to
Loadingbefore the task arrives. - Emitting
LoadedwithisOverdue = truewhen the task is overdue. - Emitting
LoadedwithisOverdue = falsewhen completed. - Posting
NavigateBackon back click. - Saving a copied task with
completionDate = nowwhen mark-completed is clicked. - Deleting the task and posting
NavigateBackwhen delete is clicked. - Posting
ShareTask(text)when share is clicked.
Task Detail screen tests
Add
TaskDetailScreenTest.ktfor:- Rendering title, description, due date, priority, creation date, and button labels.
- Hiding the URL row when URL is absent.
- Showing the URL row when URL is present.
- Showing the overdue indicator when overdue.
- Showing the completed indicator when completed.
- Hiding the status indicator when on-track and unfinished.
- Emitting
BackClicked,MarkCompletedClicked,DeleteClicked, andShareClicked.
Avoid over-testing color values directly unless the Compose test API makes it straightforward. Presence and semantics checks are enough for most of this ticket.
Verification commands
Do not run a full build.
Recommended test commands after implementation:
./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.taskdetail.TaskDetailViewModelTest"./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.taskdetail.TaskDetailScreenTest"./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.today.TodayViewModelTest"./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.feature.today.TodayScreenTest"./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.data.task.TaskRepositoryImplTest"./gradlew foliary:jvmTest --tests "dev.appoutlet.foliary.data.task.database.TaskDaoTest"
Design review checklist
The ticket explicitly requires a design review before completion. Review these items after the first implementation pass:
- Is the title visually dominant enough?
- Are section labels and values clearly separated?
- Is the status indicator visible without overpowering the screen?
- Are vertical spacing and content grouping consistent?
- Does the URL look interactive?
- Are the bottom actions ordered and weighted correctly?
- Does the layout still feel balanced on narrow mobile width and wide desktop/tablet width?
- Does
widthInCompact()keep the content readable on large screens?
Recommended order of implementation
- Extend
TaskDaoandTaskRepository. - Add
TaskDetailNavigation.ktandTaskDetailViewModel.kt. - Wire
Todayrow taps to the new route. - Build
TaskDetailScreen.ktwith URL opening and local formatting. - Add the share abstraction and hook it into route-side effects.
- Add string resources.
- Add or update tests.
- Run only the targeted JVM tests.
- Perform the design review pass.
Open decisions to resolve before or during implementation
- Should JVM desktop hide the share button, or should the ticket accept a non-share-sheet fallback there?
- Should nullable
description,dueDate, andpriorityuse fallback text, or should some of those rows be hidden? The fallback approach is recommended because it fits the current nullable model better. - When
findById(id)returnsnullfor an invalid task id, should the app show an error state or immediately navigate back? The recommended behavior is error before first successful load, back navigation after delete. - Should the completed button remain enabled after completion, or visible-but-disabled? Visible-but-disabled is the safest interpretation of the ticket.
opencode-agent commented
on Jul 26, 2026 on Jul 26, 2026 – with OpenCode AgentContributorMore actionsCreated PR #199

Description
A read-only detail screen that shows all fields of a task. Users navigate to it by tapping a task row from the Today or Upcoming screens.
The screen displays the task title, description, due date, priority, URL (as a tappable link), creation date, and completion date (if completed). Tapping the URL opens it in the platform browser.
A status indicator is shown: red when the task is overdue, grey when the task is completed, and hidden when it is a regular unfinished (on-track) task.
The screen includes a "Mark as completed" button, a "Delete" button, and a "Share" button that shares the task content as plain text via the platform share sheet.
The visual design of this screen must be reviewed as part of the ticket to ensure layout, spacing, and visual hierarchy are correct before implementation is considered complete.
Scope
In scope: TaskDetailScreen composable, TaskDetailViewModel, TaskDetailNavigation route, displaying all task fields, status indicator (red overdue, grey completed, hidden on-track), tappable URL that opens in the platform browser, "Mark as completed" button, "Delete" button, "Share" button that exports task content as plain text via platform share sheet, back navigation, design review, and string resources.
Out of scope: edit task (separate ticket), location map (separate ticket), delete confirmation dialog (separate ticket), and statistics.
Acceptance criteria
WHEN the navigation occurs
THEN the Task Detail screen is displayed for that task
WHEN the screen renders
THEN it shows the task title, description, due date, priority, creation date, and completion date
WHEN the user taps the URL
THEN the URL opens in the platform browser
WHEN the screen renders
THEN the URL field is not shown
WHEN the screen renders
THEN a red status indicator is shown
WHEN the screen renders
THEN a grey status indicator is shown
WHEN the screen renders
THEN no status indicator is shown
WHEN the screen renders
THEN a "Mark as completed" button is visible
WHEN the screen renders
THEN a "Delete" button is visible
WHEN the user taps the "Share" button
THEN the platform share sheet opens with the task content formatted as plain text
WHEN they press the back button or back arrow
THEN they navigate back to the previous screen
WHEN the ticket is reviewed
THEN the design is explicitly reviewed for layout, spacing, visual hierarchy, status indicator placement, and button placement
WHEN the app renders the screen
THEN all visible text comes from string resources