1use std::collections::HashSet;
19
20use crate::error::{Error, Result};
21use crate::library::Library;
22use crate::model::{Folder, Item};
23
24fn chain<'a>(folders: &'a [Folder], id: &str) -> Option<Vec<&'a Folder>> {
27 for f in folders {
28 if f.id == id {
29 return Some(vec![f]);
30 }
31 if let Some(mut below) = chain(&f.children, id) {
32 below.insert(0, f);
33 return Some(below);
34 }
35 }
36 None
37}
38
39pub fn folder_auto_tags(folders: &[Folder], id: &str) -> Vec<String> {
44 let Some(chain) = chain(folders, id) else { return Vec::new() };
45 let mut seen = HashSet::new();
46 let mut out: Vec<String> = Vec::new();
47 for f in chain.iter().rev() {
48 for t in &f.tags {
49 if seen.insert(t.as_str()) {
50 out.push(t.clone());
51 }
52 }
53 }
54 out.reverse();
55 out
56}
57
58pub fn auto_tags_for(folders: &[Folder], ids: &[String]) -> Vec<String> {
61 let mut out = Vec::new();
62 for id in ids {
63 append_new(&mut out, &folder_auto_tags(folders, id));
64 }
65 out
66}
67
68pub fn append_new(tags: &mut Vec<String>, add: &[String]) -> bool {
71 let before = tags.len();
72 for t in add {
73 if !tags.contains(t) {
74 tags.push(t.clone());
75 }
76 }
77 tags.len() != before
78}
79
80pub fn join_folder(item: &mut Item, folders: &[Folder], id: &str) -> bool {
85 if item.folders.iter().any(|f| f == id) {
86 return false;
87 }
88 item.folders.push(id.to_string());
89 item.is_deleted = false;
90 append_new(&mut item.tags, &folder_auto_tags(folders, id));
91 true
92}
93
94fn subtree_ids(folders: &[Folder], id: &str) -> HashSet<String> {
96 let mut out = HashSet::new();
97 if let Some(f) = chain(folders, id).and_then(|c| c.last().copied()) {
98 f.walk(&mut |x| {
99 out.insert(x.id.clone());
100 });
101 }
102 out
103}
104
105impl Library {
106 pub fn set_folder_auto_tags(&mut self, id: &str, tags: &[String]) -> Result<usize> {
114 let mut clean: Vec<String> = Vec::new();
115 append_new(&mut clean, tags);
116 let Some(folder) = chain(&self.metadata.folders, id).and_then(|c| c.last().copied()) else {
117 return Err(Error::Import { path: self.root().to_path_buf(), reason: format!("no folder {id}") });
118 };
119 if folder.tags == clean {
120 return Ok(0);
121 }
122 set_tags(&mut self.metadata.folders, id, clean.clone());
123 self.save_metadata()?;
124 let scope = subtree_ids(&self.metadata.folders, id);
125 let changed: Vec<Item> = self
126 .items()
127 .iter()
128 .filter(|i| i.folders.iter().any(|f| scope.contains(f)))
129 .filter_map(|i| {
130 let mut i = i.clone();
131 append_new(&mut i.tags, &clean).then_some(i)
132 })
133 .collect();
134 let n = changed.len();
135 for item in changed {
136 self.update_item(item)?;
137 }
138 Ok(n)
139 }
140}
141
142fn set_tags(folders: &mut [Folder], id: &str, tags: Vec<String>) {
143 for f in folders {
144 if f.id == id {
145 f.tags = tags;
146 return;
147 }
148 set_tags(&mut f.children, id, tags.clone());
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn folder(id: &str, tags: &[&str], children: Vec<Folder>) -> Folder {
157 let mut f = Folder::new(id, id, 0);
158 f.tags = tags.iter().map(|t| t.to_string()).collect();
159 f.children = children;
160 f
161 }
162
163 fn tree() -> Vec<Folder> {
164 vec![
165 folder("top", &["a", "b"], vec![folder("mid", &["c", "b"], vec![folder("leaf", &["d", "e"], vec![]), folder("bare", &[], vec![])])]),
166 folder("other", &["x"], vec![]),
167 ]
168 }
169
170 fn s(v: &[&str]) -> Vec<String> {
171 v.iter().map(|t| t.to_string()).collect()
172 }
173
174 #[test]
175 fn own_tags_then_ancestors_reversed() {
176 let t = tree();
177 assert_eq!(folder_auto_tags(&t, "top"), s(&["b", "a"]));
178 assert_eq!(folder_auto_tags(&t, "mid"), s(&["a", "b", "c"]));
180 assert_eq!(folder_auto_tags(&t, "leaf"), s(&["a", "b", "c", "e", "d"]));
182 assert_eq!(folder_auto_tags(&t, "bare"), s(&["a", "b", "c"]));
184 assert!(folder_auto_tags(&t, "missing").is_empty());
185 }
186
187 #[test]
188 fn several_folders_keep_first_copies() {
189 let t = tree();
190 assert_eq!(auto_tags_for(&t, &s(&["other", "mid"])), s(&["x", "a", "b", "c"]));
191 assert_eq!(auto_tags_for(&t, &s(&["top", "mid"])), s(&["b", "a", "c"]));
192 assert!(auto_tags_for(&t, &[]).is_empty());
193 }
194
195 #[test]
196 fn append_keeps_order_and_skips_present() {
197 let mut tags = s(&["b", "z"]);
198 assert!(append_new(&mut tags, &s(&["a", "b", "c"])));
199 assert_eq!(tags, s(&["b", "z", "a", "c"]));
200 assert!(!append_new(&mut tags, &s(&["a", "c"])));
201 }
202
203 fn item(folders: &[&str], tags: &[&str]) -> Item {
204 let mut i: Item = serde_json::from_str(r#"{"id":"I","name":"n","size":1,"btime":0,"mtime":0,"ext":"png","tags":[],"folders":[],"isDeleted":true,"url":"","annotation":"","modificationTime":0,"height":0,"width":0,"lastModified":0,"palettes":[]}"#).unwrap();
205 i.folders = s(folders);
206 i.tags = s(tags);
207 i
208 }
209
210 #[test]
211 fn joining_adds_tags_only_once() {
212 let t = tree();
213 let mut it = item(&["other"], &["c", "mine"]);
214 assert!(join_folder(&mut it, &t, "mid"));
215 assert_eq!(it.folders, s(&["other", "mid"]));
216 assert_eq!(it.tags, s(&["c", "mine", "a", "b"]));
217 assert!(!it.is_deleted);
218 it.tags = s(&["mine"]);
220 assert!(!join_folder(&mut it, &t, "mid"));
221 assert_eq!(it.tags, s(&["mine"]));
222 }
223
224 #[test]
225 fn setting_tags_reaches_items_below() {
226 let tmp = tempfile::tempdir().unwrap();
227 let mut lib = Library::create(tmp.path().join("A.library")).unwrap();
228 lib.metadata.folders = tree();
229 lib.save_metadata().unwrap();
230 for (id, folders) in [("A1", vec!["leaf"]), ("A2", vec!["top"]), ("A3", vec!["other"]), ("A4", vec!["bare"])] {
231 let mut i = item(&[], &["k"]);
232 i.id = id.into();
233 i.folders = s(&folders);
234 std::fs::create_dir_all(i.info_dir(lib.root())).unwrap();
235 std::fs::write(i.original_path(lib.root()), b"x").unwrap();
236 lib.register_item(i).unwrap();
237 }
238 assert_eq!(lib.set_folder_auto_tags("mid", &s(&["n", "k", "n"])).unwrap(), 2);
240 assert_eq!(lib.item("A1").unwrap().tags, s(&["k", "n"]));
241 assert_eq!(lib.item("A4").unwrap().tags, s(&["k", "n"]));
242 assert_eq!(lib.item("A2").unwrap().tags, s(&["k"]));
243 assert_eq!(lib.item("A3").unwrap().tags, s(&["k"]));
244 let mid = &lib.metadata.folders[0].children[0];
245 assert_eq!((mid.tags.clone(), mid.extend_tags.clone()), (s(&["n", "k"]), None));
246 assert_eq!(lib.set_folder_auto_tags("mid", &s(&["n", "k"])).unwrap(), 0);
248 assert_eq!(lib.set_folder_auto_tags("mid", &s(&["k"])).unwrap(), 0);
250 assert_eq!(lib.item("A1").unwrap().tags, s(&["k", "n"]));
251 let reopened = Library::open(lib.root()).unwrap();
252 assert_eq!(reopened.metadata.folders[0].children[0].tags, s(&["k"]));
253 assert!(lib.set_folder_auto_tags("missing", &[]).is_err());
254 }
255}