Commit graph

1164 commits

Author SHA1 Message Date
Adam Brown
a51abd58fa
Fix phantom sync conflicts: lock the baseline + migration backfill + e2e sync tests (#582)
* Lock the sync baseline to the server-confirmed hash

Editing a scene through the editor stamps `lastEdited` (an autosave side
effect), and the sync conflict baseline was being re-derived from local
state at mark-dirty time. Because `lastEdited` had already moved, the
recorded baseline disagreed with what the server stored — so a single
client editing its own scene and resyncing raised a phantom conflict on
every sync, with no other device involved.

Stop re-deriving the baseline. The conflict baseline (`originalHash`) is
now the hash the server last confirmed it holds: recorded only on a
successful transfer (the exact bytes uploaded or downloaded) and read
back when an entity next goes dirty. A field that mutates locally after
a sync can no longer taint the baseline.

- ProjectSynchronizationData gains `syncedHashes` (per-entity confirmed
  hash). EntityOriginalState.originalHash is nullable: null means the
  server never confirmed a hash (new entity, or first edit after upgrade)
  so the server skips the conflict check and the baseline self-heals on
  that upload.
- SyncJournal.markEntityAsDirty reads the baseline from syncedHashes
  rather than taking a recomputed hash; recordSyncedHash sets it.
- EntitySynchronizer.uploadEntity reports the accepted hash via a
  callback; the transfer records it on upload and download.
- The upload sources `originalHash` from syncedHashes at transfer time,
  not the dirty entry's frozen copy — so a partial sync (some entities
  uploaded, a later one failed) can't leave a stale baseline that forges
  a phantom conflict on retry.
- FinalizeSync preserves syncedHashes written mid-transfer and prunes
  deleted ids (including server-driven deletions) from the map.
- Repositories no longer recompute an entity hash to mark for sync.

Adds an integration regression test: a single client editing a scene
through the editor and resyncing must not raise a conflict (red before
this change, green after).

* Add e2e sync resync scenarios and stabilize the integration harness

High-value round-trip coverage for the sync baseline beyond the
content-edit regression, plus the test-harness fixes the new tests
needed to be reliable.

Tests:
- SyncHashStabilityTest: after a sync, the client's hash for a scene
  equals the hash the server stored (baseline and server agree).
- ResyncBaselineScenariosTest: a single client must never raise a
  phantom conflict on resync regardless of how the baseline was set —
  metadata-only edit, rename, and an edit after a download establishes
  the baseline. Each asserts the server's stored hash actually changed,
  so a resync that uploaded nothing can't pass vacuously.

Harness (RoundTripTestBase):
- Bind the client IO/Default dispatchers to the same single-threaded
  dispatcher as Main. FakeFileSystem is not thread-safe (its open-files
  list is a plain MutableList), so the real multi-threaded dispatchers
  let concurrent opens across the repos' background scopes corrupt it,
  throwing intermittently from findOpenFile — the root of the suite's
  order-dependent flakiness.
- newClient() registers each client for teardown-close, so a test that
  throws before its own cleanup can't leak a project scope (and its
  background coroutine scopes) into the next test.
- Shared syncNoConflict()/serverEntityHash() helpers replace per-file
  copies.
- Close the per-test HttpClient in EndToEndTest.tearDown; it was created
  every test and never closed, leaking engine threads.

* Add per-entity-type sync resync matrix

The lock-the-baseline fix lives in the shared sync layer, so it must
hold for every entity type. EntityTypeResyncMatrixTest exercises
create → sync → edit → resync (assert no phantom conflict) for notes,
timeline events, and encyclopedia entries; a create → sync → resync for
immutable scene drafts; and a create → sync → delete → resync that
confirms the entity is removed from the server (covering the deletion
path and the synced-hash pruning).

* Fix timeline store race on sync finalize

TimeLineRepository.storeTimeline() read timelineFlow.replayCache.first(),
which throws NoSuchElementException when the timeline was never loaded.
finalizeSync runs it on every sync — including syncs of a project that
never touched the timeline — so whether the async timeline load had
emitted yet was a race that could fail the whole sync. Guard with
firstOrNull (nothing loaded means nothing to store), matching the
pattern correctEventOrder already uses. Surfaced as order-dependent
flakiness once the e2e suite grew.

* Add sync fuzz/property test

A single client driving a seeded random interleaving of editor content
edits, metadata edits, renames, and syncs must never raise a conflict —
there is no other device, so any conflict is a phantom. Catches ordering
edge cases the hand-written scenarios miss (a sync landing between a
content edit and its debounced autosave, a rename between edits). Seeds
are fixed and printed on failure so a failing sequence can be replayed.

* Backfill sync baselines on the first post-upgrade sync

A client upgrading to the lock-the-baseline version has an empty
syncedHashes map — the field did not exist in the old sync.json. With no
baseline a dirty entity uploads with originalHash = null, and the server
skips the conflict check (ServerEntitySynchronizer only conflicts when
originalHash != null), silently overwriting a concurrent edit from
another device.

FetchLocalData now establishes a baseline for every entity that still
lacks one, before any upload reads it: the current local hash for an
in-sync entity (it equals the server's hash for an agreed entity), or
the frozen pre-edit hash carried over from the old dirty list for an
entity that was already dirty at upgrade time. The local-hash source is
safe — if local has silently diverged from the server, the backfilled
baseline is the old hash, so the next edit's upload is correctly flagged
as a conflict rather than a silent overwrite. Existing recorded baselines
are never touched, so it is a one-time, idempotent migration.

Residual: an entity edited for the very first time post-upgrade, before
any sync has run the backfill, still has no recoverable baseline.

* Remove dead SceneDraftRepository.markForSynchronization

Drafts are immutable after creation, so this was never called (its own
comment said so). Removing it leaves syncJournal unused in the repo, so
drop that field and its import too.

* Persist + assert scene content in SceneTimestampsTest

The test set content via onContentChanged + an immediate storeSceneBuffer.
onContentChanged registers the editor buffer asynchronously, so the
storeSceneBuffer found no buffer ("no buffer present"), no-op'd, and the
scene reached the server empty. The test only asserted timestamps, so it
passed while never validating that content survives the round-trip.

Persist via the synchronous storeSceneMarkdownRaw path, and assert the
server-stored entity's content equals the uploaded content so the blind
spot can't silently return.
2026-06-10 17:48:35 -07:00
Wavesonics
d24aa87cd7 Fix iOS crash on external file select 2026-06-10 17:28:22 -07:00
Wavesonics
55f2c2dc2a Fix iOS spell check 2026-06-10 17:26:17 -07:00
Adam Brown
44d694d464
Fix close scene crash 2026-06-09 00:50:21 -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
8d6cba5ec0
Halve PDF export time by avoiding TOC dry-run pass (#579)
The library's tableOfContents() prints page numbers, which forces a full
dry-run layout of the whole book to resolve them before the real render —
doubling export time. Replace it with a hand-built contents page using
anchor/linkToAnchor: clickable chapter entries with no page numbers (matching
EPUB), so a single layout pass suffices. Chapter bookmarks still drive the
reader outline.
2026-06-08 22:59:06 -07:00
Adam Brown
2c45082ffb
New Crowdin updates (#571)
* New translations strings_project_home.xml (French)

[ci skip]

* New translations strings_project_home.xml (Spanish)

[ci skip]

* New translations strings_project_home.xml (German)

[ci skip]

* New translations strings_project_home.xml (Italian)

[ci skip]

* New translations strings_project_home.xml (Ukrainian)

[ci skip]

* New translations strings_project_home.xml (Chinese Simplified)

[ci skip]

* New translations strings_project_home.xml (Portuguese, Brazilian)

[ci skip]
2026-06-08 20:41:48 -07:00
Adam Brown
c663e879e1
Export dialog now shows loading indicator 2026-06-07 23:16:01 -07:00
Adam Brown
1ac505055a
Cache scene-path scans to fix UI freezes on large projects (#563)
Path resolution re-walked the entire scenes directory from disk on every
call, so a single move or import triggered many full recursive scans —
saturating disk I/O and freezing the UI for minutes on large projects.

Cache the recursive scan in SceneDatasource, invalidated on every
structural mutation (move/create/delete). The scan runs inside a
non-reentrant lock so an invalidation can't interleave and resurrect a
stale list.

Also bound the shutdown temp-save join with a timeout so a wedged save
can't block process exit.

(cherry picked from commit b1e0391c6fde997745f7bb0a8330a47dca44fba8)
2026-06-07 23:15:10 -07:00
Adam Brown
27659be26c
Export to PDF support 2026-06-07 23:07:20 -07:00
Adam Brown
6ff3c2a021
Wrap all tomlkt failures in loadEntry as EntryLoadError
The Codacy catch-narrowing pass (8c15ab68) changed EncyclopediaDatasource.loadEntry
from catch(Exception) to catch(IOException)+catch(SerializationException). The
follow-up fix (f779996f) migrated the other narrowed TOML read sites to
readTomlOrNull but missed this one.

tomlkt doesn't route every decode failure through SerializationException: a corrupt
or hand-edited entry with a non-integer id throws NumberFormatException
(IllegalArgumentException), and parser errors throw IllegalStateException. Those
escaped loadEntry raw instead of being wrapped as EntryLoadError, so
BrowseEntriesComponent.loadEntryContent's catch(EntryLoadError) graceful-degrade
path no longer caught them and the load crashed.

Add IllegalArgumentException and IllegalStateException branches that wrap as
EntryLoadError, matching the readTomlOrNull exception set. Add a test covering a
corrupt entry id.
2026-06-07 19:22:48 -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
Adam Brown
f779996fee
A previous refactor narrowed the TOML loader catches to IOException +
SerializationException, dropping the IllegalArgumentException branch that
readJsonOrNull already has. tomlkt doesn't route every decode failure
through SerializationException: a stale stats.toml (old schema, e.g. a
non-integer key in Map<Int, Int>) throws a raw NumberFormatException.

That uncaught exception killed the parallelMap worker for the affected
project, nulled its result slot, and filterNotNull() dropped it from the
project selection list — while sync, a different path, still showed it.

Add a readTomlOrNull helper mirroring readJsonOrNull that absorbs the full
set tomlkt can throw on bad input (SerializationException,
IllegalArgumentException incl. NumberFormatException, IllegalStateException
from parser errors, IOException), with an onError callback so callers keep
their site-specific logging. Schema-version checks run after decode, so they
can't rescue a decode that throws first.

Migrate every TOML read site to it: ProjectStatisticsCacheReader,
StatisticsDatasource, ProjectDataDatasource, ProjectsListComponent,
ReferenceIndexDatasource, WritingActivityDatasource, and
SceneMetadataDatasource (which previously had no error handling at all).
Stale caches are now treated as a miss and recalculated.

Add ReadTomlOrNullTest covering each exception family and a
ProjectStatisticsCacheReaderTest reproducing the original crash.
2026-06-06 22:43:53 -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
8c15ab68fb Narrow file I/O catches and chain parse-failure exception causes
Read/decode loaders now catch IOException and SerializationException
instead of Exception (matching readJsonOrNull); filename parsers pass
the original exception as the cause of the typed filename error.
2026-06-06 15:42:35 -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
Wavesonics
88016d2ed4 Tests for ProjectsListComponent 2026-06-06 12:15:39 -07:00
Wavesonics
f2069440fd ComponentTest (the harness) gained two things:
- protected open val projectDef — the ProjectDef("Test", HPath(...)) that was copy-pasted in all four component tests now lives once in the base (open, so a test can override if needed).
  - setupComponentKoin(module) — wraps setupKoin and auto-registers the relaxed TagIndexService that every ProjectComponentBase component injects. This removes the per-test boilerplate and permanently fixes the "easy to
  forget" inconsistency the review flagged (SceneEditor had been missing it).
2026-06-06 12:02:37 -07:00
Wavesonics
dcfec0bf6f Added tests to the SceneEditor Components 2026-06-06 11:25:49 -07:00
Wavesonics
7d8a7f22a5 New Componet test harness
Added tests to the Notes Components
2026-06-06 10:33:37 -07:00
Wavesonics
4a13eb7f1f Rounding out our domain layer tests 2026-06-06 09:17:09 -07:00
Adam Brown
d9979a654b
Extract EncyclopediaService; make it the single door for UI (#560)
EncyclopediaRepository reached sideways into StatisticsRepository and
ReferenceIndexRepository (markDirty / markEntryDeleted on entry writes).
Hoist that orchestration into a new EncyclopediaService, mirroring how
SceneEditorService owns the same side-effects for scenes. The repository
is now pure data with no sibling-repo deps; the three write call-sites
(CreateEntryComponent, ViewEntryComponent, ClientEncyclopediaSynchronizer)
route writes through the service, reads stay on the repository.
2026-06-06 01:26:20 -07:00
Adam Brown
86267a1503
refactor: decouple SceneRepository — compose SceneSummary in SceneEditorService (#559)
SceneRepository drops its sibling-repo deps and emits a tree-only flow; SceneContentRepository exposes a dirtyBufferIds StateFlow; SceneEditorService composes the scene list (combine) and owns init orchestration. Dirty markers now update reactively.
2026-06-06 01:23:41 -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
fb5251fe83
Replace GlobalSearchRepository with SearchProjectUseCase + retained state (#557)
GlobalSearchRepository was a stateful class masquerading as a Repository while
fanning out across six repositories — really cross-repo coordination with UI
state bolted on. Split it to match the layering:

- SearchProjectUseCase: stateless cross-repo search (data layer)
- GlobalSearchState: component-layer holder (state + debounce), retained on
  ProjectRootComponent via InstanceKeeper so search survives the modal being
  dismissed/reopened and config changes; stateKeeper carries query+filter
  across process death
- GlobalSearchComponent: thin presenter delegating to the holder

Search state now lives in the component layer instead of a project-scoped
singleton, and MutableValue updates run on the main thread (the use case
offloads its fan-out to the default dispatcher).
2026-06-06 01:08:17 -07:00
Adam Brown
bc1a66fc1e
Decompose SceneEditorRepository into focused repositories + a service facade
Split the monolithic SceneEditorRepository into single-responsibility pieces:

- SceneRepository — scene tree, structure, ordering, paths, on-disk layout
- SceneContentRepository — in-memory buffers, autosave, dirty tracking
- SceneMetadataRepository — per-scene and project metadata
- SceneEditorService — the component-facing facade that orchestrates the three
and applies the cross-cutting side-effects (statistics, writing activity,
reference index) the repositories deliberately don't reach up to perform

Components and synchronizers now talk to SceneEditorService; the underlying
repositories hold no sibling-repo dependencies. Tests are reorganized to match
the new boundaries, and the scene-editing domain API is documented in 
docs/DESIGN_PATTERNS.md.
2026-06-06 00:57:35 -07:00
Adam Brown
38bf8bbcdc
New Crowdin updates (#547)
* New translations strings_desktop.xml (French)

[ci skip]

* New translations strings_desktop.xml (Spanish)

[ci skip]

* New translations strings_desktop.xml (German)

[ci skip]

* New translations strings_desktop.xml (Italian)

[ci skip]

* New translations strings_desktop.xml (Ukrainian)

[ci skip]

* New translations strings_desktop.xml (Chinese Simplified)

[ci skip]

* New translations strings_desktop.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_projects_list.xml (French)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_projects_list.xml (Spanish)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_projects_list.xml (German)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_projects_list.xml (Italian)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_projects_list.xml (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_projects_list.xml (Chinese Simplified)

[ci skip]

* New translations strings_sync.xml (Chinese Simplified)

[ci skip]

* New translations strings_projects_list.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]
2026-06-04 20:33:11 -07:00
Wavesonics
b4a077c739 Fix restored mid-edit drafts being silently discarded
ViewNote/ViewTimeLineEvent come back from process death with isEditing=true
  and the restored draft, but the note/event isn't loaded yet (async, or cold
  cache). In that window isEditingAndDirty() returned false, so a back-press or
  close took the silent discardEdit() path and wiped the restored text.

  Treat "editing with no loaded baseline" as dirty so those paths route to the
  confirm-discard dialog instead. Behavior is unchanged once loaded.
2026-06-04 18:00:17 -07:00
Wavesonics
e477e0de04 Correct some saved state 2026-06-04 17:44:30 -07:00
Wavesonics
b39fc05a4c More Saveable components
These will allow unsaved user input to survive process death even
2026-06-04 17:36:10 -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
6fd56b4699
fix: unzip compiles for Kotlin/Native (iOS archive) (#550)
ZipUtils.unzipBytesToDirectory used fileSystem.sink(target).buffer(),
where okio's Sink.buffer() failed to resolve for the iOS/native target
(receiver type mismatch), cascading into a 'cannot infer R' error on the
following .use{} and failing the iOS archive. Compiled fine for JVM, so
it only surfaced in the iOS publish workflow.

Use okio's multiplatform FileSystem.write(target) { } helper, which
yields a BufferedSink directly and handles buffering + closing. Verified
with :common:compileKotlinIosArm64.
2026-06-02 23:34:59 -08: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
2cf4afba8e
Fix sync wedge: phantom newIds and ID-conflict collision guard
Resolves the "Entity X not found for reId" failure that wedged every
project sync. Three related fixes:

- EntitySynchronizers.reIdEntry now skips a newId with no backing entity
  (logged) instead of throwing. The throw aborted sync before finalize,
  and finalize is the only place newIds is cleared, so a single phantom
  deadlocked every future sync. Skipping lets sync finish and self-heal.

- SyncDataRepository.recordIdDeletion prunes the id from newIds when the
  entity was never synced, instead of only appending to deletedIds. This
  stops create-then-delete from leaving a permanent phantom newId behind.

- IdConflictResolutionOperation seeds new-id assignment from
  max(serverLastId, localMaxId). If the server ever reports a lastId
  below the client max (server-side data loss), seeding from serverLastId
  alone handed out ids that collided with and clobbered real local
  entities. Now logs a WARN and assigns above the local max.

Tests reproduce each failure first, then pass after the fix.
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
Adam Brown
b284a45d89
Handle non-success GitHub responses in version check
Avoids a MissingFieldException when the releases endpoint returns a
rate-limit/error body instead of a release.
2026-06-02 21:12:08 -07:00
Adam Brown
43d5389be4
Fix Auto-sync and Close-sync-dialog toggles not reflecting state
The settings updates collector rebuilt component state without copying
syncAutomaticSync or syncAutoCloseDialog, so toggling either checkbox
persisted the change but never updated the UI. Also removes leftover
debug logging.
2026-06-02 21:12:08 -07:00
Wavesonics
d4502dc227 MacOS sandboxing 2026-06-02 20:54:18 -07:00
Wavesonics
fd8f857001 Merge branch 'macos-sandboxing' into develop
# Conflicts:
#	common/src/commonMain/composeResources/values/strings_desktop.xml
2026-06-02 20:52:18 -07:00
Wavesonics
fcf25c8caf Merge branch 'macos-sandboxing' into develop
# Conflicts:
#	common/src/commonMain/composeResources/values/strings_desktop.xml
2026-06-02 20:50:21 -07:00
Wavesonics
0fe9dc8ea8 Strings 2026-06-02 20:49:50 -07:00
Adam Brown
8779268147
New Crowdin updates (#533)
* New translations strings_desktop.xml (French)

[ci skip]

* New translations strings_desktop.xml (Spanish)

[ci skip]

* New translations strings_desktop.xml (German)

[ci skip]

* New translations strings_desktop.xml (Italian)

[ci skip]

* New translations strings_desktop.xml (Ukrainian)

[ci skip]

* New translations strings_desktop.xml (Chinese Simplified)

[ci skip]

* New translations strings_desktop.xml (Portuguese, Brazilian)

[ci skip]
2026-05-31 21:19:34 -07:00
Wavesonics
569c9faaa5 Add a desktop splash screen 2026-05-31 01:46:30 -07:00
Adam Brown
a28b82b2ec
New Crowdin updates (#532)
* New translations strings_notes.xml (French)

[ci skip]

* New translations strings_notes.xml (Spanish)

[ci skip]

* New translations strings_notes.xml (German)

[ci skip]

* New translations strings_notes.xml (Italian)

[ci skip]

* New translations strings_notes.xml (Ukrainian)

[ci skip]

* New translations strings_notes.xml (Chinese Simplified)

[ci skip]

* New translations strings_notes.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_timeline.xml (French)

[ci skip]

* New translations strings_timeline.xml (Spanish)

[ci skip]

* New translations strings_timeline.xml (German)

[ci skip]

* New translations strings_timeline.xml (Italian)

[ci skip]

* New translations strings_timeline.xml (Ukrainian)

[ci skip]

* New translations strings_timeline.xml (Chinese Simplified)

[ci skip]

* New translations strings_timeline.xml (Portuguese, Brazilian)

[ci skip]
2026-05-31 01:07:26 -07:00
Wavesonics
79880f5a52 Greatly improved markdown editor space on phones
On mobile only, we now inteligently hide pretty much everything that isn't the markdown editor when typing in there.
2026-05-31 00:36:22 -07:00
Adam Brown
4537909826
Add prose diff visualization to draft compare and conflict merge UIs (#525)
New ProseDiff module: word-level Myers diff (kotlin-multiplatform-diff)
over markdown text, with syntax stripped via the JetBrains parser and
offsets mapped back to source. Merges adjacent edits into single hunks,
emits anchors at each boundary for scroll sync, and includes a git-style
slider that aligns pure insert/delete blocks to line boundaries. Covers
plain-text diffing, prepared/reusable inputs, and an OffsetMap that maps
a position on one side to its counterpart.

Draft compare: highlights DELETED (red/strikethrough) and INSERTED
(green/underline) spans via non-destructive RichSpans in each pane's
rendered coordinate space, recomputed off-thread on a 500ms debounce.
Synchronized scrolling (compose-texteditor 2.0.7) drives either pane from
the other off raw scroll pixels with an echo guard. Both editor states are
hoisted so highlights and sync work in compact and expanded layouts and
survive theme changes. A DIFF toggle gates highlights and sync.
2026-05-30 09:49:33 -07:00
Wavesonics
9a604ec95d Mark sync failed and complete when sync job throws
Unhandled exceptions left syncComplete=false, so the sync dialog
showed the cancel confirmation when closing an already-failed sync.
2026-05-30 00:00:28 -07:00
Wavesonics
310cd423fb Add localized first-run intro dialog for sandboxed macOS builds
Show an explanatory welcome dialog before the projects-directory picker so
first-time users understand why macOS is asking them to choose a folder.
Route all user-facing strings in the sandbox first-run flow through StrRes
so they're translatable via Crowdin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:52:17 -07:00
Adam Brown
d9cc7d1b76
New Crowdin updates (#497)
* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_account_settings.xml (Spanish)

[ci skip]

* New translations strings_account_settings.xml (German)

[ci skip]

* New translations strings_account_settings.xml (Italian)

[ci skip]

* New translations strings_account_settings.xml (Ukrainian)

[ci skip]

* New translations strings_account_settings.xml (Chinese Simplified)

[ci skip]

* New translations strings_account_settings.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_project_select_navigation.xml (French)

[ci skip]

* New translations strings_project_select_navigation.xml (Spanish)

[ci skip]

* New translations strings_project_select_navigation.xml (German)

[ci skip]

* New translations strings_project_select_navigation.xml (Italian)

[ci skip]

* New translations strings_project_select_navigation.xml (Ukrainian)

[ci skip]

* New translations strings_project_select_navigation.xml (Chinese Simplified)

[ci skip]

* New translations strings_project_select_navigation.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_desktop.xml (French)

[ci skip]

* New translations strings_desktop.xml (Spanish)

[ci skip]

* New translations strings_desktop.xml (German)

[ci skip]

* New translations strings_desktop.xml (Italian)

[ci skip]

* New translations strings_desktop.xml (Ukrainian)

[ci skip]

* New translations strings_desktop.xml (Chinese Simplified)

[ci skip]

* New translations strings_desktop.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_project_home.xml (French)

[ci skip]

* New translations strings_project_home.xml (Spanish)

[ci skip]

* New translations strings_project_home.xml (German)

[ci skip]

* New translations strings_project_home.xml (Italian)

[ci skip]

* New translations strings_project_home.xml (Ukrainian)

[ci skip]

* New translations strings_project_home.xml (Chinese Simplified)

[ci skip]

* New translations strings_project_home.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_scene_editor.xml (French)

[ci skip]

* New translations strings_scene_editor.xml (Spanish)

[ci skip]

* New translations strings_scene_editor.xml (German)

[ci skip]

* New translations strings_scene_editor.xml (Italian)

[ci skip]

* New translations strings_scene_editor.xml (Ukrainian)

[ci skip]

* New translations strings_scene_editor.xml (Chinese Simplified)

[ci skip]

* New translations strings_scene_editor.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings.xml (French)

[ci skip]

* New translations strings.xml (Spanish)

[ci skip]

* New translations strings.xml (German)

[ci skip]

* New translations strings.xml (Italian)

[ci skip]

* New translations strings.xml (Ukrainian)

[ci skip]

* New translations strings.xml (Chinese Simplified)

[ci skip]

* New translations strings.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_about_app.xml (French)

[ci skip]

* New translations strings_about_app.xml (Spanish)

[ci skip]

* New translations strings_about_app.xml (German)

[ci skip]

* New translations strings_about_app.xml (Italian)

[ci skip]

* New translations strings_about_app.xml (Ukrainian)

[ci skip]

* New translations strings_about_app.xml (Chinese Simplified)

[ci skip]

* New translations strings_about_app.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_notes.xml (French)

[ci skip]

* New translations strings_notes.xml (Spanish)

[ci skip]

* New translations strings_notes.xml (German)

[ci skip]

* New translations strings_notes.xml (Italian)

[ci skip]

* New translations strings_notes.xml (Ukrainian)

[ci skip]

* New translations strings_notes.xml (Chinese Simplified)

[ci skip]

* New translations strings_notes.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_project_home.xml (French)

[ci skip]

* New translations strings_project_home.xml (Spanish)

[ci skip]

* New translations strings_project_home.xml (German)

[ci skip]

* New translations strings_project_home.xml (Italian)

[ci skip]

* New translations strings_project_home.xml (Ukrainian)

[ci skip]

* New translations strings_project_home.xml (Chinese Simplified)

[ci skip]

* New translations strings_project_home.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_timeline.xml (French)

[ci skip]

* New translations strings_timeline.xml (Spanish)

[ci skip]

* New translations strings_timeline.xml (German)

[ci skip]

* New translations strings_timeline.xml (Italian)

[ci skip]

* New translations strings_timeline.xml (Ukrainian)

[ci skip]

* New translations strings_timeline.xml (Chinese Simplified)

[ci skip]

* New translations strings_timeline.xml (Portuguese, Brazilian)

[ci skip]

* New translations messages_en.properties (French)

[ci skip]

* New translations messages_en.properties (Spanish)

[ci skip]

* New translations messages_en.properties (German)

[ci skip]

* New translations messages_en.properties (Italian)

[ci skip]

* New translations messages_en.properties (Ukrainian)

[ci skip]

* New translations messages_en.properties (Chinese Simplified)

[ci skip]

* New translations messages_en.properties (Portuguese, Brazilian)

[ci skip]
2026-05-27 00:41:14 -07:00
Wavesonics
62e137df59 Implement sandbox bookmark handling for MacOS 2026-05-27 00:29:26 -07:00