Skip to main content

mimick/library/
asset_model.rs

1//! Custom `gio::ListModel` backing the library grid.
2//!
3//! Replaces the previous `gio::ListStore`-of-`AssetObject` mirror. The model
4//! owns a `Vec<AssetObject>` reconciled from `LibraryState.assets`.
5//! `extend` is used for append-pagination (server-side date sort lets us emit
6//! a precise `items_changed(prev_n, 0, added)` so the visible viewport doesn't
7//! rebind); `reset` is used for source switches and client-side re-sorts.
8
9use 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    /// Replace all items and emit a `items_changed(0, prev, new)` reset.
73    /// Use when the caller can't promise that existing positions are stable
74    /// (source switch, client-side sort change, dedup that affected the head).
75    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    /// Append-style update for paginated loads. For server-side date sort
84    /// (`NewestFirst`/`OldestFirst`) the existing positions are stable so we
85    /// emit a precise tail-only `items_changed(prev_n, 0, added)`. For
86    /// client-side sort modes the entire vec may have re-ordered, so we fall
87    /// back to a full reset.
88    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    /// Replace all items with pre-built `AssetObject`s and emit a full reset.
110    ///
111    /// Used by the staging view which constructs local-only `AssetObject`s
112    /// from file paths rather than going through the `LibraryAsset` pipeline.
113    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    /// Append additional `AssetObject`s to the end of the model.
121    ///
122    /// Emits a tail-only `items_changed` so the existing viewport is unaffected.
123    /// Used by the staging view drop handler to add newly-dropped files.
124    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}