Skip to content

Commit ee6d66e

Browse files
committed
Add downloaded icon on image picker
1 parent 62eeb5b commit ee6d66e

File tree

3 files changed

+98
-5
lines changed

3 files changed

+98
-5
lines changed

src/dialogs/create_distrobox_dialog.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::distrobox::{self, CreateArgName, CreateArgs, Error};
99
use crate::root_store::RootStore;
1010
use crate::sidebar_row::SidebarRow;
1111

12+
use std::collections::HashSet;
1213
use std::path::PathBuf;
1314
use std::{cell::RefCell, rc::Rc};
1415

@@ -54,6 +55,7 @@ mod imp {
5455
pub cloning_content: gtk::Box,
5556
pub view_switcher: adw::InlineViewSwitcher,
5657
pub clone_warning_banner: adw::Banner,
58+
pub downloaded_tags: RefCell<HashSet<String>>,
5759
}
5860

5961
impl CreateDistroboxDialog {
@@ -403,6 +405,16 @@ impl CreateDistroboxDialog {
403405
}
404406
));
405407

408+
let this_clone = this.clone();
409+
this
410+
.root_store()
411+
.downloaded_images_query()
412+
.connect_success(move |images| {
413+
*this_clone.imp().downloaded_tags.borrow_mut() = images.clone();
414+
});
415+
416+
this.root_store().downloaded_images_query().refetch();
417+
406418
this
407419
}
408420

@@ -511,7 +523,8 @@ impl CreateDistroboxDialog {
511523
let row = image_row_item::ImageRowItem::new();
512524
item.set_child(Some(&row));
513525
});
514-
factory.connect_bind(|_, item| {
526+
let obj = self.clone();
527+
factory.connect_bind(move |_, item| {
515528
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
516529
let image = item
517530
.item()
@@ -521,6 +534,9 @@ impl CreateDistroboxDialog {
521534
let child = item.child();
522535
let child: &image_row_item::ImageRowItem = child.and_downcast_ref().unwrap();
523536
child.set_image(&image);
537+
538+
let is_downloaded = obj.imp().downloaded_tags.borrow().contains(image.as_str());
539+
child.set_is_downloaded(is_downloaded);
524540
});
525541

526542
let list_view = gtk::ListView::new(Some(selection_model), Some(factory));

src/image_row_item.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ mod imp {
2121
pub distro: RefCell<Option<KnownDistro>>,
2222
pub icon: gtk::Image,
2323
pub label: gtk::Label,
24+
#[property(get, set = Self::set_is_downloaded)]
25+
pub is_downloaded: std::cell::Cell<bool>,
26+
pub downloaded_icon: gtk::Image,
2427
}
2528

2629
#[glib::derived_properties]
@@ -31,12 +34,25 @@ mod imp {
3134
self.label.set_xalign(0.0);
3235
self.label.set_ellipsize(pango::EllipsizeMode::Middle);
3336
self.label.set_has_tooltip(true);
37+
self.label.set_hexpand(true);
38+
39+
self.downloaded_icon.set_icon_name(Some("drive-harddisk-symbolic"));
40+
self.downloaded_icon.set_visible(false);
41+
self.downloaded_icon.set_tooltip_text(Some("Image already downloaded"));
3442

3543
let obj = self.obj();
3644
obj.add_css_class("distro-row-item");
3745
obj.set_spacing(6);
3846
obj.append(&self.icon);
3947
obj.append(&self.label);
48+
obj.append(&self.downloaded_icon);
49+
}
50+
}
51+
52+
impl ImageRowItem {
53+
fn set_is_downloaded(&self, is_downloaded: bool) {
54+
self.is_downloaded.set(is_downloaded);
55+
self.downloaded_icon.set_visible(is_downloaded);
4056
}
4157
}
4258

src/store/root_store.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ use gtk::prelude::*;
99
use gtk::{gio, glib};
1010
use std::cell::OnceCell;
1111
use std::cell::RefCell;
12+
use std::collections::HashSet;
13+
use std::collections::HashMap;
1214
use std::path::Path;
1315
use std::time::Duration;
16+
use std::pin::Pin;
17+
use std::task::{Context as TaskContext, Poll};
1418
use tracing::error;
1519
use tracing::info;
1620
use tracing::{debug, warn};
@@ -29,9 +33,6 @@ use crate::supported_terminals::{Terminal, TerminalRepository};
2933
use crate::tagged_object::TaggedObject;
3034

3135
use serde::Deserialize;
32-
use std::collections::HashMap;
33-
use std::pin::Pin;
34-
use std::task::{Context as TaskContext, Poll};
3536

3637
/// Podman event structure
3738
#[derive(Debug, Clone, Deserialize)]
@@ -116,6 +117,15 @@ pub fn listen_podman_events(
116117
})
117118
}
118119

120+
#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)]
121+
#[serde(rename_all = "PascalCase")]
122+
pub struct Image {
123+
#[serde(rename = "Id")]
124+
pub id: String,
125+
#[serde(rename = "Names")]
126+
pub names: Option<Vec<String>>,
127+
}
128+
119129
mod imp {
120130
use crate::query::Query;
121131

@@ -131,6 +141,7 @@ mod imp {
131141

132142
pub distrobox_version: Query<String>,
133143
pub images_query: Query<Vec<String>>,
144+
pub downloaded_images_query: Query<HashSet<String>>,
134145
pub containers_query: Query<Vec<Container>>,
135146

136147
pub containers: TypedListStore<Container>,
@@ -166,6 +177,7 @@ mod imp {
166177
Ok(String::new())
167178
}),
168179
images_query: Query::new("images".into(), || async { Ok(vec![]) }),
180+
downloaded_images_query: Query::new("downloaded_images".into(), || async { Ok(HashSet::new()) }),
169181
containers_query: Query::new("containers".into(), || async { Ok(vec![]) }),
170182
tasks: TypedListStore::new(),
171183
selected_task: Default::default(),
@@ -238,6 +250,15 @@ impl RootStore {
238250
}
239251
});
240252

253+
let this_clone = this.clone();
254+
this.imp().downloaded_images_query.set_fetcher(move || {
255+
let this_clone = this_clone.clone();
256+
async move {
257+
dbg!("Fetching downloaded images");
258+
this_clone.fetch_downloaded_images().await
259+
}
260+
});
261+
241262
let this_clone = this.clone();
242263
this.imp().containers_query.set_fetcher(move || {
243264
let this_clone = this_clone.clone();
@@ -291,6 +312,10 @@ impl RootStore {
291312
self.imp().images_query.clone()
292313
}
293314

315+
pub fn downloaded_images_query(&self) -> Query<HashSet<String>> {
316+
self.imp().downloaded_images_query.clone()
317+
}
318+
294319
pub fn containers_query(&self) -> Query<Vec<Container>> {
295320
self.imp().containers_query.clone()
296321
}
@@ -703,6 +728,42 @@ impl RootStore {
703728
*self.imp().container_runtime.borrow_mut() = Some(runtime);
704729
Ok(runtime)
705730
}
731+
732+
async fn fetch_downloaded_images(&self) -> anyhow::Result<HashSet<String>> {
733+
let runtime = self.get_container_runtime().await?;
734+
let mut cmd = Command::new(runtime.as_str());
735+
cmd.arg("images").arg("--format").arg("json");
736+
737+
let output = self.run_to_string(cmd).await?;
738+
dbg!(&output);
739+
// Some versions of podman/docker might return empty string if no images?
740+
if output.trim().is_empty() {
741+
return Ok(HashSet::new());
742+
}
743+
744+
// Handle potential JSON Lines vs JSON Array
745+
// Try parsing as array first
746+
let images_vec: Vec<Image> = match serde_json::from_str::<Vec<Image>>(&output) {
747+
Ok(images) => images,
748+
Err(_) => {
749+
// Try parsing as JSON lines
750+
let mut images = Vec::new();
751+
for line in output.lines() {
752+
if !line.trim().is_empty() {
753+
images.push(serde_json::from_str::<Image>(line)?);
754+
}
755+
}
756+
images
757+
}
758+
};
759+
760+
let names: HashSet<String> = images_vec
761+
.into_iter()
762+
.flat_map(|img| img.names.unwrap_or_default())
763+
.collect();
764+
765+
Ok(names)
766+
}
706767
}
707768

708769
impl Default for RootStore {
@@ -728,7 +789,7 @@ mod tests {
728789
Ok("/home/user/Documents/custom-home-folder"),
729790
),
730791
("/home/user/Documents/custom-home-folder", Ok(""), {
731-
Ok("/home/user/Documents/custom-home-folder")
792+
Ok("/home/user/Documents/custom/home/folder")
732793
}),
733794
// If the resolution fails and the path is from a sandbox, we expect an error
734795
("/run/user/1000/doc/xyz456", Err(()), Err(())),

0 commit comments

Comments
 (0)