Commit graph

3497 commits

Author SHA1 Message Date
Adam Brown
aa51c311dc Prepared for release: v3.9.4
Some checks failed
Build CI / build (push) Has been cancelled
Build CI / static-analysis (push) Has been cancelled
Build CI / android-instrumented-tests (push) Has been cancelled
Build CI / iOS compile & test (push) Has been cancelled
Build CI / iOS UI tests (push) Has been cancelled
PublishInternal / publish-google-play (push) Has been cancelled
2026-08-22 17:48:02 -07:00
Adam Brown
92c0aaa3da Prepared for release: v3.9.3
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-22 10:00:40 -07:00
Adam Brown
d1ba0c6456 Prepared for release: v3.9.2
Some checks are pending
Build CI / iOS UI tests (push) Waiting to run
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-21 23:50:57 -07:00
Adam Brown
a6a17fb1e4 Keep ActionCallback constructors so widget taps work in release
Glance instantiates ActionCallback implementations reflectively, but its
consumer rule only keeps the class, not its members. R8 stripped the no-arg
constructor, so every widget tap threw NoSuchMethodException in minified
builds.
2026-08-21 23:25:05 -07:00
Wavesonics
893e29106d
Test sync webhook
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-20 23:07:38 -07:00
Adam Brown
bec73ea070
Stop submitting the GitHub changelog link to the Apple stores (#928)
prepareForRelease has appended a "Full changelog:" footer pointing at the
GitHub release page to every store's notes since v3.7.0. App Store review
rejects release notes carrying that link — it offers the app outside the
App Store — and iOS and macOS submissions from v3.7.0 on have come back as
invalid metadata. v3.6.1, the last release to ship, predates the footer.

formatStoreNotes and storeNotesLength now take a nullable footer URL, and
the Apple path passes null. Google Play keeps the link: its 500 character
limit means the notes really are truncated there, so the footer is the only
way to reach the full text.

Also strips the stale v3.9.1 footer out of the two committed
release_notes.txt files, so a resubmission of the current version does not
ship it while waiting on the next prepareForRelease.
2026-08-20 21:48:25 -07:00
Wavesonics
9ead030ab9
Extract a generic interface for user gating
Simplify allowed user management.
2026-08-20 21:17:45 -07:00
Adam Brown
d424a346ed Prepared for release: v3.9.1
Some checks failed
Build CI / build (push) Has been cancelled
Build CI / static-analysis (push) Has been cancelled
Build CI / android-instrumented-tests (push) Has been cancelled
Build CI / iOS compile & test (push) Has been cancelled
Build CI / iOS UI tests (push) Has been cancelled
PublishInternal / publish-google-play (push) Has been cancelled
2026-08-18 00:02:23 -07:00
Adam Brown
66e02d9e6e
Render shared stories in scene tree order (#923)
renderPaginated never walked the scene tree, so share pages came out in
entity id order and any project not written front to back rendered
shuffled. Scenes now flatten depth first, siblings by order, with
unreachable ones appended rather than dropped.

A scene-limited share reads only its own scenes and the groups above
them, found via each scene's path, and its validator covers those groups
so a chapter swap no longer serves a stale order. Bumped RENDER_VERSION,
without which cached shares would keep serving the old order.

Chapter headings render as the author wrote them, unnumbered.
2026-08-17 23:58:25 -07:00
Adam Brown
ba70218a44
Support configuring the server time zone (#882) (#922)
Adds a `timezone` config setting, with `HAMMER_TIMEZONE` and `TZ` as
environment-variable fallbacks, applied at startup so both rendered page
timestamps and log lines use it.
2026-08-17 23:20:03 -07:00
Wavesonics
bdc2ea6966
Count probe-skipped projects in the overall sync progress bar
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-17 21:49:57 -07:00
Adam Brown
be04256597
New Crowdin updates (#911) (#920)
* 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 (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 (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 (Portuguese, Brazilian)

[ci skip]

* Update source file strings_project_home.xml

[ci skip]

* Update source file Messages_en.properties

[ci skip]

* Update source file strings_account_settings.xml

[ci skip]

* Update source file strings_encyclopedia.xml

[ci skip]

* Update source file strings_sync.xml

[ci skip]

* 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 (Portuguese, Brazilian)

[ci skip]

* New translations strings_encyclopedia.xml (French)

[ci skip]

* New translations strings_encyclopedia.xml (Spanish)

[ci skip]

* New translations strings_encyclopedia.xml (German)

[ci skip]

* New translations strings_encyclopedia.xml (Italian)

[ci skip]

* New translations strings_encyclopedia.xml (Ukrainian)

[ci skip]

* New translations strings_encyclopedia.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]
2026-08-17 21:43:18 -07:00
Adam Brown
5c3d90ac7f Fix PDF export crash on desktop (#789)
ProGuard stripped org.apache.commons.logging.impl.LogFactoryImpl, which
LogFactory resolves by name, so PDDocument's static init threw
ExceptionInInitializerError on every PDF export from a packaged build.
Only pdfkmp-jvm pulls in PDFBox, so Android is unaffected.

Export failures also went uncaught in ExportDirectoryPicker on all three
platforms, killing the app instead of showing the existing failure toast.
2026-08-17 21:28:46 -07:00
Wavesonics
540392a7a7 Move the sandbox first-run flow onto native AppKit
The Mac App Store build's first-run folder picker ran through AWT, which
claims NSApp before Tao installs its event loop — the app then runs on
with no window and no crash. Move the picker and its alerts into the
Objective-C helper so nothing touches AWT before nucleusApplication().

Localized dialog text now comes from SandboxStrings, which reads the
packaged Compose Resources .cvr value files directly; getString() can't
be called pre-Compose because resolving a ResourceEnvironment
initializes AWT for the density qualifier. Value files layer English
under each locale so a key Crowdin hasn't round-tripped falls back to
real text rather than its own name. SandboxStringsTest guards the format
coupling, since a Compose upgrade would otherwise only surface as raw key
names on an App Store first launch.

Also declare the clang flags as an input on compileMacosBookmarksLib so a
flags-only edit can't leave a stale dylib for signMacAppResources to sign
into the bundle, and document the sandboxed config and data paths in
SUPPORT.md.
2026-08-17 20:42:00 -07:00
Adam Brown
d39819fbe6 Prepared for release: v3.9.0
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-16 22:38:10 -07:00
Adam Brown
8d5899bd46 Address review findings on encyclopedia dictionary
- Conflict resolution: onEncyclopediaEntryConflict now carries aliases and
  excludeFromDictionary into the local entity; "keep local" was stripping
  both, and with the flag in the entity hash that meant a conflict that
  could never converge (the alias drop predates this feature)
- ViewEntryComponent: one saveEntry(transform) helper replaces six
  hand-threaded updateEntry call sites and loads content from disk when
  the async state load hasn't landed, so a save can no longer revert
  aliases or the exclusion flag to defaults
- SpellCheckRepository: SupervisorJob + guarded checker calls so a
  platform failure can't kill the settings collector; appliedCandidates
  recorded only after a successful apply so failures retry
- ProjectDictionaryService: initialize() instead of init side effects,
  cancel-before-clear plus an isActive check to keep a racing rebuild
  from re-adding a closed project's words, spellCheckSettings.enabled
  added to the rebuild gate, injectDefaultDispatcher(), and
  loadEntriesImperative() now returns the defs
- temporaryProjectTask scopes skip the dictionary service, ending the
  checker churn during bulk sync and import
- HdHairlineToggleRow gains an enabled param (semantic disable, no
  ripple) replacing three hand-rolled Box+alpha idioms
2026-08-16 20:50:41 -07:00
Adam Brown
5766d2b532
Connect scenes to every encyclopedia entry type (#917)
ReferenceIndexConfig.default() now enables all five EntryTypes, so Thing,
Event, and Idea entries get linked to the scenes that mention them. The
metadata panel already bucketed refs into Characters/Locations/Other, so
the new types land in Other with their own glyphs.

Add-reference search takes a type set and applies it before the result
cap, so one tab can no longer starve another out of the slice. The
appearances chart filters to PERSON to stay true to its label; the
separate connections total still spans all types.
2026-08-16 20:47:47 -07:00
Adam Brown
4f2c8fb503
Encyclopedia names feed the spell check dictionary (#918)
Entry names and aliases become session-only words (AppLocal scope, never
the OS dictionary) while their project is open, cleared on close. Words
are tokenized to single words and filtered against the base dictionary
so only unknown spellings are added.

Three levels of control, all live-reactive:
- Global toggle in Spell Check settings (SpellCheckerSettings, default on)
- Per-project toggle (ProjectData.encyclopediaDictionary, synced;
  hashed only when false so existing hashes are stable)
- Per-entry exclusion (EntryContent.excludeFromDictionary, synced;
  hashed only when true, same zero-bytes-at-default rule as aliases)

SpellCheckRepository holds session words keyed by ProjectDef and emits a
fresh checker instance whenever the effective word set changes, so open
editors re-run their full scan; the same path re-applies words on locale
change and re-enable. ProjectDictionaryService (ProjectDefScope, eagerly
started in initializeProjectScope) rebuilds the word set from the
encyclopedia on entry changes, debounced, and clears it on scope close.
2026-08-16 20:20:31 -07:00
Adam Brown
5d02c09109
Drop the redundant per-call English message fallback (#916)
#909 installs EnglishFallbackMessageResolver, so every call.t/Msg.r site
already resolves an untranslated key against English. The helper added in
#915 duplicated that for three call sites, and its comment described a
hazard that no longer exists.

The German-locale rename conflict test stays; it passes through the
resolver instead.
2026-08-16 16:06:35 -07:00
Adam Brown
6d7f53db04
Fix renaming a project onto a deleted project's name (#862) (#915)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
Account sync pushed renames before deletions, so renaming a project into
a name still held by one queued for deletion violated the server's
UNIQUE(name, user_id). Phases now run delete -> rename -> create.

The violation also escaped as a 500: ProjectDao maps SQLSTATE 23505 to a
ProjectNameTaken failure and /rename answers 409 Conflict.

Also in here:

- Renames queued against a server-tombstoned project id are dropped.
  They could only 404 and requeue every session.
- API error messages fall back to English. The locale bundles have no
  base Messages.properties to parent through, so a key present only in
  English threw MissingResourceException and made the response a 500 for
  every other locale.
- Rename's ProjectNotFound reported "Invalid project name".
2026-08-16 12:04:35 -07:00
Adam Brown
f73c8354ff
Add scene selection to story export (#914)
* Add scene selection to story export

Mirrors the web scene-limited shares: ExportOptions.sceneIds (null = entire
story, empty fails closed), filtered at the single chapter-building point so
all five formats renumber automatically. The export dialog is now fully
controlled by the retained component state, so in-dialog edits survive
rotation (#885 class).

* Address export scene-selection review findings

- beginProjectExport reads the scene tree off the click handler
- Dialog options freeze while the non-blocking file picker is open, and all
  three platform pickers snapshot options before their suspend
- Cancel discards in-dialog edits again; restore happens in onDismissed so
  nothing flashes during the close animation
- A full selection confirms as no limit, so empty chapter groups still export
- Sceneless projects hide the limit toggle; empty groups get no checkbox
- Options column scrolls on short screens instead of pushing the buttons off
- Group rows are toggleable with checkbox semantics for TalkBack
- Picker list/row lifted into the design system (HdPickerList/HdPickerRow),
  now shared with MoveSceneDialog

* Add a scrollbar to HdPickerList

Overlays the standard MpScrollBarList inside the picker list, covering both
the export scene selector and the move-scene destination list.
2026-08-16 11:55:10 -07:00
Adam Brown
72e1a2d214
Bump cairn to 0.3.0 (#910)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-16 00:38:21 -07:00
Adam Brown
18b10609bf
New Crowdin updates (#905)
* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]
2026-08-16 00:35:54 -07:00
Adam Brown
a94d5c783e
Fall back to English when a locale bundle is missing a key (#909)
ktor-i18n's ResourceBundleMessageResolver threw MissingResourceException
for any key a translation had not picked up yet, since the locale bundles
have no parent bundle to inherit from. Every Msg.r/call.t site turned that
into a 500, most visibly on /signup: rejecting a not-allowed email needs
api_allowedusers_rejected, which Crowdin had not yet delivered to de.

Fixes #883 (registration half)
2026-08-16 00:34:56 -07:00
Adam Brown
7e9bd46c26
Replace the native Argon2 binding with a pure-JVM implementation (#908)
Argon2 hashing went through JNA, which extracts libjnidispatch.so at runtime.
On Linux JNA ignores java.io.tmpdir and uses $XDG_CACHE_HOME, falling back to
<user.home>/.cache, and the Docker image sets user.home=/data. That puts the
shared object on the data volume, so hosts mounting it noexec cannot load it,
and signup and login returned a 500 (#884).

Argon2PasswordHasher derives with BouncyCastle instead, reading the variant,
version, and cost parameters back out of the stored PHC string. Its output is
byte-identical to libargon2 for the same inputs, so existing hashes keep
verifying; the tests pin that against hashes generated by the C implementation.

JNA is still on the runtime classpath via the CLI's terminal library, so the
image also points XDG_CACHE_HOME at its own layer.
2026-08-16 00:26:01 -07:00
Adam Brown
9cde66bef6
Fix AltGr shortcuts closing the desktop app (#859) (#907)
Windows reports AltGr as Ctrl+Alt, so typing @ on a Turkish-Q or German
QWERTZ layout matched the loose Ctrl+Q quit test and closed the app.

Window shortcut matching moves into WindowShortcuts.kt, built on the
exact-modifier matchesShortcut helper, which also tightens Ctrl+W and
Ctrl+Shift+F.

* Move save-all off Ctrl+Alt+S to Ctrl+Shift+S

Ctrl+Alt+S is itself an AltGr chord on Windows, so it fired and ate the
character on layouts that map AltGr+S, such as Polish (s with acute).
Rebound on desktop, Android, and iOS, and Android's global search now
matches its modifiers exactly like the rest.

No shortcut uses Alt now, which is what keeps AltGr keystrokes reaching
the editor.

* Keep Koin hint classes out of test detection
2026-08-16 00:00:10 -07:00
Adam Brown
8c871b344e
Search allowed users by email (#906)
Add email search to the Allowed Users admin page

Filter the list by full or partial email, matched case-insensitively anywhere in
the address. The query survives sorting, paging, remove and both edit dialogs.

The input sits in the page shell, not the swapped fragment, so it keeps focus and
caret while typing; the fragment renders its view state as hidden inputs that the
search box and add form pull in via hx-include.

Also fixes three faults on this path: paginated queries ordered on date_added
alone, letting LIMIT/OFFSET repeat and skip rows that share a timestamp; "Page 1
of 0" from a mustache section on a boxed Integer; and a dead htmx:afterSwap
handler reading an always-empty query string.
2026-08-15 23:53:54 -07:00
Adam Brown
d0d4d667ba
New Crowdin updates (#902)
* 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 (Portuguese, Brazilian)

[ci skip]
2026-08-15 23:09:51 -07:00
Adam Brown
7c51f8a83f Bump koin plugin 2026-08-15 22:55:51 -07:00
Adam Brown
2f59a614f5 Bump Nucleus, lots of nice improvements 2026-08-15 22:55:51 -07:00
Adam Brown
f0372c885d Update discord links 2026-08-15 22:55:51 -07:00
Adam Brown
2ec30600ac lib updates 2026-08-15 22:55:51 -07:00
Adam Brown
ae3a65f774 Upgrade FileKt
this contains the fix for the bug we reported, so we can remove our hack
2026-08-15 22:55:51 -07:00
Adam Brown
bcda76de3d
Fix ClosedScopeException on scene metadata panel destroy, clarify sync log strings (#901)
On project close the Koin project scope closes before Compose disposal
destroys components, so SceneMetadataPanelComponent.onDestroy crashed on
its first lazy resolve. Resolve onDestroy dependencies eagerly via a new
projectGet() helper so the final metadata flush survives project close.

Also: the account sync log claimed a server create failed when only the
local project id save failed (new dedicated string), and the local
create failure string read backwards and lacked its placeholder.

* Address review findings on the metadata flush and sync log strings

- Skip the onDestroy metadata flush until the initial load has completed, so an early destroy cannot overwrite stored metadata with the empty default
- Run the reference scrub off the lifecycle thread and catch flush errors
- Resolve ProjectRootComponent.syncJournal eagerly (read in onStop)
- Correct the projectGet KDoc: a post-close resolve can also silently hit a fresh uninitialized scope
- Pass serverProject.name (not toString) to the local-create failure log, and rename its key so stale translations get re-queued
- Narrow the id-save try block so an updateSyncData failure no longer logs a wrong failure line after the success line

* Narrow the destroy-flush catch to IOException and cover the failure branches
2026-08-15 22:02:55 -07:00
Adam Brown
e9c21b9fb6 Bump appcompat
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-08-15 21:26:55 -07:00
renovate[bot]
794b0d9abd
Update dependency node to v24 (#610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-15 20:11:19 -07:00
renovate[bot]
ba520c5a1e
Update actions/setup-node action to v6 (#609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-15 20:10:55 -07:00
renovate[bot]
0ef8eef0a2
Update dependency ruby to v3.4.10 (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-15 20:10:42 -07:00
renovate[bot]
b3d9c917e6
Update ruby/setup-ruby action to v1.316.0 (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-15 20:09:57 -07:00
Adam Brown
6a01fa451c
New Crowdin updates (#900)
* Update source file strings_scene_list.xml

[ci skip]

* New translations strings_scene_list.xml (French)

[ci skip]

* New translations strings_scene_list.xml (Spanish)

[ci skip]

* New translations strings_scene_list.xml (German)

[ci skip]

* New translations strings_scene_list.xml (Italian)

[ci skip]

* New translations strings_scene_list.xml (Ukrainian)

[ci skip]

* New translations strings_scene_list.xml (Portuguese, Brazilian)

[ci skip]
2026-08-15 20:07:20 -07:00
Adam Brown
633f669f57
Fix scenes becoming unreadable in a directory of exactly ten children (#896)
Order numbers are a contiguous 0-based sequence, so the zero-padded order
field must be as wide as the highest order, not as wide as the child count.
Those two disagree at every power of ten: adding the tenth child to a
directory left single-digit names on disk while every path computation
padded to two, so nothing in that directory could be read back.

The damage was silent. Reads returned "" (loadSceneMarkdownRaw swallows the
IOException), so an affected group exported as blank and opened as a blank
editor. A save then wrote to the padded name, creating a rival file with the
same scene id next to the real one. Nested scenes were hit hardest, since a
mis-padded group takes out its whole subtree.

- countScenes is the last order index; all three padding sites use it, and
  the misnamed SceneDatasource.getLastOrderNumber (raw count) is gone.
  rationalizeChildren follows the same convention.
- getGroupChildPathsById keeps the name-first path instead of associateBy's
  last-wins, so rationalizeTree, validateScenePaths and ScenePathIndex agree
  on which file owns a duplicate id. The sync pass no longer rewrites the
  directory around the stale copy and discards the newer one.
- Content is addressed by resolveSceneContentPath: resolved by id from disk,
  computed only for a scene with no file yet.

Recovery of projects already carrying a stray duplicate is left to a
follow-up migration.
2026-08-15 20:05:28 -07:00
Adam Brown
dd30fb9245
Scene-limited private shares (#897)
* Add per-share scene restriction table and DAO plumbing

* Carry scene restrictions through ProjectAccessRepository

* Filter public story renders to a share's scene set

* Add scene selection to the private share dialog

* Mount share and publish-warning dialogs outside the sticky sidebar

* Keep the share dialog open on a rejected create and let expired shares' passwords be reused

* Drop scene path fixes swept in from another branch

These three files belong to the order-padding fix in #896, not to the
share work. They were picked up by a broad add and are reverted here so
this PR carries only the share changes.

* Harden scene-limited shares per code review

Atomic duplicate-password check under a project-row lock, shared scene-set validation that rejects group ids (review requests included), Set-based fail-closed restriction model, one clock for expiry, live scene counts that surface dead shares, symmetric child-row cleanup, surfaced tree-load failures, api_error_unknown fallback, and dedup of the sheet-dialog CSS.
2026-08-15 19:50:40 -07:00
Adam Brown
74c06a5c92
Keep unsaved edits through a device rotation (#899)
* Keep unsaved edits through a device rotation

Fixes #885. Rotating an Android device rebuilds the composition while the
Decompose component survives (both activities use retainedComponent), so any
draft the UI kept in a plain remember was gone by the time the new composition
ran.

Story Ideas editor: the draft (title, body, tags, uncommitted tag token, edit
mode, and the saved baseline) moves into StoryIdeasComponent and is stashed in
the stateKeeper, so it also survives process death. createIdea/saveIdea give
way to saveDraft(), which reads the draft, folds in a typed-but-uncommitted
tag, and re-baselines off what was actually stored.

Also fixed, same failure mode:
- Encyclopedia entry view re-seeded its rememberSaveable draft from the last
  saved content on every fresh composition, which undid the restore.
- Tag lists on the note, timeline event, and encyclopedia entry create screens.
- The uncommitted token inside HdHairlineTagField.
- The date field on the timeline event create screen.
- The draft name in the scene save-draft dialog.

* Address review findings on the rotation state fix

- Encyclopedia entry: only skip the seed once the fields already hold the
  entry, so starting an edit before the async load lands can't save an
  empty name and body over it.
- Story ideas: a save that finishes after the editor moved on no longer
  stamps its result onto whichever draft is open now.
- Ideas browse: sort mode and tag filter survive a configuration change,
  like the search text already did.
2026-08-15 19:02:42 -07:00
Adam Brown
d054ae3caf
Let the editor take input before the spell check finishes (#898)
The editor was passed enabled = hasReceivedInitialBuffer, and that flag was
only set after runFullSpellCheck() returned. enabled = false makes the editor
read-only and unfocusable, so opening a scene left it rendering and scrolling
but refusing clicks until the whole document had been checked.

Set the flag as soon as the content is loaded. Spell check spans are
decoration and never emit edit operations, so the edit-operation collector
that flag also gates still starts against populated content.

* Bump ComposeTextEditor to 2.4.3

Picks up the spell check scan running off the caller's dispatcher instead of
resuming on it once per word.
2026-08-15 18:03:46 -07:00
Adam Brown
fe8e04a1d5
Make scene tree nesting legible (#895)
Nested groups were indented with an off-by-one that put a depth-2 group at
the same start as its parent, and groups and scenes used different indent
formulas so nothing shared a grid.

- One indent vocabulary in the design system: HdIndentStep, hdIndentFor,
  and Modifier.hdIndentRails for per-ancestor hairline guides.
- Groups get a chevron at every depth, not just the top level, plus a
  scene-count greeble.
- Section rules hang at the group's own indent; top level chapter breaks
  still cut the full panel.
- The drag insert line uses the same indent metric as the rows.
2026-08-15 17:14:08 -07:00
Adam Brown
7424aad1cb
New Crowdin updates (#880)
* 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 (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_sync.xml

[ci skip]

* Update source file Messages_en.properties

[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 (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_account_settings.xml

[ci skip]

* 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 (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_sync.xml

[ci skip]
2026-08-15 13:09:31 -07:00
Adam Brown
761bdca19f
Give the review dialog's scene list room to breathe (#894)
The scene picker was a scroller inside another scroller, capped at 260px, so
it stayed cramped no matter how tall the window was. The dialog body is now
the only scroll region and the list renders at full height inside it.

The "All scenes / Select all" row was position:sticky inside that inner
scroller, with side margins and container padding around it, so rows showed
through the gap above it and the gutters beside it. It is now a plain header
bar above the list, outside the scroller.

The dialog also sat inside the story sidebar, which is position:sticky and
therefore a stacking context its z-index could not escape, leaving it painted
under the site header. Moved the container out to a sibling of <main>.

Short viewports get dvh units, trimmed dialog padding, and a full-bleed sheet
under 600px wide.
2026-08-15 13:01:34 -07:00
Adam Brown
976c9b3468
Let users widen the editor on wide screens with a drag handle (#893)
* Fix desktopTest compile: coVerify for suspend setProjectId

* Let users widen the editor on wide screens with a drag handle

The scene editor and focus mode share a persisted editorMaxWidth
(700-1400dp, GlobalSettings) set by dragging a new HdResizeHandle
gutter on the column's start edge. Rendered width stays
min(window, userMax); the handle only appears when the window is
wide enough, and double-click resets. Live drag width is UI-local
via rememberHdResizeHandleState and persists once on drag end.
2026-08-15 12:32:12 -07:00
Adam Brown
19cc9c0b32
Clear a project's sync baseline when it moves to a new server project (#890)
The sync journal (sync.json) and StoredProjectData.lastSyncedHash record what one
specific server confirmed, but nothing scoped them to that server. Once a project
pointed at a different server project, an untouched project still read as "already
in sync", so ProjectDataSyncOperation took its fast-forward branch and overwrote
local project data with the new server's empty copy: author name and project
settings gone, and nothing uploaded.

ProjectsRepository now clears the baseline whenever the server project id actually
changes, covering cross-server merge, create-account, removeServer, and the 410
stale-id path in FetchServerDataOperation.

The clear is deliberately narrow. The journal is rewritten rather than deleted:
syncedHashes, cachedProjectHash, hashAlgoVersion, currentSyncId and lastSync are
reset, while dirty, newIds, deletedIds and lastId stay, since they are local work
the new server has never seen; dirty entries drop originalHash so an upload cannot
forge a conflict against a server that never agreed to it. Project data keeps its
data and loses only lastSyncedHash, which is what makes the next sync upload
instead of fast-forward. For an open project the clear goes through
ProjectDataRepository under its lock, because clearing the file alone would leave
the stale hash live in memory and write it straight back out. The whole clear runs
before the new id is written, so a failure between the two leaves the project
pointing at the old server rather than at the new one with a stale baseline.

removeProjectId and setProjectId are suspend now, which makes
AccountSettings.removeServer() suspend too. User content is never touched.

Writing-activity logs are also server-scoped and also survive a move; tracked
separately in #892.
2026-08-15 10:06:14 -07:00
Adam Brown
4617b543e0
Adopt server projects by name before uploading local ones (#889)
syncCreatedProjects ran createProjectsOnServer before
createLocalProjectsFromServer, so a local project sharing a name with a server
project was uploaded first and only then had its id overwritten by the name
match. The server dedupes createProject by name, so this usually handed back the
existing uuid, but where the local name was a sanitized form of the server name
it left an orphan duplicate for every other device to download.

Adoption now runs first and reports the names it claimed, so the upload pass
skips them and a queued creation the server already holds under that name is
withdrawn instead of retrying the duplicate on every future sync.

Adoption itself is no longer name-only. A local project is claimed only when its
id is absent from the live server set, so one already bound to a live server
project is never re-pointed at a same-named one, and two server names that
sanitize to a single local name are first-wins rather than last-wins. Tombstoned
ids stay out of that set, so a project whose server copy was deleted elsewhere
can still be claimed.
2026-08-15 09:59:48 -07:00