diff --git a/Cargo.toml b/Cargo.toml index 87c3c88..352950c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lull" description = "a looping sound player for generating atmospheric soundscapes" -version = "1.0.4" +version = "1.1.0" authors = ["Max Bradbury "] repository = "https://tinybird.dev/max/lull" license = "MIT" @@ -9,9 +9,12 @@ edition = "2018" [dependencies] dirs = "^3.0.1" +eframe = "^0.11.0" +egui = "^0.11.0" gdk = "^0.13.2" gio = "^0" gtk = "^0" +orbtk = "^0.2.31" rodio = "^0.11.0" serde = "^1.0.125" serde_derive = "^1.0.125" diff --git a/deploy.sh b/deploy.sh index 5cbb0cd..26c482d 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,8 +1,5 @@ #!/usr/bin/env bash -rm -rf dist/windows -mv package dist/windows - butler push dist/linux ruin/lull:linux butler push dist/windows ruin/lull:windows butler push dist/sounds ruin/lull:sounds diff --git a/src/main.rs b/src/main.rs index 6f20850..b9876a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,17 @@ #![windows_subsystem = "windows"] mod config; -mod ui_gtk; +mod ui; use std::path::PathBuf; +use std::io::BufReader; +use std::fs::File; + +use rodio::{Sink, Source}; +use std::process::Command; + +const SOUNDS_BUTTON_LABEL: &'static str = "open sounds folder"; +const SPACING: i32 = 16; #[cfg(target_os = "windows")] fn file_manager() -> &'static str { @@ -21,6 +29,11 @@ fn get_sounds_dir() -> PathBuf { data_dir.push("ruin"); data_dir.push("lull"); + // on windows, data dir and config dir are both User\AppData\Roaming + #[cfg(target_os = "windows")] { + data_dir.push("sounds"); + } + if !data_dir.exists() { std::fs::create_dir_all(&data_dir) .expect("Couldn't create lull sounds directory"); @@ -29,7 +42,71 @@ fn get_sounds_dir() -> PathBuf { data_dir } -//#[cfg(not(target_os = "windows"))] -fn main() { - ui_gtk::instantiate(); +fn open_file_manager() { + let mut file_manager = Command::new(file_manager()); + file_manager.arg(crate::get_sounds_dir()); + file_manager.output().unwrap(); } + +struct Sound { + name: String, + sink: Sink, + volume: f64, +} + +fn get_sounds(device: &rodio::Device) -> Vec { + let mut sounds = Vec::new(); + + 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 = path.file_stem().unwrap().to_str().unwrap().into(); + + let file = File::open(&path) + .expect("Couldn't open audio file"); + + let source = rodio::Decoder::new( + BufReader::new(file) + ); + + #[cfg(not(target_os = "windows"))] + if source.is_err() { + ui::gtk::error_popup(&format!( + "Couldn't parse file {}. \n{}.", + path.to_str().unwrap(), + source.err().unwrap() + )); + continue; + } + + let source = source.unwrap().repeat_infinite(); + + let sink = rodio::Sink::new(&device); + sink.append(source); + sink.pause(); + + sounds.push(Sound { name, sink, volume: 0.0 }); + } + + sounds +} + +// #[cfg(not(target_os = "windows"))] +fn main() { + crate::ui::gtk::instantiate(); +} + +// #[cfg(target_os = "windows")] +// fn main() { +// eframe::run_native(Box::new(lull::ui::egui::State::default())); +// } + +// fn main() { +// crate::ui::orbtk::instantiate(); +// } diff --git a/src/ui/egui.rs b/src/ui/egui.rs new file mode 100644 index 0000000..9c75a41 --- /dev/null +++ b/src/ui/egui.rs @@ -0,0 +1,79 @@ +use eframe::{egui, epi}; + +pub(crate) use crate::{Sound, get_sounds, open_file_manager}; +use egui::{Color32, Stroke, Vec2}; + +const COLOUR_BG: Color32 = Color32::from_rgb(76, 58, 78); +const COLOUR_FG: Color32 = Color32::from_rgb(156, 143, 109); + +pub struct State { + device: rodio::Device, + sounds: Vec, +} + +impl Default for State { + fn default() -> Self { + let device = rodio::default_output_device().unwrap(); + let sounds = get_sounds(&device); + + Self { device, sounds } + } +} + +impl epi::App for State { + fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { + let State { device: _device, sounds} = self; + + for sound in sounds.into_iter() { + if sound.volume > 0.0 { + sound.sink.set_volume(sound.volume as f32); + sound.sink.play(); + } else { + sound.sink.pause(); + } + } + + egui::CentralPanel::default().frame(egui::Frame { + margin: Vec2 { x: crate::SPACING as f32, y: crate::SPACING as f32 }, + corner_radius: 0.0, + shadow: Default::default(), + fill: COLOUR_BG, + stroke: Stroke { + width: 1.0, + color: COLOUR_FG, + } + }).show(ctx, |ui| { + egui::Grid::new("grid") + .spacing([crate::SPACING as f32; 2]) + .show(ui, |ui| { + for sound in sounds { + ui.add( + egui::Label::new(&sound.name).text_color(COLOUR_FG) + ); + + ui.add( + egui::Slider::new( + &mut sound.volume, + 0.0..=1.0 + ).show_value(false) + ); + + ui.end_row(); + } + + if ui.add( + egui::Button::new(crate::SOUNDS_BUTTON_LABEL) + .text_color(COLOUR_FG) + ).clicked() { + open_file_manager(); + } + + ui.end_row(); + }); + }); + } + + fn name(&self) -> &str { + "lull" + } +} diff --git a/src/ui_gtk.rs b/src/ui/gtk.rs similarity index 61% rename from src/ui_gtk.rs rename to src/ui/gtk.rs index 2c13a9a..e2e73a2 100644 --- a/src/ui_gtk.rs +++ b/src/ui/gtk.rs @@ -1,19 +1,16 @@ use std::env::args; -use std::fs::File; -use std::io::{BufReader}; -use std::process::Command; use gio::prelude::*; use gtk::prelude::*; use gtk::Orientation; -use rodio::{Sink, Source}; -const SPACING: i32 = 16; +use crate::open_file_manager; +use crate::config::load_config; -fn error_popup(message: &str) { +pub(crate) 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_border_width(crate::SPACING as u32); popup.set_position(gtk::WindowPosition::Center); popup.set_default_size(256, 64); popup.set_type_hint(gdk::WindowTypeHint::Dialog); @@ -29,27 +26,25 @@ 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_border_width(crate::SPACING as u32); window.set_position(gtk::WindowPosition::Center); window.set_default_size(256, 128); - if let Some(config) = crate::config::load_config() { + 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("open sounds folder"); + let button_manage_sounds = gtk::Button::with_label(crate::SOUNDS_BUTTON_LABEL); button_manage_sounds.connect_clicked(|_| { - let mut file_manager = Command::new(crate::file_manager()); - file_manager.arg(crate::get_sounds_dir()); - file_manager.output().unwrap(); + open_file_manager(); }); - 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); + let sounds_manage = gtk::Box::new(Orientation::Vertical, crate::SPACING); + let columns = gtk::Box::new(Orientation::Horizontal, crate::SPACING); + let column_labels = gtk::Box::new(Orientation::Vertical, crate::SPACING); + let column_sliders = gtk::Box::new(Orientation::Vertical, crate::SPACING); columns.set_homogeneous(false); column_labels.set_homogeneous(true); @@ -66,47 +61,10 @@ fn build_ui(application: >k::Application) { let device = rodio::default_output_device().unwrap(); - let mut paths = std::fs::read_dir(crate::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"); + let sounds = crate::get_sounds(&device); - paths.sort(); - - for path in paths { - let name = path.file_stem().unwrap().to_str().unwrap(); - let extension = path.extension().unwrap().to_str().unwrap(); - - // on Windows the data dir and config dir are the same, - // so avoid parsing the config file as an audio file - if extension == "toml" { - continue; - } - - 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)); + for sound in sounds { + let label = gtk::Label::new(Some(sound.name.as_ref())); label.set_halign(gtk::Align::End); column_labels.add(&label); @@ -130,10 +88,10 @@ fn build_ui(application: >k::Application) { let volume = scale.get_value(); if volume == 0. { - sink.pause(); + sound.sink.pause(); } else { - sink.play(); - sink.set_volume(volume as f32); + sound.sink.play(); + sound.sink.set_volume(volume as f32); } }); diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..25f115e --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,3 @@ +// pub mod egui; +pub mod gtk; +// pub mod orbtk; diff --git a/src/ui/orbtk.rs b/src/ui/orbtk.rs new file mode 100644 index 0000000..df38c3c --- /dev/null +++ b/src/ui/orbtk.rs @@ -0,0 +1,15 @@ +use orbtk::{prelude::*, widgets::themes::*}; + +pub(crate) fn instantiate() { + Application::new() + .window(|ctx| { + Window::new() + .title("OrbTk - showcase example") + .position((100, 100)) + .size(1000, 730) + .resizeable(true) + .child(MainView::new().build(ctx)) + .build(ctx) + }) + .run(); +}