Commit graph

1477 commits

Author SHA1 Message Date
Adam Brown
4edff36e95
New Crowdin updates (#968)
* New translations messages_en.properties (Turkish)

[ci skip]

* New translations strings.xml (Turkish)

[ci skip]

* New translations strings_about_app.xml (Turkish)

[ci skip]

* New translations strings_account_settings.xml (Turkish)

[ci skip]

* New translations strings_desktop.xml (Turkish)

[ci skip]

* New translations strings_drafts.xml (Turkish)

[ci skip]

* New translations strings_encyclopedia.xml (Turkish)

[ci skip]

* New translations strings_notes.xml (Turkish)

[ci skip]

* New translations strings_project_home.xml (Turkish)

[ci skip]

* New translations strings_project_navigation.xml (Turkish)

[ci skip]

* New translations strings_project_select_navigation.xml (Turkish)

[ci skip]

* New translations strings_projects_list.xml (Turkish)

[ci skip]

* New translations strings_scene_editor.xml (Turkish)

[ci skip]

* New translations strings_scene_list.xml (Turkish)

[ci skip]

* New translations strings_sync.xml (Turkish)

[ci skip]

* New translations strings_timeline.xml (Turkish)

[ci skip]

* New translations strings_android.xml (Turkish)

[ci skip]

* New translations full_description.txt (Turkish)

[ci skip]

* New translations short_description.txt (Turkish)

[ci skip]

* New translations title.txt (Turkish)

[ci skip]

* New translations strings_globalsearch.xml (Turkish)

[ci skip]

* New translations strings_ideas.xml (Turkish)

[ci skip]

* New translations strings_wear_pairing.xml (Turkish)

[ci skip]
2026-09-29 23:24:46 -07:00
Adam Brown
569b6c0297
New Crowdin updates (#965)
* New translations strings_about_app.xml (Italian)

[ci skip]

* New translations messages_en.properties (Italian)

[ci skip]

* New translations strings_wear_pairing.xml (Italian)

[ci skip]

* New translations full_description.txt (Italian)

[ci skip]

* New translations strings_project_home.xml (Italian)

[ci skip]

* New translations strings_project_navigation.xml (Italian)

[ci skip]

* New translations strings_android.xml (Italian)

[ci skip]
2026-09-27 21:04:01 -07:00
Adam Brown
1cdcdad657
Recover from an undecryptable auth token keyset instead of crashing (#960)
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
* Recover from an undecryptable auth token keyset instead of crashing

Exclude the encrypted token prefs from backups, and attach the R8 mappings
to GitHub releases.

* Reset the Keystore master key along with the token prefs
2026-09-27 17:43:25 -07:00
Wavesonics
04de488fb0
Treat any-script letters, marks, and digits as word characters in reference matching
Names no longer match inside words containing accented or non-Latin letters ("Ana" in "Anaïs", "Иван" in "Иванов").
2026-09-25 17:51:38 -07:00
Adam Brown
1c22fdb926 Prepared for release: v3.11.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-09-24 23:47:29 -07:00
Wavesonics
fcbfa1bee5
Exclude standalone Markdown markers from word counts
Only count runs of non-whitespace containing a letter or digit, so
horizontal rules, heading, bullet and blockquote markers are not words.
Move countWords to :base and use it on the server too.
2026-09-23 23:55:53 -07:00
Wavesonics
61b3b21ab9
Prepared for release: v3.10.1
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-09-23 07:53:53 -07:00
Adam Brown
6394005f60 Prepared for release: v3.10.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-09-20 10:16:01 -07:00
Adam Brown
48fd14000c
New Crowdin updates (#926)
* New translations strings_about_app.xml (French)

[ci skip]

* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_encyclopedia.xml (French)

[ci skip]

* New translations messages_en.properties (French)

[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]

* Update source file Messages_en.properties

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_encyclopedia.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_about_app.xml (French)

[ci skip]

* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_about_app.xml (Spanish)

[ci skip]

* New translations strings_account_settings.xml (Spanish)

[ci skip]

* New translations strings_about_app.xml (German)

[ci skip]

* New translations strings_account_settings.xml (German)

[ci skip]

* New translations strings_about_app.xml (Italian)

[ci skip]

* New translations strings_account_settings.xml (Italian)

[ci skip]

* New translations strings_about_app.xml (Ukrainian)

[ci skip]

* New translations strings_account_settings.xml (Ukrainian)

[ci skip]

* New translations strings_about_app.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_account_settings.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_about_app.xml

[ci skip]

* Update source file strings_account_settings.xml

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

[ci skip]

* Update source file strings_about_app.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 messages_en.properties (French)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations strings_wear_pairing.xml (French)

[ci skip]

* New translations messages_en.properties (Spanish)

[ci skip]

* New translations strings_sync.xml (Spanish)

[ci skip]

* New translations strings_wear_pairing.xml (Spanish)

[ci skip]

* New translations messages_en.properties (German)

[ci skip]

* New translations strings_sync.xml (German)

[ci skip]

* New translations strings_wear_pairing.xml (German)

[ci skip]

* New translations messages_en.properties (Italian)

[ci skip]

* New translations strings_sync.xml (Italian)

[ci skip]

* New translations strings_wear_pairing.xml (Italian)

[ci skip]

* New translations messages_en.properties (Ukrainian)

[ci skip]

* New translations strings_sync.xml (Ukrainian)

[ci skip]

* New translations strings_wear_pairing.xml (Ukrainian)

[ci skip]

* New translations messages_en.properties (Portuguese, Brazilian)

[ci skip]

* New translations strings_sync.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_wear_pairing.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_account_settings.xml

[ci skip]

* Update source file strings_sync.xml

[ci skip]

* Update source file strings_wear_pairing.xml

[ci skip]

* Update source file Messages_en.properties

[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]

* New translations strings_encyclopedia.xml (French)

[ci skip]

* New translations strings_scene_editor.xml (French)

[ci skip]

* New translations strings_encyclopedia.xml (Spanish)

[ci skip]

* New translations strings_scene_editor.xml (Spanish)

[ci skip]

* New translations strings_encyclopedia.xml (German)

[ci skip]

* New translations strings_scene_editor.xml (German)

[ci skip]

* New translations strings_encyclopedia.xml (Italian)

[ci skip]

* New translations strings_scene_editor.xml (Italian)

[ci skip]

* New translations strings_encyclopedia.xml (Ukrainian)

[ci skip]

* New translations strings_scene_editor.xml (Ukrainian)

[ci skip]

* New translations strings_encyclopedia.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_scene_editor.xml (Portuguese, Brazilian)

[ci skip]
2026-09-20 10:02:03 -07:00
Adam Brown
cbc88944a4
Add a project-scoped user spelling dictionary (#939)
Add a project-scoped user spelling dictionary (#939)

Writers can whitelist words per project: "Add to dictionary" on a flagged word in
the scene editor and focus mode, plus an add/remove word list in Project Settings
under Spell checking.

Words live in ProjectData.dictionaryWords so they sync with the project. A conflict
confined to the word list merges both sides by union with no resolver; other
conflicting fields still go through the existing resolver with the dictionary
unioned. The union is verbatim, so a word a newer build stored under laxer rules
is never deleted server-side. Sync writes also re-apply any project-data edit made
after the phase snapshot, so an edit landing mid-sync is no longer clobbered.

ProjectDictionaryService now feeds user words to the checker alongside encyclopedia
words, independent of the encyclopedia toggle. HdHairlineTagField and the new
HdHairlineWordListField share an extracted HdHairlineChipInput.
2026-09-20 09:45:19 -07:00
Adam Brown
66875346d0
Widen the character set for encyclopedia and draft names (#952)
Encyclopedia entry titles and scene draft names were still on the old
restricted set while project and scene names had moved to the shared
ProjectNameValidator. Both now use that validator, so punctuation like
`. , ! ? : ( ) & - "` works, and draft names accept non-Latin letters.

Both filename formats move to the `~` delimiter, since `-` is now a legal
name character:

  encyclopedia  type~id~name.toml, images type~id~image.ext
  drafts        sceneId~draftId~name~timestamp.md

Names round-trip through encodeForFilename/decodeFromFilename, so
OS-forbidden characters become lookalikes on disk. Legacy patterns are
kept for reads, and path resolution falls back to the legacy filename,
because unlike scenes these two rebuild the filename from the def rather
than scanning the tree.

Migration2_3 renames existing files and PROJECT_DATA_VERSION goes to 3.
It canonicalises names through the encoder, otherwise a name ending in a
space (which the old rules allowed) would migrate to a file that no
rebuilt path could ever match. Draft names are now trimmed at the save
and sync boundaries the way entry names already were.

A synced name containing a path separator is no longer rejected outright;
it is encoded to a lookalike and stays one path segment, matching how
scene names already behave. Containment rests on the isWithin guard. The
reserved `~` delimiter is still rejected.
2026-09-20 09:01:26 -07:00
Adam Brown
6738235141
Wear OS app: capture notes and story ideas from the wrist (#950)
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
A standalone watch client that dictates notes into subscribed projects
and story ideas, then syncs them on its own session.

- New :wear module: capture activity, tile and complication, project
  subscriptions, sync log, and a WorkManager periodic plus
  after-capture sync behind a single SyncCoordinator
- Pairing over the Wearable Data Layer: the phone confirms the request
  and mints a session for the watch's install via the new
  /api/account/pair_install endpoint. Play Services builds only; the
  F-Droid build ships without it
- Manual sign-in on the watch, with an Android 17 local network
  permission check before contacting a LAN server
- Plaintext sync is opt-in: typing an http:// URL selects it, with a
  warning in server setup. HTTPS stays the default, and legacy
  settings always restore as HTTPS
- Account sync extracted into SyncAccountUseCase, shared by the phone
  project list and the watch
- temporaryProjectTask ref-counts concurrent users so one task no
  longer closes a scope another is still using
- Wear version codes are offset from phone codes so both can ship in
  one Play listing
- Crash handler and FileLogger moved into common for reuse
2026-09-19 10:58:19 -07:00
Adam Brown
3df7f27617
Say why a login failed instead of answering a bare 401 (#937)
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
* Say why a login failed instead of answering a bare 401

A failed login gave nobody anything to work with. StatusPages matched status 401
and re-responded with a bare status for API calls, discarding the HttpResponseError
the route had just written, and the client's own 401 branch discarded whatever
body did survive in favour of a generic string. Two layers erasing the same
message, so a self-hoster saw "401 Unauthorized" in the log, an empty body on the
wire, and a generic message in the app.

StatusPages now leaves API responses alone, since the routes answer 401 with
their own body, and the client parses the body for every status and only falls
back to a generic message when the server sent nothing usable.

On top of that:

- HttpResponseError carries an optional errorCode from a shared ApiErrorCode
  vocabulary. It is optional and the shared serializer ignores unknown keys, so
  it is compatible with servers and clients on either side of this change.
- Account creation failures use accurate statuses rather than a blanket 409:
  400 for a policy or email violation, 409 for a real conflict, 403 for the
  whitelist. Login answers 403 when the whitelist was the problem, since that is
  not a credential failure.
- The login path logs which failure occurred. The response still cannot
  distinguish an unknown account from a wrong password, because that would let
  anyone enumerate users, but the operator's log now can.

The docs gain the password policy (8 to 64 characters, no complexity rule,
nothing stripped or truncated) and the log lines to look for, which is what the
reporter of #835 asked for.

* Allow the round-trip test accounts now that Allowed Users is always on

Only the first account on a server is exempt, so every later address the test
creates has to be on the list or account creation answers 403.

* Point the login-failure note at the Allowed Users section

The section was renamed, so the anchor was dead.

* Cover the account error statuses and the client's failure body

AccountErrorCodeTest walks the create and refresh failures a client branches on:
existing email, email pending deletion, malformed email, short password, unknown
refresh token. ApiFailureBodyTest covers the other end, where a server message
has to survive instead of being replaced by the generic one.

RecordingStrRes moves to its own file so both API tests can use it.

* Mark the password error codes as non-secrets for semgrep
2026-09-14 19:05:25 -07:00
Adam Brown
404d386d12
Add a Copy Diagnostics action to the About screen (#940)
* Add a Copy Diagnostics action to the About screen

Puts the startup banner (version, channel, OS/JVM) and the last 200 lines
of the current log on the clipboard, so a bug report can carry its own
diagnostics instead of asking the reporter to find, zip, and attach a log.

The blob always includes the banner: a missing log costs the reporter the
log, not the whole report.

The buttons move into a FlowRow so three of them wrap on a narrow window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Include the latest crash dump in Copy Diagnostics

* Only report the XDG session in the desktop banner on Linux

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:36:51 -07:00
Adam Brown
1bc1e37460
Fix the Microsoft Store build, and stamp builds with their distribution channel (#938)
* Switch the desktop Ktor engine to OkHttp

`java.net.http.HttpClient`'s constructor opens an NIO `Selector`, whose
Windows wakeup pipe is an AF_UNIX socket. The MSIX sandbox rejects the
connect with EINVAL, so every Microsoft Store build hard-crashes on the
first network call, before any request is sent (JDK-8312215, open since
Java 17 with no fix in sight). OkHttp uses blocking socket IO and is
unaffected.

Ships unconditionally rather than gated on the distribution channel:
OkHttp works everywhere, and a second engine would be a second code path
to test forever. Android already uses it.

Includes the unchecked-IO mapping from `fix/unchecked-io-network-errors`,
so a client that cannot be built at all surfaces as a network error
rather than taking down the app.

* Add an Export Logs button to the desktop About screen

Opening the log directory hands the shell a path that does not exist under
an MSIX container: the app's writes to %LOCALAPPDATA% are redirected into
the package's LocalCache, but reads fall through, so the app sees the
directory and Explorer does not. Snap confinement will do the same on
Linux. Exporting a zip to a location the user picks works on every
distribution vehicle, which makes it the one support instruction that
never needs a per-channel caveat.

Also walk up to the nearest existing ancestor before handing a directory
to the shell, so the open button degrades instead of erroring.

* Bake the distribution channel into the build

Per-vehicle rules (self-update, store payment policy, "get the app" links,
sandbox-aware paths) were tracked by hand. `-Pchannel=<token>` now resolves
to a `DistributionChannel` constant, defaulting to DEV, and an unknown
token fails the build rather than quietly shipping a store binary as DEV.
Every release pipeline passes its own channel; the existing F-Droid build
flags map to FDROID without their call sites changing.

Flat enum rather than capability flags: every channel-conditional branch is
a greppable `when` on the type. Flags can emerge later from actual
duplication.

The startup banner and all three crash dumps now carry the channel. That
line pays for itself immediately: this investigation started from a crash
log that did not say which build produced it.

* Un-redirect container paths before handing them to the shell

MSIX filesystem redirection is asymmetric: the app's writes under
%LOCALAPPDATA% land inside the package container, but reads fall through,
so File.exists() is true from inside the container and the app is
satisfied. Explorer runs outside it and correctly reports nothing at the
literal path, which is the "location is not available" dialog the
open-logs button produced on the Store build.

Rewrite the path into the container for the Microsoft Store channel, both
where it is shown and where it is handed to the shell. The package family
name is hardcoded: Windows derives its hash suffix from the publisher ID,
so it is not in AppxManifest.xml and cannot be computed from it, and it
only changes if the Store identity does.

`Windows.Storage.ApplicationData.Current.LocalCacheFolder` is the native
answer but means a WinRT dependency on a KMP desktop target for one path
lookup. Worth revisiting only if more packaged-app APIs are needed.

* Cover the About log section with a render test, document the channel

* Suppress TooGenericExceptionCaught on the unchecked IO catch
2026-09-14 15:38:15 -07:00
Adam Brown
4b2dff8c23
Map unchecked IO failures to a real error message (#933)
* Map unchecked IO failures to a real network error message

The JDK builds its `java.net.http.HttpClient` lazily on the first request,
and `HttpClientImpl`'s constructor wraps a failed `Selector.open()` in an
`UncheckedIOException`. On Windows that selector is a loopback socket pair,
so a machine with a firewall, VPN filter driver, or exhausted ephemeral port
range fails with `java.io.IOException: Unable to establish loopback
connection` before a request is ever sent.

`UncheckedIOException` is a `RuntimeException`, so it slipped past every
catch in `Api.makeRequest` and surfaced in the sync log as a raw Java class
name. Route it through the same mapping, with a dedicated message: nothing
reached the network, so this must not read as a server or connectivity
problem.

* Only log "Sync complete!" when the sync actually succeeded

`handleSyncFailure` calls `onComplete`, which logged the success line
unconditionally, so a failed sync ended with an error immediately followed
by "Sync complete!". Thread the outcome through `onComplete` and gate the
log on it; the progress reset still runs either way.

* Suppress TooGenericExceptionCaught on the unchecked IO catch
2026-09-14 15:38:11 -07:00
Adam Brown
ca909f4a6b
Add a shared network JSON serializer that tolerates unknown keys (#934)
`SYNCING-PROTOCOL.md` makes adding a field to a synced model a client-only
change: the server stores content as an opaque blob, and a peer that predates
the field is expected to drop it on decode. That only holds if the decode
ignores unknown keys.

Every `ContentNegotiation` install used a bare `json()`, which is Ktor's
`DefaultJson`: `encodeDefaults` and `isLenient`, but no `ignoreUnknownKeys`. So
instead of dropping the field, an older peer failed the whole sync. A 3.6.0
client hitting project data written by 3.9 got:

    Sync failed: Illegal input: Unexpected JSON token at offset 72:
    Encountered an unknown key 'language' at path: $.data

`createJsonSerializer` already sets `ignoreUnknownKeys`, but it is meant for
human-facing files: `prettyPrint` would put tabs and newlines in every request
body, and `coerceInputValues` would silently coerce bad values on the wire.
Split the two: files keep that one, machine-facing content gets
`createNetworkJsonSerializer`, resolved through `NetworkJsonQualifier`.

Applies to the three `ContentNegotiation` installs and the four classes that
decode wire content with an injected `Json`: `ServerProjectApi` (entity
payloads), `ProjectDataApi` and `ServerIdeasApi` (conflict bodies), and
`GithubVersionCheckDataSource`. The file-facing injections are unchanged.

This cannot reach already-shipped builds; 3.6 through 3.9.x keep failing
against projects a newer client has touched.
2026-09-14 15:33:32 -07:00
Adam Brown
375d4553c6
Stop credential fields from auto-capitalizing what is typed (#936)
* Stop credential fields from auto-capitalizing what is typed

HdHairlineField defaults to KeyboardCapitalization.Sentences, and neither the
server setup dialog nor the reauthentication dialog overrode it on their email or
password fields. An IME that honours CAP_SENTENCES on a password variation
capitalizes the first character as it is entered, so the account is created with
a password the user cannot reproduce in a browser, where nothing capitalizes
anything. The failure looks exactly like a rejected password.

HdPasswordField and HdEmailField pin the keyboard options that credential entry
needs (no auto-capitalization, no autocorrect) so a screen cannot forget them.
HdPasswordField also absorbs the show/hide toggle that both dialogs had inlined
identically, and carries the 8 to 64 character rule as a field hint, formatted
from PasswordValidator so it cannot drift from what the server enforces.

* Add a design-system preview for the credential fields

Covers the password field masked with its length hint, revealed with an error,
and the email field beside them.
2026-09-14 15:33:17 -07:00
Wavesonics
59d1537950
Prepared for release: v3.9.8
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-09-12 00:54:08 -07:00
Wavesonics
72304dfa87 Prepared for release: v3.9.7 2026-09-04 20:08:54 -07:00
Wavesonics
e833d57f8d Prepared for release: v3.9.6
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-09-03 20:25:54 -07:00
Wavesonics
9fd796c342
Prepared for release: v3.9.5 2026-09-01 20:51:00 -07:00
Wavesonics
4ad20162f4
Squash merge no-in-app-changelog-on-apple into develop 2026-09-01 20:27:32 -07:00
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
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
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
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
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
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
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
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
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
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
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
Adam Brown
03863ea420
Ask Merge vs Replace on login instead of "Remove Local Content?" (#888)
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
Server setup popped an unskippable "Remove Local Content?" confirm on both the
Create-account and first-time Log In paths, and ran the delete before the network
call, so a typo'd password or a declined ToS still wiped every local project.

Only prompt when the answer matters: logging in to a different server while
holding real local work. Create-account never deletes, re-auth against the
configured server never prompts, and a login holding only the bundled example
drops it silently. Replace requires a second destructive confirmation.

The wipe now runs in the Success branch, so a failed setup costs nothing. The
component owns the decision and re-reads the projects directory when it acts;
the UI only ever asks for the non-destructive setup.

Also releases the pending setup before the wipe suspends, so a close landing in
that window cannot delete the freshly obtained tokens, and guards the
example-project drop with a same-server check so a token-expiry re-auth keeps it.
2026-08-14 23:45:54 -07:00
Adam Brown
df35452429
Render story prose the way the editor shows it, on the web and in every export (#887)
A page of dialogue reached readers as one packed block. CommonMark reflows
prose: single newlines become spaces and any run of blank lines collapses to
one paragraph break, so what an author sees while writing was not what anyone
else saw. Prose is now laid out as it was typed, on every surface that renders
it: every newline starts a new line, every blank line is a blank line.

- base/markdown/ProseHtml.kt holds that layout as AST-level generating
  providers, shared by the server's pages and the client's EPUB export. Lists,
  tables, code and headings keep markdown's own layout by construction, so the
  old fence-tracking preprocessor is gone.
- parseProseMarkdown does the same for the DOCX, RTF and PDF exports, with a
  ProseBlock.Blank for an authored blank line. A body paragraph carries space
  after it only where prose ends, so lines that run on sit tight and the
  indent parts them.
- Quoted passages keep their lines, and a quote's '>' markers no longer leak
  into the text of a continuation line in the document exports.

Walking every construct the flavour can emit turned up three more faults. A
stripped element keeps its text, so tables (which the sanitizer allowed none
of) arrived as their cells run together; they are allowed and styled now,
column alignment included. An ordered list starting at 5 rendered as 1. Line
endings went unnormalized once the preprocessor that had done it incidentally
was removed.

The story's declared language now reaches the prose on the author's own page
as well as the public one, so a French story is not hyphenated by the rules of
whatever locale the author reads Hammer in.

RENDER_VERSION goes to v4 and moves into the fingerprint: it keyed the disk
cache but not the ETag, so a bump alone would have served stale prose to
anyone holding the old validator.
2026-08-14 23:41:47 -07:00
Adam Brown
84f7223a77
Fix sync failure when order padding width changes during rationalizeTree (#886)
* Fix sync failure when order padding width changes during rationalizeTree

rationalizeTree computed each destination path with per-segment order
padding read from live disk counts, which its own moves mutate mid-pass.
When a sync dropped a directory across a digit boundary (10 -> 9 items),
later items computed ancestor names that did not exist on disk yet and
atomicMove failed with FileNotFoundException.

The pass now derives padding from the tree's child counts and moves
parents before children, building each destination from the parent's
already-finalized path, so the layout is computed once and every
destination directory exists.

* Pad rationalizeTree from disk-present children and reuse path snapshot

Padding now counts only children that resolve on disk, keeping written
names recomputable by getSceneFilePath when a tree child is missing.
Snapshot each directory's child paths once instead of rescanning after
every move, and extract the shared childScenePath helper.

* Tighten rationalizeChildren doc comment
2026-08-14 23:04:30 -07:00
Adam Brown
e25d80c051 Prepared for release: v3.8.2
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-11 01:05:31 -07:00
Adam Brown
da41f56998 Revert "New Crowdin updates (#879)"
This reverts commit c529bd063b.
2026-08-11 00:51:47 -07:00