linux appimage build with docker, egl fallback, and webkitgtk fixes
This commit is contained in:
@@ -2,7 +2,7 @@ use crate::AppState;
|
||||
use crate::os_detection;
|
||||
use rusqlite::params;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{Manager, State};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Client {
|
||||
@@ -1102,6 +1102,17 @@ pub struct TrackedApp {
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
// Platform detection
|
||||
#[tauri::command]
|
||||
pub fn get_platform() -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{ "linux".to_string() }
|
||||
#[cfg(target_os = "windows")]
|
||||
{ "windows".to_string() }
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{ "unknown".to_string() }
|
||||
}
|
||||
|
||||
// OS Detection commands
|
||||
#[tauri::command]
|
||||
pub fn get_idle_seconds() -> Result<u64, String> {
|
||||
@@ -3754,3 +3765,137 @@ fn get_default_templates() -> Vec<InvoiceTemplate> {
|
||||
]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn quit_app(app: AppHandle) {
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
/// Play a synthesized notification sound on Linux via system audio tools.
|
||||
/// Generates a WAV in memory and pipes it through paplay/pw-play/aplay.
|
||||
/// `tones` is a JSON array of {freq, duration_ms, delay_ms, detune} objects.
|
||||
/// `volume` is 0.0-1.0.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tauri::command]
|
||||
pub fn play_sound(tones: Vec<SoundTone>, volume: f64) {
|
||||
std::thread::spawn(move || {
|
||||
play_sound_inner(&tones, volume);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[tauri::command]
|
||||
pub fn play_sound(_tones: Vec<SoundTone>, _volume: f64) {
|
||||
// No-op on non-Linux; frontend uses Web Audio API directly
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SoundTone {
|
||||
pub freq: f64,
|
||||
pub duration_ms: u32,
|
||||
pub delay_ms: u32,
|
||||
#[serde(default)]
|
||||
pub freq_end: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub detune: Option<f64>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn play_sound_inner(tones: &[SoundTone], volume: f64) {
|
||||
const SAMPLE_RATE: u32 = 44100;
|
||||
let vol = volume.clamp(0.0, 1.0) as f32;
|
||||
|
||||
// Calculate total duration
|
||||
let total_ms: u32 = tones.iter().map(|t| t.delay_ms + t.duration_ms).sum();
|
||||
let total_samples = ((total_ms as f64 / 1000.0) * SAMPLE_RATE as f64) as usize + SAMPLE_RATE as usize / 10; // +100ms padding
|
||||
let mut samples = vec![0i16; total_samples];
|
||||
|
||||
let mut offset_ms: u32 = 0;
|
||||
for tone in tones {
|
||||
offset_ms += tone.delay_ms;
|
||||
let start_sample = ((offset_ms as f64 / 1000.0) * SAMPLE_RATE as f64) as usize;
|
||||
let num_samples = ((tone.duration_ms as f64 / 1000.0) * SAMPLE_RATE as f64) as usize;
|
||||
let attack_samples = (0.010 * SAMPLE_RATE as f64) as usize; // 10ms attack
|
||||
let release_samples = (0.050 * SAMPLE_RATE as f64) as usize; // 50ms release
|
||||
|
||||
let freq_start = tone.freq;
|
||||
let freq_end = tone.freq_end.unwrap_or(tone.freq);
|
||||
|
||||
for i in 0..num_samples {
|
||||
if start_sample + i >= samples.len() {
|
||||
break;
|
||||
}
|
||||
let t = i as f64 / SAMPLE_RATE as f64;
|
||||
let progress = i as f64 / num_samples as f64;
|
||||
|
||||
// Frequency interpolation (for slides)
|
||||
let freq = freq_start + (freq_end - freq_start) * progress;
|
||||
|
||||
// Envelope
|
||||
let env = if i < attack_samples {
|
||||
i as f32 / attack_samples as f32
|
||||
} else if i > num_samples - release_samples {
|
||||
(num_samples - i) as f32 / release_samples as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let mut sample = (t * freq * 2.0 * std::f64::consts::PI).sin() as f32;
|
||||
|
||||
// Optional detuned second oscillator for warmth
|
||||
if let Some(detune_cents) = tone.detune {
|
||||
let freq2 = freq * (2.0_f64).powf(detune_cents / 1200.0);
|
||||
sample += (t * freq2 * 2.0 * std::f64::consts::PI).sin() as f32;
|
||||
sample *= 0.5; // normalize
|
||||
}
|
||||
|
||||
sample *= env * vol;
|
||||
let val = (sample * 32000.0) as i16;
|
||||
samples[start_sample + i] = samples[start_sample + i].saturating_add(val);
|
||||
}
|
||||
offset_ms += tone.duration_ms;
|
||||
}
|
||||
|
||||
// Build WAV in memory
|
||||
let data_size = (samples.len() * 2) as u32;
|
||||
let file_size = 36 + data_size;
|
||||
let mut wav = Vec::with_capacity(file_size as usize + 8);
|
||||
|
||||
// RIFF header
|
||||
wav.extend_from_slice(b"RIFF");
|
||||
wav.extend_from_slice(&file_size.to_le_bytes());
|
||||
wav.extend_from_slice(b"WAVE");
|
||||
// fmt chunk
|
||||
wav.extend_from_slice(b"fmt ");
|
||||
wav.extend_from_slice(&16u32.to_le_bytes()); // chunk size
|
||||
wav.extend_from_slice(&1u16.to_le_bytes()); // PCM
|
||||
wav.extend_from_slice(&1u16.to_le_bytes()); // mono
|
||||
wav.extend_from_slice(&SAMPLE_RATE.to_le_bytes());
|
||||
wav.extend_from_slice(&(SAMPLE_RATE * 2).to_le_bytes()); // byte rate
|
||||
wav.extend_from_slice(&2u16.to_le_bytes()); // block align
|
||||
wav.extend_from_slice(&16u16.to_le_bytes()); // bits per sample
|
||||
// data chunk
|
||||
wav.extend_from_slice(b"data");
|
||||
wav.extend_from_slice(&data_size.to_le_bytes());
|
||||
for s in &samples {
|
||||
wav.extend_from_slice(&s.to_le_bytes());
|
||||
}
|
||||
|
||||
// Try available audio players in order
|
||||
for cmd in &["paplay", "pw-play", "aplay"] {
|
||||
if let Ok(mut child) = std::process::Command::new(cmd)
|
||||
.arg("--")
|
||||
.arg("/dev/stdin")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
if let Some(ref mut stdin) = child.stdin {
|
||||
use std::io::Write;
|
||||
let _ = stdin.write_all(&wav);
|
||||
}
|
||||
let _ = child.wait();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user