diff --git a/src/api.rs b/src/api.rs index f480f16..8de5370 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,171 +1,170 @@ -use serde_derive::*; use lazy_static::*; -use workerpool::Worker; -use std::{ - sync::{ - Arc, - mpsc::{channel, Receiver}, - Mutex, - }, +use serde_derive::*; +use std::sync::{ + mpsc::{channel, Receiver}, + Arc, Mutex, }; +use workerpool::Worker; lazy_static! { - pub static ref API: Arc>> - = Arc::new(Mutex::new(None)); - - static ref JOBS: workerpool::Pool - = workerpool::Pool::new(5); + pub static ref API: Arc>> = Arc::new(Mutex::new(None)); + static ref JOBS: workerpool::Pool = workerpool::Pool::new(5); } pub fn execute(req: reqwest::RequestBuilder) -> Receiver { - let (tx, rx) = channel(); - JOBS.execute_to(tx, req); - rx + let (tx, rx) = channel(); + JOBS.execute_to(tx, req); + rx } pub struct RequestContext { - token: String, - instance: String, - client: reqwest::Client, + token: String, + instance: String, + client: reqwest::Client, } impl RequestContext { - pub fn new(instance: String) -> Self { - RequestContext { - token: String::new(), - instance: instance.clone(), - client: reqwest::Client::new() - } - } + pub fn new(instance: String) -> Self { + RequestContext { + token: String::new(), + instance, + client: reqwest::Client::new(), + } + } - pub fn auth(&mut self, token: String) { - self.token = token.clone(); - } + pub fn auth(&mut self, token: String) { + self.token = token; + } - pub fn get>(&self, url: S) -> reqwest::RequestBuilder { - self.client - .get(&format!("{}{}", self.instance, url.as_ref())) - .header(reqwest::header::AUTHORIZATION, format!("JWT {}", self.token)) - } + pub fn get>(&self, url: S) -> reqwest::RequestBuilder { + self + .client + .get(&format!("{}{}", self.instance, url.as_ref())) + .header( + reqwest::header::AUTHORIZATION, + format!("JWT {}", self.token), + ) + } - /// Warning: no authentication, since it is only used for login - pub fn post>(&self, url: S) -> reqwest::RequestBuilder { - self.client - .post(&format!("{}{}", self.instance, url.as_ref())) - } + /// Warning: no authentication, since it is only used for login + pub fn post>(&self, url: S) -> reqwest::RequestBuilder { + self + .client + .post(&format!("{}{}", self.instance, url.as_ref())) + } - pub fn to_json(&self) -> serde_json::Value { - serde_json::json!({ - "token": self.token, - "instance": self.instance, - }) - } + pub fn to_json(&self) -> serde_json::Value { + serde_json::json!({ + "token": self.token, + "instance": self.instance, + }) + } } #[derive(Default)] pub struct Req; impl Worker for Req { - type Input = reqwest::RequestBuilder; - type Output = reqwest::Response; + type Input = reqwest::RequestBuilder; + type Output = reqwest::Response; - fn execute(&mut self, req: Self::Input) -> Self::Output { - req.send().expect("Error while sending request") - } + fn execute(&mut self, req: Self::Input) -> Self::Output { + req.send().expect("Error while sending request") + } } #[derive(Deserialize, Serialize)] pub struct LoginData { - pub password: String, - pub username: String, + pub password: String, + pub username: String, } #[derive(Deserialize, Serialize)] pub struct LoginInfo { - pub token: String + pub token: String, } #[derive(Deserialize, Serialize)] pub struct UserInfo { - pub username: String, - pub avatar: Image, + pub username: String, + pub avatar: Image, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Image { - pub medium_square_crop: Option, - pub small_square_crop: Option, - pub original: Option, - pub square_crop: Option, + pub medium_square_crop: Option, + pub small_square_crop: Option, + pub original: Option, + pub square_crop: Option, } #[derive(Deserialize, Serialize)] pub struct SearchQuery { - pub query: String, + pub query: String, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct SearchResult { - pub artists: Vec, - pub albums: Vec, - pub tracks: Vec, + pub artists: Vec, + pub albums: Vec, + pub tracks: Vec, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Artist { - pub name: String, - pub albums: Option>, + pub name: String, + pub albums: Option>, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Album { - pub title: String, - pub artist: ArtistPreview, - pub tracks: Option>, - pub cover: Image, - pub id: i32, + pub title: String, + pub artist: ArtistPreview, + pub tracks: Option>, + pub cover: Image, + pub id: i32, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ArtistAlbum { - pub title: String, - pub tracks_count: i32, - pub id: i32, - pub cover: Image, + pub title: String, + pub tracks_count: i32, + pub id: i32, + pub cover: Image, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Track { - pub id: i32, - pub title: String, - pub album: Album, - pub artist: ArtistPreview, - pub listen_url: String, + pub id: i32, + pub title: String, + pub album: Album, + pub artist: ArtistPreview, + pub listen_url: String, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ArtistPreview { - pub name: String, + pub name: String, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct AlbumTrack { - pub id: i32, - pub title: String, - pub artist: ArtistPreview, - pub listen_url: String, + pub id: i32, + pub title: String, + pub artist: ArtistPreview, + 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, - } - } + pub fn into_full(self, album: &Album) -> Track { + let mut album = album.clone(); + album.tracks = None; + Track { + 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 ddae0a0..d7d6a56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ use gtk::{self, prelude::*, *}; use std::{ - collections::HashMap, - cell::RefCell, - rc::Rc, - sync::{Arc, Mutex}, - fs, - path::PathBuf, + cell::RefCell, + collections::HashMap, + fs, + path::PathBuf, + rc::Rc, + sync::{Arc, Mutex}, }; macro_rules! clone { @@ -35,34 +35,32 @@ macro_rules! rc { } 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) - }, - } - }) - } + ($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()) + () => { + crate::api::API.lock().unwrap().as_ref().unwrap() + }; } mod api; @@ -70,194 +68,230 @@ mod ui; #[derive(Debug)] pub struct AppState { - window: Rc>, - stack: Stack, - error: InfoBar, - header: HeaderBar, + window: Rc>, + stack: Stack, + error: InfoBar, + header: HeaderBar, } pub type State = Rc>; #[derive(Debug, Clone, PartialEq)] pub enum DlStatus { - Planned, - Started, - Done, - Cancelled, + Planned, + Started, + Done, + Cancelled, } #[derive(Debug, Clone)] pub struct Download { - url: String, - status: DlStatus, - output: PathBuf, - track: api::Track, + 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; - } + 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 DOWNLOADS: Arc>> = Arc::new(Mutex::new(HashMap::new())); - static ref DL_JOBS: workerpool::Pool = workerpool::Pool::new(5); + static ref DL_JOBS: workerpool::Pool = workerpool::Pool::new(5); } #[derive(Default)] struct TrackDl; impl workerpool::Worker for TrackDl { - type Input = Download; - type Output = (); + type Input = Download; + type Output = (); - fn execute(&mut self, dl: Self::Input) -> Self::Output { - if dl.status == DlStatus::Cancelled { - return; - } + 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 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 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"); + 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(); + 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(); + 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); - } - } + 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; - } + 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); + 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(); + 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(); + 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) - }); + Inhibit(false) + }); - init(Rc::new(RefCell::new(window))); + init(Rc::new(RefCell::new(window))); - gtk::main(); + 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); + 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(); + 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 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 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(); + 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"); - } + 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); + 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) -} \ No newline at end of file + 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) +} diff --git a/src/ui/card.rs b/src/ui/card.rs index c69cb2a..2bf2a50 100644 --- a/src/ui/card.rs +++ b/src/ui/card.rs @@ -1,240 +1,270 @@ +use crate::{api, ui::network_image::NetworkImage, DlStatus, Download}; use gtk::*; -use std::{thread, sync::mpsc::channel, rc::Rc, cell::RefCell}; -use crate::{Download, DlStatus, api, ui::network_image::NetworkImage}; +use std::{cell::RefCell, rc::Rc, sync::mpsc::channel, thread}; -pub fn render(model: T) -> Rc> where T: CardModel + 'static { - let card = Grid::new(); - card.set_column_spacing(12); - card.set_valign(Align::Start); +pub fn render(model: T) -> Rc> +where + T: CardModel + 'static, +{ + let card = Grid::new(); + card.set_column_spacing(12); + card.set_valign(Align::Start); - if let Some(url) = model.image_url() { - let img = NetworkImage::new(url); - card.attach(&*img.img.borrow(), 0, 0, 1, 2); - } + if let Some(url) = model.image_url() { + let img = NetworkImage::new(url); + card.attach(&*img.img.borrow(), 0, 0, 1, 2); + } - let main_text = Label::new(model.text().as_ref()); - main_text.get_style_context().map(|c| c.add_class("h3")); - main_text.set_hexpand(true); - main_text.set_halign(Align::Start); - let sub_text = Label::new(model.subtext().as_ref()); - sub_text.get_style_context().map(|c| c.add_class("dim-label")); - sub_text.set_hexpand(true); - sub_text.set_halign(Align::Start); + let main_text = Label::new(model.text().as_ref()); + if let Some(c) = main_text.get_style_context() { + c.add_class("h3") + } + main_text.set_hexpand(true); + main_text.set_halign(Align::Start); + let sub_text = Label::new(model.subtext().as_ref()); + if let Some(c) = sub_text.get_style_context() { + c.add_class("dim-label") + } + sub_text.set_hexpand(true); + sub_text.set_halign(Align::Start); - 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!(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); + if let Some(c) = open_bt.get_style_context() { + c.add_class("suggested-action") + } - 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); + 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); - let open_bt = Button::new_with_label("View File"); - open_bt.set_valign(Align::Center); - open_bt.set_vexpand(true); + 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 out = dl.output; + open_bt.connect_clicked(move |_| { + open::that(&(*out.parent().unwrap())).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); + if let Some(c) = cancel_bt.get_style_context() { + 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); + 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")); + 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); + if let Some(c) = dl_bt.get_style_context() { + 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()); + 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); - } - }); - } + 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); - } - }); - } + 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); - } + 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); - } + { + let card = card.borrow(); + card.attach(&main_text, 1, 0, 1, 1); + card.attach(&sub_text, 1, 1, 1, 1); + } - card + card } pub trait CardModel: Clone + Send + Sync { - fn text(&self) -> String; - fn subtext(&self) -> String { - String::new() - } - fn image_url(&self) -> Option { - None - } + fn text(&self) -> String; + fn subtext(&self) -> String { + String::new() + } + fn image_url(&self) -> Option { + None + } - fn downloads(&self) -> Vec; + fn downloads(&self) -> Vec; - fn download_status(&self) -> Option { - None - } + fn download_status(&self) -> Option { + None + } } impl CardModel for api::Artist { - fn text(&self) -> String { - self.name.clone() - } + fn text(&self) -> String { + self.name.clone() + } - fn subtext(&self) -> String { - format!("{} albums", self.albums.clone().unwrap().len()) - } + fn subtext(&self) -> String { + format!("{} albums", self.albums.clone().unwrap().len()) + } - fn image_url(&self) -> Option { - self.albums.clone()?.iter() - .next() - .and_then(|album| album.cover.medium_square_crop.clone()) - } + fn image_url(&self) -> Option { + self + .albums + .clone()? + .iter() + .next() + .and_then(|album| album.cover.medium_square_crop.clone()) + } - fn downloads(&self) -> Vec { - let mut dls = vec![]; - for album in self.albums.clone().unwrap_or_default() { - let album: api::Album = client!().get(&format!("/api/v1/albums/{}/", album.id)) - .send().unwrap() - .json().unwrap(); + fn downloads(&self) -> Vec { + let mut dls = vec![]; + for album in self.albums.clone().unwrap_or_default() { + let album: api::Album = client!() + .get(&format!("/api/v1/albums/{}/", album.id)) + .send() + .unwrap() + .json() + .unwrap(); - 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())), - status: DlStatus::Planned, - track: track.clone().into_full(&album), - }); - } - } - dls - } + 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())), + status: DlStatus::Planned, + track: track.clone().into_full(&album), + }); + } + } + dls + } } impl CardModel for api::Album { - fn text(&self) -> String { - self.title.clone() - } + fn text(&self) -> String { + self.title.clone() + } - fn subtext(&self) -> String { - format!("{} tracks, by {}", self.tracks.clone().map(|t| t.len()).unwrap_or_default(), self.artist.name) - } + fn subtext(&self) -> String { + format!( + "{} tracks, by {}", + self.tracks.clone().map(|t| t.len()).unwrap_or_default(), + self.artist.name + ) + } - fn image_url(&self) -> Option { - self.cover.medium_square_crop.clone() - } + fn image_url(&self) -> Option { + self.cover.medium_square_crop.clone() + } - fn downloads(&self) -> Vec { - self.tracks.clone().unwrap_or_default().iter().map(|track| - Download { - url: track.listen_url.clone(), - output: dirs::audio_dir().unwrap() - .join(self.artist.name.clone()) - .join(self.title.clone()) - .join(format!("{}.mp3", track.title.clone())), - status: DlStatus::Planned, - track: track.clone().into_full(&self), - } - ).collect() - } + fn downloads(&self) -> Vec { + self + .tracks + .clone() + .unwrap_or_default() + .iter() + .map(|track| Download { + url: track.listen_url.clone(), + output: dirs::audio_dir() + .unwrap() + .join(self.artist.name.clone()) + .join(self.title.clone()) + .join(format!("{}.mp3", track.title.clone())), + status: DlStatus::Planned, + track: track.clone().into_full(&self), + }) + .collect() + } } impl CardModel for api::Track { - fn text(&self) -> String { - self.title.clone() - } + fn text(&self) -> String { + self.title.clone() + } - fn subtext(&self) -> String { - format!("By {}, in {}", self.artist.name, self.album.title) - } + fn subtext(&self) -> String { + format!("By {}, in {}", self.artist.name, self.album.title) + } - fn image_url(&self) -> Option { - self.album.cover.medium_square_crop.clone() - } + fn image_url(&self) -> Option { + self.album.cover.medium_square_crop.clone() + } - fn downloads(&self) -> Vec { - vec![Download { - url: self.listen_url.clone(), - output: dirs::audio_dir().unwrap() - .join(self.artist.name.clone()) - .join(self.album.title.clone()) - .join(format!("{}.mp3", self.title.clone())), - status: DlStatus::Planned, - track: self.clone(), - }] - } + fn downloads(&self) -> Vec { + vec![Download { + url: self.listen_url.clone(), + output: dirs::audio_dir() + .unwrap() + .join(self.artist.name.clone()) + .join(self.album.title.clone()) + .join(format!("{}.mp3", self.title.clone())), + status: DlStatus::Planned, + track: self.clone(), + }] + } - fn download_status(&self) -> Option { - crate::DOWNLOADS.lock().ok()?.get(&self.id).map(|x| x.clone()) - } + fn download_status(&self) -> Option { + crate::DOWNLOADS.lock().ok()?.get(&self.id).cloned() + } } diff --git a/src/ui/dl_list.rs b/src/ui/dl_list.rs index f791c11..00eb083 100644 --- a/src/ui/dl_list.rs +++ b/src/ui/dl_list.rs @@ -1,37 +1,37 @@ +use crate::ui::card; 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 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 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 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 + 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 0700e89..3e68bcc 100644 --- a/src/ui/login_page.rs +++ b/src/ui/login_page.rs @@ -1,29 +1,25 @@ +use crate::{api::*, ui::title, State}; use gtk::*; -use std::{ - rc::Rc, - cell::RefCell, -}; -use crate::{State, api::*, ui::title}; +use std::{cell::RefCell, rc::Rc}; pub fn render(state: State) -> gtk::Box { - let cont = gtk::Box::new(Orientation::Vertical, 24); - cont.set_halign(Align::Center); - cont.set_valign(Align::Center); - cont.set_size_request(300, -1); - let title = title("Login"); + let cont = gtk::Box::new(Orientation::Vertical, 24); + cont.set_halign(Align::Center); + cont.set_valign(Align::Center); + cont.set_size_request(300, -1); + let title = title("Login"); - let instance = Input::new("Instance URL") - .with_placeholder("demo.funkwhale.audio"); - let username = Input::new("Username"); - let password = Input::new_password("Password"); + let instance = Input::new("Instance URL").with_placeholder("demo.funkwhale.audio"); + let username = Input::new("Username"); + let password = Input::new_password("Password"); - let login_bt = Button::new_with_label("Login"); - login_bt.get_style_context().map(|c| c.add_class("suggested-action")); - login_bt.set_margin_bottom(48); - let widgets = Rc::new(RefCell::new(( - instance, username, password - ))); - login_bt.connect_clicked(clone!(state, widgets => move |_| { + let login_bt = Button::new_with_label("Login"); + if let Some(c) = login_bt.get_style_context() { + c.add_class("suggested-action") + } + login_bt.set_margin_bottom(48); + let widgets = Rc::new(RefCell::new((instance, username, password))); + login_bt.connect_clicked(clone!(state, widgets => move |_| { let mut api_ctx = crate::api::API.lock().unwrap(); let mut instance_url = widgets.borrow().0.get_text().unwrap().trim_end_matches('/').to_string(); if !(instance_url.starts_with("http://") || instance_url.starts_with("https://")) { @@ -35,8 +31,8 @@ pub fn render(state: State) -> gtk::Box { let state = state.clone(); wait!(execute(api_ctx.as_ref().unwrap().post("/api/v1/token/").json(&LoginData { - username: widgets.borrow().1.get_text().clone().unwrap(), - password: widgets.borrow().2.get_text().clone().unwrap(), + username: widgets.borrow().1.get_text().unwrap(), + password: widgets.borrow().2.get_text().unwrap(), })) => |res| { let res: Result = res.json(); @@ -44,7 +40,7 @@ pub fn render(state: State) -> gtk::Box { 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()); + client.auth(res.token); } let state = state.borrow(); @@ -68,66 +64,70 @@ pub fn render(state: State) -> gtk::Box { }); })); - { + { + let (ref instance, ref username, ref password) = *widgets.borrow(); + cont.add(&title); + cont.add(&instance.render()); + cont.add(&username.render()); + cont.add(&password.render()); + cont.add(&login_bt); + } - let (ref instance, ref username, ref password) = *widgets.borrow(); - cont.add(&title); - cont.add(&instance.render()); - cont.add(&username.render()); - cont.add(&password.render()); - cont.add(&login_bt); - } + widgets + .borrow() + .0 + .entry + .connect_activate(clone!(widgets => move |_| { + widgets.borrow().1.entry.grab_focus(); + })); + widgets + .borrow() + .1 + .entry + .connect_activate(clone!(widgets => move |_| { + widgets.borrow().2.entry.grab_focus(); + })); + widgets.borrow().2.entry.connect_activate(move |_| { + login_bt.clicked(); + }); - widgets.borrow().0.entry.connect_activate(clone!(widgets => move |_| { - widgets.borrow().1.entry.grab_focus(); - })); - widgets.borrow().1.entry.connect_activate(clone!(widgets => move |_| { - widgets.borrow().2.entry.grab_focus(); - })); - widgets.borrow().2.entry.connect_activate(move |_| { - login_bt.clicked(); - }); - - cont.show_all(); - cont + cont.show_all(); + cont } struct Input<'a> { - label: &'a str, - entry: gtk::Entry, + label: &'a str, + entry: gtk::Entry, } impl<'a> Input<'a> { - fn new(text: &'a str) -> Input { - let entry = gtk::Entry::new(); - Input { - label: text, - entry - } - } + fn new(text: &'a str) -> Input { + let entry = gtk::Entry::new(); + Input { label: text, entry } + } - fn new_password(text: &'a str) -> Input { - let input = Input::new(text); - input.entry.set_visibility(false); - input - } + fn new_password(text: &'a str) -> Input { + let input = Input::new(text); + input.entry.set_visibility(false); + input + } - fn with_placeholder(self, ph: &'a str) -> Input { - self.entry.set_placeholder_text(ph); - self - } + fn with_placeholder(self, ph: &'a str) -> Input { + self.entry.set_placeholder_text(ph); + self + } - fn get_text(&self) -> Option { - self.entry.get_text() - } + fn get_text(&self) -> Option { + self.entry.get_text() + } - fn render(&self) -> gtk::Box { - let label = gtk::Label::new(self.label); - label.set_halign(Align::Start); + fn render(&self) -> gtk::Box { + let label = gtk::Label::new(self.label); + label.set_halign(Align::Start); - let cont = gtk::Box::new(gtk::Orientation::Vertical, 6); - cont.add(&label); - cont.add(&self.entry); - cont - } + let cont = gtk::Box::new(gtk::Orientation::Vertical, 6); + cont.add(&label); + cont.add(&self.entry); + cont + } } diff --git a/src/ui/main_page.rs b/src/ui/main_page.rs index 73cd25c..e55c2ab 100644 --- a/src/ui/main_page.rs +++ b/src/ui/main_page.rs @@ -1,26 +1,32 @@ +use crate::{ + api::{self, execute}, + ui::{card, title}, +}; use gdk::ContextExt; use gdk_pixbuf::PixbufExt; use gtk::*; -use std::{ - cell::RefCell, - fs, - rc::Rc, -}; -use crate::{api::{self, execute}, ui::{title, card}}; +use std::{cell::RefCell, fs, rc::Rc}; -pub fn render(window: Rc>, header: &HeaderBar, switcher: &StackSwitcher) -> gtk::Box { - let cont = gtk::Box::new(Orientation::Vertical, 12); - cont.set_margin_top(48); - cont.set_margin_bottom(48); - cont.set_margin_start(96); - cont.set_margin_end(96); +pub fn render( + window: Rc>, + header: &HeaderBar, + switcher: &StackSwitcher, +) -> gtk::Box { + let cont = gtk::Box::new(Orientation::Vertical, 12); + cont.set_margin_top(48); + cont.set_margin_bottom(48); + cont.set_margin_start(96); + cont.set_margin_end(96); - let avatar_path = dirs::cache_dir().unwrap().join("mobydick").join("avatar.png"); + let avatar_path = dirs::cache_dir() + .unwrap() + .join("mobydick") + .join("avatar.png"); - let avatar = DrawingArea::new(); - 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) + let avatar = DrawingArea::new(); + 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 = 32.0f64; let height = 32.0f64; @@ -50,89 +56,88 @@ pub fn render(window: Rc>, header: &HeaderBar, switcher: &StackS Inhibit(false) })); - header.pack_start(&avatar); - header.set_custom_title(&*switcher); + header.pack_start(&avatar); + header.set_custom_title(&*switcher); - let logout_bt = Button::new_from_icon_name("system-log-out", IconSize::LargeToolbar.into()); - logout_bt.connect_clicked(clone!(window => move |_| { - crate::logout(window.clone()); - })); - header.pack_end(&logout_bt); - header.show_all(); + let logout_bt = Button::new_from_icon_name("system-log-out", IconSize::LargeToolbar.into()); + logout_bt.connect_clicked(clone!(window => move |_| { + crate::logout(window.clone()); + })); + header.pack_end(&logout_bt); + header.show_all(); - let search = SearchEntry::new(); - search.set_placeholder_text("Search"); - cont.add(&search); + let search = SearchEntry::new(); + search.set_placeholder_text("Search"); + cont.add(&search); - let results = gtk::Box::new(Orientation::Vertical, 12); - results.set_valign(Align::Start); - cont.add(&results); + let results = gtk::Box::new(Orientation::Vertical, 12); + results.set_valign(Align::Start); + cont.add(&results); - rc!(avatar, results); - clone!(avatar, results, avatar_path); - wait!(execute(client!().get("/api/v1/users/users/me")) => |res| { - let res: Result = res.json(); - match res { - Ok(res) => { - avatar.borrow().set_tooltip_text(format!("Connected as {}.", res.username).as_ref()); + rc!(avatar, results); + clone!(avatar, results, avatar_path); + wait!(execute(client!().get("/api/v1/users/users/me")) => |res| { + let res: Result = res.json(); + match res { + Ok(res) => { + 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| { - fs::create_dir_all(avatar_path.parent().unwrap()).unwrap(); - let mut avatar_file = fs::File::create(avatar_path.clone()).unwrap(); - avatar_dl.copy_to(&mut avatar_file).unwrap(); - avatar.borrow().queue_draw(); - }); - }, - Err(_) => { - crate::logout(window.clone()); - } - } - }); + clone!(avatar_path, avatar); + wait!(execute(client!().get(&res.avatar.medium_square_crop.unwrap_or_default())) => |avatar_dl| { + fs::create_dir_all(avatar_path.parent().unwrap()).unwrap(); + let mut avatar_file = fs::File::create(avatar_path.clone()).unwrap(); + avatar_dl.copy_to(&mut avatar_file).unwrap(); + avatar.borrow().queue_draw(); + }); + }, + Err(_) => { + crate::logout(window.clone()); + } + } + }); - search.connect_activate(move |s| { - let results = results.clone(); - wait!(execute(client!().get("/api/v1/search").query(&api::SearchQuery { - query: s.get_text().unwrap_or_default() - })) => |res| { - update_results(res.json().unwrap(), &results.borrow()); - }); - }); + search.connect_activate(move |s| { + let results = results.clone(); + wait!(execute(client!().get("/api/v1/search").query(&api::SearchQuery { + query: s.get_text().unwrap_or_default() + })) => |res| { + update_results(res.json().unwrap(), &results.borrow()); + }); + }); - cont.show_all(); - cont + cont.show_all(); + cont } fn update_results(res: api::SearchResult, cont: >k::Box) { - for ch in cont.get_children() { - cont.remove(&ch); - } + for ch in cont.get_children() { + cont.remove(&ch); + } - if res.artists.is_empty() && res.albums.is_empty() && res.tracks.is_empty() { - cont.add(&Label::new("No results. Try something else.")); - } + if res.artists.is_empty() && res.albums.is_empty() && res.tracks.is_empty() { + cont.add(&Label::new("No results. Try something else.")); + } - if !res.artists.is_empty() { - cont.add(&title("Artists")); - for artist in res.artists.clone() { - cont.add(&*card::render(artist).borrow()); - } - } + if !res.artists.is_empty() { + cont.add(&title("Artists")); + for artist in res.artists.clone() { + cont.add(&*card::render(artist).borrow()); + } + } - if !res.albums.is_empty() { - cont.add(&title("Albums")); - for album in res.albums.clone() { - cont.add(&*card::render(album).borrow()); - } - } + if !res.albums.is_empty() { + cont.add(&title("Albums")); + for album in res.albums.clone() { + cont.add(&*card::render(album).borrow()); + } + } - if !res.tracks.is_empty() { - cont.add(&title("Songs")); - for track in res.tracks.clone() { - cont.add(&*card::render(track).borrow()); - } - } + if !res.tracks.is_empty() { + cont.add(&title("Songs")); + for track in res.tracks.clone() { + cont.add(&*card::render(track).borrow()); + } + } - cont.show_all(); + cont.show_all(); } - diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 49604bd..ef8284b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,7 +7,9 @@ pub mod main_page; pub mod network_image; fn title(text: &str) -> gtk::Label { - let lbl = gtk::Label::new(text); - lbl.get_style_context().map(|c| c.add_class("h2")); - lbl + let lbl = gtk::Label::new(text); + if let Some(c) = lbl.get_style_context() { + c.add_class("h2") + } + lbl } diff --git a/src/ui/network_image.rs b/src/ui/network_image.rs index 593a52d..c5f61f5 100644 --- a/src/ui/network_image.rs +++ b/src/ui/network_image.rs @@ -1,39 +1,37 @@ -use gtk::*; -use std::{ - cell::RefCell, - fs, rc::Rc, -}; use crate::api::execute; +use gtk::{Image, ImageExt}; +use std::{cell::RefCell, fs, rc::Rc}; pub struct NetworkImage { - pub img: Rc>, + pub img: Rc>, } impl NetworkImage { - pub fn new(url: String) -> NetworkImage { - let image = Image::new_from_icon_name("image-loading", 4); - rc!(image); + pub fn new(url: String) -> NetworkImage { + let image = Image::new_from_icon_name("image-loading", 4); + rc!(image); - let dest_file = url.split("/media/").last().unwrap().replace('/', "-"); - let dest = dirs::cache_dir().unwrap().join(env!("CARGO_PKG_NAME")).join(dest_file.to_string()); + let dest_file = url.split("/media/").last().unwrap().replace('/', "-"); + let dest = dirs::cache_dir() + .unwrap() + .join(env!("CARGO_PKG_NAME")) + .join(dest_file); - if dest.exists() { - let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest, 64, 64, true).unwrap(); - image.borrow().set_from_pixbuf(&pb); - } else { - clone!(image); - wait!(execute(client!().get(&url)) => |res| { - fs::create_dir_all(dest.parent().unwrap()).unwrap(); - let mut file = fs::File::create(dest.clone()).unwrap(); - res.copy_to(&mut file).unwrap(); + if dest.exists() { + let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest, 64, 64, true).unwrap(); + image.borrow().set_from_pixbuf(&pb); + } else { + clone!(image); + wait!(execute(client!().get(&url)) => |res| { + fs::create_dir_all(dest.parent().unwrap()).unwrap(); + let mut file = fs::File::create(dest.clone()).unwrap(); + res.copy_to(&mut file).unwrap(); - let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest.clone(), 64, 64, true).unwrap(); - image.borrow().set_from_pixbuf(&pb); - }); - } + let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest.clone(), 64, 64, true).unwrap(); + image.borrow().set_from_pixbuf(&pb); + }); + } - NetworkImage { - img: image.clone(), - } - } + NetworkImage { img: image } + } }