transmission_gobject/
file_model.rs

1use std::cell::{Cell, RefCell};
2
3use gio::prelude::*;
4use gio::subclass::prelude::*;
5use glib::Properties;
6use indexmap::map::IndexMap;
7use transmission_client::TorrentFiles;
8
9use crate::{TrFile, TrTorrent};
10
11mod imp {
12    use super::*;
13
14    #[derive(Debug, Default, Properties)]
15    #[properties(wrapper_type = super::TrFileModel)]
16    pub struct TrFileModel {
17        /// The top level file
18        #[property(get, nullable)]
19        pub top_level: RefCell<Option<TrFile>>,
20        /// Whether this contains data, and can be consumed
21        #[property(get)]
22        pub is_ready: Cell<bool>,
23
24        /// All files and folders (Full path + TrFile)
25        pub map: RefCell<IndexMap<String, TrFile>>,
26    }
27
28    #[glib::object_subclass]
29    impl ObjectSubclass for TrFileModel {
30        const NAME: &'static str = "TrFileModel";
31        type ParentType = glib::Object;
32        type Type = super::TrFileModel;
33        type Interfaces = (gio::ListModel,);
34    }
35
36    #[glib::derived_properties]
37    impl ObjectImpl for TrFileModel {}
38
39    impl ListModelImpl for TrFileModel {
40        fn item_type(&self) -> glib::Type {
41            TrFile::static_type()
42        }
43
44        fn n_items(&self) -> u32 {
45            self.map.borrow().len() as u32
46        }
47
48        fn item(&self, position: u32) -> Option<glib::Object> {
49            self.map
50                .borrow()
51                .get_index(position.try_into().unwrap())
52                .map(|(_, o)| o.clone().upcast::<glib::Object>())
53        }
54    }
55
56    impl TrFileModel {
57        pub fn add_file(&self, file: &TrFile, torrent: &TrTorrent) {
58            if self.obj().file_by_name(&file.name()).is_some() {
59                warn!("File {:?} already exists in model", file.name());
60                return;
61            }
62
63            let mut map = self.map.borrow_mut();
64            let full_path = file.name();
65
66            // Resolve individual folders to make sure that they're added as TrFile to the
67            // model eg. "there/can/be/nested/folders/file.txt"
68            // -> there, can, be, nested, folders
69            let mut folder_names = Vec::new();
70            let folder_name = if full_path.contains('/') {
71                let slashes = full_path.match_indices('/');
72                let folder_name = full_path[..slashes.clone().next_back().unwrap().0].to_string();
73
74                for slash in slashes {
75                    let folder_name = full_path[..slash.0].to_string();
76                    folder_names.push(folder_name);
77                }
78
79                Some(folder_name)
80            } else {
81                // No folders, it's a single file torrent
82                self.top_level.borrow_mut().replace(file.clone());
83                self.obj().notify_top_level();
84                None
85            };
86
87            // Make sure that nested folders are related to their parent
88            let mut parent: Option<TrFile> = None;
89            for folder_name in folder_names {
90                let folder = if let Some(folder) = map.get(&folder_name) {
91                    folder.clone()
92                } else {
93                    let folder = TrFile::new_folder(&folder_name, torrent);
94
95                    // We know that the folder is related / a subfolder of the parent
96                    if let Some(parent) = parent {
97                        parent.add_related(&folder);
98                    } else {
99                        // No parent -> the current file is the toplevel folder
100                        self.top_level.borrow_mut().replace(folder.clone());
101                        self.obj().notify_top_level();
102                    }
103
104                    map.insert(folder_name.clone(), folder.clone());
105                    folder
106                };
107
108                parent = Some(folder);
109            }
110
111            // Now since we made sure that all folders are added,
112            // we can take care of the actual file
113            map.insert(full_path.clone(), file.clone());
114            let pos = (map.len() - 1) as u32;
115            self.obj().items_changed(pos, 0, 1);
116
117            if let Some(folder_name) = folder_name {
118                // Get the actual folder to add the file as related file (child)
119                map.get_mut(&folder_name).unwrap().add_related(file);
120            }
121        }
122    }
123}
124
125glib::wrapper! {
126    pub struct TrFileModel(ObjectSubclass<imp::TrFileModel>) @implements gio::ListModel;
127}
128
129impl TrFileModel {
130    pub(crate) fn refresh_files(&self, rpc_files: &TorrentFiles, torrent: &TrTorrent) {
131        let is_initial = self.top_level().is_none();
132
133        for (index, rpc_file) in rpc_files.files.iter().enumerate() {
134            let rpc_file_stat = rpc_files.file_stats.get(index).cloned().unwrap_or_default();
135            if let Some(file) = self.file_by_name(&rpc_file.name) {
136                file.refresh_values(&rpc_file_stat);
137            } else {
138                let file = TrFile::from_rpc_file(index.try_into().unwrap(), rpc_file, torrent);
139                file.refresh_values(&rpc_file_stat);
140
141                self.imp().add_file(&file, torrent);
142            }
143        }
144
145        if is_initial && self.top_level().is_some() {
146            self.imp().is_ready.set(true);
147            self.notify_is_ready();
148        }
149    }
150
151    pub(crate) fn related_files_by_path(&self, path: &str) -> Vec<TrFile> {
152        let imp = self.imp();
153        let mut result = Vec::new();
154
155        for (file_path, file) in &*imp.map.borrow() {
156            if file_path.contains(path) && !file.is_folder() {
157                result.push(file.clone());
158            }
159        }
160
161        result
162    }
163
164    /// Returns a [TrFile] based on its name (path)
165    pub fn file_by_name(&self, name: &str) -> Option<TrFile> {
166        self.imp()
167            .map
168            .borrow()
169            .get(name)
170            .map(|o| o.clone().downcast().unwrap())
171    }
172
173    /// Returns parent folder for [TrFile]
174    pub fn parent(&self, folder: &TrFile) -> Option<TrFile> {
175        let folder_name = folder.name();
176        if folder_name.contains('/') {
177            let slashes = folder_name.match_indices('/');
178            let parent_name = folder.name()[0..slashes.clone().next_back().unwrap().0].to_string();
179
180            let parent = self.file_by_name(&parent_name);
181            return parent;
182        }
183        None
184    }
185}
186
187impl Default for TrFileModel {
188    fn default() -> Self {
189        glib::Object::new()
190    }
191}