|
1 | 1 | import fse from 'fs-extra'; |
| 2 | +import { getAttribute, setAttribute } from 'fs-xattr' |
2 | 3 | import { action, computed, makeObservable, observable, runInAction } from 'mobx'; |
3 | 4 |
|
4 | 5 | import { getThumbnailPath } from 'common/fs'; |
@@ -128,6 +129,92 @@ class FileStore { |
128 | 129 | } |
129 | 130 | } |
130 | 131 |
|
| 132 | + @action.bound async readExtendedAttributeTagsFromFiles(): Promise<void> { |
| 133 | + // NOTE: https://wiki.archlinux.org/title/Extended_attributes |
| 134 | + // implemented KDE Dolphin tags and ratings |
| 135 | + const toastKey = 'read-tags-from-file'; |
| 136 | + try { |
| 137 | + const numFiles = this.fileList.length; |
| 138 | + for (let i = 0; i < numFiles; i++) { |
| 139 | + AppToaster.show( |
| 140 | + { |
| 141 | + message: `Reading tags from files ${((100 * i) / numFiles).toFixed(0)}%...`, |
| 142 | + timeout: 0, |
| 143 | + }, |
| 144 | + toastKey, |
| 145 | + ); |
| 146 | + const file = runInAction(() => this.fileList[i]); |
| 147 | + |
| 148 | + const absolutePath = file.absolutePath; |
| 149 | + |
| 150 | + try { |
| 151 | + // a buffer with comma separated strings |
| 152 | + const kdeTags = await getAttribute(absolutePath, 'user.xdg.tags') |
| 153 | + // balooScore is 5/5 stars, but is double in xfattr |
| 154 | + const balooScore = await getAttribute(absolutePath, 'user.baloo.rating') |
| 155 | + // convert buffer to string, then split in array. Also remove trailing whitespace |
| 156 | + let tagsNameHierarchies = kdeTags.toString().split(',').filter(String) |
| 157 | + tagsNameHierarchies.push('score:' + balooScore) |
| 158 | + |
| 159 | + // Now that we know the tag names in file metadata, add them to the files in OneFolder |
| 160 | + |
| 161 | + const { tagStore } = this.rootStore; |
| 162 | + for (const tagHierarchy of tagsNameHierarchies) { |
| 163 | + const match = tagStore.findByName(tagHierarchy[tagHierarchy.length - 1]); |
| 164 | + if (match) { |
| 165 | + // If there is a match to the leaf tag, just add it to the file |
| 166 | + file.addTag(match); |
| 167 | + } else { |
| 168 | + // If there is no direct match to the leaf, insert it in the tag hierarchy: first check if any of its parents exist |
| 169 | + // parent tags are written as: parentTag/subparent/subtag for example |
| 170 | + if (tagHierarchy.includes('/')) { |
| 171 | + let curTag = tagStore.root; |
| 172 | + // further check for subparents |
| 173 | + for (const nodeName of tagHierarchy.split('/')) { |
| 174 | + const nodeMatch = tagStore.findByName(nodeName); |
| 175 | + if (nodeMatch) { |
| 176 | + curTag = nodeMatch; |
| 177 | + } else { |
| 178 | + curTag = await tagStore.create(curTag, nodeName); |
| 179 | + } |
| 180 | + } |
| 181 | + file.addTag(curTag); |
| 182 | + } else { |
| 183 | + // base tag, not a parent tag |
| 184 | + let curTag = tagStore.root; |
| 185 | + const nodeMatch = tagStore.findByName(tagHierarchy); |
| 186 | + if (nodeMatch) { |
| 187 | + curTag = nodeMatch; |
| 188 | + } else { |
| 189 | + curTag = await tagStore.create(curTag, tagHierarchy); |
| 190 | + } |
| 191 | + file.addTag(curTag); |
| 192 | + } |
| 193 | + } |
| 194 | + } |
| 195 | + } catch (e) { |
| 196 | + console.error('Could not import tags for', absolutePath, e); |
| 197 | + } |
| 198 | + } |
| 199 | + AppToaster.show( |
| 200 | + { |
| 201 | + message: 'Reading tags from files... Done!', |
| 202 | + timeout: 5000, |
| 203 | + }, |
| 204 | + toastKey, |
| 205 | + ); |
| 206 | + } catch (e) { |
| 207 | + console.error('Could not read tags', e); |
| 208 | + AppToaster.show( |
| 209 | + { |
| 210 | + message: 'Reading tags from files failed. Check the dev console for more details', |
| 211 | + timeout: 5000, |
| 212 | + }, |
| 213 | + toastKey, |
| 214 | + ); |
| 215 | + } |
| 216 | + } |
| 217 | + |
131 | 218 | // @action.bound async readFacesAnnotationsFromFiles(): Promise<void> { |
132 | 219 | // const toastKey = 'read-faces-annotations-from-file'; |
133 | 220 | // try { |
@@ -233,6 +320,80 @@ class FileStore { |
233 | 320 | } |
234 | 321 | } |
235 | 322 |
|
| 323 | + @action.bound async writeExtendedAttributeTagsToFiles(): Promise<void> { |
| 324 | + const toastKey = 'write-tags-to-file'; |
| 325 | + try { |
| 326 | + const numFiles = this.fileList.length; |
| 327 | + const tagFilePairs = runInAction(() => |
| 328 | + this.fileList.map((f) => ({ |
| 329 | + absolutePath: f.absolutePath, |
| 330 | + tagHierarchy: Array.from( |
| 331 | + f.tags, |
| 332 | + action((t) => t.path), |
| 333 | + ), |
| 334 | + })), |
| 335 | + ); |
| 336 | + let lastToastVal = '0'; |
| 337 | + for (let i = 0; i < tagFilePairs.length; i++) { |
| 338 | + const newToastVal = ((100 * i) / numFiles).toFixed(0); |
| 339 | + if (lastToastVal !== newToastVal) { |
| 340 | + lastToastVal = newToastVal; |
| 341 | + AppToaster.show( |
| 342 | + { |
| 343 | + message: `Writing tags to files ${newToastVal}%...`, |
| 344 | + timeout: 0, |
| 345 | + }, |
| 346 | + toastKey, |
| 347 | + ); |
| 348 | + } |
| 349 | + |
| 350 | + const { absolutePath, tagHierarchy } = tagFilePairs[i]; |
| 351 | + try { |
| 352 | + let tagArray = [] |
| 353 | + let balooScore = '0' |
| 354 | + for (const tagH of tagHierarchy) { |
| 355 | + // readExtendedAttributeTagsFromFiles creates score:# for balooScore |
| 356 | + // tagH is an array; even with one element |
| 357 | + if (tagH[0].includes('score')) { |
| 358 | + balooScore = tagH[0].split(':')[1] |
| 359 | + // skipping adding it to tagArray |
| 360 | + continue |
| 361 | + } |
| 362 | + if (typeof(tagH) != 'string') { |
| 363 | + // concatenate parents with their sub elements |
| 364 | + tagArray.push(tagH.join('/')) |
| 365 | + } else { |
| 366 | + tagArray.push(tagH) |
| 367 | + } |
| 368 | + }; |
| 369 | + |
| 370 | + // tagArray now must be joined into one string, comma separated |
| 371 | + await setAttribute(absolutePath, 'user.xdg.tags', tagArray.join(',')); |
| 372 | + await setAttribute(absolutePath, 'user.baloo.rating', String(balooScore)); |
| 373 | + |
| 374 | + } catch (e) { |
| 375 | + console.error('Could not write tags to', absolutePath, tagHierarchy, e); |
| 376 | + } |
| 377 | + } |
| 378 | + AppToaster.show( |
| 379 | + { |
| 380 | + message: 'Writing tags to files... Done!', |
| 381 | + timeout: 5000, |
| 382 | + }, |
| 383 | + toastKey, |
| 384 | + ); |
| 385 | + } catch (e) { |
| 386 | + console.error('Could not write tags', e); |
| 387 | + AppToaster.show( |
| 388 | + { |
| 389 | + message: 'Writing tags to files failed. Check the dev console for more details', |
| 390 | + timeout: 5000, |
| 391 | + }, |
| 392 | + toastKey, |
| 393 | + ); |
| 394 | + } |
| 395 | + } |
| 396 | + |
236 | 397 | @computed get showsAllContent(): boolean { |
237 | 398 | return this.content === Content.All; |
238 | 399 | } |
|
0 commit comments