transmission_gobject/
file.rs

1use std::cell::{Cell, OnceCell, RefCell};
2use std::collections::{HashMap, HashSet};
3
4use gio::prelude::*;
5use glib::subclass::prelude::{ObjectSubclass, *};
6use glib::{Properties, clone};
7use transmission_client::{File, FileStat};
8
9use crate::{TrRelatedModel, TrTorrent};
10
11mod imp {
12    use super::*;
13
14    #[derive(Debug, Default, Properties)]
15    #[properties(wrapper_type = super::TrFile)]
16    pub struct TrFile {
17        #[property(get, set, construct_only)]
18        torrent: OnceCell<TrTorrent>,
19
20        #[property(get, set, construct_only)]
21        id: Cell<i32>,
22        #[property(get, set, construct_only)]
23        name: OnceCell<String>,
24        #[property(get, set, construct_only)]
25        title: OnceCell<String>,
26        #[property(get, set, construct_only)]
27        is_folder: Cell<bool>,
28        #[property(get)]
29        pub bytes_completed: Cell<i64>,
30        #[property(get, set, construct_only)]
31        length: Cell<i64>,
32        #[property(get, set = Self::set_wanted)]
33        pub wanted: Cell<bool>,
34        #[property(get)]
35        wanted_inconsistent: Cell<bool>,
36        #[property(get)]
37        related: TrRelatedModel,
38
39        related_wanted: RefCell<HashSet<String>>,
40        related_length: RefCell<HashMap<String, i64>>,
41        related_bytes_completed: RefCell<HashMap<String, i64>>,
42    }
43
44    #[glib::object_subclass]
45    impl ObjectSubclass for TrFile {
46        const NAME: &'static str = "TrFile";
47        type ParentType = glib::Object;
48        type Type = super::TrFile;
49    }
50
51    #[glib::derived_properties]
52    impl ObjectImpl for TrFile {}
53
54    impl TrFile {
55        fn set_wanted(&self, wanted: bool) {
56            let fut = clone!(
57                #[weak(rename_to = this)]
58                self,
59                async move {
60                    // Collect ids of files which shall get updated
61                    let ids = if this.obj().is_folder() {
62                        let path = this.obj().name();
63
64                        // We can't use `related()` here, since that wouldn't return all nested folders / files
65                        let files = this.obj().torrent().files().related_files_by_path(&path);
66
67                        let mut ids = Vec::new();
68                        for file in files {
69                            ids.push(file.id());
70                        }
71
72                        ids
73                    } else {
74                        vec![this.obj().id()]
75                    };
76
77                    if wanted {
78                        this.torrent
79                            .get()
80                            .unwrap()
81                            .set_wanted_files(ids)
82                            .await
83                            .unwrap();
84                    } else {
85                        this.torrent
86                            .get()
87                            .unwrap()
88                            .set_unwanted_files(ids)
89                            .await
90                            .unwrap();
91                    }
92
93                    this.wanted.set(wanted);
94                    this.obj().notify_wanted();
95                }
96            );
97            glib::spawn_future_local(fut);
98        }
99
100        pub fn find_title(name: &str) -> String {
101            if !name.contains('/') {
102                return name.to_string();
103            }
104
105            let slashes = name.match_indices('/');
106            name[(slashes.clone().next_back().unwrap().0) + 1..name.len()].to_string()
107        }
108
109        /// Update `self` when a related file changes its `wanted` state
110        pub fn update_related_wanted(&self, related_file: &super::TrFile) {
111            assert!(self.obj().is_folder());
112
113            if related_file.wanted() && !related_file.wanted_inconsistent() {
114                self.related_wanted.borrow_mut().insert(related_file.name());
115            } else {
116                self.related_wanted
117                    .borrow_mut()
118                    .remove(&related_file.name());
119            }
120
121            // Check if this folder is in a inconsistent state
122            // (mixed wanted/not wanted related files)
123            let files_count = self.obj().related().n_items() as usize;
124            let wanted_count = self.related_wanted.borrow().len();
125
126            if files_count == wanted_count {
127                self.wanted.set(true);
128                self.wanted_inconsistent.set(false);
129            } else if wanted_count == 0 {
130                self.wanted.set(false);
131                self.wanted_inconsistent.set(false);
132            } else {
133                self.wanted.set(true);
134                self.wanted_inconsistent.set(true);
135            }
136
137            self.obj().notify_wanted();
138            self.obj().notify_wanted_inconsistent();
139        }
140
141        /// Update `self` when a related file changes its `length` state
142        pub fn update_related_length(&self, related_file: &super::TrFile) {
143            assert!(self.obj().is_folder());
144
145            let obj = self.obj();
146            let mut related_length = self.related_length.borrow_mut();
147
148            let mut value_changed = false;
149            let mut previous_value: i64 = 0;
150
151            if let Some(length) = related_length.get(&related_file.name()) {
152                if length != &related_file.length() {
153                    value_changed = true;
154                    previous_value = *length;
155                }
156            } else {
157                related_length.insert(related_file.name(), related_file.length());
158                self.length.set(obj.length() + related_file.length());
159                self.obj().notify_length();
160            }
161
162            if value_changed {
163                related_length.insert(related_file.name(), related_file.length());
164                self.length.set(obj.length() - previous_value);
165                self.length.set(obj.length() + related_file.length());
166                self.obj().notify_length();
167            }
168        }
169
170        /// Update `self` when a related file changes its `bytes_completed`
171        /// state
172        pub fn update_related_bytes_completed(&self, related_file: &super::TrFile) {
173            assert!(self.obj().is_folder());
174
175            let obj = self.obj();
176            let mut related_bytes_completed = self.related_bytes_completed.borrow_mut();
177
178            let mut value_changed = false;
179            let mut previous_value: i64 = 0;
180
181            if let Some(bytes_completed) = related_bytes_completed.get(&related_file.name()) {
182                if bytes_completed != &related_file.bytes_completed() {
183                    value_changed = true;
184                    previous_value = *bytes_completed;
185                }
186            } else {
187                related_bytes_completed.insert(related_file.name(), related_file.bytes_completed());
188                self.bytes_completed
189                    .set(obj.bytes_completed() + related_file.bytes_completed());
190                self.obj().notify_bytes_completed();
191            }
192
193            if value_changed {
194                related_bytes_completed.insert(related_file.name(), related_file.bytes_completed());
195                self.bytes_completed
196                    .set(obj.bytes_completed() - previous_value);
197                self.bytes_completed
198                    .set(obj.bytes_completed() + related_file.bytes_completed());
199                self.obj().notify_bytes_completed();
200            }
201        }
202    }
203}
204
205glib::wrapper! {
206    pub struct TrFile(ObjectSubclass<imp::TrFile>);
207}
208
209impl TrFile {
210    pub(crate) fn from_rpc_file(id: i32, rpc_file: &File, torrent: &TrTorrent) -> Self {
211        let name = rpc_file.name.clone();
212        let title = imp::TrFile::find_title(&name);
213
214        glib::Object::builder()
215            .property("id", id)
216            .property("name", &name)
217            .property("title", &title)
218            .property("length", rpc_file.length)
219            .property("is-folder", false)
220            .property("torrent", torrent)
221            .build()
222    }
223
224    pub(crate) fn new_folder(name: &str, torrent: &TrTorrent) -> Self {
225        let title = imp::TrFile::find_title(name);
226
227        glib::Object::builder()
228            .property("id", -1)
229            .property("name", name)
230            .property("title", &title)
231            .property("is-folder", true)
232            .property("torrent", torrent)
233            .build()
234    }
235
236    /// Add a new related [TrFile] to `self` (which is a folder)
237    pub(crate) fn add_related(&self, file: &TrFile) {
238        assert!(self.is_folder());
239        let imp = self.imp();
240
241        self.related().add_file(file);
242
243        file.connect_notify_local(
244            Some("wanted"),
245            clone!(
246                #[weak(rename_to = this)]
247                imp,
248                move |file, _| {
249                    this.update_related_wanted(file);
250                }
251            ),
252        );
253        file.connect_notify_local(
254            Some("wanted-inconsistent"),
255            clone!(
256                #[weak(rename_to = this)]
257                imp,
258                move |file, _| {
259                    this.update_related_wanted(file);
260                }
261            ),
262        );
263        imp.update_related_wanted(file);
264
265        file.connect_notify_local(
266            Some("length"),
267            clone!(
268                #[weak(rename_to = this)]
269                imp,
270                move |file, _| {
271                    this.update_related_length(file);
272                }
273            ),
274        );
275        imp.update_related_length(file);
276
277        file.connect_notify_local(
278            Some("bytes-completed"),
279            clone!(
280                #[weak(rename_to = this)]
281                imp,
282                move |file, _| {
283                    this.update_related_bytes_completed(file);
284                }
285            ),
286        );
287        imp.update_related_bytes_completed(file);
288    }
289
290    /// Updates the values of `self` (which is not a folder)
291    pub(crate) fn refresh_values(&self, rpc_file_stat: &FileStat) {
292        assert!(!self.is_folder());
293        let imp = self.imp();
294
295        // bytes_completed
296        if imp.bytes_completed.get() != rpc_file_stat.bytes_completed {
297            imp.bytes_completed.set(rpc_file_stat.bytes_completed);
298            self.notify_bytes_completed();
299        }
300
301        // wanted
302        if imp.wanted.get() != rpc_file_stat.wanted {
303            imp.wanted.set(rpc_file_stat.wanted);
304            self.notify_wanted();
305        }
306    }
307}