Fixed: Clippy & Formatting

This commit is contained in:
Floréal Toumikian 2020-08-28 16:06:31 +02:00
parent 286912dbf2
commit 3aadd2b2fe
8 changed files with 750 additions and 682 deletions

View File

@ -1,171 +1,170 @@
use serde_derive::*;
use lazy_static::*; use lazy_static::*;
use workerpool::Worker; use serde_derive::*;
use std::{ use std::sync::{
sync::{ mpsc::{channel, Receiver},
Arc, Arc, Mutex,
mpsc::{channel, Receiver},
Mutex,
},
}; };
use workerpool::Worker;
lazy_static! { lazy_static! {
pub static ref API: Arc<Mutex<Option<RequestContext>>> pub static ref API: Arc<Mutex<Option<RequestContext>>> = Arc::new(Mutex::new(None));
= Arc::new(Mutex::new(None)); static ref JOBS: workerpool::Pool<Req> = workerpool::Pool::new(5);
static ref JOBS: workerpool::Pool<Req>
= workerpool::Pool::new(5);
} }
pub fn execute(req: reqwest::RequestBuilder) -> Receiver<reqwest::Response> { pub fn execute(req: reqwest::RequestBuilder) -> Receiver<reqwest::Response> {
let (tx, rx) = channel(); let (tx, rx) = channel();
JOBS.execute_to(tx, req); JOBS.execute_to(tx, req);
rx rx
} }
pub struct RequestContext { pub struct RequestContext {
token: String, token: String,
instance: String, instance: String,
client: reqwest::Client, client: reqwest::Client,
} }
impl RequestContext { impl RequestContext {
pub fn new(instance: String) -> Self { pub fn new(instance: String) -> Self {
RequestContext { RequestContext {
token: String::new(), token: String::new(),
instance: instance.clone(), instance,
client: reqwest::Client::new() client: reqwest::Client::new(),
} }
} }
pub fn auth(&mut self, token: String) { pub fn auth(&mut self, token: String) {
self.token = token.clone(); self.token = token;
} }
pub fn get<S: AsRef<str>>(&self, url: S) -> reqwest::RequestBuilder { pub fn get<S: AsRef<str>>(&self, url: S) -> reqwest::RequestBuilder {
self.client self
.get(&format!("{}{}", self.instance, url.as_ref())) .client
.header(reqwest::header::AUTHORIZATION, format!("JWT {}", self.token)) .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 /// Warning: no authentication, since it is only used for login
pub fn post<S: AsRef<str>>(&self, url: S) -> reqwest::RequestBuilder { pub fn post<S: AsRef<str>>(&self, url: S) -> reqwest::RequestBuilder {
self.client self
.post(&format!("{}{}", self.instance, url.as_ref())) .client
} .post(&format!("{}{}", self.instance, url.as_ref()))
}
pub fn to_json(&self) -> serde_json::Value { pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({ serde_json::json!({
"token": self.token, "token": self.token,
"instance": self.instance, "instance": self.instance,
}) })
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct Req; pub struct Req;
impl Worker for Req { impl Worker for Req {
type Input = reqwest::RequestBuilder; type Input = reqwest::RequestBuilder;
type Output = reqwest::Response; type Output = reqwest::Response;
fn execute(&mut self, req: Self::Input) -> Self::Output { fn execute(&mut self, req: Self::Input) -> Self::Output {
req.send().expect("Error while sending request") req.send().expect("Error while sending request")
} }
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct LoginData { pub struct LoginData {
pub password: String, pub password: String,
pub username: String, pub username: String,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct LoginInfo { pub struct LoginInfo {
pub token: String pub token: String,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct UserInfo { pub struct UserInfo {
pub username: String, pub username: String,
pub avatar: Image, pub avatar: Image,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Image { pub struct Image {
pub medium_square_crop: Option<String>, pub medium_square_crop: Option<String>,
pub small_square_crop: Option<String>, pub small_square_crop: Option<String>,
pub original: Option<String>, pub original: Option<String>,
pub square_crop: Option<String>, pub square_crop: Option<String>,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct SearchQuery { pub struct SearchQuery {
pub query: String, pub query: String,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SearchResult { pub struct SearchResult {
pub artists: Vec<Artist>, pub artists: Vec<Artist>,
pub albums: Vec<Album>, pub albums: Vec<Album>,
pub tracks: Vec<Track>, pub tracks: Vec<Track>,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Artist { pub struct Artist {
pub name: String, pub name: String,
pub albums: Option<Vec<ArtistAlbum>>, pub albums: Option<Vec<ArtistAlbum>>,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Album { pub struct Album {
pub title: String, pub title: String,
pub artist: ArtistPreview, pub artist: ArtistPreview,
pub tracks: Option<Vec<AlbumTrack>>, pub tracks: Option<Vec<AlbumTrack>>,
pub cover: Image, pub cover: Image,
pub id: i32, pub id: i32,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ArtistAlbum { pub struct ArtistAlbum {
pub title: String, pub title: String,
pub tracks_count: i32, pub tracks_count: i32,
pub id: i32, pub id: i32,
pub cover: Image, pub cover: Image,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Track { pub struct Track {
pub id: i32, pub id: i32,
pub title: String, pub title: String,
pub album: Album, pub album: Album,
pub artist: ArtistPreview, pub artist: ArtistPreview,
pub listen_url: String, pub listen_url: String,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ArtistPreview { pub struct ArtistPreview {
pub name: String, pub name: String,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AlbumTrack { pub struct AlbumTrack {
pub id: i32, pub id: i32,
pub title: String, pub title: String,
pub artist: ArtistPreview, pub artist: ArtistPreview,
pub listen_url: String, pub listen_url: String,
} }
impl AlbumTrack { impl AlbumTrack {
pub fn into_full(self, album: &Album) -> Track { pub fn into_full(self, album: &Album) -> Track {
let mut album = album.clone(); let mut album = album.clone();
album.tracks = None; album.tracks = None;
Track { Track {
album: album, album,
id: self.id, id: self.id,
title: self.title, title: self.title,
artist: self.artist, artist: self.artist,
listen_url: self.listen_url, listen_url: self.listen_url,
} }
} }
} }

View File

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

View File

@ -1,240 +1,270 @@
use crate::{api, ui::network_image::NetworkImage, DlStatus, Download};
use gtk::*; use gtk::*;
use std::{thread, sync::mpsc::channel, rc::Rc, cell::RefCell}; use std::{cell::RefCell, rc::Rc, sync::mpsc::channel, thread};
use crate::{Download, DlStatus, api, ui::network_image::NetworkImage};
pub fn render<T>(model: T) -> Rc<RefCell<Grid>> where T: CardModel + 'static { pub fn render<T>(model: T) -> Rc<RefCell<Grid>>
let card = Grid::new(); where
card.set_column_spacing(12); T: CardModel + 'static,
card.set_valign(Align::Start); {
let card = Grid::new();
card.set_column_spacing(12);
card.set_valign(Align::Start);
if let Some(url) = model.image_url() { if let Some(url) = model.image_url() {
let img = NetworkImage::new(url); let img = NetworkImage::new(url);
card.attach(&*img.img.borrow(), 0, 0, 1, 2); card.attach(&*img.img.borrow(), 0, 0, 1, 2);
} }
let main_text = Label::new(model.text().as_ref()); let main_text = Label::new(model.text().as_ref());
main_text.get_style_context().map(|c| c.add_class("h3")); if let Some(c) = main_text.get_style_context() {
main_text.set_hexpand(true); c.add_class("h3")
main_text.set_halign(Align::Start); }
let sub_text = Label::new(model.subtext().as_ref()); main_text.set_hexpand(true);
sub_text.get_style_context().map(|c| c.add_class("dim-label")); main_text.set_halign(Align::Start);
sub_text.set_hexpand(true); let sub_text = Label::new(model.subtext().as_ref());
sub_text.set_halign(Align::Start); 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); rc!(card);
if let Some(dl) = model.download_status() { if let Some(dl) = model.download_status() {
match dl.status { match dl.status {
DlStatus::Done => { DlStatus::Done => {
let open_bt = Button::new_with_label("Play"); let open_bt = Button::new_with_label("Play");
open_bt.set_valign(Align::Center); open_bt.set_valign(Align::Center);
open_bt.set_vexpand(true); open_bt.set_vexpand(true);
open_bt.get_style_context().map(|c| c.add_class("suggested-action")); if let Some(c) = open_bt.get_style_context() {
c.add_class("suggested-action")
}
let out = dl.output.clone(); let out = dl.output.clone();
open_bt.connect_clicked(move |_| { open_bt.connect_clicked(move |_| {
open::that(out.clone()).unwrap(); open::that(out.clone()).unwrap();
println!("opened file"); println!("opened file");
}); });
card.borrow().attach(&open_bt, 3, 0, 1, 2); card.borrow().attach(&open_bt, 3, 0, 1, 2);
let open_bt = Button::new_with_label("View File"); let open_bt = Button::new_with_label("View File");
open_bt.set_valign(Align::Center); open_bt.set_valign(Align::Center);
open_bt.set_vexpand(true); open_bt.set_vexpand(true);
let out = dl.output.clone(); let out = dl.output;
open_bt.connect_clicked(move |_| { open_bt.connect_clicked(move |_| {
open::that(out.parent().unwrap().clone()).unwrap(); open::that(&(*out.parent().unwrap())).unwrap();
println!("opened folder"); println!("opened folder");
}); });
card.borrow().attach(&open_bt, 2, 0, 1, 2); card.borrow().attach(&open_bt, 2, 0, 1, 2);
}, }
DlStatus::Planned | DlStatus::Started => { DlStatus::Planned | DlStatus::Started => {
let cancel_bt = Button::new_with_label("Cancel"); let cancel_bt = Button::new_with_label("Cancel");
cancel_bt.set_valign(Align::Center); cancel_bt.set_valign(Align::Center);
cancel_bt.set_vexpand(true); cancel_bt.set_vexpand(true);
cancel_bt.get_style_context().map(|c| c.add_class("destructive-action")); if let Some(c) = cancel_bt.get_style_context() {
c.add_class("destructive-action")
}
let track_id = dl.track.id; let track_id = dl.track.id;
cancel_bt.connect_clicked(move |_| { cancel_bt.connect_clicked(move |_| {
let mut dls = crate::DOWNLOADS.lock().unwrap(); let mut dls = crate::DOWNLOADS.lock().unwrap();
let mut dl = dls.get_mut(&track_id).unwrap(); let mut dl = dls.get_mut(&track_id).unwrap();
dl.status = DlStatus::Cancelled; dl.status = DlStatus::Cancelled;
println!("Cancelled"); println!("Cancelled");
}); });
card.borrow().attach(&cancel_bt, 3, 0, 1, 2); card.borrow().attach(&cancel_bt, 3, 0, 1, 2);
if dl.status == DlStatus::Planned { if dl.status == DlStatus::Planned {
sub_text.set_text(format!("{} — Waiting to download", model.subtext()).as_ref()); sub_text.set_text(format!("{} — Waiting to download", model.subtext()).as_ref());
} else { } else {
sub_text.set_text(format!("{} — Download in progress", model.subtext()).as_ref()); sub_text.set_text(format!("{} — Download in progress", model.subtext()).as_ref());
} }
} }
DlStatus::Cancelled => { DlStatus::Cancelled => {
sub_text.set_text(format!("{} — Cancelled", model.subtext()).as_ref()); sub_text.set_text(format!("{} — Cancelled", model.subtext()).as_ref());
} }
} }
} else { } else {
let dl_bt = Button::new_with_label("Download"); let dl_bt = Button::new_with_label("Download");
dl_bt.set_valign(Align::Center); dl_bt.set_valign(Align::Center);
dl_bt.set_vexpand(true); dl_bt.set_vexpand(true);
dl_bt.get_style_context().map(|c| c.add_class("suggested-action")); if let Some(c) = dl_bt.get_style_context() {
c.add_class("suggested-action")
}
rc!(dl_bt); rc!(dl_bt);
{ {
clone!(dl_bt, card); clone!(dl_bt, card);
wait!({ // Fetch the list of files to download wait!({ // Fetch the list of files to download
let (tx, rx) = channel(); let (tx, rx) = channel();
thread::spawn(move || { thread::spawn(move || {
let dl_list = model.downloads(); let dl_list = model.downloads();
tx.send(dl_list).unwrap(); tx.send(dl_list).unwrap();
}); });
rx rx
} => | const dl_list | { } => | const dl_list | {
let dl_bt = dl_bt.borrow(); let dl_bt = dl_bt.borrow();
if dl_list.is_empty() { // Nothing to download if dl_list.is_empty() { // Nothing to download
dl_bt.set_label("Not available"); dl_bt.set_label("Not available");
dl_bt.set_sensitive(false); dl_bt.set_sensitive(false);
} else { } else {
clone!(dl_list); clone!(dl_list);
dl_bt.connect_clicked(move |_| { dl_bt.connect_clicked(move |_| {
for dl in dl_list.clone() { for dl in dl_list.clone() {
let mut dls = crate::DOWNLOADS.lock().unwrap(); let mut dls = crate::DOWNLOADS.lock().unwrap();
dls.insert(dl.track.id, dl.clone()); 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 if dl_list.len() > 1 { // Not only one song
let more_bt = Button::new_with_label("Details"); let more_bt = Button::new_with_label("Details");
more_bt.set_valign(Align::Center); more_bt.set_valign(Align::Center);
more_bt.set_vexpand(true); more_bt.set_vexpand(true);
card.borrow().attach(&more_bt, 2, 0, 1, 2); 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(); let card = card.borrow();
card.attach(&main_text, 1, 0, 1, 1); card.attach(&main_text, 1, 0, 1, 1);
card.attach(&sub_text, 1, 1, 1, 1); card.attach(&sub_text, 1, 1, 1, 1);
} }
card card
} }
pub trait CardModel: Clone + Send + Sync { pub trait CardModel: Clone + Send + Sync {
fn text(&self) -> String; fn text(&self) -> String;
fn subtext(&self) -> String { fn subtext(&self) -> String {
String::new() String::new()
} }
fn image_url(&self) -> Option<String> { fn image_url(&self) -> Option<String> {
None None
} }
fn downloads(&self) -> Vec<Download>; fn downloads(&self) -> Vec<Download>;
fn download_status(&self) -> Option<Download> { fn download_status(&self) -> Option<Download> {
None None
} }
} }
impl CardModel for api::Artist { impl CardModel for api::Artist {
fn text(&self) -> String { fn text(&self) -> String {
self.name.clone() self.name.clone()
} }
fn subtext(&self) -> String { fn subtext(&self) -> String {
format!("{} albums", self.albums.clone().unwrap().len()) format!("{} albums", self.albums.clone().unwrap().len())
} }
fn image_url(&self) -> Option<String> { fn image_url(&self) -> Option<String> {
self.albums.clone()?.iter() self
.next() .albums
.and_then(|album| album.cover.medium_square_crop.clone()) .clone()?
} .iter()
.next()
.and_then(|album| album.cover.medium_square_crop.clone())
}
fn downloads(&self) -> Vec<Download> { fn downloads(&self) -> Vec<Download> {
let mut dls = vec![]; let mut dls = vec![];
for album in self.albums.clone().unwrap_or_default() { for album in self.albums.clone().unwrap_or_default() {
let album: api::Album = client!().get(&format!("/api/v1/albums/{}/", album.id)) let album: api::Album = client!()
.send().unwrap() .get(&format!("/api/v1/albums/{}/", album.id))
.json().unwrap(); .send()
.unwrap()
.json()
.unwrap();
for track in album.clone().tracks.unwrap_or_default() { for track in album.clone().tracks.unwrap_or_default() {
dls.push(Download { dls.push(Download {
url: track.listen_url.clone(), url: track.listen_url.clone(),
output: dirs::audio_dir().unwrap() output: dirs::audio_dir()
.join(self.name.clone()) .unwrap()
.join(album.title.clone()) .join(self.name.clone())
.join(format!("{}.mp3", track.title.clone())), .join(album.title.clone())
status: DlStatus::Planned, .join(format!("{}.mp3", track.title.clone())),
track: track.clone().into_full(&album), status: DlStatus::Planned,
}); track: track.clone().into_full(&album),
} });
} }
dls }
} dls
}
} }
impl CardModel for api::Album { impl CardModel for api::Album {
fn text(&self) -> String { fn text(&self) -> String {
self.title.clone() self.title.clone()
} }
fn subtext(&self) -> String { fn subtext(&self) -> String {
format!("{} tracks, by {}", self.tracks.clone().map(|t| t.len()).unwrap_or_default(), self.artist.name) format!(
} "{} tracks, by {}",
self.tracks.clone().map(|t| t.len()).unwrap_or_default(),
self.artist.name
)
}
fn image_url(&self) -> Option<String> { fn image_url(&self) -> Option<String> {
self.cover.medium_square_crop.clone() self.cover.medium_square_crop.clone()
} }
fn downloads(&self) -> Vec<Download> { fn downloads(&self) -> Vec<Download> {
self.tracks.clone().unwrap_or_default().iter().map(|track| self
Download { .tracks
url: track.listen_url.clone(), .clone()
output: dirs::audio_dir().unwrap() .unwrap_or_default()
.join(self.artist.name.clone()) .iter()
.join(self.title.clone()) .map(|track| Download {
.join(format!("{}.mp3", track.title.clone())), url: track.listen_url.clone(),
status: DlStatus::Planned, output: dirs::audio_dir()
track: track.clone().into_full(&self), .unwrap()
} .join(self.artist.name.clone())
).collect() .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 { impl CardModel for api::Track {
fn text(&self) -> String { fn text(&self) -> String {
self.title.clone() self.title.clone()
} }
fn subtext(&self) -> String { fn subtext(&self) -> String {
format!("By {}, in {}", self.artist.name, self.album.title) format!("By {}, in {}", self.artist.name, self.album.title)
} }
fn image_url(&self) -> Option<String> { fn image_url(&self) -> Option<String> {
self.album.cover.medium_square_crop.clone() self.album.cover.medium_square_crop.clone()
} }
fn downloads(&self) -> Vec<Download> { fn downloads(&self) -> Vec<Download> {
vec![Download { vec![Download {
url: self.listen_url.clone(), url: self.listen_url.clone(),
output: dirs::audio_dir().unwrap() output: dirs::audio_dir()
.join(self.artist.name.clone()) .unwrap()
.join(self.album.title.clone()) .join(self.artist.name.clone())
.join(format!("{}.mp3", self.title.clone())), .join(self.album.title.clone())
status: DlStatus::Planned, .join(format!("{}.mp3", self.title.clone())),
track: self.clone(), status: DlStatus::Planned,
}] track: self.clone(),
} }]
}
fn download_status(&self) -> Option<Download> { fn download_status(&self) -> Option<Download> {
crate::DOWNLOADS.lock().ok()?.get(&self.id).map(|x| x.clone()) crate::DOWNLOADS.lock().ok()?.get(&self.id).cloned()
} }
} }

View File

@ -1,37 +1,37 @@
use crate::ui::card;
use gtk::{prelude::*, *}; use gtk::{prelude::*, *};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use crate::{ui::card};
pub fn render() -> Rc<RefCell<gtk::Box>> { pub fn render() -> Rc<RefCell<gtk::Box>> {
let cont = gtk::Box::new(Orientation::Vertical, 12); let cont = gtk::Box::new(Orientation::Vertical, 12);
cont.set_valign(Align::Start); cont.set_valign(Align::Start);
cont.set_margin_top(48); cont.set_margin_top(48);
cont.set_margin_bottom(48); cont.set_margin_bottom(48);
cont.set_margin_start(96); cont.set_margin_start(96);
cont.set_margin_end(96); cont.set_margin_end(96);
let active = crate::DL_JOBS.active_count(); let active = crate::DL_JOBS.active_count();
rc!(cont, active); rc!(cont, active);
gtk::idle_add(clone!(cont => move || { gtk::idle_add(clone!(cont => move || {
let active_now = crate::DL_JOBS.active_count(); let active_now = crate::DL_JOBS.active_count();
if active_now != *active.borrow() { if active_now != *active.borrow() {
*active.borrow_mut() = active_now; *active.borrow_mut() = active_now;
let cont = cont.borrow(); let cont = cont.borrow();
for ch in cont.get_children() { for ch in cont.get_children() {
cont.remove(&ch); cont.remove(&ch);
} }
let dl_list = { let dl_list = {
crate::DOWNLOADS.lock().unwrap().clone() crate::DOWNLOADS.lock().unwrap().clone()
}; };
for (_, dl) in dl_list { for (_, dl) in dl_list {
cont.add(&*card::render(dl.track).borrow()); cont.add(&*card::render(dl.track).borrow());
} }
cont.show_all(); cont.show_all();
} }
glib::Continue(true) glib::Continue(true)
})); }));
cont.borrow().show_all(); cont.borrow().show_all();
cont cont
} }

View File

@ -1,29 +1,25 @@
use crate::{api::*, ui::title, State};
use gtk::*; use gtk::*;
use std::{ use std::{cell::RefCell, rc::Rc};
rc::Rc,
cell::RefCell,
};
use crate::{State, api::*, ui::title};
pub fn render(state: State) -> gtk::Box { pub fn render(state: State) -> gtk::Box {
let cont = gtk::Box::new(Orientation::Vertical, 24); let cont = gtk::Box::new(Orientation::Vertical, 24);
cont.set_halign(Align::Center); cont.set_halign(Align::Center);
cont.set_valign(Align::Center); cont.set_valign(Align::Center);
cont.set_size_request(300, -1); cont.set_size_request(300, -1);
let title = title("Login"); let title = title("Login");
let instance = Input::new("Instance URL") let instance = Input::new("Instance URL").with_placeholder("demo.funkwhale.audio");
.with_placeholder("demo.funkwhale.audio"); let username = Input::new("Username");
let username = Input::new("Username"); let password = Input::new_password("Password");
let password = Input::new_password("Password");
let login_bt = Button::new_with_label("Login"); let login_bt = Button::new_with_label("Login");
login_bt.get_style_context().map(|c| c.add_class("suggested-action")); if let Some(c) = login_bt.get_style_context() {
login_bt.set_margin_bottom(48); c.add_class("suggested-action")
let widgets = Rc::new(RefCell::new(( }
instance, username, password login_bt.set_margin_bottom(48);
))); let widgets = Rc::new(RefCell::new((instance, username, password)));
login_bt.connect_clicked(clone!(state, widgets => move |_| { login_bt.connect_clicked(clone!(state, widgets => move |_| {
let mut api_ctx = crate::api::API.lock().unwrap(); let mut api_ctx = crate::api::API.lock().unwrap();
let mut instance_url = widgets.borrow().0.get_text().unwrap().trim_end_matches('/').to_string(); 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://")) { 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(); let state = state.clone();
wait!(execute(api_ctx.as_ref().unwrap().post("/api/v1/token/").json(&LoginData { wait!(execute(api_ctx.as_ref().unwrap().post("/api/v1/token/").json(&LoginData {
username: widgets.borrow().1.get_text().clone().unwrap(), username: widgets.borrow().1.get_text().unwrap(),
password: widgets.borrow().2.get_text().clone().unwrap(), password: widgets.borrow().2.get_text().unwrap(),
})) => |res| { })) => |res| {
let res: Result<LoginInfo, _> = res.json(); let res: Result<LoginInfo, _> = 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."), Err(_) => crate::show_error(state.clone(), "Somehting went wrong, check your username and password, and the URL of your instance."),
Ok(res) => { Ok(res) => {
if let Some(ref mut client) = *crate::api::API.lock().unwrap() { if let Some(ref mut client) = *crate::api::API.lock().unwrap() {
client.auth(res.token.clone()); client.auth(res.token);
} }
let state = state.borrow(); 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(); widgets
cont.add(&title); .borrow()
cont.add(&instance.render()); .0
cont.add(&username.render()); .entry
cont.add(&password.render()); .connect_activate(clone!(widgets => move |_| {
cont.add(&login_bt); 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 |_| { cont.show_all();
widgets.borrow().1.entry.grab_focus(); cont
}));
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
} }
struct Input<'a> { struct Input<'a> {
label: &'a str, label: &'a str,
entry: gtk::Entry, entry: gtk::Entry,
} }
impl<'a> Input<'a> { impl<'a> Input<'a> {
fn new(text: &'a str) -> Input { fn new(text: &'a str) -> Input {
let entry = gtk::Entry::new(); let entry = gtk::Entry::new();
Input { Input { label: text, entry }
label: text, }
entry
}
}
fn new_password(text: &'a str) -> Input { fn new_password(text: &'a str) -> Input {
let input = Input::new(text); let input = Input::new(text);
input.entry.set_visibility(false); input.entry.set_visibility(false);
input input
} }
fn with_placeholder(self, ph: &'a str) -> Input { fn with_placeholder(self, ph: &'a str) -> Input {
self.entry.set_placeholder_text(ph); self.entry.set_placeholder_text(ph);
self self
} }
fn get_text(&self) -> Option<String> { fn get_text(&self) -> Option<String> {
self.entry.get_text() self.entry.get_text()
} }
fn render(&self) -> gtk::Box { fn render(&self) -> gtk::Box {
let label = gtk::Label::new(self.label); let label = gtk::Label::new(self.label);
label.set_halign(Align::Start); label.set_halign(Align::Start);
let cont = gtk::Box::new(gtk::Orientation::Vertical, 6); let cont = gtk::Box::new(gtk::Orientation::Vertical, 6);
cont.add(&label); cont.add(&label);
cont.add(&self.entry); cont.add(&self.entry);
cont cont
} }
} }

View File

@ -1,26 +1,32 @@
use crate::{
api::{self, execute},
ui::{card, title},
};
use gdk::ContextExt; use gdk::ContextExt;
use gdk_pixbuf::PixbufExt; use gdk_pixbuf::PixbufExt;
use gtk::*; use gtk::*;
use std::{ use std::{cell::RefCell, fs, rc::Rc};
cell::RefCell,
fs,
rc::Rc,
};
use crate::{api::{self, execute}, ui::{title, card}};
pub fn render(window: Rc<RefCell<Window>>, header: &HeaderBar, switcher: &StackSwitcher) -> gtk::Box { pub fn render(
let cont = gtk::Box::new(Orientation::Vertical, 12); window: Rc<RefCell<Window>>,
cont.set_margin_top(48); header: &HeaderBar,
cont.set_margin_bottom(48); switcher: &StackSwitcher,
cont.set_margin_start(96); ) -> gtk::Box {
cont.set_margin_end(96); 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(); let avatar = DrawingArea::new();
avatar.set_size_request(32, 32); avatar.set_size_request(32, 32);
avatar.set_halign(Align::Center); 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) 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; use std::f64::consts::PI;
let width = 32.0f64; let width = 32.0f64;
let height = 32.0f64; let height = 32.0f64;
@ -50,89 +56,88 @@ pub fn render(window: Rc<RefCell<Window>>, header: &HeaderBar, switcher: &StackS
Inhibit(false) Inhibit(false)
})); }));
header.pack_start(&avatar); header.pack_start(&avatar);
header.set_custom_title(&*switcher); header.set_custom_title(&*switcher);
let logout_bt = Button::new_from_icon_name("system-log-out", IconSize::LargeToolbar.into()); let logout_bt = Button::new_from_icon_name("system-log-out", IconSize::LargeToolbar.into());
logout_bt.connect_clicked(clone!(window => move |_| { logout_bt.connect_clicked(clone!(window => move |_| {
crate::logout(window.clone()); crate::logout(window.clone());
})); }));
header.pack_end(&logout_bt); header.pack_end(&logout_bt);
header.show_all(); header.show_all();
let search = SearchEntry::new(); let search = SearchEntry::new();
search.set_placeholder_text("Search"); search.set_placeholder_text("Search");
cont.add(&search); cont.add(&search);
let results = gtk::Box::new(Orientation::Vertical, 12); let results = gtk::Box::new(Orientation::Vertical, 12);
results.set_valign(Align::Start); results.set_valign(Align::Start);
cont.add(&results); cont.add(&results);
rc!(avatar, results); rc!(avatar, results);
clone!(avatar, results, avatar_path); clone!(avatar, results, avatar_path);
wait!(execute(client!().get("/api/v1/users/users/me")) => |res| { wait!(execute(client!().get("/api/v1/users/users/me")) => |res| {
let res: Result<api::UserInfo, _> = res.json(); let res: Result<api::UserInfo, _> = res.json();
match res { match res {
Ok(res) => { Ok(res) => {
avatar.borrow().set_tooltip_text(format!("Connected as {}.", res.username).as_ref()); avatar.borrow().set_tooltip_text(format!("Connected as {}.", res.username).as_ref());
clone!(avatar_path, avatar); clone!(avatar_path, avatar);
wait!(execute(client!().get(&res.avatar.medium_square_crop.unwrap_or_default())) => |avatar_dl| { wait!(execute(client!().get(&res.avatar.medium_square_crop.unwrap_or_default())) => |avatar_dl| {
fs::create_dir_all(avatar_path.parent().unwrap()).unwrap(); fs::create_dir_all(avatar_path.parent().unwrap()).unwrap();
let mut avatar_file = fs::File::create(avatar_path.clone()).unwrap(); let mut avatar_file = fs::File::create(avatar_path.clone()).unwrap();
avatar_dl.copy_to(&mut avatar_file).unwrap(); avatar_dl.copy_to(&mut avatar_file).unwrap();
avatar.borrow().queue_draw(); avatar.borrow().queue_draw();
}); });
}, },
Err(_) => { Err(_) => {
crate::logout(window.clone()); crate::logout(window.clone());
} }
} }
}); });
search.connect_activate(move |s| { search.connect_activate(move |s| {
let results = results.clone(); let results = results.clone();
wait!(execute(client!().get("/api/v1/search").query(&api::SearchQuery { wait!(execute(client!().get("/api/v1/search").query(&api::SearchQuery {
query: s.get_text().unwrap_or_default() query: s.get_text().unwrap_or_default()
})) => |res| { })) => |res| {
update_results(res.json().unwrap(), &results.borrow()); update_results(res.json().unwrap(), &results.borrow());
}); });
}); });
cont.show_all(); cont.show_all();
cont cont
} }
fn update_results(res: api::SearchResult, cont: &gtk::Box) { fn update_results(res: api::SearchResult, cont: &gtk::Box) {
for ch in cont.get_children() { for ch in cont.get_children() {
cont.remove(&ch); cont.remove(&ch);
} }
if res.artists.is_empty() && res.albums.is_empty() && res.tracks.is_empty() { if res.artists.is_empty() && res.albums.is_empty() && res.tracks.is_empty() {
cont.add(&Label::new("No results. Try something else.")); cont.add(&Label::new("No results. Try something else."));
} }
if !res.artists.is_empty() { if !res.artists.is_empty() {
cont.add(&title("Artists")); cont.add(&title("Artists"));
for artist in res.artists.clone() { for artist in res.artists.clone() {
cont.add(&*card::render(artist).borrow()); cont.add(&*card::render(artist).borrow());
} }
} }
if !res.albums.is_empty() { if !res.albums.is_empty() {
cont.add(&title("Albums")); cont.add(&title("Albums"));
for album in res.albums.clone() { for album in res.albums.clone() {
cont.add(&*card::render(album).borrow()); cont.add(&*card::render(album).borrow());
} }
} }
if !res.tracks.is_empty() { if !res.tracks.is_empty() {
cont.add(&title("Songs")); cont.add(&title("Songs"));
for track in res.tracks.clone() { for track in res.tracks.clone() {
cont.add(&*card::render(track).borrow()); cont.add(&*card::render(track).borrow());
} }
} }
cont.show_all(); cont.show_all();
} }

View File

@ -7,7 +7,9 @@ pub mod main_page;
pub mod network_image; pub mod network_image;
fn title(text: &str) -> gtk::Label { fn title(text: &str) -> gtk::Label {
let lbl = gtk::Label::new(text); let lbl = gtk::Label::new(text);
lbl.get_style_context().map(|c| c.add_class("h2")); if let Some(c) = lbl.get_style_context() {
lbl c.add_class("h2")
}
lbl
} }

View File

@ -1,39 +1,37 @@
use gtk::*;
use std::{
cell::RefCell,
fs, rc::Rc,
};
use crate::api::execute; use crate::api::execute;
use gtk::{Image, ImageExt};
use std::{cell::RefCell, fs, rc::Rc};
pub struct NetworkImage { pub struct NetworkImage {
pub img: Rc<RefCell<Image>>, pub img: Rc<RefCell<Image>>,
} }
impl NetworkImage { impl NetworkImage {
pub fn new(url: String) -> NetworkImage { pub fn new(url: String) -> NetworkImage {
let image = Image::new_from_icon_name("image-loading", 4); let image = Image::new_from_icon_name("image-loading", 4);
rc!(image); rc!(image);
let dest_file = url.split("/media/").last().unwrap().replace('/', "-"); 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 = dirs::cache_dir()
.unwrap()
.join(env!("CARGO_PKG_NAME"))
.join(dest_file);
if dest.exists() { if dest.exists() {
let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest, 64, 64, true).unwrap(); let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest, 64, 64, true).unwrap();
image.borrow().set_from_pixbuf(&pb); image.borrow().set_from_pixbuf(&pb);
} else { } else {
clone!(image); clone!(image);
wait!(execute(client!().get(&url)) => |res| { wait!(execute(client!().get(&url)) => |res| {
fs::create_dir_all(dest.parent().unwrap()).unwrap(); fs::create_dir_all(dest.parent().unwrap()).unwrap();
let mut file = fs::File::create(dest.clone()).unwrap(); let mut file = fs::File::create(dest.clone()).unwrap();
res.copy_to(&mut file).unwrap(); res.copy_to(&mut file).unwrap();
let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest.clone(), 64, 64, true).unwrap(); let pb = gdk_pixbuf::Pixbuf::new_from_file_at_scale(dest.clone(), 64, 64, true).unwrap();
image.borrow().set_from_pixbuf(&pb); image.borrow().set_from_pixbuf(&pb);
}); });
} }
NetworkImage { NetworkImage { img: image }
img: image.clone(), }
}
}
} }