diff --git a/Cargo.toml b/Cargo.toml index ee66986..b3f2233 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ path = "src/main.rs" druid = "0.7.0" log = "0.4.14" +rodio = "0.15.0" diff --git a/resources/sounds/mixkit-alert-bells-echo-765.wav b/resources/sounds/mixkit-alert-bells-echo-765.wav new file mode 100644 index 0000000..98d1c08 Binary files /dev/null and b/resources/sounds/mixkit-alert-bells-echo-765.wav differ diff --git a/resources/sounds/mixkit-alert-quick-chime-766.wav b/resources/sounds/mixkit-alert-quick-chime-766.wav new file mode 100644 index 0000000..1763ae3 Binary files /dev/null and b/resources/sounds/mixkit-alert-quick-chime-766.wav differ diff --git a/resources/sounds/mixkit-clear-announce-tones-2861.wav b/resources/sounds/mixkit-clear-announce-tones-2861.wav new file mode 100644 index 0000000..d2a1ee2 Binary files /dev/null and b/resources/sounds/mixkit-clear-announce-tones-2861.wav differ diff --git a/src/comp/break_timer.rs b/src/comp/break_timer.rs index 8c956a2..76e201c 100644 --- a/src/comp/break_timer.rs +++ b/src/comp/break_timer.rs @@ -1,25 +1,30 @@ use crate::cmd; use crate::comp; +use crate::sound; use crate::state; use druid::widget::Label; use druid::{Key, Widget, WidgetExt}; +use std::rc::Rc; pub fn build( name: &str, duration_env_key: Key, postpone_duration_env_key: Key, rest_duration_env_key: Key, + sound_sender: Rc, ) -> impl Widget { comp::flex::row_sta_sta() .with_child(Label::new(name).align_right().fix_width(50.0)) .with_child( comp::timer::build() .controller( - comp::timer::TimerController::new(|ctx, rest_duration_secs| { + comp::timer::TimerController::new(move |ctx, rest_duration_secs| { + sound_sender.send(sound::Type::EndBreakTimer).ok(); + ctx.submit_command(cmd::PAUSE_ALL_TIMER_COMP); ctx.submit_command( cmd::OPEN_NOTIFIER_WINDOW.with((ctx.widget_id(), rest_duration_secs)), - ) + ); }) .with_duration(duration_env_key.clone()) .with_postpone_duration(postpone_duration_env_key.clone()) diff --git a/src/delegate.rs b/src/delegate.rs index 5f55fd1..9155aea 100644 --- a/src/delegate.rs +++ b/src/delegate.rs @@ -12,18 +12,26 @@ impl AppDelegate for Delegate { ctx: &mut DelegateCtx, _target: Target, cmd: &Command, - _data: &mut state::App, + data: &mut state::App, _env: &Env, ) -> Handled { match cmd { _ if cmd.is(cmd::OPEN_NOTIFIER_WINDOW) => { let (widget_id, rest_duration_secs) = *cmd.get_unchecked(cmd::OPEN_NOTIFIER_WINDOW); - ctx.new_window(win::notifier::create(widget_id, rest_duration_secs)); + ctx.new_window(win::notifier::create( + widget_id, + rest_duration_secs, + data.sound_sender.clone(), + )); Handled::Yes } _ if cmd.is(cmd::OPEN_IDLE_WINDOW) => { let (widget_id, rest_duration_secs) = *cmd.get_unchecked(cmd::OPEN_IDLE_WINDOW); - ctx.new_window(win::rest::create(widget_id, rest_duration_secs)); + ctx.new_window(win::rest::create( + widget_id, + rest_duration_secs, + data.sound_sender.clone(), + )); Handled::Yes } _ => Handled::No, diff --git a/src/main.rs b/src/main.rs index cee4745..21b71e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,23 +2,38 @@ mod cmd; mod comp; mod delegate; mod env; +mod sound; mod state; mod win; use delegate::Delegate; use druid::AppLauncher; +use std::sync::mpsc::channel; +use std::thread; + fn main() { + let (tx, rx) = channel::(); + + let boombox = thread::spawn(move || loop { + rx.recv() + .map_err(From::from) + .and_then(|sound_type| sound::try_play(sound_type.into())) + .ok(); + }); + let initial_state = state::App { paused: false, micro_break: state::BreakTimer::new(), rest_break: state::BreakTimer::new(), notifier: state::Timer::new(), + sound_sender: std::rc::Rc::new(tx.clone()), }; - AppLauncher::with_window(win::status::create()) + AppLauncher::with_window(win::status::create(initial_state.sound_sender.clone())) .delegate(Delegate) .configure_env(env::configure) .launch(initial_state) .expect("Failed to launch application"); + boombox.join().unwrap(); } diff --git a/src/sound.rs b/src/sound.rs new file mode 100644 index 0000000..d4bcf6e --- /dev/null +++ b/src/sound.rs @@ -0,0 +1,31 @@ +pub type Sender = std::sync::mpsc::Sender; + +pub enum Type { + EndBreakTimer, + EndNotifier, + EndRest, +} + +impl From for &'static [u8] { + fn from(sound_type: Type) -> Self { + match sound_type { + Type::EndBreakTimer => ALERT_CLEAR_ANNOUNCE_TONES, + Type::EndRest => ALERT_BELLS_ECHO, + Type::EndNotifier => ALERT_QUICK_CHIME, + } + } +} + +const ALERT_BELLS_ECHO: &[u8] = + include_bytes!("../resources/sounds/mixkit-alert-bells-echo-765.wav"); +const ALERT_QUICK_CHIME: &[u8] = + include_bytes!("../resources/sounds/mixkit-alert-quick-chime-766.wav"); +const ALERT_CLEAR_ANNOUNCE_TONES: &[u8] = + include_bytes!("../resources/sounds/mixkit-clear-announce-tones-2861.wav"); + +pub fn try_play(bytes: &'static [u8]) -> Result<(), Box> { + let (_stream, stream_handle) = rodio::OutputStream::try_default()?; + let sink = stream_handle.play_once(std::io::Cursor::new(bytes))?; + sink.sleep_until_end(); + Ok(()) +} diff --git a/src/state.rs b/src/state.rs index f1c32e7..3968055 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,9 +1,12 @@ +use crate::sound; use druid::{Data, Lens}; use std::ops::Div; +use std::rc::Rc; use std::time::Duration; #[derive(Clone, Data, Lens)] pub struct App { + pub sound_sender: Rc, pub paused: bool, pub micro_break: BreakTimer, pub rest_break: BreakTimer, diff --git a/src/win/notifier.rs b/src/win/notifier.rs index 902ddc7..508b311 100644 --- a/src/win/notifier.rs +++ b/src/win/notifier.rs @@ -1,11 +1,17 @@ use crate::cmd; use crate::comp; use crate::env; +use crate::sound; use crate::state; use druid::widget::Button; use druid::{MenuDesc, Target, Widget, WidgetExt, WidgetId, WindowDesc}; +use std::rc::Rc; -pub fn create(parent_widget_id: WidgetId, rest_duration_secs: f64) -> WindowDesc { +pub fn create( + parent_widget_id: WidgetId, + rest_duration_secs: f64, + sound_sender: Rc, +) -> WindowDesc { let win_width = 200.0; let win_height = 100.0; @@ -13,7 +19,7 @@ pub fn create(parent_widget_id: WidgetId, rest_duration_secs: f64) -> WindowDesc let x = (rect.width() - win_width) / 2.0; let y = 0.0; - return WindowDesc::new(move || build(parent_widget_id, rest_duration_secs)) + return WindowDesc::new(move || build(parent_widget_id, rest_duration_secs, sound_sender)) .show_titlebar(false) .menu(MenuDesc::empty()) .set_position((x, y)) @@ -21,10 +27,15 @@ pub fn create(parent_widget_id: WidgetId, rest_duration_secs: f64) -> WindowDesc .window_size((win_width, win_height)); } -fn build(parent_widget_id: WidgetId, rest_duration_secs: f64) -> impl Widget { +fn build( + parent_widget_id: WidgetId, + rest_duration_secs: f64, + sound_sender: Rc, +) -> impl Widget { comp::flex::col_cen_cen() .with_child( - build_notifier_timer(parent_widget_id, rest_duration_secs).lens(state::App::notifier), + build_notifier_timer(parent_widget_id, rest_duration_secs, sound_sender) + .lens(state::App::notifier), ) .with_default_spacer() .with_child(build_postpone_btn(parent_widget_id)) @@ -34,10 +45,13 @@ fn build(parent_widget_id: WidgetId, rest_duration_secs: f64) -> impl Widget, ) -> impl Widget { comp::timer::build() .controller( comp::timer::TimerController::new(move |ctx, _| { + sound_sender.send(sound::Type::EndNotifier).ok(); + ctx.submit_command(cmd::DEINIT_COMP.to(Target::Widget(ctx.widget_id()))); ctx.submit_command( cmd::OPEN_IDLE_WINDOW diff --git a/src/win/rest.rs b/src/win/rest.rs index d30a555..1d4726c 100644 --- a/src/win/rest.rs +++ b/src/win/rest.rs @@ -1,11 +1,17 @@ use crate::cmd; use crate::comp; use crate::env; +use crate::sound; use crate::state; use druid::widget::Button; use druid::{MenuDesc, Target, Widget, WidgetExt, WidgetId, WindowDesc}; +use std::rc::Rc; -pub fn create(parent_widget_id: WidgetId, rest_duration_secs: f64) -> WindowDesc { +pub fn create( + parent_widget_id: WidgetId, + rest_duration_secs: f64, + sound_sender: Rc, +) -> WindowDesc { let win_width = 450.0; let win_height = 200.0; @@ -13,7 +19,7 @@ pub fn create(parent_widget_id: WidgetId, rest_duration_secs: f64) -> WindowDesc let x = (rect.width() - win_width) / 2.0; let y = (rect.height() - win_height) / 2.0; - return WindowDesc::new(move || build(parent_widget_id, rest_duration_secs)) + return WindowDesc::new(move || build(parent_widget_id, rest_duration_secs, sound_sender)) .show_titlebar(false) .menu(MenuDesc::empty()) .set_position((x, y)) @@ -21,12 +27,16 @@ pub fn create(parent_widget_id: WidgetId, rest_duration_secs: f64) -> WindowDesc .window_size((win_width, win_height)); } -fn build(parent_widget_id: WidgetId, rest_duration_secs: f64) -> impl Widget { +fn build( + parent_widget_id: WidgetId, + rest_duration_secs: f64, + sound_sender: Rc, +) -> impl Widget { comp::flex::col_cen_cen() .with_child( comp::flex::col_sta_end() .with_child( - build_idle_timer(parent_widget_id, rest_duration_secs) + build_idle_timer(parent_widget_id, rest_duration_secs, sound_sender) .lens(state::App::notifier), ) .with_default_spacer() @@ -38,10 +48,13 @@ fn build(parent_widget_id: WidgetId, rest_duration_secs: f64) -> impl Widget, ) -> impl Widget { comp::timer::build() .controller( comp::timer::TimerController::new(move |ctx, _| { + sound_sender.send(sound::Type::EndRest).ok(); + ctx.submit_command(cmd::DEINIT_COMP.to(Target::Widget(ctx.widget_id()))); ctx.submit_command(cmd::UNPAUSE_ALL_TIMER_COMP.with(false).to(Target::Global)); ctx.submit_command(cmd::RESTART_TIMER_COMP.to(Target::Widget(parent_widget_id))); diff --git a/src/win/status.rs b/src/win/status.rs index 03c7010..e5e25b9 100644 --- a/src/win/status.rs +++ b/src/win/status.rs @@ -1,29 +1,31 @@ use crate::cmd; use crate::comp; use crate::env; +use crate::sound; use crate::state; use druid::widget::{Button, Either}; use druid::{LocalizedString, MenuDesc, Widget, WidgetExt, WindowDesc}; +use std::rc::Rc; -pub fn create() -> WindowDesc { +pub fn create(sender: Rc) -> WindowDesc { let win_width = 220.0; let win_height = 100.0; - return WindowDesc::new(build) + WindowDesc::new(|| build(sender)) .title(LocalizedString::new("HWT Status")) .menu(MenuDesc::empty()) .with_min_size((win_width, win_height)) - .window_size((win_width, win_height)); + .window_size((win_width, win_height)) } -fn build() -> impl Widget { +fn build(sender: Rc) -> impl Widget { comp::flex::col_sta_sta() - .with_child(build_timers()) + .with_child(build_timers(sender)) .with_default_spacer() .with_child(build_pause_btn()) .padding((8.0, 8.0)) } -fn build_timers() -> impl Widget { +fn build_timers(sender: Rc) -> impl Widget { comp::flex::col_sta_sta() .with_child( comp::break_timer::build( @@ -31,6 +33,7 @@ fn build_timers() -> impl Widget { env::MICRO_BREAK_TIMER_DURATION, env::MICRO_BREAK_TIMER_POSTPONE_DURATION, env::MICRO_BREAK_TIMER_REST_DURATION, + sender.clone(), ) .lens(state::App::micro_break), ) @@ -41,6 +44,7 @@ fn build_timers() -> impl Widget { env::REST_BREAK_TIMER_DURATION, env::REST_BREAK_TIMER_POSTPONE_DURATION, env::REST_BREAK_TIMER_REST_DURATION, + sender.clone(), ) .lens(state::App::rest_break), )