Init commit

This commit is contained in:
Ultradesu
2026-08-12 01:30:57 +01:00
commit 423ea59371
67 changed files with 23280 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
//! Narrow adapters for operating-system desktop services.
use std::path::{Path, PathBuf};
mod media;
pub use media::{MediaCommand, MediaSession};
/// Opens the platform-native directory picker.
///
/// The dialog is blocking and must be invoked away from the Slint event loop.
#[must_use]
pub fn choose_library_directory(initial: Option<&Path>) -> Option<PathBuf> {
let mut dialog = rfd::FileDialog::new().set_title("Choose Furumi library folder");
if let Some(path) = initial {
dialog = dialog.set_directory(path);
}
dialog.pick_folder()
}
+185
View File
@@ -0,0 +1,185 @@
//! Cross-platform OS media session backed by souvlaki.
use std::time::{Duration, Instant};
use souvlaki::{
MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaCommand {
Toggle,
Play,
Pause,
Next,
Previous,
Stop,
}
pub struct MediaSession {
controls: MediaControls,
metadata: Option<(String, String, String, u64)>,
playback: Option<(bool, bool)>,
last_position_update: Option<Instant>,
}
impl MediaSession {
pub fn new(on_command: impl Fn(MediaCommand) + Send + 'static) -> Option<Self> {
let dbus_name = platform_dbus_name();
let mut controls = MediaControls::new(PlatformConfig {
display_name: "Furumi Desktop",
dbus_name: &dbus_name,
hwnd: platform_window(),
})
.ok()?;
controls
.attach(move |event| {
let command = match event {
MediaControlEvent::Toggle => MediaCommand::Toggle,
MediaControlEvent::Play => MediaCommand::Play,
MediaControlEvent::Pause => MediaCommand::Pause,
MediaControlEvent::Next => MediaCommand::Next,
MediaControlEvent::Previous => MediaCommand::Previous,
MediaControlEvent::Stop => MediaCommand::Stop,
_ => return,
};
on_command(command);
})
.ok()?;
Some(Self {
controls,
metadata: None,
playback: None,
last_position_update: None,
})
}
pub fn update_metadata(
&mut self,
title: &str,
artist: &str,
album: &str,
duration_seconds: f64,
) {
let duration_ms = duration_millis(duration_seconds);
let next = (
title.to_owned(),
artist.to_owned(),
album.to_owned(),
duration_ms,
);
if self.metadata.as_ref() == Some(&next) {
return;
}
let _ = self.controls.set_metadata(MediaMetadata {
title: Some(title),
artist: Some(artist),
album: Some(album),
duration: (duration_ms > 0).then(|| Duration::from_millis(duration_ms)),
cover_url: None,
});
self.metadata = Some(next);
}
pub fn update_playback(&mut self, playing: bool, paused: bool, position_seconds: f64) {
let next = (playing, paused);
let state_changed = self.playback != Some(next);
if !state_changed
&& self
.last_position_update
.is_some_and(|last| last.elapsed() < Duration::from_secs(2))
{
return;
}
let progress = Some(MediaPosition(Duration::from_millis(duration_millis(
position_seconds,
))));
let state = if !playing {
MediaPlayback::Stopped
} else if paused {
MediaPlayback::Paused { progress }
} else {
MediaPlayback::Playing { progress }
};
let _ = self.controls.set_playback(state);
self.playback = Some(next);
self.last_position_update = Some(Instant::now());
}
}
impl Drop for MediaSession {
fn drop(&mut self) {
let _ = self.controls.detach();
}
}
fn duration_millis(seconds: f64) -> u64 {
if seconds.is_finite() && seconds > 0.0 {
u64::try_from(Duration::from_secs_f64(seconds).as_millis()).unwrap_or(u64::MAX)
} else {
0
}
}
fn platform_dbus_name() -> String {
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "ios", target_os = "android"))
))]
{
return format!("cy.hexor.furumi.desktop.instance{}", std::process::id());
}
#[allow(unreachable_code)]
"cy.hexor.furumi.desktop".into()
}
#[cfg(not(windows))]
fn platform_window() -> Option<*mut std::ffi::c_void> {
None
}
#[cfg(windows)]
fn platform_window() -> Option<*mut std::ffi::c_void> {
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, RegisterClassW, WNDCLASSW,
};
unsafe {
let class_name: Vec<u16> = "furumi_desktop_media\0".encode_utf16().collect();
let instance = GetModuleHandleW(std::ptr::null());
let mut class: WNDCLASSW = core::mem::zeroed();
class.lpfnWndProc = Some(DefWindowProcW);
class.hInstance = instance;
class.lpszClassName = class_name.as_ptr();
if RegisterClassW(&class) == 0 {
return None;
}
let window = CreateWindowExW(
0,
class_name.as_ptr(),
class_name.as_ptr(),
0,
0,
0,
0,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
instance,
std::ptr::null(),
);
(!window.is_null()).then_some(window.cast())
}
}
#[cfg(test)]
mod tests {
use super::duration_millis;
#[test]
fn invalid_media_durations_are_safe() {
assert_eq!(duration_millis(f64::NAN), 0);
assert_eq!(duration_millis(-1.0), 0);
assert_eq!(duration_millis(1.25), 1_250);
}
}