diff --git a/src-tauri/src/commands_v2.rs b/src-tauri/src/commands_v2.rs index fb71352df..3afd6b559 100644 --- a/src-tauri/src/commands_v2.rs +++ b/src-tauri/src/commands_v2.rs @@ -10449,6 +10449,18 @@ pub fn v2_is_running_in_flatpak() -> bool { crate::flatpak::is_running_in_flatpak() } +#[tauri::command] +pub fn v2_is_running_in_snap() -> bool { + crate::snap::is_running_in_snap() +} + +#[tauri::command] +pub fn v2_mark_snap_welcome_shown( + state: State<'_, crate::updates::UpdatesState>, +) -> Result<(), String> { + crate::updates::mark_snap_welcome_shown(state) +} + #[tauri::command] pub async fn v2_detect_legacy_cached_files( cache_state: State<'_, OfflineCacheState>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a49012152..dcdfda124 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ pub mod config; pub mod credentials; pub mod discogs; pub mod flatpak; +pub mod snap; pub mod lastfm; pub mod library; pub mod listenbrainz; @@ -1183,6 +1184,7 @@ pub fn run() { flatpak::get_flatpak_help_text, config::legal_settings::get_qobuz_tos_accepted, updates::has_flatpak_welcome_been_shown, + updates::has_snap_welcome_been_shown, lyrics::commands::lyrics_get, config::favorites_cache::is_track_favorite, commands_v2::v2_set_api_locale, @@ -1356,6 +1358,8 @@ pub fn run() { commands_v2::v2_remote_control_regenerate_token, commands_v2::v2_remote_control_get_pairing_qr, commands_v2::v2_is_running_in_flatpak, + commands_v2::v2_is_running_in_snap, + commands_v2::v2_mark_snap_welcome_shown, commands_v2::v2_detect_legacy_cached_files, commands_v2::v2_reco_log_event, commands_v2::v2_reco_train_scores, diff --git a/src-tauri/src/snap.rs b/src-tauri/src/snap.rs new file mode 100644 index 000000000..214cdbbdb --- /dev/null +++ b/src-tauri/src/snap.rs @@ -0,0 +1,16 @@ +//! Snap runtime detection and utilities +//! +//! Detects when QBZ is running inside a Snap sandbox +//! and provides sandbox-specific guidance. + +use std::env; + +/// Check if QBZ is running inside a Snap sandbox +pub fn is_snap() -> bool { + env::var("SNAP").is_ok() +} + +#[tauri::command] +pub fn is_running_in_snap() -> bool { + is_snap() +} diff --git a/src-tauri/src/updates/mod.rs b/src-tauri/src/updates/mod.rs index d665e1f45..e37a4d786 100644 --- a/src-tauri/src/updates/mod.rs +++ b/src-tauri/src/updates/mod.rs @@ -143,6 +143,11 @@ impl UpdatesStore { CREATE TABLE IF NOT EXISTS flatpak_welcome_shown ( id INTEGER PRIMARY KEY CHECK (id = 1), shown_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS snap_welcome_shown ( + id INTEGER PRIMARY KEY CHECK (id = 1), + shown_at INTEGER NOT NULL );", ) .map_err(|e| format!("Failed to create updates tables: {}", e))?; @@ -273,6 +278,26 @@ impl UpdatesStore { .map_err(|e| format!("Failed to mark flatpak_welcome_shown: {}", e))?; Ok(()) } + + pub fn has_snap_welcome_been_shown(&self) -> bool { + self.conn + .query_row( + "SELECT 1 FROM snap_welcome_shown WHERE id = 1 LIMIT 1", + [], + |_row| Ok(()), + ) + .is_ok() + } + + pub fn mark_snap_welcome_shown(&self) -> Result<(), String> { + self.conn + .execute( + "INSERT OR REPLACE INTO snap_welcome_shown (id, shown_at) VALUES (1, ?1)", + params![now_epoch_seconds()], + ) + .map_err(|e| format!("Failed to mark snap_welcome_shown: {}", e))?; + Ok(()) + } } /// Thread-safe state wrapper @@ -543,6 +568,16 @@ pub fn mark_flatpak_welcome_shown(state: tauri::State) -> Result<( state.with_store(|store| store.mark_flatpak_welcome_shown())? } +#[tauri::command] +pub fn has_snap_welcome_been_shown(state: tauri::State) -> Result { + state.with_store(|store| store.has_snap_welcome_been_shown()) +} + +#[tauri::command] +pub fn mark_snap_welcome_shown(state: tauri::State) -> Result<(), String> { + state.with_store(|store| store.mark_snap_welcome_shown())? +} + #[tauri::command] pub fn get_current_version(state: tauri::State) -> String { state.current_version() diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 25ff0d05e..1bb8c2fce 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -279,6 +279,9 @@ // Flatpak detection state let isFlatpak = $state(false); let flatpakHelpText = $state(''); + + // Snap detection state + let isSnap = $state(false); let isCheckingNetwork = $state(false); // Updates state @@ -383,13 +386,13 @@ { id: 'developer', labelKey: 'settings.developer.title' }, ]; - // 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 - ? [...navSectionIds, { id: 'flatpak', labelKey: 'nav.flatpak' }] - : navSectionIds - ); + // Navigation section definitions (dynamic: includes sandbox sections when detected) + const navSectionDefs = $derived.by(() => { + const sections = [...navSectionIds]; + if (isFlatpak) sections.push({ id: 'flatpak', labelKey: 'nav.flatpak' }); + if (isSnap) sections.push({ id: 'snap', labelKey: 'nav.snap' }); + return sections; + }); async function handleUpdateCheckOnLaunchToggle(enabled: boolean): Promise { @@ -1382,8 +1385,9 @@ updatesCurrentVersion = getUpdatesCurrentVersion(); }); - // Detect if running in Flatpak + // Detect sandbox environments loadFlatpakStatus(); + loadSnapStatus(); // Check for legacy cached files checkLegacyCachedFiles(); @@ -2539,6 +2543,14 @@ } } + async function loadSnapStatus() { + try { + isSnap = await invoke('v2_is_running_in_snap'); + } catch (err) { + console.error('Failed to check Snap status:', err); + } + } + async function loadAudioSettings() { try { const settings = await invoke('v2_get_audio_settings'); @@ -5395,8 +5407,42 @@ showLogsModal = false} /> + + {#if activeSection === 'snap' && isSnap} +
+

Snap Sandbox

+
+

+ QBZ is running inside a Snap sandbox. Some audio interfaces need to be connected manually for the best experience. +

+
+

Required Plug Connections

+

Run these commands to enable full audio support:

+
+
sudo snap connect qbz-player:alsa
+sudo snap connect qbz-player:pulseaudio
+sudo snap connect qbz-player:pipewire
+sudo snap connect qbz-player:mpris
+ +
+

Optional (External Drives / NAS)

+
+
sudo snap connect qbz-player:removable-media
+ +
+

+ Note: These settings persist across reboots and updates. You only need to run them once, then restart QBZ. +

+
+
+
+ {/if} + - {#if activeSection === 'flatpak' && isFlatpak}

Flatpak Sandbox

diff --git a/src/lib/i18n/locales/de.json b/src/lib/i18n/locales/de.json index de62c02ad..619e6e156 100644 --- a/src/lib/i18n/locales/de.json +++ b/src/lib/i18n/locales/de.json @@ -20,6 +20,7 @@ "backTo": "Zurück zu", "updates": "Updates", "flatpak": "Flatpak", + "snap": "Snap", "purchases": "Einkäufe" }, "home": { diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index bbc7106c3..673a0f055 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -20,6 +20,7 @@ "backTo": "Back to", "updates": "Updates", "flatpak": "Flatpak", + "snap": "Snap", "purchases": "Purchases" }, "home": { diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index ad135b245..736f0f05b 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -20,6 +20,7 @@ "backTo": "Volver a", "updates": "Actualizaciones", "flatpak": "Flatpak", + "snap": "Snap", "purchases": "Compras" }, "home": { diff --git a/src/lib/i18n/locales/fr.json b/src/lib/i18n/locales/fr.json index ff588c1f0..375765148 100644 --- a/src/lib/i18n/locales/fr.json +++ b/src/lib/i18n/locales/fr.json @@ -20,6 +20,7 @@ "backTo": "Retour à", "updates": "Mises à jour", "flatpak": "Flatpak", + "snap": "Snap", "purchases": "Achats" }, "home": { diff --git a/src/lib/services/updatesService.ts b/src/lib/services/updatesService.ts index f777e542f..d42fc1545 100644 --- a/src/lib/services/updatesService.ts +++ b/src/lib/services/updatesService.ts @@ -144,6 +144,7 @@ function getDevFakeRelease(currentVersion: string): ReleaseInfo | null { export interface LaunchUpdateDecision { currentVersion: string; showFlatpakWelcome: boolean; + showSnapWelcome: boolean; whatsNewRelease: ReleaseInfo | null; updateRelease: ReleaseInfo | null; } @@ -173,6 +174,31 @@ export async function markFlatpakWelcomeShown(): Promise { } } +/** + * Check if running in Snap and if welcome modal has been shown. + */ +async function shouldShowSnapWelcome(): Promise { + try { + const isSnap = await invoke('v2_is_running_in_snap'); + if (!isSnap) return false; + const alreadyShown = await invoke('has_snap_welcome_been_shown'); + return !alreadyShown; + } catch { + return false; + } +} + +/** + * Mark Snap welcome modal as shown. + */ +export async function markSnapWelcomeShown(): Promise { + try { + await invoke('v2_mark_snap_welcome_shown'); + } catch (error) { + console.debug('[Updates] Failed to mark snap welcome shown:', error); + } +} + /** * Decide which modals should be shown on launch. * @@ -186,6 +212,7 @@ export async function decideLaunchModals(): Promise { return { currentVersion: getCurrentVersion(), showFlatpakWelcome: false, + showSnapWelcome: false, whatsNewRelease: null, updateRelease: null, }; @@ -203,17 +230,21 @@ export async function decideLaunchModals(): Promise { const currentVersion = getCurrentVersion(); if (getSessionFlag(SESSION_CHECK_DONE_KEY)) { - return { currentVersion, showFlatpakWelcome: false, whatsNewRelease: null, updateRelease: null }; + return { currentVersion, showFlatpakWelcome: false, showSnapWelcome: false, whatsNewRelease: null, updateRelease: null }; } setSessionFlag(SESSION_CHECK_DONE_KEY); // Collect all modals that need to be shown let showFlatpakWelcome = false; + let showSnapWelcome = false; let whatsNewRelease: ReleaseInfo | null = null; let updateRelease: ReleaseInfo | null = null; - // Phase 1: Flatpak welcome (highest priority, shown first) + // Phase 1: Sandbox welcome (highest priority, shown first) showFlatpakWelcome = await shouldShowFlatpakWelcome(); + if (!showFlatpakWelcome) { + showSnapWelcome = await shouldShowSnapWelcome(); + } // Phase 2: What's New for current version if (prefs.showWhatsNewOnLaunch && currentVersion) { @@ -254,7 +285,7 @@ export async function decideLaunchModals(): Promise { } } - return { currentVersion, showFlatpakWelcome, whatsNewRelease, updateRelease }; + return { currentVersion, showFlatpakWelcome, showSnapWelcome, whatsNewRelease, updateRelease }; } export async function openReleasePageAndAcknowledge(release: ReleaseInfo): Promise { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4f2880195..152c9f232 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -375,6 +375,7 @@ 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 SnapWelcomeModal from '$lib/components/updates/SnapWelcomeModal.svelte'; import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte'; import KeybindingsSettings from '$lib/components/KeybindingsSettings.svelte'; import LinkResolverModal from '$lib/components/LinkResolverModal.svelte'; @@ -386,6 +387,7 @@ disableUpdateChecks, ignoreReleaseVersion, markFlatpakWelcomeShown, + markSnapWelcomeShown, openReleasePageAndAcknowledge, resetLaunchFlow, } from '$lib/services/updatesService'; @@ -437,6 +439,7 @@ let isReminderModalOpen = $state(false); let isWhatsNewModalOpen = $state(false); let isFlatpakWelcomeOpen = $state(false); + let isSnapWelcomeOpen = $state(false); let updatesLaunchTriggered = $state(false); let sessionReady = $state(false); @@ -511,17 +514,21 @@ updatesCurrentVersion = decision.currentVersion; // Store pending modals for sequential display - // Order: Flatpak → What's new → Update available + // Order: Flatpak/Snap → What's new → Update available pendingWhatsNewRelease = decision.whatsNewRelease; pendingUpdateRelease = decision.updateRelease; - // Show first modal in queue (Flatpak has highest priority) + // Show first modal in queue (sandbox welcome has highest priority) if (decision.showFlatpakWelcome) { isFlatpakWelcomeOpen = true; return; } + if (decision.showSnapWelcome) { + isSnapWelcomeOpen = true; + return; + } - // No Flatpak modal, try What's New + // No sandbox modal, try What's New showNextModalInQueue(); } @@ -581,6 +588,13 @@ showNextModalInQueue(); } + function handleSnapWelcomeClose(): void { + isSnapWelcomeOpen = false; + void markSnapWelcomeShown(); + // Show next modal in queue + showNextModalInQueue(); + } + function handleWhatsNewClose(): void { isWhatsNewModalOpen = false; whatsNewRelease = null; @@ -4628,6 +4642,11 @@ onClose={handleFlatpakWelcomeClose} /> + +