Conversation
Default light/dark schemes now use the iOS system greys, blue accent and red destructive colour. Adds Dark, Verdant and Wallpaper theme modes so the previous looks stay selectable.
…ked trend segments StatRange presets now carry a StatPeriod and StatOffset so the Dashboard's two segmented controls map to a range through StatRange.preset. Half-month ranges (1st-15th, 16th-end) join Day, Week and Month; Last7Days is gone. Each TrendBucket carries per-project segments ordered by range total.
A floating pill tab bar now hosts Timer, Dashboard and Settings. Calendar and Review open from the Timer header and keep their routes, so deep links and review notifications still resolve. Everything from the old Track drawer lives in a grouped Settings tab, with reminders, templates and the sync center added. Pushed screens keep their owning tab selected; re-tapping a tab pops to its root.
Equal-width Role.Tab segments in a selectable group on a rounded track, with a raised thumb that slides to the selection. The visible track is 32dp but each segment keeps a 48dp touch column. Adds the segmented and dashboard chart tokens to Dimens and the track/thumb colour roles.
SelectorStyle.Grouped draws the project, task and tag selectors as iOS-style rows while keeping their pickers and test tags; the tagged node owns the value text so tests can read it in either semantics tree.
Large title with export, period and Current/Previous segmented controls, a grouped filter row, a total card with billable, average, entry count and the change against the previous window, stacked per-project bars by day or week with an hour axis, and a per-project breakdown with share bars. Tapping a bar or project keeps opening the drill-down sheet. The donut, the old bar charts, the KPI grid and the comparison card are removed with the strings and tests only they used; PeriodComparison keeps just the total delta the new card shows.
The timer bar is a compact row with the running description, project, clock time, pause and a red stop button; tapping it edits the running entry. Idle, it shows a borderless 'what are you working on' field with a play button and grouped project, task, tag and billable rows. History renders as full-width rows in grey day sections with clock-style totals, tag chips and a billable marker. Tap edits, swipe left deletes, swipe right resumes (also exposed as TalkBack actions).
Both the Timer sheet and the Calendar/Inbox editor now share a Cancel/title/Save header, a borderless description cell, grouped project, task, tag and billable rows, a Time section with date and time chips plus a duration stepper, and a destructive Delete entry cell. The Timer sheet gains delete (with the existing undo snackbar). Unused field-style helpers are removed.
surfaceContainerHighest now equals the grouped-cell colour, so default Material cards (Inbox, Review, Statistics) read as iOS cells; chips and switch tracks use surfaceVariant. Calendar, now pushed from the Timer header, shows a back button.
NavController.currentBackStack is library-internal (lint RestrictedApi). The selected tab now follows destination changes: tab routes select themselves and pushed destinations keep the tab they were opened from. Also drops strings the redesign orphaned.
Adds Timer idle and Settings shots, relabels Track/Statistics as Timer and Dashboard, and renders the hero set in the default dark theme.
Day headers now read "Wed, Jun 10", adding the year only outside the current year. Today and Yesterday keep their labels.
Replace the floating tab bar with a side menu (Time Tracker, Calendar, Reports, Review, Settings) whose header shows the account and the organization switcher. Time Tracker docks the running timer at the bottom with an orange stop button; idle, a + button unfolds into Timer (the start-timer sheet with favourites and continue-last) and Manual. Calendar opens on a day view: date and total, a week strip with the selected day circled, grey entry cards with the project line in its colour, and Today, Day/Week/Month, break, overlay and settings in the overflow menu. The month grid uses explicit colours so dark mode no longer dims it. Coloured project dots are gone outside the dashboard legend.
Calendar: swipe left/right pages the day, week or month; entries move after a hold, so swipes and scrolling work from anywhere; a tap opens Continue, Edit, Duplicate, Split and Delete. Hour labels sit centred on their lines beside a gutter divider. Time Tracker: search moves behind a header search button, with the options in a sheet. Deletes ask first, and swiping a stack left deletes the whole stack after an "N entries" prompt. The entry menu offers Continue. Review checks (no project or description, overlap, long entry) show on each card, and a failed sync retries from a refresh button. Review leaves the side menu for Settings, and every page is opaque so the back swipe no longer shows the page below through it. Calendar months now load from a month before their start instead of the whole history, reuse months loaded in the last minute when paging, and wait out HTTP 429 Retry-After before retrying.
- Moves and splits are written with formatTimeEntryInstant (UTC Z), like every other local writer, so Room ORDER BY start and identity lookups stay text-sortable offline. - A drop compares instants, not strings, so a hold released on the entry's own slot no longer sends an update. - The drop is the entry's real start plus the dragged time, so an entry that starts before the visible hours (drawn clipped at the grid top) no longer jumps by the hidden part. - The month heat colour no longer divides by zero when no loaded day has work time.
The calendar's Room entry and sync-state streams were collected from setOrganization for the ViewModel's whole life, so after one visit every database write rebuilt the day buckets even on other tabs. uiState now joins them on with stateIn(WhileSubscribed(5s)) and the screen collects with collectAsStateWithLifecycle. Buckets are rebuilt when the account zone changes, data from a previous organization is never shown under a new one, and "stale" is derived from cached days being on screen. Months stay fresh for five minutes instead of one (Retry still forces a reload, and returning to the calendar refreshes months that went stale), and adjacent months are prefetched only after the visible page loaded and settled, so quick paging does not download months it passes.
- Calendar drag read the gesture inside the block's own drag offset, so each pointer position was relative to the moving block and the entry chased itself. The gesture is now read outside the offset, the block settles on the snapped slot until the move lands instead of flashing back, a second drag starts from the new position, and a dragged entry draws above the neighbouring week columns. - Entry actions and Sync & recovery offer Retry for waiting and retrying changes, not only failed ones. - The running-timer bar has a handle and opens the running entry's details when swiped up; the details keep the live clock with pause, resume and stop on top.
With a running entry visible, the week grid and the month's day view
read a per-second clock high in the tree, so every second each column
re-ran layoutTrackedEntries (parsing every entry), recomposed every
block and drag modifier, and rebuilt the header formatters.
The clock is now a State: layout reads a minute-granular derived value
and a day's layout is remembered, relaid out each minute only when it
holds a running entry. Each entry block is a keyed composable of its
own, and only a running block reads the second clock for its height and
duration. The now line moves in an offset {} lambda without
recomposing, the period and date titles are remembered, and the drag
modifier no longer parses the entry start on every pass.
GroupedRow gains a status-icon tint, a TalkBack click label and a single-line mode for rows that show user data; EntryDatePickerDialog takes the confirm label, so other date pickers can share its filled confirm and text Cancel. Both are new parameters with defaults. Adds the Time Tracker sweep strings (English, Dutch, Japanese).
- Search options follow the entry form: a Date choice (any time, today, 7 days, custom), client, project, task and tags as full-width rows, then billable and the status checks as switches. - Statistics filters use the same layout: billable as a segmented choice, then projects, clients, tasks and tags as full-width rows. Projects were a chip for every project in one scrolling column, which lagged on large catalogues; every row now opens a searchable, lazily listed picker like the entry form's project picker. - Custom date ranges open a full-screen range picker instead of the range picker squeezed into a date dialog.
Creating a project picked its client from a dropdown menu composing every client at once. It is now a grouped Client row opening a lazy, searchable picker: No client first, then Create client (named after the search) so it is reachable without scrolling, then active clients. The client-picker and create-client test tags are unchanged. The overlap warning parsed every existing entry four times on each time change. Entries are now parsed once per list, narrowed to the edited days (plus a day either side) only when a date changes, and checked with the same half-open rule as EntryTrustRules.overlaps.
…king state Running entry (B1, B2): saving the running entry's details now asks Room whether it still runs. If Pause, the notification or another surface ended it while the sheet was open, the edits are saved onto the ended row with its recorded end instead of clearing the end and restarting the timer; a start after that end is refused with a message. Saving the running entry, or an edit to its Room row from anywhere, updates the editing fields that Stop and Resume commit, so they no longer write the old description, project or tags back. Stop and Pause take the details sheet's pending fields, including a changed start, and commit them with the stop. Deleting the running timer (B9) ends it locally like Stop: timer, docked bar, notification and widget, and keeps the 10-second poll from showing the still-active server copy until the deferred DELETE syncs; undo brings it back. Editor requests (B6, B10): Duplicate and Split can skip the Time Tracker's open-the-copy request, and such a request expires after 10 seconds. continueEntry refuses while a timer runs or is paused, so a paused timer's fields are never overwritten. Performance: no runBlocking for the time-zone policy (device zone until the account policy is read off the main thread); the first-frame cache holds only the newest entries and the catalogue items they show, and is rewritten debounced and only when that changed; observeLocalData projects Room without waiting for a refresh; the paginated history merge runs on a background thread in one pass and is redone if the window changed meanwhile; the active-entry poll re-posts the notification only when its content changed.
AppSheet gives every bottom sheet the search-options layout, ConfirmDialog one confirm style (red for destructive actions), AppTimePickerDialog one time picker that follows the device 12/24-hour setting. Full-screen pickers put close on the left like the date-range picker, grouped selector icons are tinted like GroupedRow, option pickers tick the choice on the right, and the unused SearchableSingleSelectDialog is gone.
- Entry actions: AppSheet with a short summary, the unsent change as its own section (state, Retry/Discard/Sync centre, explanation as the footer), actions as grouped rows (Continue first, Edit, Stop, Duplicate, Split) and Delete as a destructive row. - Settings: AppSheet with Done; grid size and snap interval as segmented controls; visible hours as rows opening an hour list. - Overlay: AppSheet; the switch as a grouped switch row with the privacy note as its footer, calendars as toggle rows in one section (no nested fixed-height scroll) with the overlay status as footer. - Delete uses the destructive ConfirmDialog; the split dialog uses the device 12/24-hour clock, a date value chip opening the app's date picker, and a filled Split button. - The running-timer card is a grouped cell with a red stop, like the Time Tracker timer bar. - Continue is hidden while a timer is running or paused: a paused timer has no running entry, and continuing then only overwrote its details. CalendarScreen takes timerActive (default false) for that. - The sync-status map, the rationale check and the dialogs' entry list are no longer rebuilt on every recomposition. Existing test tags stay on the equivalent clickable nodes; the snap setting is now selected by its segment tag instead of a value label.
The Dashboard paged the entire history from the first entry (500 per page, sequentially) on every subscribe and range change, a steady source of HTTP 429s, and once the server answered it ignored Room, so offline creates, edits and deletions only showed after the range changed. Fetch only the selected and comparison periods plus a two-day carry-in, reuse each window for five minutes (Retry and pull to refresh expire it while the old result stays visible), and overlay Room on the snapshot by id: Room rows win, entries queued for deletion drop out, and an entry Room has shown stays hidden after its deletion syncs.
…r re-anchoring - BootReceiver started the dataSync foreground service from BOOT_COMPLETED. With targetSdk 35+ Android forbids that and throws from the service's startForeground(), long after the guarded startForegroundService() call returned, so the app crashed. Boot now always posts the plain resume prompt (and consults Room when the network is not up yet); startForeground() failures are caught and the notification is posted without the foreground service. - The tile's error notification reused id 1001, the running-timer notification, so an error replaced the timer and its Stop action. It has its own id now. - ReminderScheduler re-anchored with UPDATE and an initial delay, but WorkManager applies an initial delay only to the first period: after one run, a changed reminder time (and the worker's own DST re-anchor) was ignored and the reminder kept firing 24 h after the last run. The request now also sets the next-run override. The new test fails without it (next run 24 h out).
The drill-down behind a tapped bar or project row used a default partially expanded sheet, a regular title, bare dividers, an ad-hoc spinner and a list capped at 460 dp. It now opens fully expanded on the page background with the bold sheet title, lists the entries as one grouped section drawn per lazy item (so a busy slice composes only its visible rows and fills the sheet), and uses the shared loading and empty states.
…rune old copies The per-organization history is read unbounded and sorted by start on every Room invalidation, and observeTimeEntries combined three flows (entries, tags, tag refs), so one write produced two or three emissions through every screen. - Schema v9: a composite (organizationId, start) index replaces the organization-only index (MIGRATION_8_9, exported schema 9.json). - observeTimeEntries is one joined query (entries + tag refs + catalogue tags, tag order kept) with distinctUntilChanged, so a write emits once and writes that change nothing visible are not re-delivered. The public flow keeps its shape, ordering and tag semantics. - A conservative prune, once per process after a successful refresh, drops SYNCED copies started over 400 days ago and unwritten for 30 days. Pending, conflicted, running or outbox-referenced rows and anything inside a month loaded in this process are never pruned; a month load or pull restores them.
The dashboard was one verticalScroll column whose legend and breakdown held a row per project and whose estimates card listed every estimated project in the organization, whatever the range. The cards are now lazy items under pull to refresh, loading and the failed refresh use the shared state views, and the legend, bars and breakdown name the six largest projects with the rest folded into one Other swatch, segment and row that opens their entries in the drill-down. Estimates list only projects with time in the filtered range, six most urgent first.
loadMonth started each month query 31 days early so an entry starting before the month still showed up. Every month load therefore also downloaded most of the previous month, while CalendarViewModel already prefetches the adjacent months (whose own loads put any long entry into Room). Two days of carry-in cover entries that cross midnight into the month.
The template sheet used outlined fields, a FilterChip cloud over every tag, a checkbox, bottom-aligned Cancel/Save buttons and a red text-button delete confirmation. It now mirrors the entry form on the page background: Cancel, title and Save across the top, borderless name and description cells, the grouped project/task selector, the searchable tags picker and a billable switch, a favorite switch with its explanation beneath, and when editing a Delete row behind the shared destructive confirmation.
refreshAll ran on every foreground return (>30 s away) and fetched all catalogue pages, then the entries, then the memberships one after the other, and rewrote every row even when nothing had changed, waking every Room observer. - Catalogue, entries and memberships are fetched concurrently. - The catalogue is reused when it was fetched less than 5 minutes ago in this process and Room still holds that pull; refreshAll gained an optional forceCatalog flag so an explicit user refresh can bypass it (existing callers are unchanged). - Catalogue and membership lists are only upserted when a row is new or differs, and applyServerEntries skips entries identical to Room, so an unchanged refresh performs no writes.
Templates were elevated cards with shadows, the screen drew its own loading, error and empty layouts, and it used an extended FAB. The templates are now one grouped section drawn per lazy item (tap a row to edit; the star and the overflow menu stay), the states use the shared views, the add button is the round primary FAB used on Track and Calendar, and editing offers Delete. The README screenshot composition follows the grouped layout; its baseline is not re-recorded here.
observeCachedAuth mapped every change notification to a fresh getCachedAuth(), so each subscriber (TemporalPolicyProvider for every screen, Sync center) decoded the user and membership JSON again, and emitted even when nothing changed. The decode is memoised per change version and the flow is distinct; TemporalPolicyProvider.policy is distinct too, so a profile or membership rewrite that keeps zone and week start no longer makes every statistics/calendar collector re-resolve and re-fetch. The synchronous first-frame read is unchanged. cacheCurrentMembership now bumps the change version because it is part of the cached auth.
ktlint max-line-length (spotlessCheck) flagged test fixtures added in the preceding commits.
Wires the calendar's timerActive so Continue stays hidden while a timer is paused.
Reports and Templates both build grouped sections from lazy items; the helper belongs with GroupedSection.
…rces Calendar day buckets were built on Dispatchers.Default, so under a loaded full-suite run a test could assert before the first state arrived. Tests now build them on their own dispatcher. Snackbar text in Privacy and Review day reads LocalResources, which follows configuration changes.
…efetches the catalogue The repository now sends billable on START; the Time Tracker passes the editing choice, so the Billable switch in the start sheet, Continue and favourites is no longer dropped. A user refresh bypasses the repository's five-minute catalogue reuse.
JVM renders (Roborazzi) of the search options, pickers, Reports filters, date range, calendar sheets, running-timer details, template editor and Sync Center with sample data, for the pull request description.
AKolenda
force-pushed
the
clockify-redesign
branch
from
September 24, 2026 23:49
e2e8695 to
e2d8ff7
Compare
AKolenda
force-pushed
the
clockify-redesign
branch
from
September 25, 2026 19:52
e2d8ff7 to
5f65421
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Superseded by #17.