marche
This commit is contained in:
commit
f74c5b474c
6 changed files with 2158 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1877
Cargo.lock
generated
Normal file
1877
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "timeurs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-channel = "2.1"
|
||||
gtk = { version = "0.7", package = "gtk4", features = ["v4_12"] }
|
||||
oneshot = "0.1"
|
||||
rodio = "0.17"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
BIN
assets/ding.ogg
Normal file
BIN
assets/ding.ogg
Normal file
Binary file not shown.
163
src/main.rs
Normal file
163
src/main.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use std::ops::Mul;
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{glib, Application, ApplicationWindow, Button, Entry, Label, Orientation};
|
||||
use state::{Cmd, State};
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
// Create a new application
|
||||
let app = Application::builder()
|
||||
.application_id("org.gtk_rs.HelloWorld2")
|
||||
.build();
|
||||
|
||||
let (send, rec) = mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
State::initial().spin(rec);
|
||||
});
|
||||
|
||||
app.connect_activate(move |app| build_ui(app, send.clone()));
|
||||
app.run()
|
||||
}
|
||||
mod state;
|
||||
fn build_ui(app: &Application, cmd: Sender<Cmd>) {
|
||||
let label = Label::builder().label("-").build();
|
||||
|
||||
// entry
|
||||
let entry = Entry::builder().text("0:00:10").build();
|
||||
let set_button = Button::builder().label("set").build();
|
||||
let entry_container = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(8)
|
||||
.build();
|
||||
entry_container.append(&entry);
|
||||
entry_container.append(&set_button);
|
||||
|
||||
let passed = cmd.clone();
|
||||
set_button.connect_clicked(move |_| {
|
||||
let cmd = &passed;
|
||||
let inputted = entry.text().to_string();
|
||||
let inputted = parse_input_time(&inputted);
|
||||
cmd.send(Cmd::SetInput(inputted)).unwrap();
|
||||
cmd.send(Cmd::Reset).unwrap();
|
||||
});
|
||||
|
||||
// pause & reset
|
||||
let pause_button = Button::builder().label("start").build();
|
||||
let reset_button = Button::builder().label("reset").build();
|
||||
let button_container = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(8)
|
||||
.build();
|
||||
button_container.append(&pause_button);
|
||||
button_container.append(&reset_button);
|
||||
|
||||
let passed = cmd.clone();
|
||||
pause_button.connect_clicked(move |button| {
|
||||
let cmd = &passed;
|
||||
cmd.send(Cmd::TogglePaused).unwrap();
|
||||
let (send, rec) = oneshot::channel();
|
||||
cmd.send(Cmd::GetIsPaused(send)).unwrap();
|
||||
let is_paused = rec.recv().unwrap();
|
||||
button.set_label(if is_paused { "start" } else { "pause" });
|
||||
});
|
||||
|
||||
let passed = cmd.clone();
|
||||
reset_button.connect_clicked(move |_| {
|
||||
let cmd = &passed;
|
||||
pause_button.set_label("start");
|
||||
cmd.send(Cmd::Reset).unwrap();
|
||||
});
|
||||
|
||||
// container
|
||||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(8)
|
||||
.margin_top(12)
|
||||
.margin_bottom(12)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.build();
|
||||
container.append(&label);
|
||||
container.append(&entry_container);
|
||||
container.append(&button_container);
|
||||
|
||||
let passed = cmd.clone();
|
||||
glib::spawn_future_local(clone!(@weak label => async move {
|
||||
let cmd = &passed;
|
||||
loop {
|
||||
glib::timeout_future(Duration::from_millis(10)).await;
|
||||
let (send, rec) = oneshot::channel();
|
||||
cmd.send(Cmd::GetTime(send)).unwrap();
|
||||
let time = rec.recv().unwrap();
|
||||
label.set_text(&format_time(time));
|
||||
}
|
||||
}));
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("My GTK App")
|
||||
.child(&container)
|
||||
.build();
|
||||
window.present();
|
||||
}
|
||||
|
||||
fn parse_input_time(inputted: &str) -> f32 {
|
||||
fn parse_empty_zero(input: &str) -> i32 {
|
||||
if input.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
input.parse().unwrap()
|
||||
}
|
||||
inputted
|
||||
.split(':')
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|part| parse_empty_zero(part))
|
||||
.enumerate()
|
||||
.map(|(index, item)| 60i32.pow(index as u32) * item)
|
||||
.sum::<i32>() as f32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_input_time() {
|
||||
assert_eq!(parse_input_time(""), 0f32);
|
||||
assert_eq!(parse_input_time(":"), 0f32);
|
||||
assert_eq!(parse_input_time("3"), 3f32);
|
||||
assert_eq!(parse_input_time(":3"), 3f32);
|
||||
assert_eq!(parse_input_time("3:"), 180f32);
|
||||
assert_eq!(parse_input_time(":3:"), 180f32);
|
||||
assert_eq!(parse_input_time("3::"), 10800f32);
|
||||
}
|
||||
|
||||
fn format_time(input: f32) -> String {
|
||||
// les maths 🧙
|
||||
let decimal = input.rem_euclid(1.).mul(100.) as i32;
|
||||
let rest = input.div_euclid(1.);
|
||||
let secs = rest.rem_euclid(60.) as i32;
|
||||
let rest = rest.div_euclid(60.);
|
||||
let min = rest.rem_euclid(60.) as i32;
|
||||
let hours = rest.div_euclid(60.) as i32;
|
||||
match (hours == 0, min == 0) {
|
||||
(true, true) => format!("{secs}.{decimal:02}"),
|
||||
(true, _) => format!("{min}:{secs:02}.{decimal:02}"),
|
||||
_ => format!("{hours}:{min:02}:{secs:02}.{decimal:02}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time() {
|
||||
assert_eq!(format_time(0.), "0.00");
|
||||
assert_eq!(format_time(0.5), "0.50");
|
||||
assert_eq!(format_time(0.25), "0.25");
|
||||
assert_eq!(format_time(10.), "10.00");
|
||||
assert_eq!(format_time(60.), "1:00.00");
|
||||
assert_eq!(format_time(600.), "10:00.00");
|
||||
assert_eq!(format_time(3600.), "1:00:00.00");
|
||||
assert_eq!(format_time(3600.5), "1:00:00.50");
|
||||
assert_eq!(format_time(3660.5), "1:01:00.50");
|
||||
}
|
100
src/state.rs
Normal file
100
src/state.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use std::{io::Cursor, sync::mpsc, time::Instant};
|
||||
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
||||
|
||||
pub enum Cmd {
|
||||
GetTime(oneshot::Sender<f32>),
|
||||
GetIsPaused(oneshot::Sender<bool>),
|
||||
SetInput(f32),
|
||||
TogglePaused,
|
||||
Reset,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
stream: OutputStream,
|
||||
stream_handle: OutputStreamHandle,
|
||||
last_start: Instant,
|
||||
input_time: f32,
|
||||
ellapsed_at_last_pause: f32,
|
||||
is_paused: bool,
|
||||
reached_target: bool,
|
||||
player: Option<Sink>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn initial() -> Self {
|
||||
let (stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
|
||||
Self {
|
||||
stream,
|
||||
stream_handle,
|
||||
last_start: Instant::now(),
|
||||
input_time: 0.,
|
||||
ellapsed_at_last_pause: 0.,
|
||||
is_paused: true,
|
||||
reached_target: false,
|
||||
player: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spin(mut self, commands: mpsc::Receiver<Cmd>) {
|
||||
while let Ok(cmd) = commands.recv() {
|
||||
match cmd {
|
||||
Cmd::GetTime(returns) => {
|
||||
let mut total_ellapsed = self.ellapsed_at_last_pause;
|
||||
if !self.is_paused {
|
||||
total_ellapsed += self.ellapsed_since_last_start();
|
||||
}
|
||||
let res = self.input_time - total_ellapsed;
|
||||
if res < 0. && !self.reached_target {
|
||||
self.reached_target = true;
|
||||
self.start_playing();
|
||||
}
|
||||
returns.send(res.max(0.)).ok(); // note : sends zero instead of negatives.
|
||||
}
|
||||
Cmd::GetIsPaused(returns) => {
|
||||
returns.send(self.is_paused).ok();
|
||||
}
|
||||
Cmd::SetInput(inputted) => {
|
||||
self.input_time = inputted;
|
||||
}
|
||||
Cmd::TogglePaused => {
|
||||
self.is_paused = !self.is_paused;
|
||||
if self.is_paused {
|
||||
self.ellapsed_at_last_pause += self.ellapsed_since_last_start();
|
||||
} else {
|
||||
self.last_start = Instant::now();
|
||||
}
|
||||
}
|
||||
Cmd::Reset => {
|
||||
self.is_paused = true;
|
||||
self.ellapsed_at_last_pause = 0.;
|
||||
self.reached_target = false;
|
||||
self.stop_playing();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ellapsed_since_last_start(&self) -> f32 {
|
||||
Instant::now().duration_since(self.last_start).as_secs_f32()
|
||||
}
|
||||
|
||||
pub fn start_playing(&mut self) {
|
||||
println!("start");
|
||||
let cursed = Cursor::new(DING);
|
||||
// let source = Decoder::new_mp3(cursed).unwrap();
|
||||
let control = self.stream_handle.play_once(cursed).unwrap();
|
||||
control.play();
|
||||
self.player.replace(control);
|
||||
}
|
||||
|
||||
pub fn stop_playing(&self) {
|
||||
println!("stop");
|
||||
if let Some(player) = &self.player {
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DING: &[u8] = include_bytes!("../assets/ding.ogg");
|
Loading…
Add table
Add a link
Reference in a new issue