Skip to content

Keep library view settings per account - #1204

Merged
r0adkll merged 2 commits into
refactor/split-campfire-settingsfrom
refactor/user-library-view-settings
Oct 6, 2026
Merged

r0adkll merged 2 commits into
refactor/split-campfire-settingsfrom
refactor/user-library-view-settings

Conversation

@r0adkll

@r0adkll r0adkll commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Stack

Closes #607. Each account now keeps its own library sorting and layout. Everything else stays app-wide, including the theme, and all settings stay on the existing preference-backed store.

What changed

  • UserPrefixedSettings is an ObservableSettings that keeps one account's values in the app's existing preferences, under user:<userId>: keys.
    • Accounts share the app's storage, and its Android backup, without mixing with each other.
    • Its keys, size and clear() only see that account's settings.
    • The prefix stays short because desktop's java.util.prefs rejects keys longer than 80 characters. A test checks every prefixed key against that limit.
  • LibraryViewSettingsImpl moves into the user graph, on the account's settings (@ForScope(UserScope::class) ObservableSettings). Its observers run in the user graph's coroutine scope, so they end with it.
    • Signed out, there's no account, so the app-wide values are used.
    • The root UI takes the settings from the user graph it already receives. Every other consumer was already user-scoped.
  • DefaultUserSettingsStore hands each user graph its account's settings.
    • An account's settings start from the library view values the app kept for everyone, copied the first time they're opened. Accounts added later start from the same values.
    • Logging out clears that account's keys, through the new UserSettingsStore API.
  • The app's store is now qualified too: @ForScope(AppScope::class) ObservableSettings everywhere it's provided and injected, so each injection site says which store it reads.

Verification

  • ./scripts/ktlint --check passes. Desktop, Android (alpha debug) and iOS (:app:ios) compile, and the Metro graph checks pass.
  • ./gradlew jvmTest test passes. The two native-FFmpeg audio engine modules were excluded locally, because the bundled FFmpeg native library doesn't load on this machine. New tests:
    • UserPrefixedSettingsTest: per-account keys, isolation from other accounts and the app, scoped keys/size/clear(), listeners;
    • DefaultUserSettingsStoreTest: seeding once, keeping an account's changes, accounts apart, signed out, clearing;
    • UserPrefixedKeyLengthTest: desktop's key length limit.
  • On device (emulator, local testbed server, fossDebug, two accounts):
    • Upgrading from Split CampfireSettings into focused settings interfaces #1189 kept account A's list layout sorted by duration and its series sort. A's prefixed copies were written, and the app-wide values stayed in place.
    • B started from the same values, then changed to grid sorted by size and series by name. Switching A ↔ B four times showed each account's own layout and sort immediately.
    • A cold start on B showed B's grid with no frame of A's.
    • Signing out of B removed only B's keys. Signing out of A left no per-account keys. Signing A back in started it again from the app-wide values.
    • No settings errors or missing bindings in logcat.

Follow-ups

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Warnings
⚠️ Big PR, try to keep changes smaller if you can
Messages
📖 This PR has been checked by Danger

Generated by 🚫 Danger Kotlin against ee58b79

@Rotom-Bot

Rotom-Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Code Coverage

File Coverage [39.26%] ❌
app/common/src/commonMain/kotlin/app/campfire/common/di/UserComponent.kt 0.00% ❌
app/common/src/commonMain/kotlin/app/campfire/common/root/CampfireContent.kt 0.00% ❌
data/account/impl/src/commonMain/kotlin/app/campfire/account/DefaultAccountManager.kt 0.00% ❌
data/account/impl/src/commonMain/kotlin/app/campfire/account/restore/AccountRestoreStore.kt 97.06% ✅
data/bookinfo/impl/src/commonMain/kotlin/app/campfire/bookinfo/DefaultBookInfoProviderSettings.kt 72.22% ❌
features/discover/impl/src/commonMain/kotlin/app/campfire/discover/DiscoverScanStateStore.kt 95.65% ✅
features/libraries/impl/src/commonMain/kotlin/app/campfire/libraries/purge/RemovedLibraryItemReconciler.kt 81.40% ✅
features/settings/impl/src/androidMain/kotlin/app/campfire/settings/PreferencesPlatformComponent.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/AndroidAutoSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/AppStateSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/AudioOutputSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/ConnectionSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/DefaultUserSettingsStore.kt 86.67% ✅
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/DevSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/DeviceSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/EqualizerSettingsImpl.kt 91.67% ✅
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/LayoutSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/LibraryViewSettingsImpl.kt 75.86% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/MobileDataSettingsImpl.kt 90.91% ✅
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/PlaybackSettingsImpl.kt 77.12% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/PrivacySettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/SleepSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/ThemeSettingsImpl.kt 0.00% ❌
features/settings/impl/src/commonMain/kotlin/app/campfire/settings/UserPrefixedSettings.kt 33.33% ❌
features/settings/impl/src/iosMain/kotlin/app/campfire/settings/PreferencesPlatformComponent.kt 0.00% ❌
features/settings/impl/src/jvmMain/kotlin/app/campfire/settings/PreferencesPlatformComponent.kt 0.00% ❌
Total Project Coverage 23.59% ❌

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Android APK @ c4a3f19

@r0adkll
r0adkll added this pull request to stack #1207 October 6, 2026 14:23
LibraryViewSettings moves into the user graph, on the signed-in account's
settings: UserPrefixedSettings keeps them in the app's existing
preferences under "user:<id>:" keys, so they share its storage and backup
without mixing with another account's. Signed out, there's no account, so
the app-wide values are used.

Each account starts from the library view settings the app kept for
everyone, copied the first time its settings are opened; accounts added
later start from the same values. Logging out clears the account's keys.
The root UI takes the settings from the user graph it already receives.
The account's ObservableSettings is bound with @Forscope(UserScope::class)
instead of a custom @UserSettings qualifier, and the app's is now bound
and injected with @Forscope(AppScope::class), so every injection site
says which store it reads.
@r0adkll
r0adkll force-pushed the refactor/user-library-view-settings branch from 26c1527 to ee58b79 Compare October 6, 2026 14:27
@r0adkll
r0adkll merged commit 4c09696 into main Oct 6, 2026
7 checks passed
@r0adkll
r0adkll deleted the refactor/user-library-view-settings branch October 6, 2026 14:52
r0adkll added a commit that referenced this pull request Oct 6, 2026
<!-- pr-stack:start -->
## Stack

- [This PR] #1189
  - #1204
<!-- pr-stack:end -->

First step of #607. `CampfireSettings` mixed device identity, privacy
choices, app state, window layout, the socket toggle and library view
preferences in one interface. This splits it by concern so each group
can later move to the right storage layer (global vs per-user, then
DataStore).

## What changed
- `CampfireSettings` is split into:
- `PrivacySettings`: consent, crash/analytics reporting, keep signed in
after reinstall
  - `DeviceSettings`: device id, analytics id, current user
- `AppStateSettings`: last seen version, app update prompts, widget
pinning
  - `LayoutSettings`: navigation rail, supporting pane width
  - `ConnectionSettings`: socket toggle
- `LibraryViewSettings`: sort/display state, marquee, time in book,
download confirmation
- `themeId`, `themeMode` and `observeTheme()` move onto the existing
`ThemeSettings`.
- `TestCampfireSettings` is split the same way into `Test*Settings`.
- Call sites inject only the interfaces they use. `AnalyticsInitializer`
and `PlayerWidgetComponent` held a `CampfireSettings` they never read;
those are removed.

## What didn't change
The members were moved over unchanged: the `var` + `observeX()` API,
`AppSettings` delegates, storage keys and defaults are all the same.
There's no user-facing change, so no changelog entry.

## Verification
- `./scripts/ktlint --check` passes.
- `:app:desktop:compileKotlin` and
`:app:android:compileAlphaDebugKotlin` compile, and the touched modules
compile for the iOS simulator.
- `./gradlew jvmTest test`: everything passes except the four ffmpeg
tests in `infra/audioplayer/engine-tests`, which fail locally because
the native library doesn't load (`UnsatisfiedLinkError: no jniavutil`).
This PR doesn't touch anything those tests use.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Break-up the CampfireSettings monolith into appScope/userScope layers

2 participants