diff --git a/.gitignore b/.gitignore index c4722b5..0b52ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target **/*.rs.bk .buildconfig -data.json \ No newline at end of file +data.json +debian \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 98b7bc2..7381d55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,24 +315,6 @@ name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "funkload" -version = "0.1.0" -dependencies = [ - "cairo-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "gtk 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.86 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.86 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", - "workerpool 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "futures" version = "0.1.25" @@ -713,6 +695,25 @@ dependencies = [ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mobydick" +version = "0.1.0" +dependencies = [ + "cairo-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "gtk 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "open 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.86 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.86 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "workerpool 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "native-tls" version = "0.2.2" @@ -753,6 +754,14 @@ dependencies = [ "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "open" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "openssl" version = "0.10.16" @@ -1566,6 +1575,7 @@ dependencies = [ "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum num_cpus 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5a69d464bdc213aaaff628444e99578ede64e9c854025aa43b9796530afa9238" +"checksum open 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eedfa0ca7b54d84d948bfd058b8f82e767d11f362dd78c36866fd1f69c175867" "checksum openssl 0.10.16 (registry+https://github.com/rust-lang/crates.io-index)" = "ec7bd7ca4cce6dbdc77e7c1230682740d307d1218a87fb0349a571272be749f9" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.40 (registry+https://github.com/rust-lang/crates.io-index)" = "1bb974e77de925ef426b6bc82fce15fd45bdcbeb5728bffcfc7cdeeb7ce1c2d6" diff --git a/Cargo.toml b/Cargo.toml index 12a076c..b2396e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "funkload" +name = "mobydick" version = "0.1.0" authors = ["Baptiste Gelez "] edition = "2018" @@ -10,10 +10,11 @@ dirs = "1.0" gdk = "0.9" gdk-pixbuf = "0.5" glib = "0.6" -gtk = { version = "0.5", features = [ "v3_22" ] } +gtk = { version = "0.5", features = [ "v3_22_29" ] } serde = "1.0" serde_derive = "1.0" serde_json = "1.0" reqwest = "0.9" workerpool = "1.1.1" -lazy_static = "1.2" \ No newline at end of file +lazy_static = "1.2" +open = "1.2" \ No newline at end of file diff --git a/icons/128.svg b/icons/128.svg new file mode 100644 index 0000000..8699227 --- /dev/null +++ b/icons/128.svg @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/16.svg b/icons/16.svg new file mode 100644 index 0000000..f98316f --- /dev/null +++ b/icons/16.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/24.svg b/icons/24.svg new file mode 100644 index 0000000..f105888 --- /dev/null +++ b/icons/24.svg @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/32.svg b/icons/32.svg new file mode 100644 index 0000000..852b3d3 --- /dev/null +++ b/icons/32.svg @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/48.svg b/icons/48.svg new file mode 100644 index 0000000..a4d9762 --- /dev/null +++ b/icons/48.svg @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/64.svg b/icons/64.svg new file mode 100644 index 0000000..459a2cb --- /dev/null +++ b/icons/64.svg @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..055e276 --- /dev/null +++ b/install.sh @@ -0,0 +1,7 @@ +cargo build --release +sudo cp target/release/mobydick $PREFIX/bin/xyz.gelez.mobydick +sudo cp *.appdata.xml $PREFIX/share/appdata/ +sudo cp *.desktop $PREFIX/share/applications/ +for s in "16" "24" "32" "48" "64" "128"; do + sudo cp icons/$s.svg $PREFIX/share/icons/hicolor/${s}x${s}/mobydick.svg +done \ No newline at end of file diff --git a/screen-list.png b/screen-list.png new file mode 100644 index 0000000..689d6f1 Binary files /dev/null and b/screen-list.png differ diff --git a/screen-main.png b/screen-main.png new file mode 100644 index 0000000..1e1ac24 Binary files /dev/null and b/screen-main.png differ diff --git a/src/api.rs b/src/api.rs index 60d2fa6..9b9d38a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -74,22 +74,6 @@ impl Worker for Req { } } -#[derive(Deserialize)] -#[serde(untagged)] -pub enum ApiResult { - Ok(T), - Err -} - -impl Into> for ApiResult { - fn into(self) -> Result { - match self { - ApiResult::Ok(t) => Ok(t), - ApiResult::Err => Err(()), - } - } -} - #[derive(Deserialize, Serialize)] pub struct LoginData { pub password: String, @@ -172,3 +156,16 @@ pub struct AlbumTrack { pub listen_url: String, } +impl AlbumTrack { + pub fn into_full(self, album: &Album) -> Track { + let mut album = album.clone(); + album.tracks = None; + Track { + album: album, + id: self.id, + title: self.title, + artist: self.artist, + listen_url: self.listen_url, + } + } +} diff --git a/src/main.rs b/src/main.rs index 63a9dfb..7b3ecd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use gtk::{self, prelude::*, *}; use std::{ + collections::HashMap, cell::RefCell, rc::Rc, sync::{Arc, Mutex}, @@ -70,22 +71,78 @@ mod ui; #[derive(Debug)] pub struct AppState { stack: Stack, - err_revealer: Revealer, - err_label: Label, - downloads: Arc>>, + 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, - done: bool, + status: DlStatus, output: PathBuf, + track: api::Track, } -lazy_static! { - static ref DOWNLOADS: Arc>> = Arc::new(Mutex::new(vec![])); +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.clone().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() { @@ -95,10 +152,11 @@ fn main() { } let window = Window::new(WindowType::Toplevel); + window.set_icon_from_file("icons/128.svg"); window.set_title("Funkload"); window.set_default_size(1080, 720); - let connected = fs::read("data.json").ok().and_then(|f| { + 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()); @@ -108,37 +166,74 @@ fn main() { Some(()) }).is_some(); - let err_revealer = Revealer::new(); - let err_label = Label::new("Error"); - err_revealer.add(&err_label); let state = Rc::new(RefCell::new(AppState { - stack: Stack::new(), - err_revealer: err_revealer, - err_label: err_label, - downloads: Arc::new(RefCell::new(Vec::new())), + 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("Funkload"); + h + }, })); - let login_page = ui::login_page::render(state.clone()); + let main_box = gtk::Box::new(Orientation::Vertical, 0); + main_box.add(&state.borrow().error); + main_box.add(&state.borrow().stack); - state.borrow().stack.add_named(&login_page, "login"); let scrolled = ScrolledWindow::new(None, None); - scrolled.add(&state.borrow().stack); + scrolled.add(&main_box); window.add(&scrolled); + window.set_titlebar(&state.borrow().header); window.show_all(); if connected { - let main_page = ui::main_page::render(); - state.borrow().stack.add_named(&main_page, "main"); + let main_page = ui::main_page::render(&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"); } window.connect_delete_event(move |_, _| { gtk::main_quit(); - fs::write("data.json", serde_json::to_string(&client!().to_json()).unwrap()).unwrap(); + fs::write( + dirs::config_dir().unwrap().join("mobydick").join("data.json"), + serde_json::to_string(&client!().to_json()).unwrap() + ).unwrap(); Inhibit(false) }); gtk::main(); } + +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); +} diff --git a/src/ui/card.rs b/src/ui/card.rs index 1baeea0..c69cb2a 100644 --- a/src/ui/card.rs +++ b/src/ui/card.rs @@ -1,6 +1,6 @@ use gtk::*; -use std::{fs, thread, sync::mpsc::channel, rc::Rc, cell::RefCell}; -use crate::{Download, api::{self, execute}, ui::network_image::NetworkImage}; +use std::{thread, sync::mpsc::channel, rc::Rc, cell::RefCell}; +use crate::{Download, DlStatus, api, ui::network_image::NetworkImage}; pub fn render(model: T) -> Rc> where T: CardModel + 'static { let card = Grid::new(); @@ -21,66 +21,107 @@ pub fn render(model: T) -> Rc> where T: CardModel + 'static { sub_text.set_hexpand(true); sub_text.set_halign(Align::Start); - let dl_bt = Button::new_with_label("Download"); - dl_bt.set_valign(Align::Center); - dl_bt.set_vexpand(true); - dl_bt.get_style_context().map(|c| c.add_class("suggested-action")); + rc!(card); + if let Some(dl) = model.download_status() { + match dl.status { + DlStatus::Done => { + let open_bt = Button::new_with_label("Play"); + open_bt.set_valign(Align::Center); + open_bt.set_vexpand(true); + open_bt.get_style_context().map(|c| c.add_class("suggested-action")); - rc!(dl_bt, card); - { - clone!(dl_bt, card); - wait!({ // Fetch the list of files to download - let (tx, rx) = channel(); - thread::spawn(move || { - let dl_list = model.downloads(); - tx.send(dl_list).unwrap(); - }); - rx - } => | const dl_list | { - let dl_bt = dl_bt.borrow(); - println!("DLs: {:?}", dl_list); - if dl_list.is_empty() { // Nothing to download - dl_bt.set_label("Not available"); - dl_bt.set_sensitive(false); - } else { - clone!(dl_list); - dl_bt.connect_clicked(move |_| { - for dl in dl_list.clone() { - thread::spawn(move || { - 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.clone().parent().unwrap()).unwrap(); - let mut out = dl.output.clone(); - out.set_extension(ext); - let mut file = fs::File::create(out).unwrap(); - - println!("saving {} in {:?}", dl.url.clone(), dl.output.clone()); - res.copy_to(&mut file).unwrap(); - println!("saved {:?}", dl.output); - }); - } + let out = dl.output.clone(); + open_bt.connect_clicked(move |_| { + open::that(out.clone()).unwrap(); + println!("opened file"); }); - } + card.borrow().attach(&open_bt, 3, 0, 1, 2); - if dl_list.len() > 1 { // Not only one song - let more_bt = Button::new_with_label("Details"); - more_bt.set_valign(Align::Center); - more_bt.set_vexpand(true); - card.borrow().attach(&more_bt, 2, 0, 1, 2); + let open_bt = Button::new_with_label("View File"); + open_bt.set_valign(Align::Center); + open_bt.set_vexpand(true); + + let out = dl.output.clone(); + open_bt.connect_clicked(move |_| { + open::that(out.parent().unwrap().clone()).unwrap(); + println!("opened folder"); + }); + card.borrow().attach(&open_bt, 2, 0, 1, 2); + }, + DlStatus::Planned | DlStatus::Started => { + let cancel_bt = Button::new_with_label("Cancel"); + cancel_bt.set_valign(Align::Center); + cancel_bt.set_vexpand(true); + cancel_bt.get_style_context().map(|c| c.add_class("destructive-action")); + + let track_id = dl.track.id; + cancel_bt.connect_clicked(move |_| { + let mut dls = crate::DOWNLOADS.lock().unwrap(); + let mut dl = dls.get_mut(&track_id).unwrap(); + dl.status = DlStatus::Cancelled; + println!("Cancelled"); + }); + card.borrow().attach(&cancel_bt, 3, 0, 1, 2); + + if dl.status == DlStatus::Planned { + sub_text.set_text(format!("{} — Waiting to download", model.subtext()).as_ref()); + } else { + sub_text.set_text(format!("{} — Download in progress", model.subtext()).as_ref()); + } } - }); + DlStatus::Cancelled => { + sub_text.set_text(format!("{} — Cancelled", model.subtext()).as_ref()); + } + } + } else { + let dl_bt = Button::new_with_label("Download"); + dl_bt.set_valign(Align::Center); + dl_bt.set_vexpand(true); + dl_bt.get_style_context().map(|c| c.add_class("suggested-action")); + + rc!(dl_bt); + { + clone!(dl_bt, card); + wait!({ // Fetch the list of files to download + let (tx, rx) = channel(); + thread::spawn(move || { + let dl_list = model.downloads(); + tx.send(dl_list).unwrap(); + }); + rx + } => | const dl_list | { + let dl_bt = dl_bt.borrow(); + if dl_list.is_empty() { // Nothing to download + dl_bt.set_label("Not available"); + dl_bt.set_sensitive(false); + } else { + clone!(dl_list); + dl_bt.connect_clicked(move |_| { + for dl in dl_list.clone() { + let mut dls = crate::DOWNLOADS.lock().unwrap(); + dls.insert(dl.track.id, dl.clone()); + + crate::DL_JOBS.execute(dl); + } + }); + } + + if dl_list.len() > 1 { // Not only one song + let more_bt = Button::new_with_label("Details"); + more_bt.set_valign(Align::Center); + more_bt.set_vexpand(true); + card.borrow().attach(&more_bt, 2, 0, 1, 2); + } + }); + } + + card.borrow().attach(&*dl_bt.borrow(), 3, 0, 1, 2); } { let card = card.borrow(); card.attach(&main_text, 1, 0, 1, 1); card.attach(&sub_text, 1, 1, 1, 1); - card.attach(&*dl_bt.borrow(), 3, 0, 1, 2); } card @@ -96,6 +137,10 @@ pub trait CardModel: Clone + Send + Sync { } fn downloads(&self) -> Vec; + + fn download_status(&self) -> Option { + None + } } impl CardModel for api::Artist { @@ -120,14 +165,15 @@ impl CardModel for api::Artist { .send().unwrap() .json().unwrap(); - for track in album.tracks.unwrap_or_default() { + for track in album.clone().tracks.unwrap_or_default() { dls.push(Download { url: track.listen_url.clone(), output: dirs::audio_dir().unwrap() .join(self.name.clone()) .join(album.title.clone()) .join(format!("{}.mp3", track.title.clone())), - done: false, + status: DlStatus::Planned, + track: track.clone().into_full(&album), }); } } @@ -156,7 +202,8 @@ impl CardModel for api::Album { .join(self.artist.name.clone()) .join(self.title.clone()) .join(format!("{}.mp3", track.title.clone())), - done: false, + status: DlStatus::Planned, + track: track.clone().into_full(&self), } ).collect() } @@ -182,7 +229,12 @@ impl CardModel for api::Track { .join(self.artist.name.clone()) .join(self.album.title.clone()) .join(format!("{}.mp3", self.title.clone())), - done: false, + status: DlStatus::Planned, + track: self.clone(), }] } + + fn download_status(&self) -> Option { + crate::DOWNLOADS.lock().ok()?.get(&self.id).map(|x| x.clone()) + } } diff --git a/src/ui/dl_list.rs b/src/ui/dl_list.rs new file mode 100644 index 0000000..f791c11 --- /dev/null +++ b/src/ui/dl_list.rs @@ -0,0 +1,37 @@ +use gtk::{prelude::*, *}; +use std::{cell::RefCell, rc::Rc}; +use crate::{ui::card}; + +pub fn render() -> Rc> { + let cont = gtk::Box::new(Orientation::Vertical, 12); + cont.set_valign(Align::Start); + cont.set_margin_top(48); + cont.set_margin_bottom(48); + cont.set_margin_start(96); + cont.set_margin_end(96); + + let active = crate::DL_JOBS.active_count(); + rc!(cont, active); + gtk::idle_add(clone!(cont => move || { + let active_now = crate::DL_JOBS.active_count(); + if active_now != *active.borrow() { + *active.borrow_mut() = active_now; + + let cont = cont.borrow(); + for ch in cont.get_children() { + cont.remove(&ch); + } + + let dl_list = { + crate::DOWNLOADS.lock().unwrap().clone() + }; + for (_, dl) in dl_list { + cont.add(&*card::render(dl.track).borrow()); + } + cont.show_all(); + } + glib::Continue(true) + })); + cont.borrow().show_all(); + cont +} diff --git a/src/ui/login_page.rs b/src/ui/login_page.rs index 92001ee..77ecc89 100644 --- a/src/ui/login_page.rs +++ b/src/ui/login_page.rs @@ -34,15 +34,28 @@ pub fn render(state: State) -> gtk::Box { username: widgets.borrow().1.get_text().clone().unwrap(), password: widgets.borrow().2.get_text().clone().unwrap(), })) => |res| { - let res: Result<_, _> = res.json::>().unwrap().into(); + let res: Result = res.json(); - if let Some(ref mut client) = *crate::api::API.lock().unwrap() { - client.auth(res.unwrap().token.clone()); + match res { + Err(_) => crate::show_error(state.clone(), "Somehting went wrong, check your username and password, and the URL of your instance."), + Ok(res) => { + if let Some(ref mut client) = *crate::api::API.lock().unwrap() { + client.auth(res.token.clone()); + } + + let state = state.borrow(); + state.error.set_revealed(false); + state.stack.add_titled(&crate::ui::main_page::render(&state.header, &{ + let s = StackSwitcher::new(); + s.set_stack(&state.stack); + s + }), "main", "Search Music"); + state.stack.set_visible_child_name("main"); + state.stack.add_titled(&*crate::ui::dl_list::render().borrow(), "downloads", "Downloads"); + state.stack.remove(&state.stack.get_child_by_name("login").unwrap()); // To avoid having a "Login" tab in the header + state.stack.show_all(); + } } - - state.borrow_mut().stack.add_named(&crate::ui::main_page::render(), "main"); - state.borrow_mut().stack.set_visible_child_name("main"); - state.borrow_mut().stack.show_all(); }); })); diff --git a/src/ui/main_page.rs b/src/ui/main_page.rs index d1f47d9..8133b08 100644 --- a/src/ui/main_page.rs +++ b/src/ui/main_page.rs @@ -6,7 +6,7 @@ use std::{ }; use crate::{api::{self, execute}, ui::{title, card}}; -pub fn render() -> gtk::Box { +pub fn render(header: &HeaderBar, switcher: &StackSwitcher) -> gtk::Box { let cont = gtk::Box::new(Orientation::Vertical, 12); cont.set_margin_top(48); cont.set_margin_bottom(48); @@ -16,12 +16,12 @@ pub fn render() -> gtk::Box { let avatar_path = dirs::cache_dir().unwrap().join("funkload").join("avatar.png"); let avatar = DrawingArea::new(); - avatar.set_size_request(128, 128); + avatar.set_size_request(32, 32); avatar.set_halign(Align::Center); avatar.connect_draw(clone!(avatar_path => move |da, g| { // More or less stolen from Fractal (https://gitlab.gnome.org/GNOME/fractal/blob/master/fractal-gtk/src/widgets/avatar.rs) use std::f64::consts::PI; - let width = 128.0f64; - let height = 128.0f64; + let width = 32.0f64; + let height = 32.0f64; g.set_antialias(cairo::Antialias::Best); @@ -37,8 +37,8 @@ pub fn render() -> gtk::Box { ); g.clip(); - let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(avatar_path.clone(), 128, 128, true) - .unwrap_or_else(|_| IconTheme::get_default().unwrap().load_icon("avatar-default", 128, IconLookupFlags::empty()).unwrap().unwrap()); + let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(avatar_path.clone(), 32, 32, true) + .unwrap_or_else(|_| IconTheme::get_default().unwrap().load_icon("avatar-default", 32, IconLookupFlags::empty()).unwrap().unwrap()); let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0; g.set_source_pixbuf(&pb, 0.0, hpos); @@ -48,10 +48,9 @@ pub fn render() -> gtk::Box { Inhibit(false) })); - cont.add(&avatar); - let welcome = Label::new("Welcome."); - welcome.get_style_context().map(|c| c.add_class("h1")); - cont.add(&welcome); + header.pack_start(&avatar); + header.set_custom_title(&*switcher); + header.show_all(); let search = SearchEntry::new(); search.set_placeholder_text("Search"); @@ -61,12 +60,12 @@ pub fn render() -> gtk::Box { results.set_valign(Align::Start); cont.add(&results); - rc!(welcome, avatar, results); - clone!(welcome, avatar, results, avatar_path); + rc!(avatar, results); + clone!(avatar, results, avatar_path); wait!(execute(client!().get("/api/v1/users/users/me")) => |res| { let res: api::UserInfo = res.json().unwrap(); - welcome.borrow().set_text(format!("Welcome {}.", res.username).as_ref()); + avatar.borrow().set_tooltip_text(format!("Connected as {}.", res.username).as_ref()); clone!(avatar_path, avatar); wait!(execute(client!().get(&res.avatar.medium_square_crop.unwrap_or_default())) => |avatar_dl| { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index af5e172..49604bd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,7 @@ use gtk::prelude::*; pub mod card; +pub mod dl_list; pub mod login_page; pub mod main_page; pub mod network_image; diff --git a/xyz.gelez.mobydick.appdata.xml b/xyz.gelez.mobydick.appdata.xml new file mode 100644 index 0000000..eca16f0 --- /dev/null +++ b/xyz.gelez.mobydick.appdata.xml @@ -0,0 +1,17 @@ + + + xyz.gelez.mobydick + Mobydick + Download music from your Funkwhale instance + + + https://raw.githubusercontent.com/BaptisteGelez/mobydick/master/screen-main.png + + + https://raw.githubusercontent.com/BaptisteGelez/mobydick/master/screen-list.png + + + https://github.com/BaptisteGelez/mobydick/issues + CC0-1.0 + GPL-3.0 + \ No newline at end of file diff --git a/xyz.gelez.mobydick.desktop b/xyz.gelez.mobydick.desktop new file mode 100644 index 0000000..2b6c0d0 --- /dev/null +++ b/xyz.gelez.mobydick.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Mobydick +Comment=Download music from Funkwhale +Exec=xyz.gelez.mobydick %U +Icon=mobydick +Keywords=Music;Funkwhale; +Terminal=false +Type=Application +StartupNotify=true +Categories=GNOME;GTK;Network;Music;