From 561f7487d012edc3db577cb88000ee68917f4323 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Tue, 27 Jan 2026 04:37:17 -0600 Subject: [PATCH] Add Flatpak welcome modal and sequential modal priority system - Add FlatpakWelcomeModal component shown once for Flatpak users - Add persistence for Flatpak welcome shown state (updates.db) - Implement sequential modal priority: Flatpak > What's new > Update available - Add Flatpak section to Settings navigation (only when running in Flatpak) - Clean up duplicate Flatpak section and invalid style block in SettingsView - Add copyable command styling for Flatpak section --- src-tauri/src/lib.rs | 2 + src-tauri/src/updates/mod.rs | 35 ++++ .../updates/FlatpakWelcomeModal.svelte | 116 +++++++++++ src/lib/components/views/SettingsView.svelte | 185 ++++++++---------- src/lib/services/updatesService.ts | 108 ++++++---- src/routes/+page.svelte | 51 ++++- 6 files changed, 353 insertions(+), 144 deletions(-) create mode 100644 src/lib/components/updates/FlatpakWelcomeModal.svelte diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bd4b87987..24f35fa42 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -771,6 +771,8 @@ pub fn run() { updates::is_release_ignored, updates::has_whats_new_been_shown, updates::mark_whats_new_shown, + updates::has_flatpak_welcome_been_shown, + updates::mark_flatpak_welcome_shown, updates::get_current_version, updates::check_for_updates, updates::fetch_release_for_version, diff --git a/src-tauri/src/updates/mod.rs b/src-tauri/src/updates/mod.rs index a97d1f542..a23d26cf9 100644 --- a/src-tauri/src/updates/mod.rs +++ b/src-tauri/src/updates/mod.rs @@ -123,6 +123,11 @@ impl UpdatesStore { CREATE TABLE IF NOT EXISTS whats_new_shown ( version TEXT PRIMARY KEY, shown_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS flatpak_welcome_shown ( + id INTEGER PRIMARY KEY CHECK (id = 1), + shown_at INTEGER NOT NULL );", ) .map_err(|e| format!("Failed to create updates tables: {}", e))?; @@ -233,6 +238,26 @@ impl UpdatesStore { .map_err(|e| format!("Failed to mark whats_new_shown: {}", e))?; Ok(()) } + + pub fn has_flatpak_welcome_been_shown(&self) -> bool { + self.conn + .query_row( + "SELECT 1 FROM flatpak_welcome_shown WHERE id = 1 LIMIT 1", + [], + |_row| Ok(()), + ) + .is_ok() + } + + pub fn mark_flatpak_welcome_shown(&self) -> Result<(), String> { + self.conn + .execute( + "INSERT OR REPLACE INTO flatpak_welcome_shown (id, shown_at) VALUES (1, ?1)", + params![now_epoch_seconds()], + ) + .map_err(|e| format!("Failed to mark flatpak_welcome_shown: {}", e))?; + Ok(()) + } } /// Thread-safe state wrapper @@ -448,6 +473,16 @@ pub fn mark_whats_new_shown(version: String, state: tauri::State) state.with_store(|store| store.mark_whats_new_shown(&version)) } +#[tauri::command] +pub fn has_flatpak_welcome_been_shown(state: tauri::State) -> bool { + state.with_store(|store| store.has_flatpak_welcome_been_shown()) +} + +#[tauri::command] +pub fn mark_flatpak_welcome_shown(state: tauri::State) -> Result<(), String> { + state.with_store(|store| store.mark_flatpak_welcome_shown()) +} + #[tauri::command] pub fn get_current_version(state: tauri::State) -> String { state.current_version() diff --git a/src/lib/components/updates/FlatpakWelcomeModal.svelte b/src/lib/components/updates/FlatpakWelcomeModal.svelte new file mode 100644 index 000000000..d1b215968 --- /dev/null +++ b/src/lib/components/updates/FlatpakWelcomeModal.svelte @@ -0,0 +1,116 @@ + + +
+ + + + {#snippet footer()} + + {/snippet} + +
+ + diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 085c0c216..747254764 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -162,10 +162,11 @@ let integrationsSection: HTMLElement; let updatesSection: HTMLElement; let storageSection: HTMLElement; + let flatpakSection: HTMLElement; let activeSection = $state('audio'); - // Navigation section definitions (static, refs resolved at click/scroll time) - const navSectionDefs = [ + // Base navigation sections (always present) + const baseNavSectionDefs = [ { id: 'audio', label: 'Audio' }, { id: 'playback', label: 'Playback' }, { id: 'offline', label: 'Offline' }, @@ -177,6 +178,14 @@ { id: 'storage', label: 'Storage' }, ]; + // Navigation section definitions (dynamic: includes Flatpak only when running in Flatpak) + // NOTE: If adding new sections, add them BEFORE Flatpak. Flatpak must always be last. + const navSectionDefs = $derived( + isFlatpak + ? [...baseNavSectionDefs, { id: 'flatpak', label: 'Flatpak' }] + : baseNavSectionDefs + ); + // Get section element by id (resolved at call time, not definition time) function getSectionEl(id: string): HTMLElement | undefined { switch (id) { @@ -189,6 +198,7 @@ case 'integrations': return integrationsSection; case 'updates': return updatesSection; case 'storage': return storageSection; + case 'flatpak': return flatpakSection; default: return undefined; } } @@ -2081,96 +2091,6 @@ - - {#if isFlatpak} -
-

Flatpak Sandbox

-
-

- QBZ is running inside a Flatpak sandbox. For offline libraries on NAS, network mounts, or external disks, direct filesystem access is required. -

-
-

Grant Filesystem Access

-

Use Flatseal (GUI) or run this command for each folder you want to add:

-
-
flatpak override --user --filesystem=/path/to/your/music com.blitzfc.qbz
- -
-

Examples

-
-
# CIFS / Samba mount
-flatpak override --user --filesystem=/mnt/nas com.blitzfc.qbz
-
-# SSHFS mount
-flatpak override --user --filesystem=$HOME/music-nas com.blitzfc.qbz
-
-# Custom folder (edit as needed)
-flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz
- -
-

- Note: This setting is persistent and survives reboots and updates.
- Tip: You can repeat the command for as many folders as you need. -

-
-
-

Chromecast & DLNA Device Discovery

-

- To detect Chromecast and DLNA devices on your network, you must grant network sharing permissions to the app: -

-
-
flatpak override --user --share=network com.blitzfc.qbz
- -
-

- Note: Without this, device discovery will not work.
- You only need to do this once. -

-
-
-
- {/if} - -

{$t('settings.library.title')}

@@ -2501,8 +2421,9 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz
+ {#if isFlatpak} -
+

Flatpak Sandbox

@@ -2510,16 +2431,46 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz

Grant Filesystem Access

-

Use Flatseal (GUI) or run this command:

-
flatpak override --user --filesystem=/path/to/music com.blitzfc.qbz
+

Use Flatseal (GUI) or run this command for each folder you want to add:

+
+
flatpak override --user --filesystem=/path/to/your/music com.blitzfc.qbz
+ +

Examples

-
# CIFS / Samba mount
+          
+
# CIFS / Samba mount
 flatpak override --user --filesystem=/mnt/nas com.blitzfc.qbz
 
 # SSHFS mount
-flatpak override --user --filesystem=$HOME/music-nas com.blitzfc.qbz
+flatpak override --user --filesystem=$HOME/music-nas com.blitzfc.qbz + +# Custom folder (edit as needed) +flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz
+ +

- Note: This setting is persistent and survives reboots and updates. + Note: This setting is persistent and survives reboots and updates.
+ Tip: You can repeat the command for as many folders as you need. +

+
+
+

Chromecast & DLNA Device Discovery

+

+ To detect Chromecast and DLNA devices on your network, you must grant network sharing permissions to the app: +

+
+
flatpak override --user --share=network com.blitzfc.qbz
+ +
+

+ Note: Without this, device discovery will not work.
+ You only need to do this once.

@@ -3312,6 +3263,44 @@ flatpak override --user --filesystem=$HOME/music-nas com.blitzfc.qbz cursor: pointer; border: none; } + + /* Flatpak copyable command styling */ + .copyable-command { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 8px; + } + + .copyable-command .code-block { + margin: 0; + font-size: 13px; + background: var(--bg-tertiary); + border-radius: 6px; + padding: 8px 12px; + user-select: all; + min-width: 0; + flex: 1; + white-space: pre-wrap; + word-break: break-all; + } + + .copy-btn { + background: var(--accent-primary); + color: white; + border: none; + border-radius: 6px; + padding: 6px 14px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; + flex-shrink: 0; + } + + .copy-btn:hover { + background: var(--accent-secondary); + } { + try { + const isFlatpak = await invoke('is_running_in_flatpak'); + if (!isFlatpak) return false; + const alreadyShown = await invoke('has_flatpak_welcome_been_shown'); + return !alreadyShown; + } catch { + return false; + } +} + +/** + * Mark Flatpak welcome modal as shown. + */ +export async function markFlatpakWelcomeShown(): Promise { + try { + await invoke('mark_flatpak_welcome_shown'); + } catch (error) { + console.debug('[Updates] Failed to mark flatpak welcome shown:', error); + } +} + +/** + * Decide which modals should be shown on launch. * - * Priority: - * 1) Update available modal - * 2) What's New modal + * Priority (shown sequentially, one after another): + * 1) Flatpak welcome modal (only for Flatpak users, once ever) + * 2) What's New modal (once per version) + * 3) Update available modal (when newer version exists) */ export async function decideLaunchModals(): Promise { if (launchFlowStarted) { return { currentVersion: getCurrentVersion(), - updateRelease: null, + showFlatpakWelcome: false, whatsNewRelease: null, + updateRelease: null, }; } launchFlowStarted = true; @@ -150,38 +179,17 @@ export async function decideLaunchModals(): Promise { const currentVersion = getCurrentVersion(); if (getSessionFlag(SESSION_CHECK_DONE_KEY)) { - return { currentVersion, updateRelease: null, whatsNewRelease: null }; + return { currentVersion, showFlatpakWelcome: false, whatsNewRelease: null, updateRelease: null }; } setSessionFlag(SESSION_CHECK_DONE_KEY); - // Phase 1: update available - if (prefs.checkOnLaunch) { - const devFake = getDevFakeRelease(currentVersion); - if (devFake) { - const devScopedKey = `${SESSION_UPDATE_SHOWN_KEY}:${devFake.version}`; - if (!getSessionFlag(devScopedKey)) { - setSessionFlag(devScopedKey); - return { - currentVersion, - updateRelease: devFake, - whatsNewRelease: null, - }; - } - } + // Collect all modals that need to be shown + let showFlatpakWelcome = false; + let whatsNewRelease: ReleaseInfo | null = null; + let updateRelease: ReleaseInfo | null = null; - const result = await checkForUpdates('launch'); - if (result.status === 'update_available' && result.release) { - const scopedUpdateKey = `${SESSION_UPDATE_SHOWN_KEY}:${result.release.version}`; - if (!getSessionFlag(scopedUpdateKey)) { - setSessionFlag(scopedUpdateKey); - return { - currentVersion, - updateRelease: result.release, - whatsNewRelease: null, - }; - } - } - } + // Phase 1: Flatpak welcome (highest priority, shown first) + showFlatpakWelcome = await shouldShowFlatpakWelcome(); // Phase 2: What's New for current version if (prefs.showWhatsNewOnLaunch && currentVersion) { @@ -192,19 +200,37 @@ export async function decideLaunchModals(): Promise { const release = await fetchReleaseForVersion(currentVersion); if (release) { setSessionFlag(scopedWhatsNewKey); - // Persist "shown once per version" at the time we decide to show it. await markWhatsNewShown(currentVersion); - return { - currentVersion, - updateRelease: null, - whatsNewRelease: release, - }; + whatsNewRelease = release; } } } } - return { currentVersion, updateRelease: null, whatsNewRelease: null }; + // Phase 3: Update available (lowest priority, shown last) + if (prefs.checkOnLaunch) { + const devFake = getDevFakeRelease(currentVersion); + if (devFake) { + const devScopedKey = `${SESSION_UPDATE_SHOWN_KEY}:${devFake.version}`; + if (!getSessionFlag(devScopedKey)) { + setSessionFlag(devScopedKey); + updateRelease = devFake; + } + } + + if (!updateRelease) { + const result = await checkForUpdates('launch'); + if (result.status === 'update_available' && result.release) { + const scopedUpdateKey = `${SESSION_UPDATE_SHOWN_KEY}:${result.release.version}`; + if (!getSessionFlag(scopedUpdateKey)) { + setSessionFlag(scopedUpdateKey); + updateRelease = result.release; + } + } + } + } + + return { currentVersion, showFlatpakWelcome, whatsNewRelease, updateRelease }; } export async function openReleasePageAndAcknowledge(release: ReleaseInfo): Promise { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 473a4ece6..e513b6be7 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -301,11 +301,13 @@ import UpdateAvailableModal from '$lib/components/updates/UpdateAvailableModal.svelte'; import UpdateReminderModal from '$lib/components/updates/UpdateReminderModal.svelte'; import WhatsNewModal from '$lib/components/updates/WhatsNewModal.svelte'; + import FlatpakWelcomeModal from '$lib/components/updates/FlatpakWelcomeModal.svelte'; import type { ReleaseInfo } from '$lib/stores/updatesStore'; import { decideLaunchModals, disableUpdateChecks, ignoreReleaseVersion, + markFlatpakWelcomeShown, openReleasePageAndAcknowledge, } from '$lib/services/updatesService'; @@ -342,8 +344,13 @@ let isUpdateModalOpen = $state(false); let isReminderModalOpen = $state(false); let isWhatsNewModalOpen = $state(false); + let isFlatpakWelcomeOpen = $state(false); let updatesLaunchTriggered = $state(false); + // Sequential modal queue: Flatpak → What's new → Update available + let pendingWhatsNewRelease = $state(null); + let pendingUpdateRelease = $state(null); + // Album, Artist and Label data are fetched, so kept local let selectedAlbum = $state(null); let selectedArtist = $state(null); @@ -370,15 +377,35 @@ const decision = await decideLaunchModals(); updatesCurrentVersion = decision.currentVersion; - if (decision.updateRelease) { - updateRelease = decision.updateRelease; - isUpdateModalOpen = true; + // Store pending modals for sequential display + // Order: Flatpak → What's new → Update available + pendingWhatsNewRelease = decision.whatsNewRelease; + pendingUpdateRelease = decision.updateRelease; + + // Show first modal in queue (Flatpak has highest priority) + if (decision.showFlatpakWelcome) { + isFlatpakWelcomeOpen = true; return; } - if (decision.whatsNewRelease) { - whatsNewRelease = decision.whatsNewRelease; + // No Flatpak modal, try What's New + showNextModalInQueue(); + } + + function showNextModalInQueue(): void { + // What's New has second priority + if (pendingWhatsNewRelease) { + whatsNewRelease = pendingWhatsNewRelease; + pendingWhatsNewRelease = null; isWhatsNewModalOpen = true; + return; + } + + // Update Available has lowest priority + if (pendingUpdateRelease) { + updateRelease = pendingUpdateRelease; + pendingUpdateRelease = null; + isUpdateModalOpen = true; } } @@ -414,9 +441,18 @@ void disableUpdateChecks(); } + function handleFlatpakWelcomeClose(): void { + isFlatpakWelcomeOpen = false; + void markFlatpakWelcomeShown(); + // Show next modal in queue + showNextModalInQueue(); + } + function handleWhatsNewClose(): void { isWhatsNewModalOpen = false; whatsNewRelease = null; + // Show next modal in queue + showNextModalInQueue(); } $effect(() => { @@ -3119,6 +3155,11 @@ /> {/if} + +