#![windows_subsystem = "windows"] use gio::prelude::*; use gtk::prelude::*; use gtk::Orientation; use rodio::{Sink, Source}; use serde_derive::{Serialize, Deserialize}; use std::env::args; use std::fs::File; use std::io::{BufReader, Write, Read}; use std::path::PathBuf; use std::process::Command; const SPACING: i32 = 16; #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] struct Config { position: (i32, i32), size: (i32, i32), } fn save_config(config: Config) { let mut config_path = dirs::config_dir() .expect("Couldn't find user config directory"); config_path.push("ruin"); config_path.push("lull"); if !config_path.exists() { std::fs::create_dir_all(&config_path) .expect("Couldn't create lull config directory"); } let toml = toml::to_string(&config) .expect("Couldn't convert config to toml"); config_path.push("lull.toml"); let mut config_file = File::create(config_path) .expect("Couldn't create config file"); config_file.write(&toml.into_bytes()) .expect("Couldn't write config file"); } fn load_config() -> Option { let mut config_path = dirs::config_dir() .expect("Couldn't find user config directory"); config_path.push("ruin"); config_path.push("lull"); config_path.push("lull.toml"); let file = File::open(config_path); if file.is_err() { return None; } let mut toml = String::new(); file.unwrap().read_to_string(&mut toml) .expect("Couldn't read config file"); let config = toml::from_str(&toml) .expect("Couldn't parse config"); Some(config) } #[cfg(target_os = "windows")] fn file_manager() -> &'static str { "explorer" } #[cfg(not(target_os = "windows"))] fn file_manager() -> &'static str { "xdg-open" } fn error_popup(message: &str) { let popup = gtk::Window::new(gtk::WindowType::Toplevel); popup.set_title("error"); popup.set_border_width(SPACING as u32); popup.set_position(gtk::WindowPosition::Center); popup.set_default_size(256, 64); popup.set_type_hint(gdk::WindowTypeHint::Dialog); popup.set_resizable(false); let message = gtk::Label::new(Some(message)); popup.add(&message); popup.show_all(); } fn get_sounds_dir() -> PathBuf { let mut data_dir = dirs::data_dir().expect("Couldn't find user data directory"); data_dir.push("ruin"); data_dir.push("lull"); if !data_dir.exists() { std::fs::create_dir_all(&data_dir).expect("Couldn't create lull data directory"); } data_dir } fn build_ui(application: >k::Application) { let window = gtk::ApplicationWindow::new(application); window.set_title("lull"); window.set_border_width(SPACING as u32); window.set_position(gtk::WindowPosition::Center); window.set_default_size(256, 128); if let Some(config) = load_config() { window.move_(config.position.0, config.position.1); window.resize(config.size.0, config.size.1); } let button_manage_sounds = gtk::Button::with_label("manage sounds"); button_manage_sounds.connect_clicked(|_| { let mut file_manager = Command::new(file_manager()); file_manager.arg(get_sounds_dir()); file_manager.output().unwrap(); }); let sounds_manage = gtk::Box::new(Orientation::Vertical, SPACING); let columns = gtk::Box::new(Orientation::Horizontal, SPACING); let column_labels = gtk::Box::new(Orientation::Vertical, SPACING); let column_sliders = gtk::Box::new(Orientation::Vertical, SPACING); columns.set_homogeneous(false); column_labels.set_homogeneous(true); column_sliders.set_homogeneous(true); column_labels.set_property_expand(false); column_sliders.set_property_expand(true); column_sliders.set_property_width_request(128); window.add(&sounds_manage); sounds_manage.add(&columns); sounds_manage.add(&button_manage_sounds); columns.add(&column_labels); columns.add(&column_sliders); let device = rodio::default_output_device().unwrap(); let mut paths = std::fs::read_dir(get_sounds_dir()) .expect("Couldn't read lull sounds directory") .map(|res| res.map(|e| e.path())) .collect::, std::io::Error>>() .expect("Couldn't read files from lull sounds directory"); paths.sort(); for path in paths { let name: &str = path.file_stem().unwrap().to_str().unwrap(); let file = File::open(&path) .expect("Couldn't open audio file"); let source = rodio::Decoder::new( BufReader::new(file) ); if source.is_err() { error_popup(&format!( "Couldn't parse file {}. \n{}.", path.to_str().unwrap(), source.err().unwrap() )); continue; } let source = source.unwrap().repeat_infinite(); let sink = Sink::new(&device); sink.append(source); sink.pause(); let label = gtk::Label::new(Some(name)); label.set_halign(gtk::Align::End); column_labels.add(&label); let adjustment = gtk::Adjustment::new( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 ); let slider = gtk::Scale::new( Orientation::Horizontal, Some(&adjustment) ); slider.set_draw_value(false); slider.connect_value_changed(move |scale| { let volume = scale.get_value(); if volume == 0. { sink.pause(); } else { sink.play(); sink.set_volume(volume as f32); } }); column_sliders.add(&slider); } window.show_all(); window.connect_delete_event(|window, _event| { save_config(Config { position: window.get_position(), size: window.get_size() }); Inhibit(false) }); } fn main() { let application = gtk::Application::new( Some("dev.tinybird.max.lull"), Default::default() ).expect("Initialization failed..."); application.connect_activate(|app| { build_ui(app); }); application.run(&args().collect::>()); }