use gtk::{self, prelude::*, *}; use std::{ cell::RefCell, collections::HashMap, fs, path::PathBuf, rc::Rc, sync::{Arc, Mutex}, }; macro_rules! clone { (@param _) => ( _ ); (@param $x:ident) => ( $x ); ($($n:ident),+ => move || $body:expr) => ( { $( let $n = $n.clone(); )+ move || $body } ); ($($n:ident),+ => move |$($p:tt),+| $body:expr) => ( { $( let $n = $n.clone(); )+ move |$(clone!(@param $p),)+| $body } ); ($($n:ident),+) => ( $( let $n = $n.clone(); )+ ) } macro_rules! rc { ($($n:ident),+) => ( $( let $n = std::rc::Rc::new(std::cell::RefCell::new($n)); )+ ) } macro_rules! wait { ($exp:expr => | const $res:ident | $then:block) => { let rx = $exp; gtk::idle_add(move || match rx.try_recv() { Err(_) => glib::Continue(true), Ok($res) => { $then; glib::Continue(false) } }) }; ($exp:expr => | $res:ident | $then:block) => { let rx = $exp; gtk::idle_add(move || match rx.try_recv() { Err(_) => glib::Continue(true), Ok(mut $res) => { $then; glib::Continue(false) } }) }; } macro_rules! client { () => { crate::api::API.lock().unwrap().as_ref().unwrap() }; } mod api; mod ui; #[derive(Debug)] pub struct AppState { window: Rc>, stack: Stack, error: InfoBar, header: HeaderBar, } pub type State = Rc>; #[derive(Debug, Clone, PartialEq)] pub enum DlStatus { Planned, Started, Done, Cancelled, } #[derive(Debug, Clone)] pub struct Download { url: String, status: DlStatus, output: PathBuf, track: api::Track, } impl Download { pub fn ended(&mut self, out: PathBuf) { self.status = DlStatus::Done; self.output = out; } } lazy_static::lazy_static! { static ref DOWNLOADS: Arc>> = Arc::new(Mutex::new(HashMap::new())); static ref DL_JOBS: workerpool::Pool = workerpool::Pool::new(5); } #[derive(Default)] struct TrackDl; impl workerpool::Worker for TrackDl { type Input = Download; type Output = (); fn execute(&mut self, dl: Self::Input) -> Self::Output { if dl.status == DlStatus::Cancelled { return; } { let mut dls = DOWNLOADS.lock().unwrap(); let mut dl = dls.get_mut(&dl.track.id).unwrap(); dl.status = DlStatus::Started; } let mut res = client!().get(&dl.url).send().unwrap(); let ext = res .headers() .get(reqwest::header::CONTENT_DISPOSITION) .and_then(|h| h.to_str().ok()) .unwrap_or(".mp3") .rsplitn(2, '.') .next() .unwrap_or("mp3"); fs::create_dir_all(dl.output.parent().unwrap()).unwrap(); let mut out = dl.output.clone(); out.set_extension(ext); let mut file = fs::File::create(out.clone()).unwrap(); res.copy_to(&mut file).unwrap(); let mut dls = DOWNLOADS.lock().unwrap(); if let Some(dl) = dls.get_mut(&dl.track.id) { dl.ended(out); } } } fn main() { if gtk::init().is_err() { println!("Failed to initialize GTK."); return; } let window = Window::new(WindowType::Toplevel); window.set_icon_from_file("icons/128.svg").ok(); window.set_title("Mobydick"); window.set_default_size(1080, 720); window.connect_delete_event(move |_, _| { gtk::main_quit(); fs::create_dir_all(dirs::config_dir().unwrap().join("mobydick")).unwrap(); fs::write( dirs::config_dir() .unwrap() .join("mobydick") .join("data.json"), serde_json::to_string(&client!().to_json()).unwrap(), ) .unwrap(); Inhibit(false) }); init(Rc::new(RefCell::new(window))); gtk::main(); } fn init(window: Rc>) { let connected = fs::read( dirs::config_dir() .unwrap() .join("mobydick") .join("data.json"), ) .ok() .and_then(|f| { let json: serde_json::Value = serde_json::from_slice(&f).ok()?; let mut api_ctx = crate::api::API.lock().ok()?; let mut ctx = api::RequestContext::new(json["instance"].as_str()?.to_string()); ctx.auth(json["token"].as_str()?.to_string()); *api_ctx = Some(ctx); Some(()) }) .is_some(); let state = Rc::new(RefCell::new(AppState { window: window.clone(), stack: { let s = Stack::new(); s.set_vexpand(true); s }, error: { let error = InfoBar::new(); error.set_revealed(false); error.set_message_type(MessageType::Error); error .get_content_area() .unwrap() .downcast::() .unwrap() .add(&Label::new("Test test")); error.set_show_close_button(true); error.connect_close(|e| e.set_revealed(false)); error.connect_response(|e, _| e.set_revealed(false)); error }, header: { let h = HeaderBar::new(); h.set_show_close_button(true); h.set_title("Mobydick"); h }, })); let main_box = gtk::Box::new(Orientation::Vertical, 0); main_box.add(&state.borrow().error); main_box.add(&state.borrow().stack); let scrolled = ScrolledWindow::new(None, None); scrolled.add(&main_box); window.borrow().add(&scrolled); window.borrow().set_titlebar(&state.borrow().header); window.borrow().show_all(); if connected { let main_page = ui::main_page::render(state.borrow().window.clone(), &state.borrow().header, &{ let s = StackSwitcher::new(); s.set_stack(&state.borrow().stack); s }); state .borrow() .stack .add_titled(&main_page, "main", "Search Music"); state .borrow() .stack .add_titled(&*ui::dl_list::render().borrow(), "downloads", "Downloads"); state.borrow().stack.set_visible_child_name("main"); } else { let login_page = ui::login_page::render(state.clone()); state.borrow().stack.add_named(&login_page, "login"); } } fn show_error(state: State, msg: &str) { let b = state .borrow() .error .get_content_area() .unwrap() .downcast::() .unwrap(); for ch in b.get_children() { b.remove(&ch); } b.add(&Label::new(msg)); state.borrow().error.show_all(); state.borrow().error.set_revealed(true); } fn logout(window: Rc>) { fs::remove_file( dirs::config_dir() .unwrap() .join("mobydick") .join("data.json"), ) .ok(); *api::API.lock().unwrap() = None; *DOWNLOADS.lock().unwrap() = HashMap::new(); { let window = window.borrow(); for ch in window.get_children() { window.remove(&ch); } } init(window) }