Commit graph

746 commits

Author SHA1 Message Date
Adam Brown
668adfc940 Animate list items only while reordering
animateItem() was applied to every non-dragged item unconditionally, so a
fast fling to the top of the list animated item placement during plain
scrolling. Gate it on an active drag so only the displaced items animate.
2026-06-24 14:45:36 -07:00
Adam Brown
ac8222bde5 Move the right item after the list order changes
rememberDragDropListState captures its callbacks once via keyless remember,
so the external confirmReorder kept invoking the first composition's closure.
That closure indexed into a stale event list, so every reorder after the
first moved the wrong item. Route the callback through rememberUpdatedState
so the remembered state always calls the latest closure.
2026-06-24 14:45:36 -07:00
Adam Brown
678f352597 Fix timeline reorder snapping back after the first move
DragDropList tracked the last external list in a plain `var` initialized
from `remember { items }`, so the value was frozen at first composition
and reassignment never persisted. That left the reset guard permanently
true after the first committed reorder, clobbering the in-progress drag
preview on every recompose and snapping items back to their committed
order. Make the tracker state-backed so the guard fires only on a real
external change.
2026-06-24 14:45:36 -07:00
renovate[bot]
6b3f2b631b
Update plugin ee.schimke.composeai.preview to v0.16.1 (#608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 11:45:30 -07:00
Adam Brown
3e259bcf10 Merge branch 'fix/android-scrollbar-side' into develop 2026-06-22 11:27:14 -07:00
Adam Brown
ae1c0cb095 Fix Android scrollbar rendering on left instead of right
The Android MpScrollBarList dropped its modifier parameter, so the
Alignment.CenterEnd passed by the project list never reached the Canvas.
The bar fell back to the Box default alignment (TopStart) and rendered
on the left. Thread the modifier through to the Canvas.
2026-06-22 10:51:48 -07:00
Adam Brown
c5e155adb9 Keep scene list footer reachable when list can't scroll (#615)
The Outline Overview and add buttons auto-hide on scroll-down and only
reappeared on scroll-up. Once the list became non-scrollable (e.g. all
groups collapsed) while hidden, nothing could flip them back. Tie footer
visibility to canScrollForward so it always shows when the list can't
scroll down to it.
2026-06-22 10:51:02 -07:00
Adam Brown
bf5277a056 Warn before discarding in-progress timeline edits on project close (#588)
Exiting a project with auto-sync on runs requestClose(), which queued
CloseConfirm.Sync and tore down open editors before the sync ran. Scenes,
notes, and encyclopedia entries flagged unsaved edits via shouldConfirmClose(),
but TimeLineComponent returned emptySet(), so an in-progress timeline event
edit was silently discarded with no warning.

Wire TimeLineComponent.shouldConfirmClose() to the existing isEditingAndDirty()
check and add a CloseConfirm.Timeline confirmation dialog on Android/common and
desktop, mirroring the notes/encyclopedia pattern.

Closes #588
2026-06-22 10:09:23 -07:00
Adam Brown
6be3651f9f Show snackbar on "Save All" 2026-06-22 01:54:39 -07:00
Matt Van Horn
9e5a3103f2
feat: add Ctrl+Alt+S save-all keyboard shortcut on desktop (#626)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-22 01:46:33 -07:00
Adam Brown
e7d2d96616 Render only visible scene-tree nodes to fix group label flicker (#613)
The scene tree emitted every node as a LazyColumn item and hid collapsed
descendants with AnimatedVisibility, leaving them as zero-height rows. On a
large, mostly-collapsed tree the LazyColumn composes through those trailing
zero-height rows trying to fill its viewport, and combined with animateItem()
the last visible row's placement never settles -- it flickers.

Compute the visible nodes (everything except the root scene and anything under
a collapsed ancestor) and emit only those as items. Collapsed subtrees are
absent rather than zero-height, so there is nothing for the list to churn over.
Drag/drop is keyed by node id, not list index, so reordering is unaffected; it
also no longer sees zero-height rows as drop targets.
2026-06-22 01:35:01 -07:00
Adam Brown
584cf62271 Allow Windows reserved names in scene/group titles (#586)
Scene and group titles are stored wrapped as `order~name~id`, so a leading
dot or a Windows reserved word (CON, PRN, COM0-9, LPT0-9, ...) can never
collide on disk for them — yet name validation rejected them as if the title
were a raw filesystem basename. Project names, which become directories
verbatim, keep the strict rules.

- ProjectNameValidator.validate gains usedAsRawFilename (default true); the
  leading-dot and reserved-name checks now only apply to raw filenames.
  Trailing dot/space stay rejected for everyone (the on-disk encoder strips
  them). Reserved set extended with COM0/LPT0.
- Thread the flag through ProjectsRepository.validateFileName and
  SceneRepository.validateSceneName.
- Consolidate the six naming dialogs onto a shared rememberNameValidation
  hook keyed by NameKind (Project vs SceneItem), so strictness lives in one
  place instead of a per-call-site boolean.
2026-06-22 01:10:52 -07:00
Adam Brown
3318c1f284
Make SceneTree state Compose-stable with immutable collections (#621)
Mark the scene-tree state types @Immutable/@Stable and move them onto
kotlinx.collections.immutable so Compose can skip recomposition when the
tree is unchanged: TreeValue.children becomes ImmutableList, SceneSummary
.hasDirtyBuffer a PersistentSet (sourced as such from SceneContentRepository),
and SceneList.State.archivedScenes an ImmutableList. Also cache ImmutableTree
.nodeIndex/hashCode lazily and gate compose-compiler stability reports behind
the composeCompilerReports property.

* Harden onSceneBufferUpdate to reduce from oldState

Read the scene summary from the getAndUpdate lambda's oldState argument
instead of a snapshot captured before the CAS, so the reducer stays a pure
function of its input and composes correctly if buffer updates ever run off
the main dispatcher.
2026-06-19 23:29:41 -07:00
Adam Brown
ae8432eea6 Fix Windows CoInitializeEx crash in desktop file dialogs
FileKit's Compose remember*Launcher runs the native picker on a shared
Dispatchers.IO thread and calls CoInitializeEx(STA), which throws
"CoInitializeEx failed" (RPC_E_CHANGED_MODE) when that pooled thread was
already COM-initialized MTA. Intermittent and crashes the app.

Add retryingFileDialog and route every desktop-reachable picker through
FileKit's suspend API instead of the Compose launchers, retrying past the
transient COM failure and returning null rather than crashing. iOS/Android
keep the Compose launchers.
2026-06-16 02:27:23 -07:00
Adam Brown
74407030e1 Add tablet screen previews, Screen-prefix naming, and preview backgrounds
- Tablet variants: new TabletPreviewSurface provides a wide ScreenCharacteristics
  so screens render their expanded layout; @Preview(widthDp/heightDp) sizes the
  canvas to a landscape tablet. Added for ProjectStats, ProjectSettings,
  TimeLineOverview, BrowseEntries, BrowseNotes, and ViewEntry.
- Naming: prefix every top-level screen preview (phone and tablet) with "Screen"
  so they group together and are easy to pick out from component previews.
- Backgrounds: the shared Padded wrapper now applies the app theme + surface
  background; ProjectCard and SceneItem previews now render against the app
  background instead of the renderer's transparent default.
2026-06-14 17:36:48 -07:00
Adam Brown
ccc6c2245e Make all Compose previews renderable
The compose-preview renderer invokes @Preview functions reflectively and
cannot access private top-level functions, so most existing previews failed
to render with IllegalAccessException. Drop `private` from every @Preview
function.

Also fix the remaining render failures:
- Wrap encyclopedia/scene previews that touched Koin in KoinApplicationPreview.
- Register a minimal coil3 ImageLoader in the preview Koin graph so AsyncImage
  screens (ViewEntry, CreateEntry) resolve it.

Result: 88/88 previews render, 0 errors (was 19/88).
2026-06-14 17:10:47 -07:00
Adam Brown
42a1f48089 Provide fake SpellCheckRepository to preview Koin graph
MarkdownEditField injects SpellCheckRepository, so every editor-based
preview (CreateNoteUi, CreateTimeLineEventUi, ...) crashed at render with
a missing Koin definition. Register a real SpellCheckRepository backed by
in-memory settings datasources in KoinApplicationPreview's base module.
Lazy single, so non-editor previews are unaffected.
2026-06-14 16:34:02 -07:00
Adam Brown
7b83e8edf2 Wire up compose reports 2026-06-14 16:28:36 -07:00
Adam Brown
8ae81a631e Add Compose previews for router destination screens
Cover the destination screens behind the project-home, notes, timeline,
and story-editor routers with @Preview fixtures:
- ProjectStatsUi, ProjectSettingsUi (project home)
- CreateNoteUi (notes)
- TimeLineOverviewUi, CreateTimeLineEventUi, ViewTimeLineEventUi (timeline)
- OutlineOverviewUi (story editor dialog)

Each preview hand-rolls a fake component implementing the Decompose
interface so the screen renders without runtime dependencies.
2026-06-14 12:01:04 -07:00
renovate[bot]
fa5c8d9e1e
Update plugin ee.schimke.composeai.preview to v0.15.2 (#540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-14 01:33:33 -07:00
Adam Brown
df2468747e
Add keyboard shortcuts for bold, italic and strikethrough (#601)
The markdown editor only exposed inline styles through format-bar buttons;
the underlying composetexteditor library handles editing/navigation shortcuts
(Ctrl+C/V/X/Z/Y, arrows, etc.) but has no bindings for bold/italic, so those
keys were simply dropped.
2026-06-13 20:54:24 -07:00
Adam Brown
66ae689cdb Add visual draggable indicator 2026-06-13 12:42:19 -07:00
Adam Brown
fde00d28f6 Fix compose edit stream 2026-06-13 11:50:43 -07:00
Adam Brown
caeab9c94c Fix timeline event re-ordering 2026-06-13 11:41:40 -07:00
Adam Brown
4b912129b4
Replace fluidsonic (#597) 2026-06-13 02:38:21 -07:00
Adam Brown
e77ef954d3 Remove timeline sorting
It didn't really make sense anyway
2026-06-12 23:12:45 -07:00
Wavesonics
e8f6858e2f Fix iOS Lifecycle handling
- Update platform specific UI for iOS
2026-06-11 23:10:35 -07:00
Adam Brown
0791954de0
Add docx export format 2026-06-11 00:18:00 -07:00
Wavesonics
47d1f7bfe9 Fix more iOS file path handling 2026-06-10 20:56:05 -07:00
Adam Brown
00a9bcafd1
Add a warning state for conflicted project syncs 2026-06-10 20:50:40 -07:00
Wavesonics
d23431f0ac Fix iOS crash on export 2026-06-10 17:50:14 -07:00
Adam Brown
5fdf42dd9a
Revert "fix: Story Outline collapse arrows and Collapse All (#572)"
This reverts commit b7c5a87dc4.
2026-06-09 00:37:51 -07:00
Matt Van Horn
b7c5a87dc4
fix: Story Outline collapse arrows and Collapse All (#572)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-09 00:31:51 -07:00
Adam Brown
9640f7ce85
Fix confirm dialog on narrow screens 2026-06-09 00:01:20 -07:00
Adam Brown
9c1ac1c7dc
Spell Checker dictionary is now selectable on Desktop and iOS 2026-06-09 00:01:20 -07:00
Adam Brown
bd4ae9160b
Swap to dropdown 2026-06-08 00:15:32 -07:00
Adam Brown
b44c315431
Target Android API 37 (Android 17)
Bump compileSdk/targetSdk to 37 and AGP to 9.1.1 (9.0.x maxes out at
API 36.1 and can't resolve the minor-versioned android-37.0 platform).

Handle the two breaking behavior changes for API 37 targets:

- Cleartext traffic: usesCleartextTraffic is now ignored without a
  network security config. Self-hosted servers can be plain HTTP
  (ServerSettings.ssl = false), so add network_security_config.xml
  permitting cleartext and reference it from both manifests.

- Local network access: declare ACCESS_LOCAL_NETWORK and request it at
  runtime via a new expect/actual RequestLocalNetworkPermission,
  triggered when the server-setup dialog opens so the grant resolves
  before any LAN connection. No-op on desktop/iOS.
2026-06-07 23:57:01 -07:00
Adam Brown
c663e879e1
Export dialog now shows loading indicator 2026-06-07 23:16:01 -07:00
Adam Brown
27659be26c
Export to PDF support 2026-06-07 23:07:20 -07:00
Adam Brown
164e9f4a4c
Fixed toasts now showing on Android 2026-06-07 20:54:57 -07:00
Adam Brown
a2998adcba
Re-enable public-storage projects on F-Droid builds (#568)
Restores the "store projects in public storage" feature, gated to F-Droid builds (the required MANAGE_EXTERNAL_STORAGE permission is disallowed on Google Play).

- Expose the build channel at runtime via BuildConfig.FDROID in the common module.
- Declare the storage permissions only in src/fdroid/AndroidManifest.xml, swapped in for F-Droid builds.
- Restore the storage-location toggle + file-access UI, gated on BuildConfig.FDROID; reconcile the toggle with the real location on open.
- Build the GitHub release APK as the F-Droid flavor.
- Extract the directory move into a tested FileSystem.moveDirectory() helper (fixes the same-path data-loss crash; runs off the UI thread).
- Read the fdroid flag consistently across settings.gradle.kts and module scripts.
- Document the F-Droid build flag in DEVELOPMENT.md.
2026-06-07 19:16:42 -07:00
Wavesonics
59f5172c93 Extract DetailStampRow into the design system 2026-06-06 22:13:49 -07:00
Adam Brown
eca76803fa
Fix flaky NotesWorkflowTest create-note race (#562)
* Fix flaky NotesWorkflowTest create-note race

createNoteThenOpenIt clicked the create confirm button immediately after
typeIntoEditor. The markdown editor reports text changes through an async
editOperations flow that waitForIdle() doesn't await, so noteText could
still be empty at confirm time -> createNote("") returns NoteError.EMPTY,
the create screen never dismisses, and the browse grid's note-card- nodes
never reappear -> 10s timeout.

Wait for the word/char counter to reflect the typed body before confirming.
Tag the counter (NOTES_CREATE_META_TAG) and add a textOf() harness helper to
read it. Verified on emulator (4/4 runs green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Guard project list load against a concurrently-deleted project

loadProjectList lists project dirs then reads each one's metadata in a
parallelMap. If a project is deleted between the listing and the read
(another window, or a refresh racing a delete), loadMetadata's recovery
path tries to recreate project.toml in a directory that no longer exists
and throws FileNotFoundException from inside its own catch block, failing
the entire list load (and crashing the instrumented test that exposed it).

Catch the per-project load failure and skip the vanished project - this
also makes the previously-dead `if (metadata != null)` branch live, which
was the original intent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:50:47 -07:00
Wavesonics
52dbde1daf Suppress generic-exception catches at must-not-crash boundaries
Sync, background jobs, API/email send, and UI boundaries legitimately
catch broadly; annotate each with @Suppress and a reason, and log the
exception where it was previously swallowed.
2026-06-06 15:42:35 -07:00
Adam Brown
d1e265a44c
Add Foundation primitives tier; rename Id/Sync/Settings repos (#558)
* Rename IdRepository to IdAllocator
It is a special foundational primative

* Rename SyncDataRepository to SyncJournal
It is a special foundational primative

* Rename GlobalSettingsRepository to GlobalSettingsStore
It is a special foundational primitive

* docs: add Foundation primitives tier to the architecture doc

Document IdAllocator, SyncJournal, and GlobalSettingsStore as a fixed set of
stateful, cross-cutting primitives that the whole data layer may depend on —
acyclic leaves (GlobalSettingsStore <- SyncJournal <- IdAllocator) that named
the dependency reality instead of treating it as a no-sibling violation.
2026-06-06 01:17:13 -07:00
Adam Brown
04a9772d03
Expand Android instrumented UI test coverage across editor screens (#553)
Add a reusable EditorTestHarness (seed a project via Koin, launch straight
into ProjectRootActivity, navigate, tear down without racing the scope flush)
and one happy-path instrumented test per feature area: navigation smoke,
scene list/editor, notes, encyclopedia, timeline, project home, and global
search.

To make the UI addressable, add optional testTag params to shared design-system
components (FormField, MarkdownEditField, HdHairlineField/TagField/SearchField/
TypePicker, HdBottomBar/HdNavRail) and colocated testTag consts on the relevant
screens. The custom text editor consumes key events rather than Compose SetText
semantics, so the harness types into it via injected keystrokes.

All 9 new tests pass on an api-34 emulator.
2026-06-04 00:20:52 -07:00
Adam Brown
da516e1772
Android e2e UI test harness (on-device project lifecycle) (#516)
ProjectLifecycleTest launches the real ProjectSelectActivity, creates a project,
waits for it to appear in the list, and confirms opening it launches
ProjectRootActivity (via ActivityMonitor). Cleans up in @After.

- Wire jetbrains-compose ui-test-junit4 into the androidTest source set.
- Tag the create-project affordance (CreateProjectButtonTestTag); "Create
  Project" otherwise appears as three separate on-screen texts.
- Run the instrumented suite on an emulator in CI (android-emulator-runner) with
  AVD snapshot caching.
- Convert HashTest from JUnit Jupiter to JUnit4 so the AndroidJUnit4 runner can
  execute it on-device (it had "no runnable methods" otherwise); use assertEquals
  since assert() is a no-op when assertions are disabled on a device. It now
  verifies EntityHasher's golden vector on Android ART.
- Fix a scope-close crash: getSceneBufferDirectory used a non-recursive
  createDirectory, so closing a project whose scenes/ dir is absent threw on the
  teardown path and crashed the process. Use createDirectories (matching its
  siblings) and order the test teardown so it doesn't delete the project mid-close.
2026-06-03 22:11:14 -07:00
Adam Brown
c026fd852b
Project Sync dialog improvements
- Conflicted projects now properly report has failing
- Allow filtering of sync log by project
2026-06-02 21:12:09 -07:00
Adam Brown
abac7b5bda
Restructure compact project row metadata into an aligned greeble grid
The phone project-list row showed created/opened/words as one flowing mono
line that wrapped at arbitrary points. Replace it with equal-weight
label/value columns so the fields align down the whole list, with faint
captions over muted values to keep the title the only bright anchor.
2026-06-02 21:12:09 -07:00
Adam Brown
590c9971ca
Restyle server re-auth dialog with design system 2026-06-02 21:12:08 -07:00