Skip to content

Superseded by #17 - #16

Closed
AKolenda wants to merge 72 commits into
Tricked-dev:masterfrom
AKolenda:clockify-redesign
Closed

AKolenda wants to merge 72 commits into
Tricked-dev:masterfrom
AKolenda:clockify-redesign

Conversation

@AKolenda

@AKolenda AKolenda commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by #17.

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 AKolenda changed the title Clockify-style redesign, consistent UI, and sync/data-loss fixes Superseded by #17 Sep 24, 2026
@AKolenda AKolenda closed this Sep 24, 2026
@AKolenda
AKolenda deleted the clockify-redesign branch September 24, 2026 23:49
@AKolenda
AKolenda restored the clockify-redesign branch September 25, 2026 19:52
@AKolenda AKolenda reopened this Sep 25, 2026
@AKolenda AKolenda closed this Sep 25, 2026
@AKolenda
AKolenda deleted the clockify-redesign branch September 25, 2026 19:53
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.

1 participant