feat(sound): add sounds for each timer

chore(deps): add rodio 0.15
feat(sound): add free resources

Closes #1
This commit is contained in:
Dmitriy Pleshevskiy 2022-02-16 23:57:20 +03:00
parent d9c9733bcf
commit 94ea30a142
12 changed files with 114 additions and 20 deletions

View File

@ -19,3 +19,4 @@ path = "src/main.rs"
druid = "0.7.0"
log = "0.4.14"
rodio = "0.15.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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<f64>,
postpone_duration_env_key: Key<f64>,
rest_duration_env_key: Key<f64>,
sound_sender: Rc<sound::Sender>,
) -> impl Widget<state::BreakTimer> {
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())

View File

@ -12,18 +12,26 @@ impl AppDelegate<state::App> 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,

View File

@ -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::<sound::Type>();
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();
}

31
src/sound.rs Normal file
View File

@ -0,0 +1,31 @@
pub type Sender = std::sync::mpsc::Sender<Type>;
pub enum Type {
EndBreakTimer,
EndNotifier,
EndRest,
}
impl From<Type> 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<dyn std::error::Error>> {
let (_stream, stream_handle) = rodio::OutputStream::try_default()?;
let sink = stream_handle.play_once(std::io::Cursor::new(bytes))?;
sink.sleep_until_end();
Ok(())
}

View File

@ -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<sound::Sender>,
pub paused: bool,
pub micro_break: BreakTimer,
pub rest_break: BreakTimer,

View File

@ -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<state::App> {
pub fn create(
parent_widget_id: WidgetId,
rest_duration_secs: f64,
sound_sender: Rc<sound::Sender>,
) -> WindowDesc<state::App> {
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<state::App> {
fn build(
parent_widget_id: WidgetId,
rest_duration_secs: f64,
sound_sender: Rc<sound::Sender>,
) -> impl Widget<state::App> {
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<sta
fn build_notifier_timer(
parent_widget_id: WidgetId,
rest_duration_secs: f64,
sound_sender: Rc<sound::Sender>,
) -> impl Widget<state::Timer> {
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

View File

@ -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<state::App> {
pub fn create(
parent_widget_id: WidgetId,
rest_duration_secs: f64,
sound_sender: Rc<sound::Sender>,
) -> WindowDesc<state::App> {
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<state::App> {
fn build(
parent_widget_id: WidgetId,
rest_duration_secs: f64,
sound_sender: Rc<sound::Sender>,
) -> impl Widget<state::App> {
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<sta
fn build_idle_timer(
parent_widget_id: WidgetId,
rest_duration_secs: f64,
sound_sender: Rc<sound::Sender>,
) -> impl Widget<state::Timer> {
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)));

View File

@ -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<state::App> {
pub fn create(sender: Rc<sound::Sender>) -> WindowDesc<state::App> {
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<state::App> {
fn build(sender: Rc<sound::Sender>) -> impl Widget<state::App> {
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<state::App> {
fn build_timers(sender: Rc<sound::Sender>) -> impl Widget<state::App> {
comp::flex::col_sta_sta()
.with_child(
comp::break_timer::build(
@ -31,6 +33,7 @@ fn build_timers() -> impl Widget<state::App> {
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<state::App> {
env::REST_BREAK_TIMER_DURATION,
env::REST_BREAK_TIMER_POSTPONE_DURATION,
env::REST_BREAK_TIMER_REST_DURATION,
sender.clone(),
)
.lens(state::App::rest_break),
)