mimick/library/
asset_model.rs1use std::cell::RefCell;
10
11use gtk::gio;
12use gtk::glib;
13use gtk::prelude::*;
14use gtk::subclass::prelude::*;
15
16use crate::api_client::LibraryAsset;
17use crate::app_context::AppContext;
18use crate::library::asset_object::{AssetInit, AssetObject};
19use crate::library::state::LibrarySortMode;
20
21mod imp {
22 use super::*;
23 use gio::subclass::prelude::ListModelImpl;
24
25 #[derive(Default)]
26 pub struct LibraryAssetModel {
27 pub items: RefCell<Vec<AssetObject>>,
28 }
29
30 #[glib::object_subclass]
31 impl ObjectSubclass for LibraryAssetModel {
32 const NAME: &'static str = "MimickLibraryAssetModel";
33 type Type = super::LibraryAssetModel;
34 type Interfaces = (gio::ListModel,);
35 }
36
37 impl ObjectImpl for LibraryAssetModel {}
38
39 impl ListModelImpl for LibraryAssetModel {
40 fn item_type(&self) -> glib::Type {
41 AssetObject::static_type()
42 }
43
44 fn n_items(&self) -> u32 {
45 self.items.borrow().len() as u32
46 }
47
48 fn item(&self, position: u32) -> Option<glib::Object> {
49 self.items
50 .borrow()
51 .get(position as usize)
52 .map(|o| o.clone().upcast())
53 }
54 }
55}
56
57glib::wrapper! {
58 pub struct LibraryAssetModel(ObjectSubclass<imp::LibraryAssetModel>) @implements gio::ListModel;
59}
60
61impl Default for LibraryAssetModel {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl LibraryAssetModel {
68 pub fn new() -> Self {
69 glib::Object::new()
70 }
71
72 pub fn reset(&self, ctx: &AppContext, assets: &[LibraryAsset], sort_mode: &LibrarySortMode) {
76 let prev_n = self.imp().items.borrow().len() as u32;
77 let new_items = build_sorted_asset_objects(assets, ctx, sort_mode);
78 let new_n = new_items.len() as u32;
79 *self.imp().items.borrow_mut() = new_items;
80 self.items_changed(0, prev_n, new_n);
81 }
82
83 pub fn extend(&self, ctx: &AppContext, assets: &[LibraryAsset], sort_mode: &LibrarySortMode) {
89 let prev_n = self.imp().items.borrow().len() as u32;
90 let new_items = build_sorted_asset_objects(assets, ctx, sort_mode);
91 let new_n = new_items.len() as u32;
92 let server_sorted = matches!(
93 sort_mode,
94 LibrarySortMode::NewestFirst | LibrarySortMode::OldestFirst
95 );
96
97 *self.imp().items.borrow_mut() = new_items;
98
99 if server_sorted && new_n >= prev_n {
100 let added = new_n - prev_n;
101 if added > 0 {
102 self.items_changed(prev_n, 0, added);
103 }
104 } else {
105 self.items_changed(0, prev_n, new_n);
106 }
107 }
108
109 pub fn reset_with_objects(&self, objects: Vec<AssetObject>) {
114 let prev_n = self.imp().items.borrow().len() as u32;
115 let new_n = objects.len() as u32;
116 *self.imp().items.borrow_mut() = objects;
117 self.items_changed(0, prev_n, new_n);
118 }
119
120 pub fn append_objects(&self, objects: &[AssetObject]) {
125 if objects.is_empty() {
126 return;
127 }
128 let prev_n = {
129 let mut items = self.imp().items.borrow_mut();
130 let prev = items.len() as u32;
131 items.extend_from_slice(objects);
132 prev
133 };
134 self.items_changed(prev_n, 0, objects.len() as u32);
135 }
136}
137
138fn build_sorted_asset_objects(
139 assets: &[LibraryAsset],
140 ctx: &AppContext,
141 sort_mode: &LibrarySortMode,
142) -> Vec<AssetObject> {
143 let mut items = build_asset_objects(assets, ctx);
144 match sort_mode {
145 LibrarySortMode::NewestFirst | LibrarySortMode::OldestFirst => {}
146 LibrarySortMode::Filename => items.sort_by_cached_key(|o| {
147 (
148 o.property::<String>("filename").to_ascii_lowercase(),
149 o.property::<String>("id"),
150 )
151 }),
152 LibrarySortMode::FileType => items.sort_by_cached_key(|o| {
153 (
154 o.property::<String>("mime-type"),
155 o.property::<String>("filename"),
156 o.property::<String>("id"),
157 )
158 }),
159 }
160 items
161}
162
163fn build_asset_objects(assets: &[LibraryAsset], ctx: &AppContext) -> Vec<AssetObject> {
164 use super::{LOCAL_ID_PREFIX, immich_checksum_to_hex};
165 use crate::library::local_source::local_sync_state;
166
167 assets
168 .iter()
169 .map(|asset| {
170 if let Some(local_path) = asset.id.strip_prefix(LOCAL_ID_PREFIX) {
171 let sync_state =
172 local_sync_state(&ctx.sync_index, std::path::Path::new(local_path));
173 let object = AssetObject::new_local(
174 &asset.id,
175 &asset.filename,
176 &asset.mime_type,
177 &asset.created_at,
178 &asset.asset_type,
179 local_path,
180 );
181 if sync_state != 1 {
182 object.set_property("sync-state", sync_state);
183 }
184 return object;
185 }
186 let local_match = asset
187 .checksum
188 .as_deref()
189 .and_then(immich_checksum_to_hex)
190 .as_deref()
191 .and_then(|hex| ctx.sync_index.local_path_for_checksum(hex));
192 let sync_state = if local_match.is_some() { 2 } else { 0 };
193 let (exif_w, exif_h) = asset
194 .exif_info
195 .as_ref()
196 .map(|e| (e.exif_image_width, e.exif_image_height))
197 .unwrap_or((None, None));
198 let width = asset.width.filter(|v| *v > 0).or(exif_w).unwrap_or(0);
199 let height = asset.height.filter(|v| *v > 0).or(exif_h).unwrap_or(0);
200 let object = AssetObject::new(AssetInit {
201 id: &asset.id,
202 filename: &asset.filename,
203 mime_type: &asset.mime_type,
204 created_at: &asset.created_at,
205 asset_type: &asset.asset_type,
206 sync_state,
207 thumbhash: asset.thumbhash.as_deref(),
208 width,
209 height,
210 });
211 if let Some(path) = local_match {
212 object.set_property("local-path", path);
213 }
214 object
215 })
216 .collect()
217}