feat(offline): add offline mode detection backend
- Create offline module with OfflineReason enum and OfflineStatus struct - Add OfflineSettings for manual mode and partial playlist preferences - Implement network connectivity check via HEAD request to qobuz.com - Add SQLite persistence for offline settings - Register Tauri commands: get_offline_status, get_offline_settings, set_manual_offline, set_show_partial_playlists - Add event emission for offline status changes cc
This commit is contained in:
1 parent
99aba9a76a
commit
21354f9b64
2 files changed
+247
No files matched your search
@@ -17,6 +17,7 @@ pub mod lastfm;
|
||||
pub mod library;
|
||||
pub mod lyrics;
|
||||
pub mod media_controls;
|
||||
pub mod offline;
|
||||
pub mod player;
|
||||
pub mod playlist_import;
|
||||
pub mod queue;
|
||||
@@ -142,6 +143,9 @@ pub fn run() {
|
||||
.expect("Failed to initialize download settings");
|
||||
// Initialize API keys state (for user-provided credentials)
|
||||
let api_keys_state = api_keys::create_api_keys_state();
|
||||
// Initialize offline mode state
|
||||
let offline_state = offline::OfflineState::new()
|
||||
.expect("Failed to initialize offline state");
|
||||
|
||||
// Read saved audio device setting for player initialization
|
||||
let saved_device = audio_settings_state
|
||||
@@ -253,6 +257,7 @@ pub fn run() {
|
||||
.manage(audio_settings_state)
|
||||
.manage(download_settings_state)
|
||||
.manage(api_keys_state)
|
||||
.manage(offline_state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Auth commands
|
||||
commands::init_client,
|
||||
@@ -496,6 +501,11 @@ pub fn run() {
|
||||
api_keys::clear_discogs_credentials,
|
||||
api_keys::has_discogs_user_credentials,
|
||||
api_keys::get_embedded_credentials_status,
|
||||
// Offline mode commands
|
||||
offline::commands::get_offline_status,
|
||||
offline::commands::get_offline_settings,
|
||||
offline::commands::set_manual_offline,
|
||||
offline::commands::set_show_partial_playlists,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Offline mode detection and settings
|
||||
//!
|
||||
//! Handles:
|
||||
//! - Network connectivity detection
|
||||
//! - Login state checking
|
||||
//! - Manual offline mode toggle
|
||||
//! - Offline settings persistence
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// Reason why the app is in offline mode
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OfflineReason {
|
||||
NoNetwork,
|
||||
NotLoggedIn,
|
||||
ManualOverride,
|
||||
}
|
||||
|
||||
/// Current offline status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OfflineStatus {
|
||||
pub is_offline: bool,
|
||||
pub reason: Option<OfflineReason>,
|
||||
pub manual_mode_enabled: bool,
|
||||
}
|
||||
|
||||
/// Persistent offline settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OfflineSettings {
|
||||
pub manual_offline_mode: bool,
|
||||
pub show_partial_playlists: bool,
|
||||
}
|
||||
|
||||
/// SQLite-backed storage for offline settings
|
||||
pub struct OfflineStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl OfflineStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("offline_settings.db");
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open offline settings database: {}", e))?;
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS offline_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
manual_offline_mode INTEGER NOT NULL DEFAULT 0,
|
||||
show_partial_playlists INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
INSERT OR IGNORE INTO offline_settings (id, manual_offline_mode, show_partial_playlists)
|
||||
VALUES (1, 0, 1);"
|
||||
).map_err(|e| format!("Failed to create offline settings table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<OfflineSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT manual_offline_mode, show_partial_playlists FROM offline_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(OfflineSettings {
|
||||
manual_offline_mode: row.get::<_, i64>(0)? != 0,
|
||||
show_partial_playlists: row.get::<_, i64>(1)? != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Failed to get offline settings: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_manual_offline_mode(&self, enabled: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE offline_settings SET manual_offline_mode = ?1 WHERE id = 1",
|
||||
params![enabled as i64],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set manual offline mode: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_show_partial_playlists(&self, enabled: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE offline_settings SET show_partial_playlists = ?1 WHERE id = 1",
|
||||
params![enabled as i64],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set show partial playlists: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper for OfflineStore
|
||||
pub struct OfflineState {
|
||||
pub store: Arc<Mutex<OfflineStore>>,
|
||||
}
|
||||
|
||||
impl OfflineState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(OfflineStore::new()?)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check network connectivity by attempting to reach Qobuz API
|
||||
pub async fn check_network_connectivity() -> bool {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build();
|
||||
|
||||
let client = match client {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Use HEAD request for minimal data transfer
|
||||
match client.head("https://www.qobuz.com").send().await {
|
||||
Ok(response) => response.status().is_success() || response.status().is_redirection(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
pub mod commands {
|
||||
use super::*;
|
||||
use crate::AppState;
|
||||
use tauri::State;
|
||||
|
||||
/// Get current offline status
|
||||
#[tauri::command]
|
||||
pub async fn get_offline_status(
|
||||
offline_state: State<'_, OfflineState>,
|
||||
app_state: State<'_, AppState>,
|
||||
) -> Result<OfflineStatus, String> {
|
||||
let settings = {
|
||||
let store = offline_state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.get_settings()?
|
||||
};
|
||||
|
||||
// If manual offline mode is enabled, return immediately
|
||||
if settings.manual_offline_mode {
|
||||
return Ok(OfflineStatus {
|
||||
is_offline: true,
|
||||
reason: Some(OfflineReason::ManualOverride),
|
||||
manual_mode_enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is logged in
|
||||
let is_logged_in = {
|
||||
let client = app_state.client.lock().await;
|
||||
client.is_logged_in().await
|
||||
};
|
||||
|
||||
if !is_logged_in {
|
||||
return Ok(OfflineStatus {
|
||||
is_offline: true,
|
||||
reason: Some(OfflineReason::NotLoggedIn),
|
||||
manual_mode_enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Check network connectivity
|
||||
let has_network = check_network_connectivity().await;
|
||||
|
||||
if !has_network {
|
||||
return Ok(OfflineStatus {
|
||||
is_offline: true,
|
||||
reason: Some(OfflineReason::NoNetwork),
|
||||
manual_mode_enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(OfflineStatus {
|
||||
is_offline: false,
|
||||
reason: None,
|
||||
manual_mode_enabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get offline settings
|
||||
#[tauri::command]
|
||||
pub fn get_offline_settings(
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<OfflineSettings, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
/// Enable or disable manual offline mode
|
||||
#[tauri::command]
|
||||
pub async fn set_manual_offline(
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
app_state: State<'_, AppState>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<OfflineStatus, String> {
|
||||
{
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_manual_offline_mode(enabled)?;
|
||||
}
|
||||
|
||||
// Get updated status
|
||||
let status = get_offline_status(state, app_state).await?;
|
||||
|
||||
// Emit event to frontend
|
||||
let _ = app_handle.emit("offline-status-changed", &status);
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Set whether to show playlists with partial local content in offline mode
|
||||
#[tauri::command]
|
||||
pub fn set_show_partial_playlists(
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_show_partial_playlists(enabled)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user