Commit graph

1164 commits

Author SHA1 Message Date
Adam Brown
565045cfbe Fix line wrapped string 2026-06-24 16:22:37 -07:00
Adam Brown
810ae23c0d Encrypt auth tokens at rest per platform (F-4)
Replace the plaintext FileAuthTokenStore binding with platform-specific
encrypted stores behind the same AuthTokenStore interface, wired via a new
expect/actual authTokenStoreModule.

Android: EncryptedSharedPrefsAuthTokenStore backed by EncryptedSharedPreferences
with a Keystore-backed AES256_GCM master key (androidx.security:security-crypto).

Desktop: EncryptedFileAuthTokenStore writes the token-map JSON as AES/GCM/NoPadding
to the config directory. The key is derived (PBKDF2WithHmacSHA256) from the OS user
name and home dir plus a static salt, with no key file on disk, so a copied token
file is useless on another machine or user. A random 12-byte IV is prepended per
write and owner-only POSIX perms are applied best-effort. Decryption failure is
treated as no tokens rather than crashing. This guards against casual disk
scraping and off-machine copies, not same-user local malware that can re-derive
the key.

iOS: still uses the plaintext file store pending a Keychain-backed implementation
(TODO marker in the iOS binding).

Migration: a legacy plaintext auth_tokens.json from an intermediate build is
imported into the encrypted store and deleted on first access; existing encrypted
tokens win on key collision so a stale plaintext entry cannot clobber a fresh
session.
2026-06-24 16:07:40 -07:00
Adam Brown
4572c33a88 Relocate sync auth tokens out of server.json (F-5)
Auth bearer/refresh tokens were stored in cleartext inside the per-workspace
server.json, which on F-Droid builds can be relocated to public external
storage, exposing the secrets.

Move only the secret token fields into a new app-private, account-keyed
AuthTokenStore. server.json stays per-workspace under projectsDir with its
non-secret fields { ssl, url, email, userId } so switching the project
directory still auto-loads that workspace's configured server/account.

- AuthTokenStore interface + FileAuthTokenStore: a single JSON file under
  getConfigDirectory() holding accountKey ("url|userId") -> AuthTokens. The
  store is keyed by account so two workspaces on the same account share the
  login; tokens are per-machine and do not travel with the workspace folder.
  The interface lets an encrypted backing be substituted later.
- PersistedServerSettings: tokenless DTO written to server.json. The in-memory
  ServerSettings shape is unchanged, so token readers (Http auth/refresh,
  AccountUseCase, AccountReauthUseCase) keep reading .bearerToken/.refreshToken.
- store writes the tokenless DTO and puts tokens in the store; load composes
  tokens back in; remove deletes server.json and clears the account's tokens.
- Migration: loading a legacy server.json with inline tokens moves them into
  the store and rewrites the file tokenless. A token-store write failure is
  logged and the in-memory session is preserved (never logs the user out).

Reuses the existing readJsonOrNull/writeJson filesystem helpers, which also
tolerate corrupt/missing files. Writes route through the guarded
ContainedFileSystem; the config dir is an allowed root.
2026-06-24 16:05:34 -07:00
Adam Brown
29d9f7c731 Validate downloaded entity id and type against the request (F-12/F-13)
downloadEntry asks the server for a specific entity id but took the
returned entity's identity and type entirely from the response, then
keyed storeEntity and recordSyncedHash off the server-chosen values.

F-12 (id forgery): a hostile server asked for entity N could answer with
id M and attacker content. The client would overwrite/forge local entity
M and record the forged hash as the conflict baseline, concealing the
tampering and re-propagating it to the user's other devices.

F-13 (type confusion): the subtype was chosen purely from the server's
entity-type header, so the server could steer the same id into the wrong
repository.

Add two checks right after reading the server entity, before dispatch:
- reject any response whose id differs from the requested id, and
- reject a type mismatch when the client already owns the id
  (findEntityType non-null). When the client does not yet own the id, a
  legitimate new-entity download has no local type to compare against, so
  it is allowed through.

A rejected entity returns CResult.failure, which the fullEntityTransfer
loop already tolerates without aborting the sync or recording a hash, so
one bad entity skips cleanly. The synced hash is now recorded under the
requested id, which provably equals the server's id once the guard passes.
2026-06-24 16:03:26 -07:00
Adam Brown
26aa7c91dd Resolve encyclopedia images by extension, limited to Coil-supported formats
Image read, delete, reId, and sync hardcoded the "jpg" extension, so a non-jpg entry image was never found: it orphaned on delete, failed to re-sync, and could not round-trip. Images are now located by their actual on-disk extension (findEntryImagePath), and setEntryImage preserves the source file's extension instead of always writing .jpg.

The accepted extensions are restricted to the raster formats Coil renders on every target (Android BitmapFactory + desktop/iOS Skia): jpg, jpeg, png, webp. heic/heif are dropped because they decode on Android but not desktop/iOS, which would store an image that cannot be displayed. This set also backs the F-1 sync image-extension allowlist, so narrowing it tightens what a malicious server may write.
2026-06-24 16:01:16 -07:00
Adam Brown
1ae79eac26 Guard datasource writes with a contained FileSystem chokepoint
Replaces the raw platform FileSystem singleton with a ForwardingFileSystem
decorator, ContainedFileSystem, that fail-closes every mutation (sink,
appendingSink, atomicMove source+target, delete, createDirectory,
createSymlink, openReadWrite) to the app's managed storage: the cache,
config, and current projects directories. A write outside all roots throws.
Reads, listings, and metadata are deliberately not guarded.

Allowed roots are supplied as a lambda re-evaluated per check because the
projects directory is user-relocatable at runtime. The projects-dir lookup
is cycle-safe: it prefers the built GlobalSettingsStore, falls back to the
settings datasource, then to the default dir, so config-root writes during
bootstrap (before the store exists) are not blocked. Directory creation uses
a relaxed rule that also permits a root's ancestors, so a clean first run can
scaffold the roots while a sibling escape stays blocked.

Consumers that legitimately write outside managed storage get a raw,
unguarded FileSystem via the RAW_FILESYSTEM qualifier:
- ExportStoryUseCase (desktop/iOS export to a user-chosen path)
- DesktopExternalFileIo (desktop docx/pdf export)
- AndroidPlatformSettingsComponent (storage relocation moves projects into
  the new managed directory before it is registered as a root)
2026-06-24 15:44:50 -07:00
Adam Brown
fd1b2affda Encode project name in cache-dir path builders (F-15/F-16)
A server-synced project name can contain `/` and `..` (the validator
permits them; `toLocalSafeName` leaves an already-valid name unchanged),
and two cache-path builders used `projectDef.name` raw as a path segment:
statistics and the reference index. A crafted name could place the cache
file outside the per-project cache directory (the same traversal class as
F-1), and on Windows the raw `/` could break path construction outright.

Both builders now encode the name with `ProjectsRepository.encodeForFilename`,
matching how the on-disk project directory already encodes it, and each
datasource gains an `isWithin(cacheRoot)` backstop before every write,
createDirectories, and delete.

This orphans any old raw-named cache directories on existing installs.
That is harmless: statistics and the reference index regenerate on demand.
2026-06-24 15:11:31 -07:00
Adam Brown
fc736a0ed7 Narrow synced-image allowlist to web-renderable formats
Drop bmp/heic/heif from ALLOWED_IMAGE_EXTENSIONS; keep jpg/jpeg/png/gif/webp.
2026-06-24 15:00:02 -07:00
Adam Brown
45c52500c2 Block sync path traversal via draft name and image extension (F-1)
A malicious or compromised sync server could supply crafted entity
fields that were used verbatim as filename components when writing
downloaded entities to disk, with no validation on the sync path
(unlike the local-create paths). Embedded `/` or `..` escaped the
intended directory and, since file contents are attacker-controlled,
gave a write-anywhere primitive on the unsandboxed Desktop target.

Two sinks were affected:
- SceneDraftEntity.name flows into the draft filename in
  SceneDraftsDatasource.insertSyncDraft.
- EncyclopediaEntryEntity.Image.fileExtension flows into the entry
  image filename in EncyclopediaDatasource.writeEntryImage.

Defense in depth, two layers:

Layer 1 (synchronizer boundary, graceful per-entity skip):
- ClientSceneDraftSynchronizer.storeEntity rejects any draft whose
  name fails the existing validDraftName check, logs a warning, and
  returns false so only that entity is skipped (no synced hash is
  recorded, so sync state is not poisoned and the run is not aborted).
- ClientEncyclopediaSynchronizer.handleImage validates the image
  file extension against a known-image allowlist (ALLOWED_IMAGE_
  EXTENSIONS); a disallowed extension skips just the image write while
  the entry itself is still stored.

Layer 2 (containment backstop): a new Path.isWithin(root) helper
(okio normalized()) is asserted right before the write in both
datasource sinks, hard-failing any path that escapes its intended
directory. With Layer 1 in place this should never fire for the known
sinks; it fail-closes any future sink that forgets validation.

Tests (TDD, confirmed failing before the fix): traversal draft names
and image extensions are blocked with no file written outside the
target directory; isWithin rejects `..`, absolute, and sibling-escape
paths; happy-path drafts and jpg/png images still round-trip.
2026-06-24 15:00:02 -07:00
Adam Brown
c5ee60feee Encode dynamic project-name path segments to stop URL segment injection
Every project-scoped client API built its request path by raw string
interpolation of projectName, e.g. "/api/project/$userId/$projectName/begin_sync".
That string reached the shared url() builder whose only path handling was
pathSegments = path.split("/"). Because the split ran on the already-interpolated
string, a projectName containing "/" was split into extra discrete path segments
and a ".." survived as a literal traversal dot-segment, so the outbound request
could target a different endpoint than the {userId}/{projectName}/{action}
template intended (e.g. a malicious sync server returning a project named
"p/../../../api/account/test_auth").

The shared ProjectNameValidator permits "/", "\" and "." (they are encoded to
disk-safe lookalikes only when used as a directory name), so a server-supplied
project name persists verbatim and then injects into every subsequent
project-scoped request under the same host with the bearer token attached.

Fix: each dynamic value is now percent-encoded into a single opaque path
segment via String.encodeUrlPathSegment() before interpolation, and the sink
sets encodedPath directly. Embedded "/" becomes %2F so it cannot create extra
segments, and an all-dots segment is encoded to %2E so a ".." name cannot act as
a traversal segment. The validator is intentionally left unchanged: tightening
it to reject "/" or "." would break syncing for already-valid existing project
names, so encoding is the backward-compatible fix and the on-disk
encodeForFilename behavior is untouched.

Adds a MockEngine test asserting a malicious projectName collapses to a single
encoded segment in the outbound URL across ProjectDataApi, ServerProjectApi and
WritingActivityApi.
2026-06-24 14:45:27 -07:00
Adam Brown
5b377eaf83 Stop ProjectRootActivity from opening caller-supplied project paths
ProjectRootActivity was exported and resolveProjectDef deserialized a caller-supplied ProjectDef whose free-form path was used verbatim for all project file I/O, letting any installed app root the editor at an attacker-controlled directory (confused deputy, CWE-926/CWE-22). Resolve the target project by name against on-disk projects only, drop the serialized EXTRA_PROJECT path extra, and mark the activity exported=false; it is only launched in-process via explicit intents and PendingIntents from widgets and shortcuts.
2026-06-24 14:14:52 -07:00
Adam Brown
5397bd7bc4 Guard timeline loading against malformed TOML files
loadTimeline caught only FileNotFoundException, so a present-but-malformed timeline.toml crashed the load. Extend the catch to SerializationException, IllegalArgumentException, and IllegalStateException, returning an empty timeline as with the missing/blank-file cases.
2026-06-24 13:36:39 -07:00
Adam Brown
ede8fb488a Guard note loading against malformed TOML files
A malformed note file, or one with a non-integer id, threw uncaught from tomlkt (IllegalStateException/IllegalArgumentException beyond SerializationException) and aborted the entire notes load. loadNotes now skips unparseable files via readTomlOrNull and loads the rest.
2026-06-24 13:31:32 -07:00
Adam Brown
8d41946d77
New Crowdin updates (#631)
* 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.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]
2026-06-22 10:28:54 -07:00
Adam Brown
bf5277a056 Warn before discarding in-progress timeline edits on project close (#588)
Exiting a project with auto-sync on runs requestClose(), which queued
CloseConfirm.Sync and tore down open editors before the sync ran. Scenes,
notes, and encyclopedia entries flagged unsaved edits via shouldConfirmClose(),
but TimeLineComponent returned emptySet(), so an in-progress timeline event
edit was silently discarded with no warning.

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

Closes #588
2026-06-22 10:09:23 -07:00
Adam Brown
6be3651f9f Show snackbar on "Save All" 2026-06-22 01:54:39 -07:00
Adam Brown
52988c3489 Match backups by directory encoding so they appear in Manage Backups (#612)
Backup filenames used an ad-hoc, lossy `space<->underscore` transform while
project directories use encodeForFilename. Two failures fell out of the
mismatch: names with underscores never matched their project on read (the
backup vanished from Manage Backups), and names with now-allowed OS-forbidden
characters produced filenames that can't be written on Windows/Android, so the
backup was silently never created.

Write backups using the same encodeForFilename as the project directory, and
match a file to a project by comparing its name-key against that encoding, with
the legacy `space->underscore` name accepted as a fallback so backups written
by older clients are still found.
2026-06-22 01:23:42 -07:00
Adam Brown
584cf62271 Allow Windows reserved names in scene/group titles (#586)
Scene and group titles are stored wrapped as `order~name~id`, so a leading
dot or a Windows reserved word (CON, PRN, COM0-9, LPT0-9, ...) can never
collide on disk for them — yet name validation rejected them as if the title
were a raw filesystem basename. Project names, which become directories
verbatim, keep the strict rules.

- ProjectNameValidator.validate gains usedAsRawFilename (default true); the
  leading-dot and reserved-name checks now only apply to raw filenames.
  Trailing dot/space stay rejected for everyone (the on-disk encoder strips
  them). Reserved set extended with COM0/LPT0.
- Thread the flag through ProjectsRepository.validateFileName and
  SceneRepository.validateSceneName.
- Consolidate the six naming dialogs onto a shared rememberNameValidation
  hook keyed by NameKind (Project vs SceneItem), so strictness lives in one
  place instead of a per-call-site boolean.
2026-06-22 01:10:52 -07:00
Adam Brown
a10cdb1d20
Short-id project URLs (fix name round-trip 404s) + shared project-name validation (#628)
Promote the client's project/file-name validation rules out of :common's
ProjectsRepository and into a shared ProjectNameValidator in :base, resolving
the long-standing TODO on the old length-only stub.

Project URLs are now /story/{slug}-{id} (and /a/{penName}/{slug}-{id}), where the
trailing id is a stable 6-char base62 hash of the project's uuid and the slug is
purely cosmetic and never parsed back. This dissolves the slug round-trip bug:
the slug can be as pretty as we like because resolution matches the embedded id
against the user's projects, not the name. A bare /story/{id} (no slug) also
resolves, for short URLs.
2026-06-21 18:02:56 -07:00
Adam Brown
2e7bdc3399
Make markdown import map heading levels to scene/group hierarchy (#625)
The importer split on a single exact heading level and treated every
other level as plain body, so round-tripping a document (including
Hammer's own export of `# Title` + `## Chapter`) collapsed everything
into one scene when H1 was chosen and produced a spurious "Untitled"
scene for the title when H2 was chosen.

Fold the heading stream into a hierarchy instead: headings shallower
than the chosen level open groups, headings at the chosen level open
scenes, and deeper headings stay as scene body. Leading content that is
only headings/whitespace no longer becomes an Untitled scene. Heading
detection now tolerates a BOM and up to three spaces of indent, and
scene bodies are trimmed both ends so the blank line after a heading
does not leak into content.

Fixes #578
2026-06-20 10:16:12 -07:00
Adam Brown
8927347aca
Fix backup culling deleting the newest backups (#624)
Order backups by file modification time instead of the date parsed from
the filename. Backups written before the date-format fix used a broken
format (ISO week-based-year YYYY and 12-hour hh), so late-December-2025
backups were stamped months in the future. Sorting by that encoded date
made those phantom-future files look newest, so culling kept them and
deleted the genuinely newest backups instead.

Also broaden the backup filename pattern so project names containing
characters outside [a-zA-Z0-9_] (apostrophes, hyphens, non-ASCII) are
recognized, and stop date parsing from throwing so one malformed
filename can't blank the entire backup list.
2026-06-20 10:16:00 -07:00
Adam Brown
3318c1f284
Make SceneTree state Compose-stable with immutable collections (#621)
Mark the scene-tree state types @Immutable/@Stable and move them onto
kotlinx.collections.immutable so Compose can skip recomposition when the
tree is unchanged: TreeValue.children becomes ImmutableList, SceneSummary
.hasDirtyBuffer a PersistentSet (sourced as such from SceneContentRepository),
and SceneList.State.archivedScenes an ImmutableList. Also cache ImmutableTree
.nodeIndex/hashCode lazily and gate compose-compiler stability reports behind
the composeCompilerReports property.

* Harden onSceneBufferUpdate to reduce from oldState

Read the scene summary from the getAndUpdate lambda's oldState argument
instead of a snapshot captured before the CAS, so the reducer stays a pure
function of its input and composes correctly if buffer updates ever run off
the main dispatcher.
2026-06-19 23:29:41 -07:00
Adam Brown
ed74a11dfc Sync illegal-named server projects down instead of dropping them
A project name the server accepts can contain characters this client
rejects (e.g. #). Such projects failed local creation during account
sync and were silently skipped, so they never appeared on the client
and could not be deleted from it.

Map server names through ProjectsRepository.toLocalSafeName when
creating or renaming local projects from server changes, and log a
failed local create as an error rather than a warning.

Also plug a test leak: SceneEditorRepository{Archive,Other}Test
mockkObject ProjectsRepository.Companion without unmocking, which
globally forced validateFileName to succeed for any later test.
2026-06-16 02:24:18 -07:00
Adam Brown
a3cc091806 Not an error, this is an expected path, no need to log it 2026-06-15 23:59:22 -07:00
Adam Brown
c85c324909 Cover EncyclopediaRepository image operations
Add tests over a real datasource/fake filesystem for setEntryImage
(store and null-clears), loadEntryImage/getEntryImagePath round-trip,
calculateEntryImageHash, removeEntryImage (success and failure branches),
and ensureEntriesLoaded caching.
2026-06-14 11:46:51 -07:00
Adam Brown
fbf04940db Cover TimeLineRepository sync-path methods
Add tests for reIdEvent, updateEventForSync (replace and append),
storeTimeline flushing in-memory edits, getTimelineEvent, and the
server-synced markForSynchronization branch.
2026-06-14 11:40:46 -07:00
Adam Brown
f21f6ad0d3 Cover SpellCheckRepository locale loading
Drive the repository over a real GlobalSettingsStore (mocked datasources)
and a mocked platform spell-check factory: loads a checker for the
configured locale, skips unsupported locales, reloads on a locale change,
ignores unrelated settings changes, plus the toSpLocale mapping.
2026-06-14 11:37:41 -07:00
Adam Brown
36686e261c Cover ProjectBackupRepository backup lifecycle
Add tests for getBackupsForProject, deleteBackup (success, missing,
failure), cullBackups (over and under budget), and the real
createBackup/restoreBackup zip round-trip over a fake filesystem.
2026-06-14 11:25:37 -07:00
Adam Brown
ee8bf2c65b Add tier 3 coverage: project data sync + synchronizer dispatch
Cover ProjectDataRepository end-to-end over a fake filesystem (load
caching, user edits invalidating the project hash, sync-only updates),
fill out every branch of ProjectDataSyncOperation (no-op, fast-forward,
upload, and conflict resolve/abort paths) by driving real repository and
broker collaborators, and exercise the EntitySynchronizers facade's
type dispatch (get, findEntityType, reIdEntry phantom skip, conflict
routing) for all entity types.
2026-06-14 01:38:08 -07:00
Adam Brown
410d043d77
New Crowdin updates (#603)
* 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]
2026-06-14 01:32:37 -07:00
Adam Brown
4e1850f9c5
Expand sync + tree test coverage (#604)
* Expand test coverage for sync synchronizers and ImmutableTree

Add a Classical-style suite for ImmutableTree covering the previously
untested query API (indexOf, findBy, isAncestorOf, getBranch, coordinate
round-trips, iterator exhaustion, equals/hashCode).

Rewrite ClientEncyclopediaSynchronizer tests to drive real repository,
service, and datasource collaborators over a fake filesystem instead of
mocks, asserting observable on-disk state, image base64 round-trips, and
create-vs-update behavior.

Extend ClientSceneSynchronizer tests with createEntityForId,
deleteEntityLocal, archived-scene-from-server, store-content failure,
and reIdEntity branches.

Extend EntityTransferOperation tests with the download/heal branches:
not-modified, not-found remote delete, stale-hash forced-upload heal,
failed-store logging, and the onlyNew upload path.

* Fail entity transfer when a downloaded entity cannot be stored

downloadEntry returned CResult.success() even when storeEntity failed,
so a failed download was logged but never marked the transfer as
unsuccessful, masking sync failures from the operation's allSuccess
tracking. Return a failure result in that case so the transfer reflects
it.

* Add tier 2 sync coverage: uploadEntity, transfer + scene branches

Cover the EntitySynchronizer.uploadEntity base method end-to-end through
the encyclopedia synchronizer's classical harness (success, force, plain
failure, conflict resolution, and failed resolution), the remaining
EntityTransferOperation download/upload failure branches, and the scene
group move-parent and unarchive-on-active paths.
2026-06-14 01:26:05 -07:00
Adam Brown
df2468747e
Add keyboard shortcuts for bold, italic and strikethrough (#601)
The markdown editor only exposed inline styles through format-bar buttons;
the underlying composetexteditor library handles editing/navigation shortcuts
(Ctrl+C/V/X/Z/Y, arrows, etc.) but has no bindings for bold/italic, so those
keys were simply dropped.
2026-06-13 20:54:24 -07:00
Adam Brown
caeab9c94c Fix timeline event re-ordering 2026-06-13 11:41:40 -07:00
Adam Brown
01727b7ee5 Clear server info on new account create 2026-06-13 11:10:04 -07:00
Wavesonics
105d82661e Locale test 2026-06-13 02:46:40 -07:00
Adam Brown
4b912129b4
Replace fluidsonic (#597) 2026-06-13 02:38:21 -07:00
Adam Brown
a97b0e682f
New Crowdin updates (#596)
* 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_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-06-13 01:53:24 -07:00
Adam Brown
e77ef954d3 Remove timeline sorting
It didn't really make sense anyway
2026-06-12 23:12:45 -07:00
Wavesonics
6b6cd25601 Fix reIdScene crash for scenes without drafts
SceneDraftsDatasource.reIdScene moved the scene's drafts directory
unconditionally, so re-IDing a scene with no drafts threw
FileNotFoundException and failed the entire sync during ID conflict
resolution.
2026-06-12 22:40:15 -07:00
Wavesonics
e8f6858e2f Fix iOS Lifecycle handling
- Update platform specific UI for iOS
2026-06-11 23:10:35 -07:00
Wavesonics
a5188760e2 Implement iOS stubs: URL launcher, date formatting, network reachability, backup share
- UrlLauncher: open URLs via UIApplication.openURL (fixes update dialog button)
- DateTimeUtils: format Instant/LocalDateTime properly (was empty / used now())
- NetworkConnectivity: real reachability via NWPathMonitor with 2s timeout
- BackupManagerService: present iOS share sheet via UIActivityViewController
- FocusModeService: documented as noop (iOS has no public DND API)
2026-06-11 22:16:20 -07:00
Adam Brown
d3e9542bf7
Move CPU heavy work off to io thread 2026-06-11 00:56:36 -07:00
Adam Brown
946d115ec9
New Crowdin updates (#580)
* 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]

* 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_projects_list.xml (French)

[ci skip]

* New translations strings_projects_list.xml (Spanish)

[ci skip]

* New translations strings_projects_list.xml (German)

[ci skip]

* New translations strings_projects_list.xml (Italian)

[ci skip]

* New translations strings_projects_list.xml (Ukrainian)

[ci skip]

* New translations strings_projects_list.xml (Chinese Simplified)

[ci skip]

* New translations strings_projects_list.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]
2026-06-11 00:46:34 -07:00
Adam Brown
347baf82f0
Reworked PDF export (#585)
Better formatting now!
2026-06-11 00:44:47 -07:00
Adam Brown
0791954de0
Add docx export format 2026-06-11 00:18:00 -07:00
Adam Brown
31b98876f7
Pre-sync change probe: skip syncing unchanged projects (#584)
On app open, sync made ~4 HTTP round-trips per project even when nothing
had changed. Add a batched pre-sync probe: the client sends a project-wide
content hash for each eligible project in one request, and the server
returns which ones still match so the client can skip syncing them.

- base: ProjectContentHasher (entity hashes + project-data hash, computed
  identically on client and server) and probe request/response DTOs
- server: POST /projects/{userId}/sync_probe, recomputing each project's
  hash from stored state; skips projects with an in-flight sync session;
  400 on a malformed body
- client: cache the hash in the project journal (written at FinalizeSync,
  invalidated on any content mutation via one chokepoint); probe is
  best-effort and degrades to a full sync on any failure
- backstop: never skip a project with pending entity or project-data work,
  so a stale cache can't drop a change

Writing activity is excluded by design (per-device, conflict-free).

Tests: ProjectContentHasher, client/server probe units, and an e2e suite
covering client/server hash agreement, the session gate, auth, and 400s.
2026-06-10 23:15:50 -07:00
Wavesonics
47d1f7bfe9 Fix more iOS file path handling 2026-06-10 20:56:05 -07:00
Adam Brown
00a9bcafd1
Add a warning state for conflicted project syncs 2026-06-10 20:50:40 -07:00
Adam Brown
db29c001c2
Navigate on main thread 2026-06-10 20:33:19 -07:00
Adam Brown
c632d048fb
Fix project sync session lockout (same-install session reclaim) (#583)
Server reclaims a project sync session only for the originating install
(derived from the auth token), so a leaked or cancelled session no longer
blocks the owner; a different active install is still rejected. Client
ends sessions under NonCancellable so end_sync isn't dropped when a sync
coroutine is cancelled.
2026-06-10 19:29:01 -07:00