feat(session): guest local-only profile — offline mode without a prior login
The 'Start offline' link was gated on has-previous-session and the shell backstop refused to open without a recorded user, so a user who never logged in (e.g. a purchases-only Qobuz account, #553) could not use the app at all. The link is now always visible and the offline shell falls back to the GUEST profile — user 0, the Tauri-era fallback this port used to refuse — building a real per-user store set (local library, mixtapes, prefs) under users/0. AppRuntime::activate_offline already resolved user 0 and never persists it as the last-user marker, so the guest never masquerades as a real session. First real login takes possession: AppRuntime::activate renames the users/0 data+cache dirs to the account's id when that account has no profile on this machine yet (existing profiles always win — the guest dirs stay parked, no merge). Best-effort, login never breaks on a failed rename. cc
This commit is contained in:
4 files changed
+74
-7
No files matched your search
@@ -193,6 +193,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> AppRuntime<A> {
|
||||
/// activates against them, and persists the last-user marker so the
|
||||
/// session can be restored on the next launch.
|
||||
pub async fn activate(&self, user_id: u64) -> Result<(), String> {
|
||||
Self::adopt_guest_profile(user_id);
|
||||
self.user_paths.set_user(user_id);
|
||||
let data_dir = self.user_paths.user_data_dir()?;
|
||||
let cache_dir = self.user_paths.user_cache_dir()?;
|
||||
@@ -201,6 +202,45 @@ impl<A: FrontendAdapter + Send + Sync + 'static> AppRuntime<A> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// First real login takes possession of the guest profile (#553): data
|
||||
/// built while logged off (local library, mixtapes, per-user prefs —
|
||||
/// `users/0/`) is renamed to this account IF the account has no profile
|
||||
/// on this machine yet. An existing profile always wins — the guest
|
||||
/// dirs then stay parked at user 0, no merge is attempted. Best-effort:
|
||||
/// a failed rename logs and falls through to a fresh profile; login
|
||||
/// must never break here.
|
||||
fn adopt_guest_profile(user_id: u64) {
|
||||
if user_id == 0 {
|
||||
return;
|
||||
}
|
||||
for (kind, guest, target) in [
|
||||
(
|
||||
"data",
|
||||
UserDataPaths::data_dir_for(0),
|
||||
UserDataPaths::data_dir_for(user_id),
|
||||
),
|
||||
(
|
||||
"cache",
|
||||
UserDataPaths::cache_dir_for(0),
|
||||
UserDataPaths::cache_dir_for(user_id),
|
||||
),
|
||||
] {
|
||||
let (Ok(guest), Ok(target)) = (guest, target) else {
|
||||
continue;
|
||||
};
|
||||
if guest.is_dir() && !target.exists() {
|
||||
match std::fs::rename(&guest, &target) {
|
||||
Ok(()) => {
|
||||
log::info!("[AppRuntime] guest profile {kind} adopted by the new session")
|
||||
}
|
||||
Err(e) => log::warn!(
|
||||
"[AppRuntime] guest profile {kind} adoption failed ({e}); starting fresh"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Activate an offline-only session using the last known user.
|
||||
///
|
||||
/// Falls back to user id `0` (an empty profile) when no previous session
|
||||
|
||||
@@ -82,6 +82,23 @@ impl UserDataPaths {
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
/// Data directory for an ARBITRARY user id (no active-user requirement):
|
||||
/// ~/.local/share/qbz/users/{uid}/ — the same layout `user_data_dir`
|
||||
/// resolves for the active user. Used by the guest-profile adoption
|
||||
/// (#553), which must compare two users' paths before either is active.
|
||||
pub fn data_dir_for(user_id: u64) -> Result<PathBuf, String> {
|
||||
Ok(Self::global_data_dir()?
|
||||
.join("users")
|
||||
.join(user_id.to_string()))
|
||||
}
|
||||
|
||||
/// Cache twin of [`Self::data_dir_for`]: ~/.cache/qbz/users/{uid}/.
|
||||
pub fn cache_dir_for(user_id: u64) -> Result<PathBuf, String> {
|
||||
Ok(Self::global_cache_dir()?
|
||||
.join("users")
|
||||
.join(user_id.to_string()))
|
||||
}
|
||||
|
||||
/// Get the global (non-user-scoped) data directory: ~/.local/share/qbz/
|
||||
pub fn global_data_dir() -> Result<PathBuf, String> {
|
||||
dirs::data_dir()
|
||||
|
||||
Reference in new issue
Block a user