Skip to content

feat: settings screen with re-pair, lyrics prefs (#84, #114)#121

Open
joelkanyi wants to merge 1 commit into
mainfrom
feat/settings-screen
Open

feat: settings screen with re-pair, lyrics prefs (#84, #114)#121
joelkanyi wants to merge 1 commit into
mainfrom
feat/settings-screen

Conversation

@joelkanyi

Copy link
Copy Markdown
Collaborator

Closes #84. Closes #114.

Adds a Settings screen, gives users a way to re-pair their device without clearing app data, surfaces the three lyrics preferences that shipped silently in #113, and fixes a handful of base URL bugs that became visible while testing the sign-out flow.

What the user sees

The Folders header now has a gear icon in the top right. Tapping it opens the Settings screen.

The screen has three sections:

Account. Shows the connected server URL. Below it, a Re-pair device button. Tapping it signs out (clears tokens, server URL, user record, in-memory holders) and routes to the QR scan screen. Pair again and you land back on Folders, signed in.

Lyrics. Three toggles wired to the existing preferences in AppSettingsRepository:

  • Online lyrics search is the master switch. Off means lyrics only come from the local Swing Music server. On allows the app to call /plugins/lyrics/search to fetch lyrics from external sources.
  • Auto-search when missing only matters with the master switch on. When a track has no local lyrics, the app automatically hits the plugin search. With this off, the lyrics screen shows a Search online button instead.
  • Upgrade unsynced lyrics also requires the master switch. When a track has plain text lyrics (no per-line timestamps), the app automatically looks online for a synced version that highlights as the song plays.

About. App version.

Pattern

The screen follows the MVI screen skill we baselined in #119:

feature/settings/.../presentation/
  state/SettingsUiState.kt
  event/SettingsUiEvent.kt
  event/SettingsUiEffect.kt
  viewmodel/SettingsViewModel.kt
  screen/SettingsScreen.kt

Initial data loads in the VM init { }. One-shot effects (navigate back, navigate to QR scan, snackbar) flow through a Channel and are collected with ObserverAsEvent. The ScreenContent is stateless and has a @Preview.

Auth changes to support sign out

  • AuthRepository.signOut() clears tokens, base URL, the stored user record, and the in-memory holders.
  • BaseUrlDao.clearBaseUrl and UserDao.clearLoggedInUser are now suspend so they don't error from a coroutine context.
  • AuthTokensDataStore.clear() wipes the auth datastore.

Base URL bugs uncovered while testing

These were not introduced by this PR but became reproducible once sign-out wiped the in-memory holder and forced the app to round-trip through storage.

  • storeBaseUrl was unconditionally appending /, so each call against an already-normalized URL added another slash. Over enough pair cycles the DB ended up with https://host///. Now uses trimEnd('/') + "/". getBaseUrl also normalizes on read so any existing bad row self-heals on the next launch.
  • storeBaseUrl did not update BaseUrlHolder in memory the way storeAuthTokens updates AuthTokenHolder. The very first request after pairing fell back to a DB read, but timing-sensitive callers (folder paging, token refresh worker) sometimes hit a null holder. Now updates the holder on write.
  • getFreshTokensFromServer made a request unconditionally. The TokenRefreshWorker fires on a schedule, so during the window between sign-out and successful sign-in, the worker hit http://default/null/auth/refresh. Early-returns now if base URL or refresh token is missing.
  • DataFolderRepository.getPagingContent built a PagingSource with "${baseUrl}folder" even when baseUrl was null, baking "nullfolder" into a long-lived URL string that kept retrying. Returns emptyFlow() when not signed in.

What's deliberately out of scope

  • User profile in the Account section. The app never stored a User record. AuthRepository.storeLoggedInUser() exists but is never called, and LogInResult doesn't carry user info. AuthViewModel.getAuthenticatedUser() is a // TODO. Showing name/email needs a profile-fetch endpoint and storage path. Separate concern.
  • Album/artist grid count and sort UI. Those preferences already exist in AppSettingsRepository with no UI either. Worth a follow-up but doesn't block this issue.
  • Theme switcher, cache management, full About page. Not in scope.

Tested on Pixel 7 Pro

Across multiple sign-out and re-pair cycles:

  • Gear icon opens Settings. Back returns to Folders.
  • Server URL shows once (no // or ///).
  • Re-pair signs out and lands at QR scan. After pairing, lands on Folders, no null... URLs in the network inspector for new requests.
  • Force-stop and reopen keeps the user signed in.
  • Lyrics toggles persist across navigation and across an app restart.
  • The three lyrics toggles also correctly enable/disable the dependent ones based on the master switch.
  • Existing folder navigation, scroll memory, and paging cache still work as expected.

Adds a Settings screen reachable via a gear icon on the Folders header.
Follows the MVI screen skill (UiState + UiEvent + UiEffect + ViewModel +
stateless ScreenContent + ObserverAsEvent for effects).

What the screen shows:
- Account section with the connected server URL and a Re-pair device
  button. Tapping it signs out and routes to QR scan.
- Lyrics section with three toggles bound to AppSettingsRepository: the
  online lyrics search master switch, auto-search when a track has no
  lyrics, and the option to upgrade unsynced lyrics to a synced version
  found online.
- About section with the app version.

Auth changes to support sign out:
- AuthRepository.signOut() clears tokens, base URL, the stored user
  record, and the in-memory holders.
- BaseUrlDao.clearBaseUrl and UserDao.clearLoggedInUser are now suspend
  so they don't error from a coroutine context.
- AuthTokensDataStore.clear wipes the auth datastore.

Fixes for null base URL issues that surfaced while testing the sign out
flow:
- storeBaseUrl normalizes the URL (trimEnd('/') + "/") so it ends with a
  single slash regardless of input. Previously each call appended another
  slash, producing legacy DB rows like https://host/// over time.
  getBaseUrl also normalizes on read so existing bad rows self heal on
  next launch.
- storeBaseUrl now updates BaseUrlHolder in memory the same way
  storeAuthTokens does, so the first request after pairing has the URL.
- getFreshTokensFromServer early returns if base URL or refresh token is
  null. The TokenRefreshWorker fires on schedule and was hitting
  http://default/null/auth/refresh in the window between sign out and
  sign in.
- DataFolderRepository.getPagingContent returns an empty flow if base URL
  is null instead of constructing a PagingSource with "nullfolder" baked
  into its URL.

Closes #84 and #114. The user profile fetch (showing name/email instead
of just server URL) is a separate concern, AuthViewModel has a TODO for
it and the LogInResult doesn't carry user info yet.
@joelkanyi joelkanyi requested a review from Ericgacoki June 11, 2026 15:37
@joelkanyi joelkanyi self-assigned this Jun 11, 2026
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.

QRCode not found at the setting menu No re-login (re-scan QR-code) UI after a long period of no-using the app

1 participant