| 
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