From fba2f8f46ba9e45591b4dc3c5ffce59033d8acaf Mon Sep 17 00:00:00 2001 From: DrRetro Date: Wed, 24 Apr 2024 11:31:53 -0400 Subject: [PATCH 01/59] Multi-Select Tag Adding --- .vscode/launch.json | 4 ++-- start_win.bat | 2 +- tagstudio/src/qt/ts_qt.py | 22 +++++++++++++--------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e9505d33..10f880f3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,12 +6,12 @@ "configurations": [ { "name": "TagStudio", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${workspaceRoot}\\TagStudio\\tagstudio.py", "console": "integratedTerminal", "justMyCode": true, - "args": [] + "args": ["--debug"] } ] } diff --git a/start_win.bat b/start_win.bat index 51a5ddf8..9258657f 100644 --- a/start_win.bat +++ b/start_win.bat @@ -1,2 +1,2 @@ @echo off -.venv\Scripts\python.exe .\TagStudio\tagstudio.py --ui qt %* \ No newline at end of file +.venv\Scripts\python.exe .\TagStudio\tagstudio.py --ui qt %* --debug \ No newline at end of file diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 45ebd0d8..2d007f46 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -395,15 +395,19 @@ class TagBoxWidget(FieldWidget): # self.base_layout.addWidget(TagWidget(self.lib, self.lib.get_tag(tag), True)) # self.tags.append(tag) logging.info(f'[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}') - if type(self.item) == Entry: - self.item.add_tag(self.lib, tag_id, field_id=-1, field_index=self.field_index) - logging.info(f'[TAG BOX WIDGET] UPDATED EMITTED: {tag_id}') - self.updated.emit() - # logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}') - # self.updated.emit() - # if tag_id not in self.tags: - # self.tags.append(tag_id) - # self.set_tags(self.tags) + logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') + for x in self.driver.selected: + if x[0] == ItemType.ENTRY: + self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=-1, field_index=self.field_index) + logging.info(f'[TAG BOX WIDGET] UPDATED EMITTED: {tag_id}') + self.updated.emit() + # logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}') + # self.updated.emit() + # if tag_id not in self.tags: + # self.tags.append(tag_id) + # self.set_tags(self.tags) + # elif type((x[0]) == ThumbButton): + def edit_tag_callback(self, tag:Tag): self.lib.update_tag(tag) From 3974c1b031e4b500abb855741075c4ccfaef216e Mon Sep 17 00:00:00 2001 From: DrRetro Date: Wed, 24 Apr 2024 12:09:07 -0400 Subject: [PATCH 02/59] Multi-Select Removing Tags --- tagstudio/src/qt/ts_qt.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 2d007f46..329cdf55 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -360,7 +360,7 @@ class TagBoxWidget(FieldWidget): # ) tw = TagWidget(self.lib, self.lib.get_tag(tag), True, True) tw.on_click.connect(lambda checked=False, q=f'tag_id: {tag}': (self.driver.main_window.searchField.setText(q), self.driver.filter_items(q))) - tw.on_remove.connect(lambda checked=False, t=tag: (self.lib.get_entry(self.item.id).remove_tag(self.lib, t, self.field_index), self.updated.emit())) + tw.on_remove.connect(lambda checked=False, t=tag: (self.remove_tag(t))) tw.on_edit.connect(lambda checked=False, t=tag: (self.edit_tag(t))) self.base_layout.addWidget(tw) self.tags = tags @@ -375,7 +375,8 @@ class TagBoxWidget(FieldWidget): # doesn't move all the way to the left. if self.base_layout.itemAt(0) and not self.base_layout.itemAt(1): self.base_layout.update() - + + def edit_tag(self, tag_id:int): btp = BuildTagPanel(self.lib, tag_id) # btp.on_edit.connect(lambda x: self.edit_tag_callback(x)) @@ -396,6 +397,7 @@ class TagBoxWidget(FieldWidget): # self.tags.append(tag) logging.info(f'[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}') logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') + #Uses selected list from driver to remove tag from all selected files. for x in self.driver.selected: if x[0] == ItemType.ENTRY: self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=-1, field_index=self.field_index) @@ -413,9 +415,14 @@ class TagBoxWidget(FieldWidget): self.lib.update_tag(tag) - def remove_tag(self): - # NOTE: You'll need to account for the add button at the end. - pass + def remove_tag(self, tag_id): + #Uses selected list from driver to remove tag from all selected files. + logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') + for x in self.driver.selected: + if x[0] == ItemType.ENTRY: + self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id, field_index=self.field_index) + self.updated.emit() + # def show_add_button(self, value:bool): # self.add_button.setHidden(not value) From def244b7325dff89784dbad6c27d271b6c401998 Mon Sep 17 00:00:00 2001 From: DrRetro Date: Wed, 24 Apr 2024 15:18:22 -0400 Subject: [PATCH 03/59] Fixed bug that occured with adding other fields to one entry. --- tagstudio/src/qt/ts_qt.py | 100 +++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 329cdf55..62a36af0 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -397,11 +397,51 @@ class TagBoxWidget(FieldWidget): # self.tags.append(tag) logging.info(f'[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}') logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') - #Uses selected list from driver to remove tag from all selected files. + # TODO: Make this more efficient. + # Iterate through the fields of the item and find the field that + # contains the tags. + correct_field_id = 0 # Initialize the field index + skip = False # Initialize the skip flag + temp_tags = self.tags.copy() # Make a copy of the tags + while correct_field_id < len(self.item.fields): + field = list(self.item.fields)[correct_field_id] + for key in dict(field).keys(): + temp = field[key] + if type(temp) == list: + # Check if the this field's tags list to see if it matches the current field's list + if self.tags == temp: + # Update the correct_field_id if a match is found + correct_field_id = key + skip = True + break + if skip: + break + correct_field_id += 1 for x in self.driver.selected: if x[0] == ItemType.ENTRY: - self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=-1, field_index=self.field_index) - logging.info(f'[TAG BOX WIDGET] UPDATED EMITTED: {tag_id}') + logging.info(f'[TAG BOX WIDGET] FIELDS:{self.driver.lib.get_entry(x[1]).fields}') + index = 0 # Initialize the index + skip = False # Initialize the skip flag + if not skip: + # Iterate through the fields of the current entry + while index < len(self.driver.lib.get_entry(x[1]).fields): + # Get the current fields + field = list(self.driver.lib.get_entry(x[1]).fields)[index] + for key in dict(field).keys(): + temp = field[key] + if type(temp) == list: + if temp_tags == temp and key == correct_field_id: + skip = True + break + if skip: + break + else: + index += 1 # Increment the index + if not skip and index >= len(self.driver.lib.get_entry(x[1]).fields): + # Add the field to the entry + self.driver.lib.add_field_to_entry(x[1],correct_field_id) + index = 0 + self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=correct_field_id, field_index=index) self.updated.emit() # logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}') # self.updated.emit() @@ -413,14 +453,59 @@ class TagBoxWidget(FieldWidget): def edit_tag_callback(self, tag:Tag): self.lib.update_tag(tag) - - + def remove_tag(self, tag_id): - #Uses selected list from driver to remove tag from all selected files. logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') + + # Iterate through the fields of the item and find the field that + # contains the tags. + # TODO: Make this more efficient. + correct_field_id = 0 # Initialize the field index + skip = False # Initialize the skip flag + temp_tags = self.tags.copy() # Make a copy of the tags + while correct_field_id < len(self.item.fields): + # Get the current field + field = list(self.item.fields)[correct_field_id] + for key in dict(field).keys(): + temp = field[key] + if type(temp) == list: + # Check if the this field's tags list to see if it matches the current field's list + if self.tags == temp: + # Update the correct_field_id if a match is found + correct_field_id = key + skip = True + break + if skip: + break + correct_field_id += 1 + # Iterate through the selected entries for x in self.driver.selected: + # Check if the current entry is of type Entry if x[0] == ItemType.ENTRY: - self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id, field_index=self.field_index) + index = 0 # Initialize the index + skip = False # Initialize the skip flag + # Check if the skip flag is False + if not skip: + # Iterate through the fields of the current entry + while index < len(self.driver.lib.get_entry(x[1]).fields): + # Get the current fields + field = list(self.driver.lib.get_entry(x[1]).fields)[index] + for key in dict(field).keys(): + temp = field[key] + if type(temp) == list: + if temp_tags == temp and key == correct_field_id: + skip = True + break + if skip: + break + else: + index += 1 # Increment the index + # Check if the skip flag is False and the index is out of bounds + if not skip and index >= len(self.driver.lib.get_entry(x[1]).fields): + # Add the field to the entry + self.driver.lib.add_field_to_entry(x[1],correct_field_id) + index = 0 # Reset the index + self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id, field_index=index) self.updated.emit() # def show_add_button(self, value:bool): @@ -428,6 +513,7 @@ class TagBoxWidget(FieldWidget): class TextWidget(FieldWidget): + def __init__(self, title, text:str) -> None: super().__init__(title) # self.item = item From c7492b78d3f442abecc42260b851cf1f3d5d7ce6 Mon Sep 17 00:00:00 2001 From: Dakota Marshall Date: Wed, 24 Apr 2024 15:39:12 -0400 Subject: [PATCH 04/59] Add flake files for working Nix environment --- flake.lock | 27 +++++++++++++++++++++ flake.nix | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..fa6aa60e --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1688072080, + "narHash": "sha256-mPsxREerkmDU2m7zi8j19ooc1F9wKPWBJ77S7swXMmI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5a8650469a9f8a1958ff9373bd27fb8e54c4365d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..ec551544 --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs, }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + in { + devShells.x86_64-linux.default = pkgs.mkShell { + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.gcc-unwrapped + pkgs.zlib + pkgs.libglvnd + pkgs.glib + pkgs.stdenv.cc.cc + pkgs.fontconfig + pkgs.libxkbcommon + pkgs.xorg.libxcb + pkgs.freetype + pkgs.dbus + pkgs.qt6.qtwayland + pkgs.zstd + ]; + buildInputs = with pkgs; [ + cmake + gdb + zstd + qt6.qtbase + qt6.full + qt6.qtwayland + qtcreator + python310Packages.pip + python310Full + python310Packages.virtualenv # run virtualenv . + # python3Packages.pyqt5 # avoid installing via pip + python310Packages.pyusb # fixes the pyusb 'No backend available' when installed directly via pip + + gcc.cc.libgcc + makeWrapper + bashInteractive + glib + libxkbcommon + freetype + binutils + dbus + coreutils + libGL + libGLU + fontconfig + # wrapQtAppsHook + xorg.libxcb + + + # this is for the shellhook portion + qt6.wrapQtAppsHook + makeWrapper + bashInteractive + ]; + # set the environment variables that Qt apps expect + shellHook = '' + export QT_QPA_PLATFORM=wayland + export LIBRARY_PATH=/usr/lib:/usr/lib64:$LIBRARY_PATH + # export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib/:/run/opengl-driver/lib/ + export QT_PLUGIN_PATH=${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.qtPluginPrefix} + bashdir=$(mktemp -d) + makeWrapper "$(type -p bash)" "$bashdir/bash" "''${qtWrapperArgs[@]}" + exec "$bashdir/bash" + ''; + }; + }; +} From c4e8d40abe84f90b5e4b16a769e75cb954ef9ded Mon Sep 17 00:00:00 2001 From: Dakota Marshall Date: Wed, 24 Apr 2024 15:39:21 -0400 Subject: [PATCH 05/59] Update bash script to use env for nix support --- TagStudio.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TagStudio.sh b/TagStudio.sh index 82036413..cff969c3 100755 --- a/TagStudio.sh +++ b/TagStudio.sh @@ -1,5 +1,5 @@ -#! /bin/bash +#! /usr/bin/env bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt -python tagstudio/tagstudio.py \ No newline at end of file +python tagstudio/tagstudio.py From bc0f8b991ea5ba6ef2ad278d815ae9b47ac4173f Mon Sep 17 00:00:00 2001 From: Dakota Marshall Date: Wed, 24 Apr 2024 15:39:36 -0400 Subject: [PATCH 06/59] Update install documentation for NixOS --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 2afe1c0b..3563733f 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,10 @@ With the virtual environment loaded, run the python file at "tagstudio/tagstudio Run the "TagStudio.sh" script, and the program should launch! (Make sure that the script is marked as executable). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `sh TagStudio.sh`. +##### NixOS + +Use the provided `flake.nix` file to create and enter a working environment by running `nix develop`. Then, run the above `TagStudio.sh` script. + ## Usage ### Creating/Opening a Library From 4184848f9ce623a6a6ab32c8f5fec62af6c75706 Mon Sep 17 00:00:00 2001 From: Dakota Marshall Date: Wed, 24 Apr 2024 16:51:06 -0400 Subject: [PATCH 07/59] Update Nix flake pinned hash for python3.12 and QT 6.6.3 support --- flake.lock | 6 +++--- flake.nix | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index fa6aa60e..5dafd370 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1688072080, - "narHash": "sha256-mPsxREerkmDU2m7zi8j19ooc1F9wKPWBJ77S7swXMmI=", + "lastModified": 1712473363, + "narHash": "sha256-TIScFAVdI2yuybMxxNjC4YZ/j++c64wwuKbpnZnGiyU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5a8650469a9f8a1958ff9373bd27fb8e54c4365d", + "rev": "e89cf1c932006531f454de7d652163a9a5c86668", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ec551544..f827ba07 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,8 @@ pkgs.freetype pkgs.dbus pkgs.qt6.qtwayland + pkgs.qt6.full + pkgs.qt6.qtbase pkgs.zstd ]; buildInputs = with pkgs; [ @@ -28,13 +30,12 @@ qt6.full qt6.qtwayland qtcreator - python310Packages.pip - python310Full - python310Packages.virtualenv # run virtualenv . - # python3Packages.pyqt5 # avoid installing via pip - python310Packages.pyusb # fixes the pyusb 'No backend available' when installed directly via pip + python312Packages.pip + python312Full + python312Packages.virtualenv # run virtualenv . + python312Packages.pyusb # fixes the pyusb 'No backend available' when installed directly via pip - gcc.cc.libgcc + libgcc makeWrapper bashInteractive glib @@ -46,7 +47,6 @@ libGL libGLU fontconfig - # wrapQtAppsHook xorg.libxcb From 9e61d45ea502d8d9deb7cf59245361a3f3232536 Mon Sep 17 00:00:00 2001 From: DrRetro Date: Wed, 24 Apr 2024 18:11:41 -0400 Subject: [PATCH 08/59] Rewrote Multi-Select to use field templates. --- tagstudio/src/qt/ts_qt.py | 114 +++++--------------------------------- 1 file changed, 13 insertions(+), 101 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 62a36af0..c5d2f942 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -292,7 +292,7 @@ class FieldWidget(QWidget): class TagBoxWidget(FieldWidget): updated = Signal() - + def __init__(self, item, title, field_index, library:Library, tags:list[int], driver:'QtDriver') -> None: super().__init__(title) # QObject.__init__(self) @@ -397,58 +397,17 @@ class TagBoxWidget(FieldWidget): # self.tags.append(tag) logging.info(f'[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}') logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') - # TODO: Make this more efficient. - # Iterate through the fields of the item and find the field that - # contains the tags. - correct_field_id = 0 # Initialize the field index - skip = False # Initialize the skip flag - temp_tags = self.tags.copy() # Make a copy of the tags - while correct_field_id < len(self.item.fields): - field = list(self.item.fields)[correct_field_id] - for key in dict(field).keys(): - temp = field[key] - if type(temp) == list: - # Check if the this field's tags list to see if it matches the current field's list - if self.tags == temp: - # Update the correct_field_id if a match is found - correct_field_id = key - skip = True - break - if skip: - break - correct_field_id += 1 + logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.title}') + id = list(self.lib.filter_field_templates(str(self.title).removesuffix(' (Tag Box)')))[0] for x in self.driver.selected: - if x[0] == ItemType.ENTRY: - logging.info(f'[TAG BOX WIDGET] FIELDS:{self.driver.lib.get_entry(x[1]).fields}') - index = 0 # Initialize the index - skip = False # Initialize the skip flag - if not skip: - # Iterate through the fields of the current entry - while index < len(self.driver.lib.get_entry(x[1]).fields): - # Get the current fields - field = list(self.driver.lib.get_entry(x[1]).fields)[index] - for key in dict(field).keys(): - temp = field[key] - if type(temp) == list: - if temp_tags == temp and key == correct_field_id: - skip = True - break - if skip: - break - else: - index += 1 # Increment the index - if not skip and index >= len(self.driver.lib.get_entry(x[1]).fields): - # Add the field to the entry - self.driver.lib.add_field_to_entry(x[1],correct_field_id) - index = 0 - self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=correct_field_id, field_index=index) + self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=id, field_index=-1) self.updated.emit() - # logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}') - # self.updated.emit() - # if tag_id not in self.tags: - # self.tags.append(tag_id) - # self.set_tags(self.tags) - # elif type((x[0]) == ThumbButton): + # logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}') + # self.updated.emit() + # if tag_id not in self.tags: + # self.tags.append(tag_id) + # self.set_tags(self.tags) + # elif type((x[0]) == ThumbButton): def edit_tag_callback(self, tag:Tag): @@ -456,57 +415,10 @@ class TagBoxWidget(FieldWidget): def remove_tag(self, tag_id): logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') - - # Iterate through the fields of the item and find the field that - # contains the tags. - # TODO: Make this more efficient. - correct_field_id = 0 # Initialize the field index - skip = False # Initialize the skip flag - temp_tags = self.tags.copy() # Make a copy of the tags - while correct_field_id < len(self.item.fields): - # Get the current field - field = list(self.item.fields)[correct_field_id] - for key in dict(field).keys(): - temp = field[key] - if type(temp) == list: - # Check if the this field's tags list to see if it matches the current field's list - if self.tags == temp: - # Update the correct_field_id if a match is found - correct_field_id = key - skip = True - break - if skip: - break - correct_field_id += 1 - # Iterate through the selected entries + id = list(self.lib.filter_field_templates(str(self.title).removesuffix(' (Tag Box)')))[0] for x in self.driver.selected: - # Check if the current entry is of type Entry - if x[0] == ItemType.ENTRY: - index = 0 # Initialize the index - skip = False # Initialize the skip flag - # Check if the skip flag is False - if not skip: - # Iterate through the fields of the current entry - while index < len(self.driver.lib.get_entry(x[1]).fields): - # Get the current fields - field = list(self.driver.lib.get_entry(x[1]).fields)[index] - for key in dict(field).keys(): - temp = field[key] - if type(temp) == list: - if temp_tags == temp and key == correct_field_id: - skip = True - break - if skip: - break - else: - index += 1 # Increment the index - # Check if the skip flag is False and the index is out of bounds - if not skip and index >= len(self.driver.lib.get_entry(x[1]).fields): - # Add the field to the entry - self.driver.lib.add_field_to_entry(x[1],correct_field_id) - index = 0 # Reset the index - self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id, field_index=index) - self.updated.emit() + self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id, field_id=id, field_index=-1) + self.updated.emit() # def show_add_button(self, value:bool): # self.add_button.setHidden(not value) From 4a55af66ee7e72c24e598d2ef707d0004b188fb4 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 24 Apr 2024 16:22:36 -0700 Subject: [PATCH 09/59] Remove duplicate tag IDs when loading library Tags with duplicate IDs inside a library save file are removed when opening the library. Cleans up the mess from #38. --- tagstudio/src/core/library.py | 68 ++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 4b74fe08..ac35ec7f 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -630,41 +630,45 @@ class Library: if 'id' in tag.keys(): id = tag['id'] - if int(id) >= self._next_tag_id: - self._next_tag_id = int(id) + 1 + # Don't load tags with duplicate IDs + if id not in [t.id for t in self.tags]: + if int(id) >= self._next_tag_id: + self._next_tag_id = int(id) + 1 - name = '' - if 'name' in tag.keys(): - name = tag['name'] - shorthand = '' - if 'shorthand' in tag.keys(): - shorthand = tag['shorthand'] - aliases = [] - if 'aliases' in tag.keys(): - aliases = tag['aliases'] - subtag_ids = [] - if 'subtag_ids' in tag.keys(): - subtag_ids = tag['subtag_ids'] - color = '' - if 'color' in tag.keys(): - color = tag['color'] + name = '' + if 'name' in tag.keys(): + name = tag['name'] + shorthand = '' + if 'shorthand' in tag.keys(): + shorthand = tag['shorthand'] + aliases = [] + if 'aliases' in tag.keys(): + aliases = tag['aliases'] + subtag_ids = [] + if 'subtag_ids' in tag.keys(): + subtag_ids = tag['subtag_ids'] + color = '' + if 'color' in tag.keys(): + color = tag['color'] - t = Tag( - id=int(id), - name=name, - shorthand=shorthand, - aliases=aliases, - subtags_ids=subtag_ids, - color=color - ) + t = Tag( + id=int(id), + name=name, + shorthand=shorthand, + aliases=aliases, + subtags_ids=subtag_ids, + color=color + ) - # NOTE: This does NOT use the add_tag_to_library() method! - # That method is only used for Tags added at runtime. - # This process uses the same inner methods, but waits until all of the - # Tags are registered in the Tags list before creating the Tag clusters. - self.tags.append(t) - self._map_tag_id_to_index(t, -1) - self._map_tag_strings_to_tag_id(t) + # NOTE: This does NOT use the add_tag_to_library() method! + # That method is only used for Tags added at runtime. + # This process uses the same inner methods, but waits until all of the + # Tags are registered in the Tags list before creating the Tag clusters. + self.tags.append(t) + self._map_tag_id_to_index(t, -1) + self._map_tag_strings_to_tag_id(t) + else: + logging.info(f'[LIBRARY]Skipping Tag with duplicate ID: {tag}') # Step 3: Map each Tag's subtags together now that all Tag objects in it. for t in self.tags: From 6e7567a192848ff2cb9f89c03c3bca85fce8acbf Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 24 Apr 2024 17:12:40 -0700 Subject: [PATCH 10/59] Update launch.json Updated launch.json to be cross-platform between Windows and UNIX systems --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e9505d33..f3e42404 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "TagStudio", "type": "python", "request": "launch", - "program": "${workspaceRoot}\\TagStudio\\tagstudio.py", + "program": "${workspaceRoot}/tagstudio/tagstudio.py", "console": "integratedTerminal", "justMyCode": true, "args": [] From 7b48e5e17e005d177e4e417474bdeea13fd226d1 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 19:48:25 -0600 Subject: [PATCH 11/59] Prevent Import collisions Rename tagstudio.py to tag_studio.py --- tagstudio/{tagstudio.py => tag_studio.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tagstudio/{tagstudio.py => tag_studio.py} (100%) diff --git a/tagstudio/tagstudio.py b/tagstudio/tag_studio.py similarity index 100% rename from tagstudio/tagstudio.py rename to tagstudio/tag_studio.py From c0c18dabc135409dc94508bf62179c245aa8266a Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 19:52:10 -0600 Subject: [PATCH 12/59] Optimize & Sort Imports --- tagstudio/src/core/library.py | 25 ++++++++++---------- tagstudio/src/core/ts_core.py | 7 ++---- tagstudio/src/qt/flowlayout.py | 3 +-- tagstudio/src/qt/main_window.py | 15 ++++-------- tagstudio/src/qt/pagination.py | 4 ++-- tagstudio/src/qt/ts_qt.py | 41 +++++++++++++++------------------ 6 files changed, 39 insertions(+), 56 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index ac35ec7f..e64b1b26 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -5,24 +5,23 @@ """The Library object and related methods for TagStudio.""" import datetime -from enum import Enum -import os -import traceback -from typing import Optional -import json import glob -from pathlib import Path -# from typing_extensions import deprecated -import src.core.ts_core as ts_core -from src.core.utils.web import * -from src.core.utils.str import * -from src.core.utils.fs import * -import xml.etree.ElementTree as ET +import json +import logging import sys import time -import logging +import traceback +import xml.etree.ElementTree as ET +from enum import Enum + import ujson +# from typing_extensions import deprecated +import tagstudio.src.core.ts_core as ts_core +from tagstudio.src.core.utils.fs import * +from tagstudio.src.core.utils.str import * +from tagstudio.src.core.utils.web import * + TYPE = ['file', 'meta', 'alt', 'mask'] # RESULT_TYPE = Enum('Result', ['ENTRY', 'COLLATION', 'TAG_GROUP']) class ItemType(Enum): diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 1ef7f028..da507e23 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -4,16 +4,13 @@ """The core classes and methods of TagStudio.""" -import os -from types import FunctionType # from typing import Dict, Optional, TypedDict, List import json -from pathlib import Path -import traceback +import os + # import requests # from bs4 import BeautifulSoup as bs from src.core.library import * -from src.core.field_template import FieldTemplate VERSION: str = '9.1.0' # Major.Minor.Patch VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release diff --git a/tagstudio/src/qt/flowlayout.py b/tagstudio/src/qt/flowlayout.py index 1e3c5075..b71c962d 100644 --- a/tagstudio/src/qt/flowlayout.py +++ b/tagstudio/src/qt/flowlayout.py @@ -4,9 +4,8 @@ """PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x""" -import sys from PySide6.QtCore import Qt, QMargins, QPoint, QRect, QSize -from PySide6.QtWidgets import QApplication, QLayout, QPushButton, QSizePolicy, QWidget +from PySide6.QtWidgets import QLayout, QSizePolicy, QWidget # class Window(QWidget): diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index 38c1c099..b82946c0 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -12,17 +12,10 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from re import S -import time -from typing import Optional -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform, QAction) -from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout, +from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect, + QSize, Qt) +from PySide6.QtGui import (QFont, QAction) +from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, QMenuBar, QPushButton, QScrollArea, QSizePolicy, QStatusBar, QWidget, QSplitter, QMenu) diff --git a/tagstudio/src/qt/pagination.py b/tagstudio/src/qt/pagination.py index 642cd103..c190fa14 100644 --- a/tagstudio/src/qt/pagination.py +++ b/tagstudio/src/qt/pagination.py @@ -5,10 +5,10 @@ """A pagination widget created for TagStudio.""" # I never want to see this code again. -from PySide6 import QtCore +from PySide6.QtCore import QObject, Signal, QSize from PySide6.QtGui import * from PySide6.QtWidgets import * -from PySide6.QtCore import QFile, QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QMimeData + # class NumberEdit(QLineEdit): # def __init__(self, parent=None) -> None: diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 45ebd0d8..0b7f1239 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -7,39 +7,34 @@ """A Qt driver for TagStudio.""" -from copy import copy, deepcopy import ctypes -import math -from os import times -import sys import logging -import threading -from time import sleep -from queue import Empty, Queue +import math +import sys import time -from typing import Optional, Union -from PySide6 import QtCore -import PySide6 -from PySide6.QtGui import * -from PySide6.QtWidgets import * -from PySide6.QtCore import QFile, QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QMimeData, QTimer -from PySide6.QtUiTools import QUiLoader -from PIL import Image, ImageOps, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance -import PySide6.QtWidgets -import humanfriendly -import pillow_avif -import cv2 from datetime import datetime as dt -from src.core.ts_core import * +from queue import Empty, Queue +from time import sleep +from typing import Optional + +import cv2 +import humanfriendly +from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance +from PySide6 import QtCore +from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer +from PySide6.QtGui import * +from PySide6.QtUiTools import QUiLoader +from PySide6.QtWidgets import * +# from typing_extensions import deprecated +from humanfriendly import format_timespan # from src.core.utils.web import * # from src.core.utils.fs import * from src.core.library import * from src.core.palette import ColorType, get_tag_color +from src.core.ts_core import * from src.qt.flowlayout import FlowLayout, FlowWidget from src.qt.main_window import Ui_MainWindow -import src.qt.resources_rc -# from typing_extensions import deprecated -from humanfriendly import format_timespan + # from src.qt.qtacrylic.qtacrylic import WindowEffect # SIGQUIT is not defined on Windows From 952ed8f27db51d48d906d471282b20c579c32b48 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 19:53:01 -0600 Subject: [PATCH 13/59] Remove Unused Imports --- tagstudio/src/core/library.py | 1 - tagstudio/src/core/ts_core.py | 3 --- tagstudio/src/core/utils/fs.py | 3 --- tagstudio/src/qt/main_window.py | 2 -- tagstudio/src/qt/ts_qt.py | 4 ---- 5 files changed, 13 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index e64b1b26..485e30b6 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -16,7 +16,6 @@ from enum import Enum import ujson -# from typing_extensions import deprecated import tagstudio.src.core.ts_core as ts_core from tagstudio.src.core.utils.fs import * from tagstudio.src.core.utils.str import * diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index da507e23..bb6ad790 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -4,12 +4,9 @@ """The core classes and methods of TagStudio.""" -# from typing import Dict, Optional, TypedDict, List import json import os -# import requests -# from bs4 import BeautifulSoup as bs from src.core.library import * VERSION: str = '9.1.0' # Major.Minor.Patch diff --git a/tagstudio/src/core/utils/fs.py b/tagstudio/src/core/utils/fs.py index 1307b0e8..7c1052a6 100644 --- a/tagstudio/src/core/utils/fs.py +++ b/tagstudio/src/core/utils/fs.py @@ -2,9 +2,6 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -import os - - def clean_folder_name(folder_name: str) -> str: cleaned_name = folder_name invalid_chars = "<>:\"/\\|?*." diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index b82946c0..8055d930 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -20,8 +20,6 @@ from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, QMenuBar, QPushButton, QScrollArea, QSizePolicy, QStatusBar, QWidget, QSplitter, QMenu) from src.qt.pagination import Pagination -# from src.qt.qtacrylic.qtacrylic import WindowEffect -# from qframelesswindow import FramelessMainWindow, StandardTitleBar class Ui_MainWindow(QMainWindow): diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 0b7f1239..96ac7b29 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -25,17 +25,13 @@ from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, from PySide6.QtGui import * from PySide6.QtUiTools import QUiLoader from PySide6.QtWidgets import * -# from typing_extensions import deprecated from humanfriendly import format_timespan -# from src.core.utils.web import * -# from src.core.utils.fs import * from src.core.library import * from src.core.palette import ColorType, get_tag_color from src.core.ts_core import * from src.qt.flowlayout import FlowLayout, FlowWidget from src.qt.main_window import Ui_MainWindow -# from src.qt.qtacrylic.qtacrylic import WindowEffect # SIGQUIT is not defined on Windows if sys.platform == "win32": From 0b4ccac5ff2006cf2722547754542324504e8cba Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 21:14:13 -0600 Subject: [PATCH 14/59] Import Humanfriendly format_size when importing format_timespan --- tagstudio/src/qt/ts_qt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 96ac7b29..67c48449 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -18,7 +18,6 @@ from time import sleep from typing import Optional import cv2 -import humanfriendly from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance from PySide6 import QtCore from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer @@ -31,6 +30,7 @@ from src.core.palette import ColorType, get_tag_color from src.core.ts_core import * from src.qt.flowlayout import FlowLayout, FlowWidget from src.qt.main_window import Ui_MainWindow +from humanfriendly import format_timespan, format_size # SIGQUIT is not defined on Windows @@ -2106,12 +2106,12 @@ class PreviewPanel(QWidget): # Stats for specific file types are displayed here. if extension in (IMAGE_TYPES + VIDEO_TYPES): - self.dimensions_label.setText(f"{extension.upper()} • {humanfriendly.format_size(os.stat(filepath).st_size)}\n{image.width} x {image.height} px") + self.dimensions_label.setText(f"{extension.upper()} • {format_size(os.stat(filepath).st_size)}\n{image.width} x {image.height} px") else: self.dimensions_label.setText(f"{extension.upper()}") if not image: - self.dimensions_label.setText(f"{extension.upper()} • {humanfriendly.format_size(os.stat(filepath).st_size)}") + self.dimensions_label.setText(f"{extension.upper()} • {format_size(os.stat(filepath).st_size)}") raise UnidentifiedImageError except (UnidentifiedImageError, FileNotFoundError, cv2.error): From 4e5b7b1c7df1810c8bdfda0bba4ea4f460e5ec85 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 21:51:22 -0600 Subject: [PATCH 15/59] Fix reference to datetime --- tagstudio/src/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 67c48449..ec304f83 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -4505,7 +4505,7 @@ class QtDriver(QObject): self.completed += 1 # logging.info(f'threshold:{len(self.lib.entries}, completed:{self.completed}') if self.completed == len(self.lib.entries): - filename = os.path.normpath(f'{self.lib.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}/collage_{datetime.datetime.utcnow().strftime("%F_%T").replace(":", "")}.png') + filename = os.path.normpath(f'{self.lib.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}/collage_{dt.utcnow().strftime("%F_%T").replace(":", "")}.png') self.collage.save(filename) self.collage = None From 2b5697ea50ecf665c7b0d5862672cbb36efdbc40 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 21:51:43 -0600 Subject: [PATCH 16/59] Remove wildcard Imports --- tagstudio/src/core/library.py | 8 ++++---- tagstudio/src/core/ts_core.py | 2 +- tagstudio/src/qt/main_window.py | 2 +- tagstudio/src/qt/pagination.py | 4 ++-- tagstudio/src/qt/ts_qt.py | 29 +++++++++++++++++++++-------- tagstudio/tag_studio.py | 7 +++---- tagstudio/tests/core/test_tags.py | 2 +- 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 485e30b6..0ece31d1 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -8,6 +8,7 @@ import datetime import glob import json import logging +import os import sys import time import traceback @@ -16,10 +17,9 @@ from enum import Enum import ujson -import tagstudio.src.core.ts_core as ts_core -from tagstudio.src.core.utils.fs import * -from tagstudio.src.core.utils.str import * -from tagstudio.src.core.utils.web import * +from tagstudio.src.core import ts_core +from tagstudio.src.core.utils.str import strip_punctuation +from tagstudio.src.core.utils.web import strip_web_protocol TYPE = ['file', 'meta', 'alt', 'mask'] # RESULT_TYPE = Enum('Result', ['ENTRY', 'COLLATION', 'TAG_GROUP']) diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index bb6ad790..4115cbb2 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -7,7 +7,7 @@ import json import os -from src.core.library import * +from tagstudio.src.core.library import Entry, Library VERSION: str = '9.1.0' # Major.Minor.Patch VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index 8055d930..daa79d6f 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -19,7 +19,7 @@ from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, QMenuBar, QPushButton, QScrollArea, QSizePolicy, QStatusBar, QWidget, QSplitter, QMenu) -from src.qt.pagination import Pagination +from tagstudio.src.qt.pagination import Pagination class Ui_MainWindow(QMainWindow): diff --git a/tagstudio/src/qt/pagination.py b/tagstudio/src/qt/pagination.py index c190fa14..074bf869 100644 --- a/tagstudio/src/qt/pagination.py +++ b/tagstudio/src/qt/pagination.py @@ -6,8 +6,8 @@ # I never want to see this code again. from PySide6.QtCore import QObject, Signal, QSize -from PySide6.QtGui import * -from PySide6.QtWidgets import * +from PySide6.QtGui import QIntValidator +from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel, QLineEdit, QSizePolicy # class NumberEdit(QLineEdit): diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index ec304f83..61c6a5c9 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -10,9 +10,13 @@ import ctypes import logging import math +import os import sys import time +import traceback +from types import FunctionType from datetime import datetime as dt +from pathlib import Path from queue import Empty, Queue from time import sleep from typing import Optional @@ -21,17 +25,26 @@ import cv2 from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance from PySide6 import QtCore from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer -from PySide6.QtGui import * +from PySide6.QtGui import (QGuiApplication, QPixmap, QEnterEvent, QMouseEvent, QResizeEvent, QPainter, QColor, QPen, + QAction, QStandardItemModel, QStandardItem, QPainterPath, QFontDatabase, QIcon) from PySide6.QtUiTools import QUiLoader -from PySide6.QtWidgets import * -from humanfriendly import format_timespan -from src.core.library import * -from src.core.palette import ColorType, get_tag_color -from src.core.ts_core import * -from src.qt.flowlayout import FlowLayout, FlowWidget -from src.qt.main_window import Ui_MainWindow +from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QPlainTextEdit, + QLineEdit, QScrollArea, QFrame, QTextEdit, QComboBox, QProgressDialog, QFileDialog, + QListView, QSplitter, QSizePolicy, QMessageBox, QBoxLayout, QCheckBox, QSplashScreen, + QMenu) from humanfriendly import format_timespan, format_size +from tagstudio.src.core.library import Collation, Entry, ItemType, Library, Tag +from tagstudio.src.core.palette import ColorType, get_tag_color +from tagstudio.src.core.ts_core import (TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES, + SHORTCUT_TYPES, PROGRAM_TYPES, ARCHIVE_TYPES, PRESENTATION_TYPES, + SPREADSHEET_TYPES, TEXT_TYPES, AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES, + LIBRARY_FILENAME, COLLAGE_FOLDER_NAME, BACKUP_FOLDER_NAME, TS_FOLDER_NAME, + VERSION_BRANCH, VERSION) +from tagstudio.src.core.utils.web import strip_web_protocol +from tagstudio.src.qt.flowlayout import FlowLayout, FlowWidget +from tagstudio.src.qt.main_window import Ui_MainWindow +import tagstudio.src.qt.resources_rc # SIGQUIT is not defined on Windows if sys.platform == "win32": diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index 50f48d6d..683529f4 100644 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -4,12 +4,11 @@ """TagStudio launcher.""" -from src.core.ts_core import TagStudioCore -from src.cli.ts_cli import CliDriver -from src.qt.ts_qt import QtDriver +from tagstudio.src.core.ts_core import TagStudioCore +from tagstudio.src.cli.ts_cli import CliDriver +from tagstudio.src.qt.ts_qt import QtDriver import argparse import traceback -# import ctypes def main(): diff --git a/tagstudio/tests/core/test_tags.py b/tagstudio/tests/core/test_tags.py index 33c8e895..7ff70d0b 100644 --- a/tagstudio/tests/core/test_tags.py +++ b/tagstudio/tests/core/test_tags.py @@ -1,4 +1,4 @@ -from src.core.library import Tag +from tagstudio.src.core.library import Tag class TestTags: From 0b1c097f978dc60f16d9646b62021a5e8e76be16 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 22:00:40 -0600 Subject: [PATCH 17/59] update tagstudio.py refrences to tag_studio.py --- .vscode/launch.json | 2 +- README.md | 4 ++-- TagStudio.sh | 2 +- start_win.bat | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f3e42404..8838fbb3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "TagStudio", "type": "python", "request": "launch", - "program": "${workspaceRoot}/tagstudio/tagstudio.py", + "program": "${workspaceRoot}/tagstudio/tag_studio.py", "console": "integratedTerminal", "justMyCode": true, "args": [] diff --git a/README.md b/README.md index 3563733f..dd78737e 100644 --- a/README.md +++ b/README.md @@ -90,14 +90,14 @@ _Learn more about setting up a virtual environment [here](https://docs.python.or To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired. -Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tagstudio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tagstudio.py`. +Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tag_studio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`. > [!CAUTION] > TagStudio on Linux & macOS likely won't function correctly at this time. If you're trying to run this in order to help test, debug, and improve compatibility, then charge on ahead! #### macOS -With the virtual environment loaded, run the python file at "tagstudio/tagstudio.py" from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tagstudio.py`. When launching the program in the future, remember to activate the virtual environment each time before launching *(an easier method is currently being worked on).* +With the virtual environment loaded, run the python file at "tagstudio/tag_studio.py" from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`. When launching the program in the future, remember to activate the virtual environment each time before launching *(an easier method is currently being worked on).* #### Linux diff --git a/TagStudio.sh b/TagStudio.sh index cff969c3..762f752f 100755 --- a/TagStudio.sh +++ b/TagStudio.sh @@ -2,4 +2,4 @@ python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt -python tagstudio/tagstudio.py +python tagstudio/tag_studio.py diff --git a/start_win.bat b/start_win.bat index 51a5ddf8..73dcfb0f 100644 --- a/start_win.bat +++ b/start_win.bat @@ -1,2 +1,2 @@ @echo off -.venv\Scripts\python.exe .\TagStudio\tagstudio.py --ui qt %* \ No newline at end of file +.venv\Scripts\python.exe .\TagStudio\tag_studio.py --ui qt %* \ No newline at end of file From f125e5a50d63af63d4f8f8f9321354d8f850af6b Mon Sep 17 00:00:00 2001 From: Xarvex Date: Wed, 24 Apr 2024 23:37:36 -0500 Subject: [PATCH 18/59] Implement file opening for Linux and MacOS, allow Windows in background Note: this is only tested on Linux --- tagstudio/src/qt/ts_qt.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 45ebd0d8..1695690f 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -41,6 +41,8 @@ import src.qt.resources_rc # from typing_extensions import deprecated from humanfriendly import format_timespan # from src.qt.qtacrylic.qtacrylic import WindowEffect +import shutil +import subprocess # SIGQUIT is not defined on Windows if sys.platform == "win32": @@ -56,13 +58,21 @@ INFO = f'[INFO]' logging.basicConfig(format="%(message)s", level=logging.INFO) -def open_file(path): - try: - os.startfile(path) - except FileNotFoundError: - logging.info('File Not Found! (Imagine this as a popup)') - except: - traceback.print_exc() +def open_file(path: str): + if sys.platform == "win32": + command_name = "start" + elif sys.platform == "darwin": + command_name = "open" + else: + command_name = "xdg-open" + command = shutil.which(command_name) + if command is not None: + try: + subprocess.Popen([command, path], close_fds=True) + except: + traceback.print_exc() + else: + logging.info(f"Could not find {command_name} on system PATH") class NavigationState(): From 8b4b2507fa841af1816493a9380230d9b6b46da5 Mon Sep 17 00:00:00 2001 From: Xarvex Date: Wed, 24 Apr 2024 23:50:26 -0500 Subject: [PATCH 19/59] Account for start being a shell builtin on Windows --- tagstudio/src/qt/ts_qt.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 1695690f..57e056a9 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -59,20 +59,21 @@ logging.basicConfig(format="%(message)s", level=logging.INFO) def open_file(path: str): - if sys.platform == "win32": - command_name = "start" - elif sys.platform == "darwin": - command_name = "open" - else: - command_name = "xdg-open" - command = shutil.which(command_name) - if command is not None: - try: - subprocess.Popen([command, path], close_fds=True) - except: - traceback.print_exc() - else: - logging.info(f"Could not find {command_name} on system PATH") + try: + if sys.platform == "win32": + subprocess.Popen(["start", path], shell=True, close_fds=True) + else: + if sys.platform == "darwin": + command_name = "open" + else: + command_name = "xdg-open" + command = shutil.which(command_name) + if command is not None: + subprocess.Popen([command, path], close_fds=True) + else: + logging.info(f"Could not find {command_name} on system PATH") + except: + traceback.print_exc() class NavigationState(): From 956ffd4663ce98dc4f124fa6287514efbb01b3c8 Mon Sep 17 00:00:00 2001 From: Xarvex Date: Thu, 25 Apr 2024 00:07:14 -0500 Subject: [PATCH 20/59] Properly detach process on Windows --- tagstudio/src/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 57e056a9..253f9d20 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -61,7 +61,7 @@ logging.basicConfig(format="%(message)s", level=logging.INFO) def open_file(path: str): try: if sys.platform == "win32": - subprocess.Popen(["start", path], shell=True, close_fds=True) + subprocess.Popen(["start", path], shell=True, close_fds=True, creationflags=subprocess.DETACHED_PROCESS) else: if sys.platform == "darwin": command_name = "open" From de09da1592202a2d1821b010bcd668fe6f726a5a Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Wed, 24 Apr 2024 23:18:16 -0600 Subject: [PATCH 21/59] remove tagstudio prefix for source libraries --- tagstudio/src/core/library.py | 6 +++--- tagstudio/src/core/ts_core.py | 2 +- tagstudio/src/qt/main_window.py | 2 +- tagstudio/src/qt/ts_qt.py | 14 +++++++------- tagstudio/tag_studio.py | 6 +++--- tagstudio/tests/core/test_tags.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 0ece31d1..97f2e871 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -17,9 +17,9 @@ from enum import Enum import ujson -from tagstudio.src.core import ts_core -from tagstudio.src.core.utils.str import strip_punctuation -from tagstudio.src.core.utils.web import strip_web_protocol +from src.core import ts_core +from src.core.utils.str import strip_punctuation +from src.core.utils.web import strip_web_protocol TYPE = ['file', 'meta', 'alt', 'mask'] # RESULT_TYPE = Enum('Result', ['ENTRY', 'COLLATION', 'TAG_GROUP']) diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 4115cbb2..5f50954d 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -7,7 +7,7 @@ import json import os -from tagstudio.src.core.library import Entry, Library +from src.core.library import Entry, Library VERSION: str = '9.1.0' # Major.Minor.Patch VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index daa79d6f..8055d930 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -19,7 +19,7 @@ from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, QMenuBar, QPushButton, QScrollArea, QSizePolicy, QStatusBar, QWidget, QSplitter, QMenu) -from tagstudio.src.qt.pagination import Pagination +from src.qt.pagination import Pagination class Ui_MainWindow(QMainWindow): diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 61c6a5c9..729f7657 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -34,17 +34,17 @@ from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QMenu) from humanfriendly import format_timespan, format_size -from tagstudio.src.core.library import Collation, Entry, ItemType, Library, Tag -from tagstudio.src.core.palette import ColorType, get_tag_color -from tagstudio.src.core.ts_core import (TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES, +from src.core.library import Collation, Entry, ItemType, Library, Tag +from src.core.palette import ColorType, get_tag_color +from src.core.ts_core import (TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES, SHORTCUT_TYPES, PROGRAM_TYPES, ARCHIVE_TYPES, PRESENTATION_TYPES, SPREADSHEET_TYPES, TEXT_TYPES, AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES, LIBRARY_FILENAME, COLLAGE_FOLDER_NAME, BACKUP_FOLDER_NAME, TS_FOLDER_NAME, VERSION_BRANCH, VERSION) -from tagstudio.src.core.utils.web import strip_web_protocol -from tagstudio.src.qt.flowlayout import FlowLayout, FlowWidget -from tagstudio.src.qt.main_window import Ui_MainWindow -import tagstudio.src.qt.resources_rc +from src.core.utils.web import strip_web_protocol +from src.qt.flowlayout import FlowLayout, FlowWidget +from src.qt.main_window import Ui_MainWindow +import src.qt.resources_rc # SIGQUIT is not defined on Windows if sys.platform == "win32": diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index 683529f4..54039eb7 100644 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -4,9 +4,9 @@ """TagStudio launcher.""" -from tagstudio.src.core.ts_core import TagStudioCore -from tagstudio.src.cli.ts_cli import CliDriver -from tagstudio.src.qt.ts_qt import QtDriver +from src.core.ts_core import TagStudioCore +from src.cli.ts_cli import CliDriver +from src.qt.ts_qt import QtDriver import argparse import traceback diff --git a/tagstudio/tests/core/test_tags.py b/tagstudio/tests/core/test_tags.py index 7ff70d0b..33c8e895 100644 --- a/tagstudio/tests/core/test_tags.py +++ b/tagstudio/tests/core/test_tags.py @@ -1,4 +1,4 @@ -from tagstudio.src.core.library import Tag +from src.core.library import Tag class TestTags: From 13c2ca1ea55cebb0b16977bf8e2b362710fe424f Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Thu, 25 Apr 2024 01:41:32 -0700 Subject: [PATCH 22/59] Allows shortcut files to show in library Shortcut files (.lnk, .url, .desktop) can now be added to the library (no thumbnail previews currently). --- tagstudio/src/core/ts_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 5f50954d..0ea35039 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -31,11 +31,11 @@ SPREADSHEET_TYPES: list[str] = ['csv', 'xls', 'xlsx', 'numbers', 'ods'] PRESENTATION_TYPES: list[str] = ['ppt', 'pptx', 'key', 'odp'] ARCHIVE_TYPES: list[str] = ['zip', 'rar', 'tar', 'tar.gz', 'tgz', '7z'] PROGRAM_TYPES: list[str] = ['exe', 'app'] -SHORTCUT_TYPES: list[str] = ['lnk', 'desktop'] +SHORTCUT_TYPES: list[str] = ['lnk', 'desktop', 'url'] ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES + AUDIO_TYPES + \ TEXT_TYPES + SPREADSHEET_TYPES + PRESENTATION_TYPES + \ - ARCHIVE_TYPES + PROGRAM_TYPES + ARCHIVE_TYPES + PROGRAM_TYPES + SHORTCUT_TYPES BOX_FIELDS = ['tag_box', 'text_box'] TEXT_FIELDS = ['text_line', 'text_box'] From b2fbc4b4a2e31fa9eecdedc555bc4cbd11419fde Mon Sep 17 00:00:00 2001 From: DrRetro Date: Thu, 25 Apr 2024 09:34:14 -0400 Subject: [PATCH 23/59] Large Changes to how code gets Field ID. --- tagstudio/src/qt/ts_qt.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index c5d2f942..6aeccd4b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -284,12 +284,14 @@ class FieldContainer(QWidget): class FieldWidget(QWidget): + field = dict def __init__(self, title) -> None: super().__init__() # self.item = item self.title = title + class TagBoxWidget(FieldWidget): updated = Signal() @@ -397,11 +399,14 @@ class TagBoxWidget(FieldWidget): # self.tags.append(tag) logging.info(f'[TAG BOX WIDGET] ADD TAG CALLBACK: T:{tag_id} to E:{self.item.id}') logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') - logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.title}') - id = list(self.lib.filter_field_templates(str(self.title).removesuffix(' (Tag Box)')))[0] + id = list(self.field.keys())[0] for x in self.driver.selected: self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=id, field_index=-1) self.updated.emit() + self.driver.update_thumbs() + + # if type((x[0]) == ThumbButton): + # # TODO: Remove space from the special search here (tag_id:x) once that system is finalized. # logging.info(f'I want to add tag ID {tag_id} to entry {self.item.filename}') # self.updated.emit() # if tag_id not in self.tags: @@ -415,10 +420,12 @@ class TagBoxWidget(FieldWidget): def remove_tag(self, tag_id): logging.info(f'[TAG BOX WIDGET] SELECTED T:{self.driver.selected}') - id = list(self.lib.filter_field_templates(str(self.title).removesuffix(' (Tag Box)')))[0] + id = list(self.field.keys())[0] for x in self.driver.selected: - self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id, field_id=id, field_index=-1) + index = self.driver.lib.get_field_index_in_entry(self.driver.lib.get_entry(x[1]),id) + self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id,field_index=index[0]) self.updated.emit() + self.driver.update_thumbs() # def show_add_button(self, value:bool): # self.add_button.setHidden(not value) @@ -2320,7 +2327,6 @@ class PreviewPanel(QWidget): container = self.containers[index] # container.inner_layout.removeItem(container.inner_layout.itemAt(1)) # container.setHidden(False) - if self.lib.get_field_attr(field, 'type') == 'tag_box': # logging.info(f'WRITING TAGBOX FOR ITEM {item.id}') container.set_title(self.lib.get_field_attr(field, 'name')) @@ -2342,7 +2348,7 @@ class PreviewPanel(QWidget): inner_container = TagBoxWidget(item, title, index, self.lib, self.lib.get_field_attr(field, 'content'), self.driver) container.set_inner_widget(inner_container) - + inner_container.field = field inner_container.updated.connect(lambda: (self.write_container(index, field), self.tags_updated.emit())) # if type(item) == Entry: # NOTE: Tag Boxes have no Edit Button (But will when you can convert field types) @@ -2949,15 +2955,17 @@ class ItemThumb(FlowWidget): # logging.info(f'Favorite Check: {value}, Mode: {self.mode}') if self.mode == ItemType.ENTRY: self.isFavorite = value - e = self.lib.get_entry(self.item_id) - if value: - self.favorite_badge.setHidden(False) - DEFAULT_META_TAG_FIELD = 8 - e.add_tag(self.lib, 1, DEFAULT_META_TAG_FIELD) - else: - e.remove_tag(self.lib, 1) + DEFAULT_META_TAG_FIELD = 8 + for x in self.panel.driver.selected: + e = self.lib.get_entry(x[1]) + if value: + self.favorite_badge.setHidden(False) + e.add_tag(self.panel.driver.lib, 1, field_id=DEFAULT_META_TAG_FIELD, field_index=-1) + else: + e.remove_tag(self.panel.driver.lib, 1) if self.panel.isOpen: self.panel.update_widgets() + self.panel.driver.update_thumbs() # def on_favorite_uncheck(self): # if self.mode == SearchItemType.ENTRY: From 794401ae5898446b9b02af9c025ca7b155e0838e Mon Sep 17 00:00:00 2001 From: DrRetro Date: Thu, 25 Apr 2024 09:40:12 -0400 Subject: [PATCH 24/59] Archive Button and Favorite Button Now work --- tagstudio/src/qt/ts_qt.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 6aeccd4b..7ddb03bd 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2936,15 +2936,18 @@ class ItemThumb(FlowWidget): # logging.info(f'Archived Check: {value}, Mode: {self.mode}') if self.mode == ItemType.ENTRY: self.isArchived = value - e = self.lib.get_entry(self.item_id) - if value: - self.archived_badge.setHidden(False) - DEFAULT_META_TAG_FIELD = 8 - e.add_tag(self.lib, 0, DEFAULT_META_TAG_FIELD) - else: - e.remove_tag(self.lib, 0) + DEFAULT_META_TAG_FIELD = 8 + for x in self.panel.driver.selected: + e = self.lib.get_entry(x[1]) + if value: + self.archived_badge.setHidden(False) + e.add_tag(self.panel.driver.lib, 0, field_id=DEFAULT_META_TAG_FIELD, field_index=-1) + else: + e.remove_tag(self.panel.driver.lib, 0) if self.panel.isOpen: self.panel.update_widgets() + self.panel.driver.update_thumbs() + # def on_archived_uncheck(self): # if self.mode == SearchItemType.ENTRY: From 2a46251831dc97df31a8b209138bea7270f715fb Mon Sep 17 00:00:00 2001 From: DrRetro Date: Thu, 25 Apr 2024 09:57:37 -0400 Subject: [PATCH 25/59] Fixed slow down from refreshing all thumbnails for every added and removed tag. --- tagstudio/src/qt/ts_qt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 5a8122ec..19daa657 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -418,7 +418,8 @@ class TagBoxWidget(FieldWidget): for x in self.driver.selected: self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=id, field_index=-1) self.updated.emit() - self.driver.update_thumbs() + if tag_id == 0 or tag_id == 1: + self.driver.update_thumbs() # if type((x[0]) == ThumbButton): # # TODO: Remove space from the special search here (tag_id:x) once that system is finalized. @@ -440,7 +441,8 @@ class TagBoxWidget(FieldWidget): index = self.driver.lib.get_field_index_in_entry(self.driver.lib.get_entry(x[1]),id) self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id,field_index=index[0]) self.updated.emit() - self.driver.update_thumbs() + if tag_id == 0 or tag_id == 1: + self.driver.update_thumbs() # def show_add_button(self, value:bool): # self.add_button.setHidden(not value) From a0dc34a5ad508cfe8efb7adb01ca500c810f71c3 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 26 Apr 2024 00:46:32 +0900 Subject: [PATCH 26/59] docs: update documentation.md minor fix --- doc/documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/documentation.md b/doc/documentation.md index 608b421f..5718e6b6 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -33,7 +33,7 @@ The Library is how TagStudio represents your chosen directory. In this Library o ## Fields -Fields are the the building blocks of metadata stored in Entires. Fields have several base types for representing different types of information, including: +Fields are the building blocks of metadata stored in Entires. Fields have several base types for representing different types of information, including: - `text_line` - A string of text, displayed as a single line. From 5c746a9950e2c65f107359ff9eb9316a08fd0f3a Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Thu, 25 Apr 2024 12:23:13 -0600 Subject: [PATCH 27/59] add Qsettings and restore last library on open Settings are currently stored as an INI file within the project directory rather than messing with registry or system configurations. --- tagstudio/src/qt/ts_qt.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index d9bbccc1..384cb5eb 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -26,7 +26,7 @@ from typing import Optional import cv2 from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance from PySide6 import QtCore -from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer +from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer, QSettings from PySide6.QtGui import (QGuiApplication, QPixmap, QEnterEvent, QMouseEvent, QResizeEvent, QPainter, QColor, QPen, QAction, QStandardItemModel, QStandardItem, QPainterPath, QFontDatabase, QIcon) from PySide6.QtUiTools import QUiLoader @@ -61,6 +61,9 @@ INFO = f'[INFO]' logging.basicConfig(format="%(message)s", level=logging.INFO) +# Keep settings in ini format in the current working directory. +QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, os.getcwd()) + def open_file(path: str): try: @@ -3573,6 +3576,8 @@ class QtDriver(QObject): self.SIGTERM.connect(self.handleSIGTERM) + self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'TagStudio') + max_threads = os.cpu_count() for i in range(max_threads): @@ -3796,10 +3801,15 @@ class QtDriver(QObject): self.splash.finish(self.main_window) self.preview_panel.update_widgets() - - if self.args.open: - self.splash.showMessage(f'Opening Library "{self.args.open}"...', int(Qt.AlignmentFlag.AlignBottom|Qt.AlignmentFlag.AlignHCenter), QColor('#9782ff')) - self.open_library(self.args.open) + # Check if a library should be opened on startup, args should override last_library + # TODO: check for behavior (open last, open default, start empty) + if self.args.open or self.settings.contains("last_library"): + if self.args.open: + lib = self.args.open + elif self.settings.value("last_library"): + lib = self.settings.value("last_library") + self.splash.showMessage(f'Opening Library "{lib}"...', int(Qt.AlignmentFlag.AlignBottom|Qt.AlignmentFlag.AlignHCenter), QColor('#9782ff')) + self.open_library(lib) app.exec_() @@ -3819,6 +3829,7 @@ class QtDriver(QObject): # Save Library on Application Exit if self.lib.library_dir: self.save_library() + self.settings.setValue("last_library", self.lib.library_dir) QApplication.quit() From 82bb63191a6033b6e4f855b68fce465659a8a3e8 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Thu, 25 Apr 2024 14:12:40 -0600 Subject: [PATCH 28/59] Tweak setting location to ensure compatibility between IDEs Explicitly call sync to save `.ini` file --- tagstudio/src/qt/ts_qt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 384cb5eb..eaad3bcb 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3577,6 +3577,7 @@ class QtDriver(QObject): self.SIGTERM.connect(self.handleSIGTERM) self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'TagStudio') + self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'tagstudio', 'TagStudio') max_threads = os.cpu_count() @@ -3830,6 +3831,7 @@ class QtDriver(QObject): if self.lib.library_dir: self.save_library() self.settings.setValue("last_library", self.lib.library_dir) + self.settings.sync() QApplication.quit() From 2514809e173087bb169eaecf87323b4b17fb879c Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Thu, 25 Apr 2024 14:20:46 -0600 Subject: [PATCH 29/59] remove doubled up line --- tagstudio/src/qt/ts_qt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index eaad3bcb..287ef0c3 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3576,7 +3576,6 @@ class QtDriver(QObject): self.SIGTERM.connect(self.handleSIGTERM) - self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'TagStudio') self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'tagstudio', 'TagStudio') From d8faa27dbf5e888a076d8787097d67dada069493 Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 26 Apr 2024 00:06:34 +0200 Subject: [PATCH 30/59] first prototype --- tagstudio/src/qt/ts_qt.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 287ef0c3..7c179787 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3709,6 +3709,10 @@ class QtDriver(QObject): create_collage_action = QAction('Create Collage', menu_bar) create_collage_action.triggered.connect(lambda: self.create_collage()) tools_menu.addAction(create_collage_action) + + folders_to_tags_action = QAction('Folders to Tags', menu_bar) + folders_to_tags_action.triggered.connect(lambda: self.folders_to_tags(self.lib)) + tools_menu.addAction(folders_to_tags_action) # Macros Menu ========================================================== self.autofill_action = QAction('Autofill', menu_bar) @@ -4536,6 +4540,39 @@ class QtDriver(QObject): ))) i = i+1 + def folders_to_tags(self,library:Library)->None: + + def find_tag(tag_list:list[str],last_tag:Tag) ->Tag: + tag_name = tag_list.pop(0) + logging.info(tag_name) + if(last_tag!=None): + tag_id = next((tag_id for tag_id in last_tag.subtag_ids if library.get_tag(tag_id).name == tag_name), None) + if(tag_id!=None): + tag = library.get_tag(tag_id) + else: + tag=None + else: + tag = next((tag for tag in library.tags if tag.name == tag_name), None) + logging.info(tag) + if tag == None: + tag_list.append(tag_name) + for tag_name in tag_list: + new_tag = Tag(-1, tag_name,"",[],([last_tag.id] if last_tag!=None else []),"black") + library.add_tag_to_library(new_tag) + last_tag = new_tag + return last_tag + if len(tag_list) == 0: + return tag + else: + return find_tag(tag_list,tag) + + + for ent in library.entries: + tag_list = ent.path.split("\\") + tag = find_tag(tag_list,None) + logging.info(tag) + ent.add_tag(library,tag.id,6) + def try_save_collage(self, increment_progress:bool): if increment_progress: self.completed += 1 From 201a63e27363b16516aeb74394c7a939ba7a176c Mon Sep 17 00:00:00 2001 From: DrRetro Date: Thu, 25 Apr 2024 20:51:12 -0400 Subject: [PATCH 31/59] Refresh_badges added to QtDriver, and favorite and archived badges checks selection. --- .vscode/launch.json | 4 ++-- tagstudio/src/qt/ts_qt.py | 45 +++++++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a6c172fe..8838fbb3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,12 +6,12 @@ "configurations": [ { "name": "TagStudio", - "type": "debugpy", + "type": "python", "request": "launch", "program": "${workspaceRoot}/tagstudio/tag_studio.py", "console": "integratedTerminal", "justMyCode": true, - "args": ["--debug"] + "args": [] } ] } diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 19daa657..653e9b67 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -419,7 +419,7 @@ class TagBoxWidget(FieldWidget): self.driver.lib.get_entry(x[1]).add_tag(self.driver.lib, tag_id, field_id=id, field_index=-1) self.updated.emit() if tag_id == 0 or tag_id == 1: - self.driver.update_thumbs() + self.driver.update_badges() # if type((x[0]) == ThumbButton): # # TODO: Remove space from the special search here (tag_id:x) once that system is finalized. @@ -442,7 +442,7 @@ class TagBoxWidget(FieldWidget): self.driver.lib.get_entry(x[1]).remove_tag(self.driver.lib, tag_id,field_index=index[0]) self.updated.emit() if tag_id == 0 or tag_id == 1: - self.driver.update_thumbs() + self.driver.update_badges() # def show_add_button(self, value:bool): # self.add_button.setHidden(not value) @@ -2954,16 +2954,27 @@ class ItemThumb(FlowWidget): if self.mode == ItemType.ENTRY: self.isArchived = value DEFAULT_META_TAG_FIELD = 8 - for x in self.panel.driver.selected: - e = self.lib.get_entry(x[1]) + temp = (ItemType.ENTRY,self.item_id) + if list(self.panel.driver.selected).count(temp) > 0: # Is the archived badge apart of the selection? + # Yes, then add archived tag to all selected. + for x in self.panel.driver.selected: + e = self.lib.get_entry(x[1]) + if value: + self.archived_badge.setHidden(False) + e.add_tag(self.panel.driver.lib, 0, field_id=DEFAULT_META_TAG_FIELD, field_index=-1) + else: + e.remove_tag(self.panel.driver.lib, 0) + else: + # No, then add archived tag to the entry this badge is on. + e = self.lib.get_entry(self.item_id) if value: - self.archived_badge.setHidden(False) + self.favorite_badge.setHidden(False) e.add_tag(self.panel.driver.lib, 0, field_id=DEFAULT_META_TAG_FIELD, field_index=-1) else: e.remove_tag(self.panel.driver.lib, 0) if self.panel.isOpen: self.panel.update_widgets() - self.panel.driver.update_thumbs() + self.panel.driver.update_badges() # def on_archived_uncheck(self): @@ -2976,8 +2987,19 @@ class ItemThumb(FlowWidget): if self.mode == ItemType.ENTRY: self.isFavorite = value DEFAULT_META_TAG_FIELD = 8 - for x in self.panel.driver.selected: - e = self.lib.get_entry(x[1]) + temp = (ItemType.ENTRY,self.item_id) + if list(self.panel.driver.selected).count(temp) > 0: # Is the favorite badge apart of the selection? + # Yes, then add favorite tag to all selected. + for x in self.panel.driver.selected: + e = self.lib.get_entry(x[1]) + if value: + self.favorite_badge.setHidden(False) + e.add_tag(self.panel.driver.lib, 1, field_id=DEFAULT_META_TAG_FIELD, field_index=-1) + else: + e.remove_tag(self.panel.driver.lib, 1) + else: + # No, then add favorite tag to the entry this badge is on. + e = self.lib.get_entry(self.item_id) if value: self.favorite_badge.setHidden(False) e.add_tag(self.panel.driver.lib, 1, field_id=DEFAULT_META_TAG_FIELD, field_index=-1) @@ -2985,7 +3007,8 @@ class ItemThumb(FlowWidget): e.remove_tag(self.panel.driver.lib, 1) if self.panel.isOpen: self.panel.update_widgets() - self.panel.driver.update_thumbs() + self.panel.driver.update_badges() + # def on_favorite_uncheck(self): # if self.mode == SearchItemType.ENTRY: @@ -4357,6 +4380,10 @@ class QtDriver(QObject): # logging.info( # f'[MAIN] Elements thumbs updated in {(end_time - start_time):.3f} seconds') + def update_badges(self): + for i, item_thumb in enumerate(self.item_thumbs, start=0): + item_thumb.update_badges() + def expand_collation(self, collation_entries: list[tuple[int, int]]): self.nav_forward([(ItemType.ENTRY, x[0]) for x in collation_entries]) From b7638046a34e48cfb0d46e6bc57e18f724b61291 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Thu, 25 Apr 2024 22:22:28 -0700 Subject: [PATCH 32/59] Addded TagStudio.ini to gitignore; Updated Qt resource comment --- .gitignore | 1 + tagstudio/src/qt/resources_rc.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c5b97173..0a5d8271 100644 --- a/.gitignore +++ b/.gitignore @@ -249,5 +249,6 @@ compile_commands.json # TagStudio .TagStudio +TagStudio.ini # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,qt diff --git a/tagstudio/src/qt/resources_rc.py b/tagstudio/src/qt/resources_rc.py index 0a3759ac..aca38cd1 100644 --- a/tagstudio/src/qt/resources_rc.py +++ b/tagstudio/src/qt/resources_rc.py @@ -1,6 +1,6 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.5.1 +# Created by: The Resource Compiler for Qt version 6.6.3 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore From 00651e6242f97c1945b06674f622b73699c836a6 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Fri, 26 Apr 2024 01:45:03 -0700 Subject: [PATCH 33/59] Fixed Incorrect Fields Being Updated in Multi-Selection Fixes #55 Co-Authored-By: Andrew Arneson Co-Authored-By: Xarvex <60973030+xarvex@users.noreply.github.com> --- tagstudio/src/qt/ts_qt.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 12b0d6ea..306fe857 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2375,14 +2375,12 @@ class PreviewPanel(QWidget): # f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' - callback = lambda: (self.remove_field(item.fields[index]), self.update_widgets()) + callback = lambda: (self.remove_field(field), self.update_widgets()) container.set_remove_callback(lambda: self.remove_message_box( prompt=prompt, callback=callback)) container.set_copy_callback(None) container.set_edit_callback(None) - # logging.info(self.common_fields) - # logging.info(f'index:{index}') else: text = 'Mixed Data' title = f"{self.lib.get_field_attr(field, 'name')} (Wacky Tag Box)" @@ -2412,15 +2410,14 @@ class PreviewPanel(QWidget): container.set_inner_widget(inner_container) # if type(item) == Entry: if not mixed: - item = self.lib.get_entry(self.selected[0][1]) # TODO TODO TODO: TEMPORARY modal = PanelModal(EditTextLine(self.lib.get_field_attr(field, 'content')), title=title, window_title=f'Edit {self.lib.get_field_attr(field, "name")}', - save_callback=(lambda content: (self.update_field(item.fields[index], content), self.update_widgets())) + save_callback=(lambda content: (self.update_field(field, content), self.update_widgets())) ) container.set_edit_callback(modal.show) prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' - callback = lambda: (self.remove_field(item.fields[index]), self.update_widgets()) + callback = lambda: (self.remove_field(field), self.update_widgets()) container.set_remove_callback(lambda: self.remove_message_box( prompt=prompt, callback=callback)) @@ -2448,17 +2445,15 @@ class PreviewPanel(QWidget): container.set_inner_widget(inner_container) # if type(item) == Entry: if not mixed: - item = self.lib.get_entry(self.selected[0][1]) # TODO TODO TODO: TEMPORARY container.set_copy_callback(None) modal = PanelModal(EditTextBox(self.lib.get_field_attr(field, 'content')), title=title, window_title=f'Edit {self.lib.get_field_attr(field, "name")}', - save_callback=(lambda content: (self.update_field(item.fields[index], content), self.update_widgets())) + save_callback=(lambda content: (self.update_field(field, content), self.update_widgets())) ) container.set_edit_callback(modal.show) - # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' - callback = lambda: (self.remove_field(item.fields[index]), self.update_widgets()) + callback = lambda: (self.remove_field(field), self.update_widgets()) container.set_remove_callback(lambda: self.remove_message_box( prompt=prompt, callback=callback)) @@ -2483,7 +2478,7 @@ class PreviewPanel(QWidget): # container.set_edit_callback(None) # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' - callback = lambda: (self.remove_field(item.fields[index]), self.update_widgets()) + callback = lambda: (self.remove_field(field), self.update_widgets()) container.set_remove_callback(lambda: self.remove_message_box( prompt=prompt, callback=callback)) @@ -2511,7 +2506,7 @@ class PreviewPanel(QWidget): container.set_edit_callback(None) # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' - callback = lambda: (self.remove_field(item.fields[index]), self.update_widgets()) + callback = lambda: (self.remove_field(field), self.update_widgets()) container.set_remove_callback(lambda: self.remove_message_box( prompt=prompt, callback=callback)) @@ -2536,7 +2531,7 @@ class PreviewPanel(QWidget): container.set_edit_callback(None) # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' - callback = lambda: (self.remove_field(item.fields[index]), self.update_widgets()) + callback = lambda: (self.remove_field(field), self.update_widgets()) # callback = lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets()) container.set_remove_callback(lambda: self.remove_message_box( prompt=prompt, From 9020aaddf4667a85d0061c87e20bcbe6929496d6 Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 26 Apr 2024 14:58:21 +0200 Subject: [PATCH 34/59] Finished folders to Tags tool --- tagstudio/src/qt/ts_qt.py | 75 +++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7c179787..94bddc88 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -4517,7 +4517,7 @@ class QtDriver(QObject): if not data_only_mode: time.sleep(5) - + self.collage = Image.new('RGB', (img_size,img_size)) i = 0 self.completed = 0 @@ -4540,38 +4540,51 @@ class QtDriver(QObject): ))) i = i+1 - def folders_to_tags(self,library:Library)->None: - - def find_tag(tag_list:list[str],last_tag:Tag) ->Tag: - tag_name = tag_list.pop(0) - logging.info(tag_name) - if(last_tag!=None): - tag_id = next((tag_id for tag_id in last_tag.subtag_ids if library.get_tag(tag_id).name == tag_name), None) - if(tag_id!=None): - tag = library.get_tag(tag_id) - else: - tag=None - else: - tag = next((tag for tag in library.tags if tag.name == tag_name), None) - logging.info(tag) - if tag == None: - tag_list.append(tag_name) - for tag_name in tag_list: - new_tag = Tag(-1, tag_name,"",[],([last_tag.id] if last_tag!=None else []),"black") + def folders_to_tags(self,library:Library): + logging.info("Converting folders to Tags") + tree = dict(dirs={}) + + def add_tag_to_tree(list:list[Tag]): + branch = tree + for tag in list: + if tag.name not in branch["dirs"]: + branch["dirs"][tag.name] = dict(dirs={},tag=tag) + branch = branch["dirs"][tag.name] + + def add_folders_to_tree(list:list[str])->Tag: + branch = tree + for folder in list: + if folder not in branch["dirs"]: + new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"black") library.add_tag_to_library(new_tag) - last_tag = new_tag - return last_tag - if len(tag_list) == 0: - return tag - else: - return find_tag(tag_list,tag) + branch["dirs"][folder] = dict(dirs={},tag=new_tag) + branch = branch["dirs"][folder] + return branch["tag"] + + def reverse_tag(tag:Tag,list:list[Tag]): + if list != None: + list.append(tag) + else: + list = [tag] + + if len(tag.subtag_ids) == 0: + add_tag_to_tree(list) + else: + for subtag_id in tag.subtag_ids: + subtag = library.get_tag(subtag_id) + reverse_tag(subtag,list) + + for tag in library.tags: + reverse_tag(tag,None) - - for ent in library.entries: - tag_list = ent.path.split("\\") - tag = find_tag(tag_list,None) - logging.info(tag) - ent.add_tag(library,tag.id,6) + for entry in library.entries: + folders = entry.path.split("\\") + tag = add_folders_to_tree(folders) + if tag: + if not entry.has_tag(library,tag.id): + entry.add_tag(library,tag.id,6) + + logging.info("Done") def try_save_collage(self, increment_progress:bool): if increment_progress: From 1774a00d3441ec7794a058913039b957f9013339 Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 26 Apr 2024 00:06:34 +0200 Subject: [PATCH 35/59] first prototype --- tagstudio/src/qt/ts_qt.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 306fe857..8833c0c8 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3749,6 +3749,10 @@ class QtDriver(QObject): create_collage_action = QAction('Create Collage', menu_bar) create_collage_action.triggered.connect(lambda: self.create_collage()) tools_menu.addAction(create_collage_action) + + folders_to_tags_action = QAction('Folders to Tags', menu_bar) + folders_to_tags_action.triggered.connect(lambda: self.folders_to_tags(self.lib)) + tools_menu.addAction(folders_to_tags_action) # Macros Menu ========================================================== self.autofill_action = QAction('Autofill', menu_bar) @@ -4580,6 +4584,39 @@ class QtDriver(QObject): ))) i = i+1 + def folders_to_tags(self,library:Library)->None: + + def find_tag(tag_list:list[str],last_tag:Tag) ->Tag: + tag_name = tag_list.pop(0) + logging.info(tag_name) + if(last_tag!=None): + tag_id = next((tag_id for tag_id in last_tag.subtag_ids if library.get_tag(tag_id).name == tag_name), None) + if(tag_id!=None): + tag = library.get_tag(tag_id) + else: + tag=None + else: + tag = next((tag for tag in library.tags if tag.name == tag_name), None) + logging.info(tag) + if tag == None: + tag_list.append(tag_name) + for tag_name in tag_list: + new_tag = Tag(-1, tag_name,"",[],([last_tag.id] if last_tag!=None else []),"black") + library.add_tag_to_library(new_tag) + last_tag = new_tag + return last_tag + if len(tag_list) == 0: + return tag + else: + return find_tag(tag_list,tag) + + + for ent in library.entries: + tag_list = ent.path.split("\\") + tag = find_tag(tag_list,None) + logging.info(tag) + ent.add_tag(library,tag.id,6) + def try_save_collage(self, increment_progress:bool): if increment_progress: self.completed += 1 From 749d7c8fc094de80cdade7046ad305a8a4e86158 Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 26 Apr 2024 14:58:21 +0200 Subject: [PATCH 36/59] Finished folders to Tags tool --- tagstudio/src/qt/ts_qt.py | 75 +++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 8833c0c8..f003fd2d 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -4561,7 +4561,7 @@ class QtDriver(QObject): if not data_only_mode: time.sleep(5) - + self.collage = Image.new('RGB', (img_size,img_size)) i = 0 self.completed = 0 @@ -4584,38 +4584,51 @@ class QtDriver(QObject): ))) i = i+1 - def folders_to_tags(self,library:Library)->None: - - def find_tag(tag_list:list[str],last_tag:Tag) ->Tag: - tag_name = tag_list.pop(0) - logging.info(tag_name) - if(last_tag!=None): - tag_id = next((tag_id for tag_id in last_tag.subtag_ids if library.get_tag(tag_id).name == tag_name), None) - if(tag_id!=None): - tag = library.get_tag(tag_id) - else: - tag=None - else: - tag = next((tag for tag in library.tags if tag.name == tag_name), None) - logging.info(tag) - if tag == None: - tag_list.append(tag_name) - for tag_name in tag_list: - new_tag = Tag(-1, tag_name,"",[],([last_tag.id] if last_tag!=None else []),"black") + def folders_to_tags(self,library:Library): + logging.info("Converting folders to Tags") + tree = dict(dirs={}) + + def add_tag_to_tree(list:list[Tag]): + branch = tree + for tag in list: + if tag.name not in branch["dirs"]: + branch["dirs"][tag.name] = dict(dirs={},tag=tag) + branch = branch["dirs"][tag.name] + + def add_folders_to_tree(list:list[str])->Tag: + branch = tree + for folder in list: + if folder not in branch["dirs"]: + new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"black") library.add_tag_to_library(new_tag) - last_tag = new_tag - return last_tag - if len(tag_list) == 0: - return tag - else: - return find_tag(tag_list,tag) + branch["dirs"][folder] = dict(dirs={},tag=new_tag) + branch = branch["dirs"][folder] + return branch["tag"] + + def reverse_tag(tag:Tag,list:list[Tag]): + if list != None: + list.append(tag) + else: + list = [tag] + + if len(tag.subtag_ids) == 0: + add_tag_to_tree(list) + else: + for subtag_id in tag.subtag_ids: + subtag = library.get_tag(subtag_id) + reverse_tag(subtag,list) + + for tag in library.tags: + reverse_tag(tag,None) - - for ent in library.entries: - tag_list = ent.path.split("\\") - tag = find_tag(tag_list,None) - logging.info(tag) - ent.add_tag(library,tag.id,6) + for entry in library.entries: + folders = entry.path.split("\\") + tag = add_folders_to_tree(folders) + if tag: + if not entry.has_tag(library,tag.id): + entry.add_tag(library,tag.id,6) + + logging.info("Done") def try_save_collage(self, increment_progress:bool): if increment_progress: From 898ce5fdc73e4b186228fc69beb7a2ca6b80438e Mon Sep 17 00:00:00 2001 From: Theasacraft <91694323+Thesacraft@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:34:06 +0200 Subject: [PATCH 37/59] Update ts_qt.py to update badges if meta tag field is removed --- tagstudio/src/qt/ts_qt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 306fe857..78e2989d 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2546,8 +2546,13 @@ class PreviewPanel(QWidget): entry = self.lib.get_entry(item_pair[1]) try: index = entry.fields.index(field) + updated_badges = False + if 8 in entry.fields[index].keys() and (1 in entry.fields[index][8] or 0 in entry.fields[index][8]): + updated_badges = True # TODO: Create a proper Library/Entry method to manage fields. entry.fields.pop(index) + if updated_badges: + self.driver.update_badges() except ValueError: logging.info(f'[PREVIEW PANEL][ERROR?] Tried to remove field from Entry ({entry.id}) that never had it') pass From 243d7862980d00c69883f2a32e5e99df6a1d0b3b Mon Sep 17 00:00:00 2001 From: Theasacraft <91694323+Thesacraft@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:41:37 +0200 Subject: [PATCH 38/59] Update ts_qt.py to update badges if meta tag field is removed --- tagstudio/src/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 78e2989d..6740767f 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2546,7 +2546,7 @@ class PreviewPanel(QWidget): entry = self.lib.get_entry(item_pair[1]) try: index = entry.fields.index(field) - updated_badges = False + updated_badges = False if 8 in entry.fields[index].keys() and (1 in entry.fields[index][8] or 0 in entry.fields[index][8]): updated_badges = True # TODO: Create a proper Library/Entry method to manage fields. From 6b1035b0f61b97d71e97d7a85aaf4503b13b1587 Mon Sep 17 00:00:00 2001 From: DrRetro Date: Fri, 26 Apr 2024 10:07:59 -0400 Subject: [PATCH 39/59] A simple TagDatabasePanel has been created based off TagSearchPanel. To access, go to Edit > Tag Database --- tagstudio/src/qt/ts_qt.py | 130 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 306fe857..612b7f20 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1147,6 +1147,126 @@ class BuildTagPanel(PanelWidget): # self.search_field.setFocus() # self.parentWidget().hide() +class TagDatabasePanel(PanelWidget): + tag_chosen = Signal(int) + def __init__(self, library): + super().__init__() + self.lib: Library = library + # self.callback = callback + self.first_tag_id = -1 + self.tag_limit = 30 + # self.selected_tag: int = 0 + + self.setMinimumSize(300, 400) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6,0,6,0) + + self.search_field = QLineEdit() + self.search_field.setObjectName('searchField') + self.search_field.setMinimumSize(QSize(0, 32)) + self.search_field.setPlaceholderText('Search Tags') + self.search_field.textEdited.connect(lambda x=self.search_field.text(): self.update_tags(x)) + self.search_field.returnPressed.connect(lambda checked=False: self.on_return(self.search_field.text())) + + # self.content_container = QWidget() + # self.content_layout = QHBoxLayout(self.content_container) + + self.scroll_contents = QWidget() + self.scroll_layout = QVBoxLayout(self.scroll_contents) + self.scroll_layout.setContentsMargins(6,0,6,0) + self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + self.scroll_area = QScrollArea() + # self.scroll_area.setStyleSheet('background: #000000;') + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + # self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setFrameShadow(QFrame.Shadow.Plain) + self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) + # sa.setMaximumWidth(self.preview_size[0]) + self.scroll_area.setWidget(self.scroll_contents) + + # self.add_button = QPushButton() + # self.root_layout.addWidget(self.add_button) + # self.add_button.setText('Add Tag') + # # self.done_button.clicked.connect(lambda checked=False, x=1101: (callback(x), self.hide())) + # self.add_button.clicked.connect(lambda checked=False, x=1101: callback(x)) + # # self.setLayout(self.root_layout) + + self.root_layout.addWidget(self.search_field) + self.root_layout.addWidget(self.scroll_area) + self.update_tags('') + + # def reset(self): + # self.search_field.setText('') + # self.update_tags('') + # self.search_field.setFocus() + + def on_return(self, text:str): + if text and self.first_tag_id >= 0: + # callback(self.first_tag_id) + self.search_field.setText('') + self.update_tags('') + else: + self.search_field.setFocus() + self.parentWidget().hide() + + def update_tags(self, query:str): + # for c in self.scroll_layout.children(): + # c.widget().deleteLater() + while self.scroll_layout.itemAt(0): + # logging.info(f"I'm deleting { self.scroll_layout.itemAt(0).widget()}") + self.scroll_layout.takeAt(0).widget().deleteLater() + + if query: + first_id_set = False + for tag_id in self.lib.search_tags(query, include_cluster=True)[:self.tag_limit-1]: + if not first_id_set: + self.first_tag_id = tag_id + first_id_set = True + c = QWidget() + l = QHBoxLayout(c) + l.setContentsMargins(0,0,0,0) + l.setSpacing(3) + tw = TagWidget(self.lib, self.lib.get_tag(tag_id), True, False) + + l.addWidget(tw) + self.scroll_layout.addWidget(c) + else: + first_id_set = False + for tag in self.lib.tags: + if not first_id_set: + self.first_tag_id = tag.id + first_id_set = True + c = QWidget() + l = QHBoxLayout(c) + l.setContentsMargins(0,0,0,0) + l.setSpacing(3) + tw = TagWidget(self.lib, tag, True, False) + tw.on_edit.connect(lambda checked=False, t=tag: (self.edit_tag(t.id))) + l.addWidget(tw) + self.scroll_layout.addWidget(c) + + self.search_field.setFocus() + + def edit_tag(self, tag_id:int): + btp = BuildTagPanel(self.lib, tag_id) + # btp.on_edit.connect(lambda x: self.edit_tag_callback(x)) + self.edit_modal = PanelModal(btp, + self.lib.get_tag(tag_id).display_name(self.lib), + 'Edit Tag', + done_callback=(self.update_tags('')), + has_save=True) + # self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t)) + panel: BuildTagPanel = self.edit_modal.widget + self.edit_modal.saved.connect(lambda: self.lib.update_tag(btp.build_tag())) + # panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag)) + self.edit_modal.show() + # def enterEvent(self, event: QEnterEvent) -> None: + # self.search_field.setFocus() + # return super().enterEvent(event) + # self.focusOutEvent + class FunctionIterator(QObject): """Iterates over a yielding function and emits progress as the 'value' signal.\n\nThread-Safe Guarantee™""" @@ -3735,6 +3855,12 @@ class QtDriver(QObject): new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) edit_menu.addAction(new_tag_action) + edit_menu.addSeparator() + + tag_database_action = QAction('Tag Database', menu_bar) + tag_database_action.triggered.connect(lambda: self.show_tag_database()) + edit_menu.addAction(tag_database_action) + # Tools Menu =========================================================== fix_unlinked_entries_action = QAction('Fix &Unlinked Entries', menu_bar) fue_modal = FixUnlinkedEntriesModal(self.lib, self) @@ -3901,6 +4027,10 @@ class QtDriver(QObject): # panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag)) self.modal.show() + def show_tag_database(self): + self.modal = PanelModal(TagDatabasePanel(self.lib),'Tag Database', 'Tag Database', has_save=False) + self.modal.show() + def add_new_files_callback(self): """Runs when user initiates adding new files to the Library.""" # # if self.lib.files_not_in_library: From f23ff1669eece3d92f04157e9906ccb96bddc728 Mon Sep 17 00:00:00 2001 From: DrRetro Date: Fri, 26 Apr 2024 10:50:19 -0400 Subject: [PATCH 40/59] Fixed Issue when Searching Tags, Editing Option was not Connected to Searched TagWidets --- tagstudio/src/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 612b7f20..7d788c46 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1229,7 +1229,7 @@ class TagDatabasePanel(PanelWidget): l.setContentsMargins(0,0,0,0) l.setSpacing(3) tw = TagWidget(self.lib, self.lib.get_tag(tag_id), True, False) - + tw.on_edit.connect(lambda checked=False, t=tag: (self.edit_tag(t.id))) l.addWidget(tw) self.scroll_layout.addWidget(c) else: From 5feb3a6d203a1d5e5b572fe88df3e937541e0e6b Mon Sep 17 00:00:00 2001 From: DrRetro Date: Fri, 26 Apr 2024 10:51:49 -0400 Subject: [PATCH 41/59] Fixed Issue with Previous Commit --- tagstudio/src/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7d788c46..6bedc6f0 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1229,7 +1229,7 @@ class TagDatabasePanel(PanelWidget): l.setContentsMargins(0,0,0,0) l.setSpacing(3) tw = TagWidget(self.lib, self.lib.get_tag(tag_id), True, False) - tw.on_edit.connect(lambda checked=False, t=tag: (self.edit_tag(t.id))) + tw.on_edit.connect(lambda checked=False, t=self.lib.get_tag(tag_id): (self.edit_tag(t.id))) l.addWidget(tw) self.scroll_layout.addWidget(c) else: From 85ae16781796ad8d38907fb10ddebb05bebaf912 Mon Sep 17 00:00:00 2001 From: DrRetro Date: Fri, 26 Apr 2024 11:01:09 -0400 Subject: [PATCH 42/59] Fixed Issue with Tags in Database not refreshing after Tag was Edited. --- tagstudio/src/qt/ts_qt.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 6bedc6f0..114fe1ed 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1255,13 +1255,18 @@ class TagDatabasePanel(PanelWidget): self.edit_modal = PanelModal(btp, self.lib.get_tag(tag_id).display_name(self.lib), 'Edit Tag', - done_callback=(self.update_tags('')), + done_callback=(self.update_tags(self.search_field.text())), has_save=True) # self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t)) panel: BuildTagPanel = self.edit_modal.widget - self.edit_modal.saved.connect(lambda: self.lib.update_tag(btp.build_tag())) + self.edit_modal.saved.connect(lambda: self.edit_tag_callback(btp)) # panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag)) self.edit_modal.show() + + def edit_tag_callback(self, btp:BuildTagPanel): + self.lib.update_tag(btp.build_tag()) + self.update_tags(self.search_field.text()) + # def enterEvent(self, event: QEnterEvent) -> None: # self.search_field.setFocus() # return super().enterEvent(event) From dde7eec946a9e7b1df407375694d7e9a496aa990 Mon Sep 17 00:00:00 2001 From: DrRetro Date: Fri, 26 Apr 2024 12:01:02 -0400 Subject: [PATCH 43/59] Keyboard Shortcuts Added to basic Functions --- tagstudio/src/qt/ts_qt.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 114fe1ed..20e30940 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3832,14 +3832,20 @@ class QtDriver(QObject): open_library_action = QAction('&Open/Create Library', menu_bar) open_library_action.triggered.connect(lambda: self.open_library_from_dialog()) + open_library_action.setShortcut(QtCore.QKeyCombination(QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), QtCore.Qt.Key.Key_O)) + open_library_action.setToolTip("Ctrl+O") file_menu.addAction(open_library_action) save_library_action = QAction('&Save Library', menu_bar) save_library_action.triggered.connect(lambda: self.callback_library_needed_check(self.save_library)) + save_library_action.setShortcut(QtCore.QKeyCombination(QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), QtCore.Qt.Key.Key_S)) + save_library_action.setStatusTip("Ctrl+S") file_menu.addAction(save_library_action) - save_library_backup_action = QAction('Save Library &Backup', menu_bar) + save_library_backup_action = QAction('&Save Library Backup', menu_bar) save_library_backup_action.triggered.connect(lambda: self.callback_library_needed_check(self.backup_library)) + save_library_backup_action.setShortcut(QtCore.QKeyCombination(QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier), QtCore.Qt.Key.Key_S)) + save_library_backup_action.setStatusTip("Ctrl+Shift+S") file_menu.addAction(save_library_backup_action) file_menu.addSeparator() @@ -3848,6 +3854,8 @@ class QtDriver(QObject): # refresh_lib_action.triggered.connect(lambda: self.lib.refresh_dir()) add_new_files_action = QAction('&Refresh Directories', menu_bar) add_new_files_action.triggered.connect(lambda: self.callback_library_needed_check(self.add_new_files_callback)) + add_new_files_action.setShortcut(QtCore.QKeyCombination(QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), QtCore.Qt.Key.Key_R)) + add_new_files_action.setStatusTip("Ctrl+R") # file_menu.addAction(refresh_lib_action) file_menu.addAction(add_new_files_action) @@ -3856,8 +3864,10 @@ class QtDriver(QObject): file_menu.addAction(QAction('&Close Library', menu_bar)) # Edit Menu ============================================================ - new_tag_action = QAction('New Tag', menu_bar) + new_tag_action = QAction('New &Tag', menu_bar) new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) + new_tag_action.setShortcut(QtCore.QKeyCombination(QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), QtCore.Qt.Key.Key_T)) + new_tag_action.setToolTip('Ctrl+T') edit_menu.addAction(new_tag_action) edit_menu.addSeparator() @@ -3886,8 +3896,10 @@ class QtDriver(QObject): self.autofill_action.triggered.connect(lambda: (self.run_macros('autofill', [x[1] for x in self.selected if x[0] == ItemType.ENTRY]), self.preview_panel.update_widgets())) macros_menu.addAction(self.autofill_action) - self.sort_fields_action = QAction('Sort Fields', menu_bar) + self.sort_fields_action = QAction('&Sort Fields', menu_bar) self.sort_fields_action.triggered.connect(lambda: (self.run_macros('sort-fields', [x[1] for x in self.selected if x[0] == ItemType.ENTRY]), self.preview_panel.update_widgets())) + self.sort_fields_action.setShortcut(QtCore.QKeyCombination(QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.AltModifier), QtCore.Qt.Key.Key_S)) + self.sort_fields_action.setToolTip('Alt+S') macros_menu.addAction(self.sort_fields_action) self.set_macro_menu_viability() @@ -4612,6 +4624,7 @@ class QtDriver(QObject): self.preview_panel.update_widgets() self.filter_items() + def create_collage(self) -> None: """Generates and saves an image collage based on Library Entries.""" From 1f7a5d3cbb00ceb33aa7edcf87dee72c2c19ac1f Mon Sep 17 00:00:00 2001 From: Xarvex Date: Fri, 26 Apr 2024 14:46:13 -0500 Subject: [PATCH 44/59] Windows: fix files w/ spaces opening cmd rather than associated program --- tagstudio/src/qt/ts_qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 306fe857..bda97854 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -68,7 +68,8 @@ QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, os.getcwd()) def open_file(path: str): try: if sys.platform == "win32": - subprocess.Popen(["start", path], shell=True, close_fds=True, creationflags=subprocess.DETACHED_PROCESS) + # Windows needs special attention to handle spaces in the file + subprocess.Popen(["start", f'"{path.replace('"', '\"')}"'], shell=True, close_fds=True, creationflags=subprocess.DETACHED_PROCESS) else: if sys.platform == "darwin": command_name = "open" From 66fec731368895f41c640e22d56e58fef559e747 Mon Sep 17 00:00:00 2001 From: Xarvex Date: Fri, 26 Apr 2024 15:05:07 -0500 Subject: [PATCH 45/59] Correct usage of start command --- tagstudio/src/qt/ts_qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index bda97854..fbd05432 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -69,7 +69,8 @@ def open_file(path: str): try: if sys.platform == "win32": # Windows needs special attention to handle spaces in the file - subprocess.Popen(["start", f'"{path.replace('"', '\"')}"'], shell=True, close_fds=True, creationflags=subprocess.DETACHED_PROCESS) + # first parameter is for title, NOT filepath + subprocess.Popen(["start", "", os.path.normpath(path)], shell=True, close_fds=True, creationflags=subprocess.DETACHED_PROCESS) else: if sys.platform == "darwin": command_name = "open" From 0541c9fb016feef55473ecbd182ab3ded6e9fd7c Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 26 Apr 2024 00:06:34 +0200 Subject: [PATCH 46/59] first prototype --- tagstudio/src/qt/ts_qt.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7e397f90..83d8ec0b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3897,6 +3897,10 @@ class QtDriver(QObject): create_collage_action = QAction('Create Collage', menu_bar) create_collage_action.triggered.connect(lambda: self.create_collage()) tools_menu.addAction(create_collage_action) + + folders_to_tags_action = QAction('Folders to Tags', menu_bar) + folders_to_tags_action.triggered.connect(lambda: self.folders_to_tags(self.lib)) + tools_menu.addAction(folders_to_tags_action) # Macros Menu ========================================================== self.autofill_action = QAction('Autofill', menu_bar) @@ -4735,6 +4739,39 @@ class QtDriver(QObject): ))) i = i+1 + def folders_to_tags(self,library:Library)->None: + + def find_tag(tag_list:list[str],last_tag:Tag) ->Tag: + tag_name = tag_list.pop(0) + logging.info(tag_name) + if(last_tag!=None): + tag_id = next((tag_id for tag_id in last_tag.subtag_ids if library.get_tag(tag_id).name == tag_name), None) + if(tag_id!=None): + tag = library.get_tag(tag_id) + else: + tag=None + else: + tag = next((tag for tag in library.tags if tag.name == tag_name), None) + logging.info(tag) + if tag == None: + tag_list.append(tag_name) + for tag_name in tag_list: + new_tag = Tag(-1, tag_name,"",[],([last_tag.id] if last_tag!=None else []),"black") + library.add_tag_to_library(new_tag) + last_tag = new_tag + return last_tag + if len(tag_list) == 0: + return tag + else: + return find_tag(tag_list,tag) + + + for ent in library.entries: + tag_list = ent.path.split("\\") + tag = find_tag(tag_list,None) + logging.info(tag) + ent.add_tag(library,tag.id,6) + def try_save_collage(self, increment_progress:bool): if increment_progress: self.completed += 1 From 3cd6fa136f3856e6e9f38c8690f37016177b1abc Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 26 Apr 2024 14:58:21 +0200 Subject: [PATCH 47/59] Finished folders to Tags tool --- tagstudio/src/qt/ts_qt.py | 75 +++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 83d8ec0b..42ef953b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -4716,7 +4716,7 @@ class QtDriver(QObject): if not data_only_mode: time.sleep(5) - + self.collage = Image.new('RGB', (img_size,img_size)) i = 0 self.completed = 0 @@ -4739,38 +4739,51 @@ class QtDriver(QObject): ))) i = i+1 - def folders_to_tags(self,library:Library)->None: - - def find_tag(tag_list:list[str],last_tag:Tag) ->Tag: - tag_name = tag_list.pop(0) - logging.info(tag_name) - if(last_tag!=None): - tag_id = next((tag_id for tag_id in last_tag.subtag_ids if library.get_tag(tag_id).name == tag_name), None) - if(tag_id!=None): - tag = library.get_tag(tag_id) - else: - tag=None - else: - tag = next((tag for tag in library.tags if tag.name == tag_name), None) - logging.info(tag) - if tag == None: - tag_list.append(tag_name) - for tag_name in tag_list: - new_tag = Tag(-1, tag_name,"",[],([last_tag.id] if last_tag!=None else []),"black") + def folders_to_tags(self,library:Library): + logging.info("Converting folders to Tags") + tree = dict(dirs={}) + + def add_tag_to_tree(list:list[Tag]): + branch = tree + for tag in list: + if tag.name not in branch["dirs"]: + branch["dirs"][tag.name] = dict(dirs={},tag=tag) + branch = branch["dirs"][tag.name] + + def add_folders_to_tree(list:list[str])->Tag: + branch = tree + for folder in list: + if folder not in branch["dirs"]: + new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"black") library.add_tag_to_library(new_tag) - last_tag = new_tag - return last_tag - if len(tag_list) == 0: - return tag - else: - return find_tag(tag_list,tag) + branch["dirs"][folder] = dict(dirs={},tag=new_tag) + branch = branch["dirs"][folder] + return branch["tag"] + + def reverse_tag(tag:Tag,list:list[Tag]): + if list != None: + list.append(tag) + else: + list = [tag] + + if len(tag.subtag_ids) == 0: + add_tag_to_tree(list) + else: + for subtag_id in tag.subtag_ids: + subtag = library.get_tag(subtag_id) + reverse_tag(subtag,list) + + for tag in library.tags: + reverse_tag(tag,None) - - for ent in library.entries: - tag_list = ent.path.split("\\") - tag = find_tag(tag_list,None) - logging.info(tag) - ent.add_tag(library,tag.id,6) + for entry in library.entries: + folders = entry.path.split("\\") + tag = add_folders_to_tree(folders) + if tag: + if not entry.has_tag(library,tag.id): + entry.add_tag(library,tag.id,6) + + logging.info("Done") def try_save_collage(self, increment_progress:bool): if increment_progress: From 79e0263e972c955a467fd6e527820542208f1756 Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Fri, 26 Apr 2024 20:09:43 -0300 Subject: [PATCH 48/59] file_label opens file path --- tagstudio/src/qt/ts_qt.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7e397f90..df53f4da 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2015,7 +2015,24 @@ class PreviewPanel(QWidget): image_layout.addWidget(self.preview_img) image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter) - self.file_label = QLabel('Filename') + class ClickableLabel(QLabel): + def __init__(self, text, parent=None): + super().__init__(text, parent) + + def setFilePath(self, filepath): + self.filepath = filepath + + def mousePressEvent(self, event): + super().mousePressEvent(event) + #open file + if hasattr(self, 'filepath'): + if os.path.exists(self.filepath): + os.startfile(self.filepath) + logging.info(f'Opening file: {self.filepath}') + else: + logging.error(f'File not found: {self.filepath}') + + self.file_label = ClickableLabel('Filename') self.file_label.setWordWrap(True) self.file_label.setTextInteractionFlags( Qt.TextInteractionFlag.TextSelectableByMouse) @@ -2245,6 +2262,7 @@ class PreviewPanel(QWidget): if (len(self.selected) == 0 or self.selected != self.driver.selected): filepath = os.path.normpath(f'{self.lib.library_dir}/{item.path}/{item.filename}') + self.file_label.setFilePath(filepath) window_title = filepath ratio: float = self.devicePixelRatio() self.tr.render_big(time.time(), filepath, (512, 512), ratio) From 18dcedd6a0b7f142252301bc1c0afa2f704d2f8b Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Fri, 26 Apr 2024 20:13:58 -0300 Subject: [PATCH 49/59] code cleanup --- tagstudio/src/qt/ts_qt.py | 20 ++------------------ tagstudio/src/qt/utils/clickableLabels.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 tagstudio/src/qt/utils/clickableLabels.py diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index df53f4da..0e352a2a 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -46,6 +46,7 @@ from src.core.ts_core import (TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELD from src.core.utils.web import strip_web_protocol from src.qt.flowlayout import FlowLayout, FlowWidget from src.qt.main_window import Ui_MainWindow +from src.qt.utils.clickableLabels import FileOpenerLabel import src.qt.resources_rc # SIGQUIT is not defined on Windows @@ -2015,24 +2016,7 @@ class PreviewPanel(QWidget): image_layout.addWidget(self.preview_img) image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter) - class ClickableLabel(QLabel): - def __init__(self, text, parent=None): - super().__init__(text, parent) - - def setFilePath(self, filepath): - self.filepath = filepath - - def mousePressEvent(self, event): - super().mousePressEvent(event) - #open file - if hasattr(self, 'filepath'): - if os.path.exists(self.filepath): - os.startfile(self.filepath) - logging.info(f'Opening file: {self.filepath}') - else: - logging.error(f'File not found: {self.filepath}') - - self.file_label = ClickableLabel('Filename') + self.file_label = FileOpenerLabel('Filename') self.file_label.setWordWrap(True) self.file_label.setTextInteractionFlags( Qt.TextInteractionFlag.TextSelectableByMouse) diff --git a/tagstudio/src/qt/utils/clickableLabels.py b/tagstudio/src/qt/utils/clickableLabels.py new file mode 100644 index 00000000..12d4cba9 --- /dev/null +++ b/tagstudio/src/qt/utils/clickableLabels.py @@ -0,0 +1,20 @@ +import os +import logging +from PySide6.QtWidgets import QLabel + +class FileOpenerLabel(QLabel): + def __init__(self, text, parent=None): + super().__init__(text, parent) + + def setFilePath(self, filepath): + self.filepath = filepath + + def mousePressEvent(self, event): + super().mousePressEvent(event) + #open file + if hasattr(self, 'filepath'): + if os.path.exists(self.filepath): + os.startfile(self.filepath) + logging.info(f'Opening file: {self.filepath}') + else: + logging.error(f'File not found: {self.filepath}') \ No newline at end of file From bcf4453c8d3bbe60404b63bff14e62b3c04a2016 Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Fri, 26 Apr 2024 20:17:54 -0300 Subject: [PATCH 50/59] clickable label to the right place --- tagstudio/src/qt/ts_qt.py | 18 +++++++++++++++++- tagstudio/src/qt/utils/clickableLabels.py | 20 -------------------- 2 files changed, 17 insertions(+), 21 deletions(-) delete mode 100644 tagstudio/src/qt/utils/clickableLabels.py diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 0e352a2a..3d8a593b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -46,7 +46,6 @@ from src.core.ts_core import (TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELD from src.core.utils.web import strip_web_protocol from src.qt.flowlayout import FlowLayout, FlowWidget from src.qt.main_window import Ui_MainWindow -from src.qt.utils.clickableLabels import FileOpenerLabel import src.qt.resources_rc # SIGQUIT is not defined on Windows @@ -1970,6 +1969,23 @@ class AddFieldModal(QWidget): self.root_layout.addStretch(1) self.root_layout.addWidget(self.button_container) +class FileOpenerLabel(QLabel): + def __init__(self, text, parent=None): + super().__init__(text, parent) + + def setFilePath(self, filepath): + self.filepath = filepath + + def mousePressEvent(self, event): + super().mousePressEvent(event) + #open file + if hasattr(self, 'filepath'): + if os.path.exists(self.filepath): + os.startfile(self.filepath) + logging.info(f'Opening file: {self.filepath}') + else: + logging.error(f'File not found: {self.filepath}') + class PreviewPanel(QWidget): """The Preview Panel Widget.""" tags_updated = Signal() diff --git a/tagstudio/src/qt/utils/clickableLabels.py b/tagstudio/src/qt/utils/clickableLabels.py deleted file mode 100644 index 12d4cba9..00000000 --- a/tagstudio/src/qt/utils/clickableLabels.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -import logging -from PySide6.QtWidgets import QLabel - -class FileOpenerLabel(QLabel): - def __init__(self, text, parent=None): - super().__init__(text, parent) - - def setFilePath(self, filepath): - self.filepath = filepath - - def mousePressEvent(self, event): - super().mousePressEvent(event) - #open file - if hasattr(self, 'filepath'): - if os.path.exists(self.filepath): - os.startfile(self.filepath) - logging.info(f'Opening file: {self.filepath}') - else: - logging.error(f'File not found: {self.filepath}') \ No newline at end of file From a9cbab40abbe7f468132f9fae61b481cd25b2e60 Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Fri, 26 Apr 2024 20:40:43 -0300 Subject: [PATCH 51/59] works as expected (open file in explorer) in windows --- tagstudio/src/qt/ts_qt.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 3d8a593b..0a265e68 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1970,21 +1970,28 @@ class AddFieldModal(QWidget): self.root_layout.addWidget(self.button_container) class FileOpenerLabel(QLabel): - def __init__(self, text, parent=None): - super().__init__(text, parent) + def __init__(self, text, parent=None): + super().__init__(text, parent) - def setFilePath(self, filepath): - self.filepath = filepath - - def mousePressEvent(self, event): - super().mousePressEvent(event) - #open file - if hasattr(self, 'filepath'): - if os.path.exists(self.filepath): - os.startfile(self.filepath) - logging.info(f'Opening file: {self.filepath}') - else: - logging.error(f'File not found: {self.filepath}') + def setFilePath(self, filepath): + self.filepath = filepath + + def mousePressEvent(self, event): + super().mousePressEvent(event) + #open file + if hasattr(self, 'filepath'): + if os.path.exists(self.filepath): + logging.info(f'Opening file: {self.filepath}') + if os.name == 'nt': # Windows + command = f'explorer /select,"{self.filepath}"' + subprocess.run(command, shell=True) + else: # macOS and Linux + command = f'nautilus --select "{self.filepath}"' # Adjust for your Linux file manager if different + if subprocess.run(command, shell=True).returncode == 0: + file_loc = os.path.dirname(self.filepath) + os.startfile(self.file_loc) + else: + logging.error(f'File not found: {self.filepath}') class PreviewPanel(QWidget): """The Preview Panel Widget.""" From 5bd2aaaf9ec9b570db2a3021bf5024487c07d025 Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Fri, 26 Apr 2024 22:04:40 -0300 Subject: [PATCH 52/59] context menu for thumbs --- tagstudio/src/qt/ts_qt.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 0a265e68..950d445b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2742,6 +2742,8 @@ class ItemThumb(FlowWidget): """ The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.). """ + on_click = Signal() + on_edit = Signal() update_cutoff: float = time.time() @@ -2871,6 +2873,14 @@ class ItemThumb(FlowWidget): # self.bg_button.setMinimumSize(*thumb_size) # self.bg_button.setMaximumSize(*thumb_size) + self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + open_file_action = QAction('Open file', self) + open_file_action.triggered.connect(self.open_file) + open_explorer_action = QAction('Open file in explorer', self) + open_explorer_action.triggered.connect(self.open_explorer) + self.thumb_button.addAction(open_file_action) + self.thumb_button.addAction(open_explorer_action) + # Static Badges ======================================================== # Item Type Badge ------------------------------------------------------ @@ -2970,6 +2980,31 @@ class ItemThumb(FlowWidget): self.set_mode(mode) + def open_file(self): + entry = self.lib.get_entry(self.item_id) + filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}') + if os.path.exists(filepath): + os.startfile(filepath) + logging.info(f'Opening file: {filepath}') + else: + logging.error(f'File not found: {filepath}') + + def open_explorer(self): + entry = self.lib.get_entry(self.item_id) + filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}') + if os.path.exists(filepath): + logging.info(f'Opening file: {filepath}') + if os.name == 'nt': # Windows + command = f'explorer /select,"{filepath}"' + subprocess.run(command, shell=True) + else: # macOS and Linux + command = f'nautilus --select "{filepath}"' # Adjust for your Linux file manager if different + if subprocess.run(command, shell=True).returncode == 0: + file_loc = os.path.dirname(filepath) + os.startfile(file_loc) + else: + logging.error(f'File not found: {filepath}') + def set_mode(self, mode: Optional[ItemType]) -> None: if mode is None: self.unsetCursor() From 6831a8939277d211386c43189c26d69d0c7f39d5 Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Fri, 26 Apr 2024 23:19:24 -0300 Subject: [PATCH 53/59] file opening preview panel context menu --- tagstudio/src/qt/ts_qt.py | 98 +++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 950d445b..69519bd8 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1969,6 +1969,34 @@ class AddFieldModal(QWidget): self.root_layout.addStretch(1) self.root_layout.addWidget(self.button_container) +class FileOpenerHelper(): + def __init__(self, filepath:str): + self.filepath = filepath + + def set_filepath(self, filepath:str): + self.filepath = filepath + + def open_file(self): + if os.path.exists(self.filepath): + os.startfile(self.filepath) + logging.info(f'Opening file: {self.filepath}') + else: + logging.error(f'File not found: {self.filepath}') + + def open_explorer(self): + if os.path.exists(self.filepath): + logging.info(f'Opening file: {self.filepath}') + if os.name == 'nt': # Windows + command = f'explorer /select,"{self.filepath}"' + subprocess.run(command, shell=True) + else: # macOS and Linux + command = f'nautilus --select "{self.filepath}"' # Adjust for your Linux file manager if different + if subprocess.run(command, shell=True).returncode == 0: + file_loc = os.path.dirname(self.filepath) + file_loc = os.path.normpath(file_loc) + os.startfile(file_loc) + else: + logging.error(f'File not found: {self.filepath}') class FileOpenerLabel(QLabel): def __init__(self, text, parent=None): super().__init__(text, parent) @@ -1978,20 +2006,8 @@ class FileOpenerLabel(QLabel): def mousePressEvent(self, event): super().mousePressEvent(event) - #open file - if hasattr(self, 'filepath'): - if os.path.exists(self.filepath): - logging.info(f'Opening file: {self.filepath}') - if os.name == 'nt': # Windows - command = f'explorer /select,"{self.filepath}"' - subprocess.run(command, shell=True) - else: # macOS and Linux - command = f'nautilus --select "{self.filepath}"' # Adjust for your Linux file manager if different - if subprocess.run(command, shell=True).returncode == 0: - file_loc = os.path.dirname(self.filepath) - os.startfile(self.file_loc) - else: - logging.error(f'File not found: {self.filepath}') + opener = FileOpenerHelper(self.filepath) + opener.open_explorer() class PreviewPanel(QWidget): """The Preview Panel Widget.""" @@ -2028,6 +2044,13 @@ class PreviewPanel(QWidget): self.preview_img = QPushButton() self.preview_img.setMinimumSize(*self.img_button_size) self.preview_img.setFlat(True) + + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + self.open_file_action = QAction('Open file', self) + self.open_explorer_action = QAction('Open file in explorer', self) + + self.preview_img.addAction(self.open_file_action) + self.preview_img.addAction(self.open_explorer_action) self.tr = ThumbRenderer() self.tr.updated.connect(lambda ts, i, s: (self.preview_img.setIcon(i))) self.tr.updated_ratio.connect(lambda ratio: (self.set_image_ratio(ratio), @@ -2246,6 +2269,7 @@ class PreviewPanel(QWidget): if len(self.driver.selected) == 0: if len(self.selected) != 0 or not self.initialized: self.file_label.setText(f"No Items Selected") + self.file_label.setFilePath('') self.dimensions_label.setText("") ratio: float = self.devicePixelRatio() self.tr.render_big(time.time(), '', (512, 512), ratio, True) @@ -2275,6 +2299,10 @@ class PreviewPanel(QWidget): self.tr.render_big(time.time(), filepath, (512, 512), ratio) self.file_label.setText("\u200b".join(filepath)) + opener = FileOpenerHelper(filepath) + self.open_file_action.triggered.connect(opener.open_file) + self.open_explorer_action.triggered.connect(opener.open_explorer) + # TODO: Do this somewhere else, this is just here temporarily. extension = os.path.splitext(filepath)[1][1:].lower() try: @@ -2347,6 +2375,7 @@ class PreviewPanel(QWidget): elif len(self.driver.selected) > 1: if self.selected != self.driver.selected: self.file_label.setText(f"{len(self.driver.selected)} Items Selected") + self.file_label.setFilePath('') self.dimensions_label.setText("") ratio: float = self.devicePixelRatio() self.tr.render_big(time.time(), '', (512, 512), ratio, True) @@ -2742,9 +2771,6 @@ class ItemThumb(FlowWidget): """ The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.). """ - on_click = Signal() - on_edit = Signal() - update_cutoff: float = time.time() collation_icon_128: Image.Image = Image.open(os.path.normpath( @@ -2874,10 +2900,11 @@ class ItemThumb(FlowWidget): # self.bg_button.setMaximumSize(*thumb_size) self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + self.opener = FileOpenerHelper('') open_file_action = QAction('Open file', self) - open_file_action.triggered.connect(self.open_file) + open_file_action.triggered.connect(lambda: print('Open file')) open_explorer_action = QAction('Open file in explorer', self) - open_explorer_action.triggered.connect(self.open_explorer) + open_explorer_action.triggered.connect(self.opener.open_explorer) self.thumb_button.addAction(open_file_action) self.thumb_button.addAction(open_explorer_action) @@ -2980,31 +3007,6 @@ class ItemThumb(FlowWidget): self.set_mode(mode) - def open_file(self): - entry = self.lib.get_entry(self.item_id) - filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}') - if os.path.exists(filepath): - os.startfile(filepath) - logging.info(f'Opening file: {filepath}') - else: - logging.error(f'File not found: {filepath}') - - def open_explorer(self): - entry = self.lib.get_entry(self.item_id) - filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}') - if os.path.exists(filepath): - logging.info(f'Opening file: {filepath}') - if os.name == 'nt': # Windows - command = f'explorer /select,"{filepath}"' - subprocess.run(command, shell=True) - else: # macOS and Linux - command = f'nautilus --select "{filepath}"' # Adjust for your Linux file manager if different - if subprocess.run(command, shell=True).returncode == 0: - file_loc = os.path.dirname(filepath) - os.startfile(file_loc) - else: - logging.error(f'File not found: {filepath}') - def set_mode(self, mode: Optional[ItemType]) -> None: if mode is None: self.unsetCursor() @@ -3100,7 +3102,15 @@ class ItemThumb(FlowWidget): def set_item_id(self, id: int): + ''' + also sets the filepath for the file opener + ''' self.item_id = id + if(id == -1): + return + entry = self.lib.get_entry(self.item_id) + filepath = os.path.normpath(f'{self.lib.library_dir}/{entry.path}/{entry.filename}') + self.opener.set_filepath(filepath) def assign_favorite(self, value: bool): # Switching mode to None to bypass mode-specific operations when the From 31f402289565b55d3a84d11f0eb6055e9bf42374 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Sat, 27 Apr 2024 02:00:18 -0700 Subject: [PATCH 54/59] Added File Extension Blacklist - All file types (minus JSON, XMP, and AAE) are now shown by default in the library (existing libraries will need to refresh) - Added the Edit -> "Ignore File Extensions" option, providing the user with a way to blacklist certain file extensions from their library - The targeted version number has been updated to 9.2.0 (this is not the final 9.2.0 release, commits will still be added before that release is packaged up) --- tagstudio/src/core/library.py | 210 ++++++++++++++++++---------------- tagstudio/src/core/ts_core.py | 2 +- tagstudio/src/qt/ts_qt.py | 53 ++++++++- 3 files changed, 166 insertions(+), 99 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 97f2e871..f08d80e7 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -323,6 +323,9 @@ class Library: # That filename can then be used to provide quick lookup to image metadata entries in the Library. # NOTE: On Windows, these strings are always lowercase. self.filename_to_entry_id_map: dict[str, int] = {} + # A list of file extensions to be ignored by TagStudio. + self.default_ext_blacklist: list = ['json', 'xmp', 'aae'] + self.ignored_extensions: list = self.default_ext_blacklist # Tags ================================================================= # List of every Tag object (ts-v8). @@ -612,6 +615,10 @@ class Library: self.verify_ts_folders() major, minor, patch = json_dump['ts-version'].split('.') + # Load Extension Blacklist --------------------------------- + if 'ignored_extensions' in json_dump.keys(): + self.ignored_extensions = json_dump['ignored_extensions'] + # Parse Tags --------------------------------------------------- if 'tags' in json_dump.keys(): start_time = time.time() @@ -850,6 +857,7 @@ class Library: Used in saving the library to disk. """ file_to_save = {"ts-version": ts_core.VERSION, + "ignored_extensions": [], "tags": [], "collations": [], "fields": [], @@ -858,6 +866,9 @@ class Library: } print('[LIBRARY] Formatting Tags to JSON...') + + file_to_save['ignored_extensions'] = [i for i in self.ignored_extensions if i is not ''] + for tag in self.tags: file_to_save["tags"].append(tag.compressed_dict()) @@ -925,6 +936,7 @@ class Library: self.missing_files.clear() self.fixed_files.clear() self.filename_to_entry_id_map: dict[str, int] = {} + self.ignored_extensions = self.default_ext_blacklist self.tags.clear() self._next_tag_id: int = 1000 @@ -950,7 +962,7 @@ class Library: # p = Path(os.path.normpath(f)) if ('$RECYCLE.BIN' not in f and ts_core.TS_FOLDER_NAME not in f and 'tagstudio_thumbs' not in f and not os.path.isdir(f)): - if os.path.splitext(f)[1][1:].lower() in ts_core.ALL_FILE_TYPES: + if os.path.splitext(f)[1][1:].lower() not in self.ignored_extensions: self.dir_file_count += 1 file = str(os.path.relpath(f, self.library_dir)) @@ -1416,101 +1428,103 @@ class Library: # non_entry_count = 0 # Iterate over all Entries ============================================================= for entry in self.entries: + allowed_ext: bool = False if os.path.splitext(entry.filename)[1][1:].lower() in self.ignored_extensions else True # try: # entry: Entry = self.entries[self.file_to_library_index_map[self._source_filenames[i]]] # print(f'{entry}') - # If the entry has tags of any kind, append them to this main tag list. - entry_tags: list[int] = [] - entry_authors: list[str] = [] - if entry.fields: - for field in entry.fields: - field_id = list(field.keys())[0] - if self.get_field_obj(field_id)['type'] == 'tag_box': - entry_tags.extend(field[field_id]) - if self.get_field_obj(field_id)['name'] == 'Author': - entry_authors.extend(field[field_id]) - if self.get_field_obj(field_id)['name'] == 'Artist': - entry_authors.extend(field[field_id]) + if allowed_ext: + # If the entry has tags of any kind, append them to this main tag list. + entry_tags: list[int] = [] + entry_authors: list[str] = [] + if entry.fields: + for field in entry.fields: + field_id = list(field.keys())[0] + if self.get_field_obj(field_id)['type'] == 'tag_box': + entry_tags.extend(field[field_id]) + if self.get_field_obj(field_id)['name'] == 'Author': + entry_authors.extend(field[field_id]) + if self.get_field_obj(field_id)['name'] == 'Artist': + entry_authors.extend(field[field_id]) - # print(f'Entry Tags: {entry_tags}') + # print(f'Entry Tags: {entry_tags}') - # Add Entries from special flags ------------------------------- - # TODO: Come up with a more user-resistent way to 'archived' and 'favorite' tags. - if only_untagged: - if not entry_tags: - results.append((ItemType.ENTRY, entry.id)) - elif only_no_author: - if not entry_authors: - results.append((ItemType.ENTRY, entry.id)) - elif only_empty: - if not entry.fields: - results.append((ItemType.ENTRY, entry.id)) - elif only_missing: - if os.path.normpath(f'{self.library_dir}/{entry.path}/{entry.filename}') in self.missing_files: - results.append((ItemType.ENTRY, entry.id)) - - # elif query == "archived": - # if entry.tags and self._tag_names_to_tag_id_map[self.archived_word.lower()][0] in entry.tags: - # self.filtered_file_list.append(file) - # pb.value = len(self.filtered_file_list) - # elif query in entry.path.lower(): - - # NOTE: This searches path and filenames. - if allow_adv: - if [q for q in query_words if (q in entry.path.lower())]: - results.append((ItemType.ENTRY, entry.id)) - elif [q for q in query_words if (q in entry.filename.lower())]: - results.append((ItemType.ENTRY, entry.id)) - elif tag_only: - if entry.has_tag(self, int(query_words[0])): - results.append((ItemType.ENTRY, entry.id)) - - # elif query in entry.filename.lower(): - # self.filtered_entries.append(index) - elif entry_tags: - # For each verified, extracted Tag term. - failure_to_union_terms = False - for term in all_tag_terms: - # If the term from the previous loop was already verified: - if not failure_to_union_terms: - cluster: set = set() - # Add the immediate associated Tags to the set (ex. Name, Alias hits) - # Since this term could technically map to multiple IDs, iterate over it - # (You're 99.9999999% likely to just get 1 item) - for id in self._tag_strings_to_id_map[term]: - cluster.add(id) - cluster = cluster.union( - set(self.get_tag_cluster(id))) - # print(f'Full Cluster: {cluster}') - # For each of the Tag IDs in the term's ID cluster: - for t in cluster: - # Assume that this ID from the cluster is not in the Entry. - # Wait to see if proven wrong. - failure_to_union_terms = True - # If the ID actually is in the Entry, - if t in entry_tags: - # There wasn't a failure to find one of the term's cluster IDs in the Entry. - # There is also no more need to keep checking the rest of the terms in the cluster. - failure_to_union_terms = False - # print(f'FOUND MATCH: {t}') - break - # print(f'\tFailure to Match: {t}') - # If there even were tag terms to search through AND they all match an entry - if all_tag_terms and not failure_to_union_terms: - # self.filter_entries.append() - # self.filtered_file_list.append(file) - # results.append((SearchItemType.ENTRY, entry.id)) - added = False - for f in entry.fields: - if self.get_field_attr(f, 'type') == 'collation': - if (self.get_field_attr(f, 'content') not in collations_added): - results.append((ItemType.COLLATION, self.get_field_attr(f, 'content'))) - collations_added.append(self.get_field_attr(f, 'content')) - added = True - - if not added: + # Add Entries from special flags ------------------------------- + # TODO: Come up with a more user-resistent way to 'archived' and 'favorite' tags. + if only_untagged: + if not entry_tags: results.append((ItemType.ENTRY, entry.id)) + elif only_no_author: + if not entry_authors: + results.append((ItemType.ENTRY, entry.id)) + elif only_empty: + if not entry.fields: + results.append((ItemType.ENTRY, entry.id)) + elif only_missing: + if os.path.normpath(f'{self.library_dir}/{entry.path}/{entry.filename}') in self.missing_files: + results.append((ItemType.ENTRY, entry.id)) + + # elif query == "archived": + # if entry.tags and self._tag_names_to_tag_id_map[self.archived_word.lower()][0] in entry.tags: + # self.filtered_file_list.append(file) + # pb.value = len(self.filtered_file_list) + # elif query in entry.path.lower(): + + # NOTE: This searches path and filenames. + if allow_adv: + if [q for q in query_words if (q in entry.path.lower())]: + results.append((ItemType.ENTRY, entry.id)) + elif [q for q in query_words if (q in entry.filename.lower())]: + results.append((ItemType.ENTRY, entry.id)) + elif tag_only: + if entry.has_tag(self, int(query_words[0])): + results.append((ItemType.ENTRY, entry.id)) + + # elif query in entry.filename.lower(): + # self.filtered_entries.append(index) + elif entry_tags: + # For each verified, extracted Tag term. + failure_to_union_terms = False + for term in all_tag_terms: + # If the term from the previous loop was already verified: + if not failure_to_union_terms: + cluster: set = set() + # Add the immediate associated Tags to the set (ex. Name, Alias hits) + # Since this term could technically map to multiple IDs, iterate over it + # (You're 99.9999999% likely to just get 1 item) + for id in self._tag_strings_to_id_map[term]: + cluster.add(id) + cluster = cluster.union( + set(self.get_tag_cluster(id))) + # print(f'Full Cluster: {cluster}') + # For each of the Tag IDs in the term's ID cluster: + for t in cluster: + # Assume that this ID from the cluster is not in the Entry. + # Wait to see if proven wrong. + failure_to_union_terms = True + # If the ID actually is in the Entry, + if t in entry_tags: + # There wasn't a failure to find one of the term's cluster IDs in the Entry. + # There is also no more need to keep checking the rest of the terms in the cluster. + failure_to_union_terms = False + # print(f'FOUND MATCH: {t}') + break + # print(f'\tFailure to Match: {t}') + # If there even were tag terms to search through AND they all match an entry + if all_tag_terms and not failure_to_union_terms: + # self.filter_entries.append() + # self.filtered_file_list.append(file) + # results.append((SearchItemType.ENTRY, entry.id)) + added = False + for f in entry.fields: + if self.get_field_attr(f, 'type') == 'collation': + if (self.get_field_attr(f, 'content') not in collations_added): + results.append((ItemType.COLLATION, self.get_field_attr(f, 'content'))) + collations_added.append(self.get_field_attr(f, 'content')) + added = True + + if not added: + results.append((ItemType.ENTRY, entry.id)) # sys.stdout.write( # f'\r[INFO][FILTER]: {len(self.filtered_file_list)} matches found') @@ -1536,15 +1550,17 @@ class Library: for entry in self.entries: added = False - for f in entry.fields: - if self.get_field_attr(f, 'type') == 'collation': - if (self.get_field_attr(f, 'content') not in collations_added): - results.append((ItemType.COLLATION, self.get_field_attr(f, 'content'))) - collations_added.append(self.get_field_attr(f, 'content')) - added = True + allowed_ext: bool = False if os.path.splitext(entry.filename)[1][1:].lower() in self.ignored_extensions else True + if allowed_ext: + for f in entry.fields: + if self.get_field_attr(f, 'type') == 'collation': + if (self.get_field_attr(f, 'content') not in collations_added): + results.append((ItemType.COLLATION, self.get_field_attr(f, 'content'))) + collations_added.append(self.get_field_attr(f, 'content')) + added = True - if not added: - results.append((ItemType.ENTRY, entry.id)) + if not added: + results.append((ItemType.ENTRY, entry.id)) # for file in self._source_filenames: # self.filtered_file_list.append(file) results.reverse() diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 0ea35039..fb247952 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -9,7 +9,7 @@ import os from src.core.library import Entry, Library -VERSION: str = '9.1.0' # Major.Minor.Patch +VERSION: str = '9.2.0' # Major.Minor.Patch VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release # The folder & file names where TagStudio keeps its data relative to a library. diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7e397f90..776691e9 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -33,7 +33,7 @@ from PySide6.QtUiTools import QUiLoader from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QPlainTextEdit, QLineEdit, QScrollArea, QFrame, QTextEdit, QComboBox, QProgressDialog, QFileDialog, QListView, QSplitter, QSizePolicy, QMessageBox, QBoxLayout, QCheckBox, QSplashScreen, - QMenu) + QMenu, QTableWidget, QTableWidgetItem) from humanfriendly import format_timespan, format_size from src.core.library import Collation, Entry, ItemType, Library, Tag @@ -1969,6 +1969,46 @@ class AddFieldModal(QWidget): self.root_layout.addStretch(1) self.root_layout.addWidget(self.button_container) +class FileExtensionModal(PanelWidget): + done = Signal() + def __init__(self, library:'Library'): + super().__init__() + self.lib = library + self.setWindowTitle(f'File Extensions') + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setMinimumSize(200, 400) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6,6,6,6) + + self.table = QTableWidget(len(self.lib.ignored_extensions), 1) + self.table.horizontalHeader().setVisible(False) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setStretchLastSection(True) + + self.add_button = QPushButton() + self.add_button.setText('&Add Extension') + self.add_button.clicked.connect(self.add_item) + self.add_button.setDefault(True) + self.add_button.setMinimumWidth(100) + + self.root_layout.addWidget(self.table) + self.root_layout.addWidget(self.add_button, alignment=Qt.AlignmentFlag.AlignCenter) + self.refresh_list() + + def refresh_list(self): + for i, ext in enumerate(self.lib.ignored_extensions): + self.table.setItem(i, 0, QTableWidgetItem(ext)) + + def add_item(self): + self.table.insertRow(self.table.rowCount()) + + def save(self): + self.lib.ignored_extensions.clear() + for i in range(self.table.rowCount()): + ext = self.table.item(i, 0) + if ext and ext.text(): + self.lib.ignored_extensions.append(ext.text()) + class PreviewPanel(QWidget): """The Preview Panel Widget.""" tags_updated = Signal() @@ -3879,6 +3919,10 @@ class QtDriver(QObject): edit_menu.addSeparator() + manage_file_extensions_action = QAction('Ignore File Extensions', menu_bar) + manage_file_extensions_action.triggered.connect(lambda: self.show_file_extension_modal()) + edit_menu.addAction(manage_file_extensions_action) + tag_database_action = QAction('Tag Database', menu_bar) tag_database_action.triggered.connect(lambda: self.show_tag_database()) edit_menu.addAction(tag_database_action) @@ -4054,6 +4098,13 @@ class QtDriver(QObject): def show_tag_database(self): self.modal = PanelModal(TagDatabasePanel(self.lib),'Tag Database', 'Tag Database', has_save=False) self.modal.show() + + def show_file_extension_modal(self): + # self.modal = FileExtensionModal(self.lib) + panel = FileExtensionModal(self.lib) + self.modal = PanelModal(panel, 'Ignored File Extensions', 'Ignored File Extensions', has_save=True) + self.modal.saved.connect(lambda: (panel.save(), self.filter_items(''))) + self.modal.show() def add_new_files_callback(self): """Runs when user initiates adding new files to the Library.""" From 955d4a0c9f2b2419061b363aced4a2df646a3362 Mon Sep 17 00:00:00 2001 From: Matheus Cirillo Date: Sat, 27 Apr 2024 09:44:04 -0300 Subject: [PATCH 55/59] fixed bug where it wouldn't open outside debug mode --- tagstudio/src/qt/ts_qt.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 69519bd8..8adaad91 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -2046,6 +2046,7 @@ class PreviewPanel(QWidget): self.preview_img.setFlat(True) self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + self.opener = FileOpenerHelper('') self.open_file_action = QAction('Open file', self) self.open_explorer_action = QAction('Open file in explorer', self) @@ -2271,6 +2272,7 @@ class PreviewPanel(QWidget): self.file_label.setText(f"No Items Selected") self.file_label.setFilePath('') self.dimensions_label.setText("") + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) ratio: float = self.devicePixelRatio() self.tr.render_big(time.time(), '', (512, 512), ratio, True) try: @@ -2299,9 +2301,10 @@ class PreviewPanel(QWidget): self.tr.render_big(time.time(), filepath, (512, 512), ratio) self.file_label.setText("\u200b".join(filepath)) - opener = FileOpenerHelper(filepath) - self.open_file_action.triggered.connect(opener.open_file) - self.open_explorer_action.triggered.connect(opener.open_explorer) + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + self.opener = FileOpenerHelper(filepath) + self.open_file_action.triggered.connect(self.opener.open_file) + self.open_explorer_action.triggered.connect(self.opener.open_explorer) # TODO: Do this somewhere else, this is just here temporarily. extension = os.path.splitext(filepath)[1][1:].lower() @@ -2377,6 +2380,7 @@ class PreviewPanel(QWidget): self.file_label.setText(f"{len(self.driver.selected)} Items Selected") self.file_label.setFilePath('') self.dimensions_label.setText("") + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) ratio: float = self.devicePixelRatio() self.tr.render_big(time.time(), '', (512, 512), ratio, True) try: @@ -2902,7 +2906,7 @@ class ItemThumb(FlowWidget): self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) self.opener = FileOpenerHelper('') open_file_action = QAction('Open file', self) - open_file_action.triggered.connect(lambda: print('Open file')) + open_file_action.triggered.connect(self.opener.open_file) open_explorer_action = QAction('Open file in explorer', self) open_explorer_action.triggered.connect(self.opener.open_explorer) self.thumb_button.addAction(open_file_action) From d5d9cd3e7ab2104c46dd09907f94b2de124be1a6 Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Sun, 28 Apr 2024 01:17:41 +0800 Subject: [PATCH 56/59] improve code readability --- tagstudio/src/cli/ts_cli.py | 54 ++++++++--------- tagstudio/src/core/library.py | 105 ++++++++++++---------------------- tagstudio/src/core/ts_core.py | 7 +-- tagstudio/src/qt/ts_qt.py | 12 ++-- 4 files changed, 75 insertions(+), 103 deletions(-) diff --git a/tagstudio/src/cli/ts_cli.py b/tagstudio/src/cli/ts_cli.py index 560d22b2..df5dd58d 100644 --- a/tagstudio/src/cli/ts_cli.py +++ b/tagstudio/src/cli/ts_cli.py @@ -493,7 +493,7 @@ class CliDriver: # print(f'Char Limit: {char_limit}, Len: {len(text)}') return char_limit - def truncate_text(self, text: str) -> int: + def truncate_text(self, text: str) -> str: """Returns a truncated string for displaying, calculated with `get_char_limit()`.""" if len(text) > self.get_char_limit(text): # print(f'Char Limit: {self.get_char_limit(text)}, Len: {len(text)}') @@ -761,7 +761,7 @@ class CliDriver: offset = 1 if (index >= row_count) and ( row_number != row_count) else 0 elif displayable % table_size != 0: - if col_num > 1 and col_num <= displayable % table_size: + if 1 < col_num <= displayable % table_size: offset += col_num - 1 elif col_num > 1 and col_num > displayable % table_size: offset = displayable % table_size @@ -1022,30 +1022,31 @@ class CliDriver: """ was_executed:bool = False message:str = '' + com_name = com[0].lower() # Backup Library ======================================================= - if (com[0].lower() == 'backup'): + if com_name == 'backup': self.backup_library(display_message=False) was_executed = True message=f'{INFO} Backed up Library to disk.' # Create Collage ======================================================= - elif (com[0].lower() == 'collage'): + elif com_name == 'collage': filename = self.create_collage() if filename: was_executed = True message = f'{INFO} Saved collage to \"{filename}\".' # Save Library ========================================================= - elif (com[0].lower() == 'save' or com[0].lower() == 'write' or com[0].lower() == 'w'): + elif com_name in ('save', 'write', 'w'): self.save_library(display_message=False) was_executed = True message=f'{INFO} Library saved to disk.' # Toggle Debug ========================================================= - elif (com[0].lower() == 'toggle-debug'): + elif com_name == 'toggle-debug': self.args.debug = not self.args.debug was_executed = True message=f'{INFO} Debug Mode Active.' if self.args.debug else f'{INFO} Debug Mode Deactivated.' # Toggle External Preview ============================================== - elif (com[0].lower() == 'toggle-external-preview'): + elif com_name == 'toggle-external-preview': self.args.external_preview = not self.args.external_preview if self.args.external_preview: self.init_external_preview() @@ -1054,11 +1055,11 @@ class CliDriver: was_executed = True message=f'{INFO} External Preview Enabled.' if self.args.external_preview else f'{INFO} External Preview Disabled.' # Quit ================================================================= - elif com[0].lower() == 'quit' or com[0].lower() == 'q': + elif com_name in ('quit', 'q'): self.exit(save=True, backup=False) was_executed = True # Quit without Saving ================================================== - elif com[0].lower() == 'quit!' or com[0].lower() == 'q!': + elif com_name in ('quit!', 'q!'): self.exit(save=False, backup=False) was_executed = True @@ -1345,7 +1346,7 @@ class CliDriver: # self.scr_library_home(clear_scr=False) # Add New Entries ================================================== elif ' '.join(com) == 'add new': - if self.is_new_file_count_init == False: + if not self.is_new_file_count_init: print( f'{INFO} Scanning for files in \'{self.lib.library_dir}\' (This may take a while)...') # if not self.lib.files_not_in_library: @@ -1390,7 +1391,7 @@ class CliDriver: for unresolved in self.lib.missing_matches: res = self.scr_choose_missing_match( self.lib.get_entry_id_from_filepath(unresolved), clear_scr=False) - if res != None and int(res) >= 0: + if res is not None and int(res) >= 0: clear() print( f'{INFO} Updated {self.lib.entries[self.lib.get_entry_id_from_filepath(unresolved)].path} -> {self.lib.missing_matches[unresolved][res]}') @@ -2049,7 +2050,7 @@ class CliDriver: '<#> Quit', BRIGHT_CYAN_FG)) print('> ', end='') - com: list[str] = input().lstrip().rstrip().split(' ') + com: list[str] = input().strip().split(' ') gc, message = self.global_commands(com) if gc: if message: @@ -2057,6 +2058,7 @@ class CliDriver: print(message) clear_scr=False else: + com_name = com[0].lower() try: # # Quit ========================================================= @@ -2069,13 +2071,13 @@ class CliDriver: # # self.cleanup() # sys.exit() # Cancel ======================================================= - if (com[0].lower() == 'cancel' or com[0].lower() == 'c' or com[0] == '0') and required==False: + if com_name in ('cancel', 'c', '0') and not required: clear() return -1 # Selection ==================================================== - elif int(com[0]) > 0 and int(com[0]) <= len(choices): + elif com_name.isdigit() and 0 < int(com_name) <= len(choices): clear() - return int(com[0]) - 1 + return int(com_name) - 1 else: # invalid_input = True # print(self.format_h1(str='Please Enter a Valid Selection Number', color=BRIGHT_RED_FG)) @@ -2554,7 +2556,7 @@ class CliDriver: f'Enter #{plural} Cancel', fg_color)) print('> ', end='') - com: list[int] = input().split(' ') + com: list[str] = input().split(' ') selected_ids: list[int] = [] try: for c in com: @@ -2625,14 +2627,14 @@ class CliDriver: self.lib.update_entry_field( entry_index, field_index, new_content.rstrip('\n').rstrip('\r'), 'replace') - def scr_list_tags(self, query: str = '', tag_ids: list[int] = [], clear_scr=True) -> None: + def scr_list_tags(self, query: str = '', tag_ids: list[int] = None, clear_scr=True) -> None: """A screen for listing out and performing CRUD operations on Library Tags.""" # NOTE: While a screen that just displays the first 40 or so random tags on your screen # isn't really that useful, this is just a temporary measure to provide a launchpad # screen for necessary commands such as adding and editing tags. # A more useful screen presentation might look like a list of ranked occurrences, but # that can be figured out and implemented later. - + tag_ids = tag_ids or [] title = f'{self.base_title} - Library \'{self.lib.library_dir}\'' @@ -2673,7 +2675,7 @@ class CliDriver: 'Create Edit <#> Delete <#> Search Close/Done', BRIGHT_MAGENTA_FG)) print('> ', end='') - com: list[str] = input().lstrip().rstrip().split(' ') + com: list[str] = input().strip().split(' ') gc, message = self.global_commands(com) if gc: if message: @@ -2681,9 +2683,9 @@ class CliDriver: print(message) clear_scr=False else: - + com_name = com[0].lower() # Search Tags ========================================================== - if (com[0].lower() == 'search' or com[0].lower() == 's'): + if com_name in ('search', 's'): if len(com) > 1: new_query: str = ' '.join(com[1:]) # self.scr_list_tags(prev_scr, query=new_query, @@ -2696,7 +2698,7 @@ class CliDriver: tag_ids=self.lib.search_tags('') # return # Edit Tag =========================================================== - elif com[0].lower() == 'edit' or com[0].lower() == 'e': + elif com_name in ('edit', 'e'): if len(com) > 1: try: index = int(com[1]) - 1 @@ -2720,7 +2722,7 @@ class CliDriver: # return # Create Tag ============================================================ - elif com[0].lower() == 'create' or com[0].lower() == 'mk': + elif com_name in ('create', 'mk'): tag = Tag(id=0, name='New Tag', shorthand='', aliases=[], subtags_ids=[], color='') self.scr_manage_tag( @@ -2731,7 +2733,7 @@ class CliDriver: # self.scr_list_tags(prev_scr, query=query, tag_ids=tag_ids) # return # Delete Tag =========================================================== - elif com[0].lower() == 'delete' or com[0].lower() == 'del': + elif com_name in ('delete', 'del'): if len(com) > 1: if len(com) > 1: try: @@ -2757,7 +2759,7 @@ class CliDriver: # tag_ids=tag_ids, clear_scr=False) # return # Close View =========================================================== - elif (com[0].lower() == 'close' or com[0].lower() == 'c' or com[0].lower() == 'done'): + elif com_name in ('close', 'c', 'done'): # prev_scr() return # # Quit ================================================================= @@ -3192,7 +3194,7 @@ class CliDriver: selected: str = input() try: - if int(selected) > 0 and int(selected) <= len(colors): + if selected.isdigit() and 0 < int(selected) <= len(colors): selected = colors[int(selected)-1] return selected # except SystemExit: diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index f08d80e7..071fa4e9 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -131,7 +131,7 @@ class Entry: # if self.fields: # if field_index != -1: # logging.info(f'[LIBRARY] ADD TAG to E:{self.id}, F-DI:{field_id}, F-INDEX:{field_index}') - field_index = -1 if field_index == None else field_index + field_index = -1 if field_index is None else field_index for i, f in enumerate(self.fields): if library.get_field_attr(f, 'id') == field_id: field_index = i @@ -631,33 +631,21 @@ class Library: # Step 2: Create a Tag object and append it to the internal Tags list, # then map that Tag's ID to its index in the Tags list. - id = 0 - if 'id' in tag.keys(): - id = tag['id'] + id = int(tag.get('id', 0)) # Don't load tags with duplicate IDs - if id not in [t.id for t in self.tags]: - if int(id) >= self._next_tag_id: - self._next_tag_id = int(id) + 1 + if id not in {t.id for t in self.tags}: + if id >= self._next_tag_id: + self._next_tag_id = id + 1 - name = '' - if 'name' in tag.keys(): - name = tag['name'] - shorthand = '' - if 'shorthand' in tag.keys(): - shorthand = tag['shorthand'] - aliases = [] - if 'aliases' in tag.keys(): - aliases = tag['aliases'] - subtag_ids = [] - if 'subtag_ids' in tag.keys(): - subtag_ids = tag['subtag_ids'] - color = '' - if 'color' in tag.keys(): - color = tag['color'] + name = tag.get('name', '') + shorthand = tag.get('shorthand', '') + aliases = tag.get('aliases', []) + subtag_ids = tag.get('subtag_ids', []) + color = tag.get('color', '') t = Tag( - id=int(id), + id=id, name=name, shorthand=shorthand, aliases=aliases, @@ -683,12 +671,11 @@ class Library: logging.info(f'[LIBRARY] Tags loaded in {(end_time - start_time):.3f} seconds') # Parse Entries ------------------------------------------------ - if 'entries' in json_dump.keys(): + if entries := json_dump.get('entries'): start_time = time.time() - for entry in json_dump['entries']: + for entry in entries: - id = 0 - if 'id' in entry.keys(): + if 'id' in entry: id = int(entry['id']) if id >= self._next_entry_id: self._next_entry_id = id + 1 @@ -697,16 +684,12 @@ class Library: id = self._next_entry_id self._next_entry_id += 1 - filename = '' - if 'filename' in entry.keys(): - filename = entry['filename'] - e_path = '' - if 'path' in entry.keys(): - e_path = entry['path'] + filename = entry.get('filename', '') + e_path = entry.get('path', '') fields = [] - if 'fields' in entry.keys(): + if 'fields' in entry: # Cast JSON str keys to ints - for f in entry['fields']: + for f in fields: f[int(list(f.keys())[0]) ] = f[list(f.keys())[0]] del f[list(f.keys())[0]] @@ -768,28 +751,17 @@ class Library: # the internal Collations list, then map that # Collation's ID to its index in the Collations list. - id = 0 - if 'id' in collation.keys(): - id = collation['id'] + id = int(collation.get('id', 0)) + if id >= self._next_collation_id: + self._next_collation_id = id + 1 - if int(id) >= self._next_collation_id: - self._next_collation_id = int(id) + 1 - - title = '' - if 'title' in collation.keys(): - title = collation['title'] - e_ids_and_pages = '' - if 'e_ids_and_pages' in collation.keys(): - e_ids_and_pages = collation['e_ids_and_pages'] - sort_order = [] - if 'sort_order' in collation.keys(): - sort_order = collation['sort_order'] - cover_id = [] - if 'cover_id' in collation.keys(): - cover_id = collation['cover_id'] + title = collation.get('title', '') + e_ids_and_pages = collation.get('e_ids_and_pages', '') + sort_order = collation.get('sort_order', []) + cover_id = collation.get('cover_id', []) c = Collation( - id=int(id), + id=id, title=title, e_ids_and_pages=e_ids_and_pages, sort_order=sort_order, @@ -1408,7 +1380,7 @@ class Library: only_no_author: bool = True if 'no author' in query or 'no artist' in query else False # Preprocess the Tag terms. - if len(query_words) > 0: + if query_words: for i, term in enumerate(query_words): for j, term in enumerate(query_words): if query_words[i:j+1] and " ".join(query_words[i:j+1]) in self._tag_strings_to_id_map: @@ -1566,7 +1538,7 @@ class Library: results.reverse() return results - def search_tags(self, query: str, include_cluster=False, ignore_builtin=False, threshold: int = 1, context: list[str] = []) -> list[int]: + def search_tags(self, query: str, include_cluster=False, ignore_builtin=False, threshold: int = 1, context: list[str] = None) -> list[int]: """Returns a list of Tag IDs returned from a string query.""" # tag_ids: list[int] = [] # if query: @@ -1659,7 +1631,6 @@ class Library: # Contextual Weighing if context and ((len(id_weights) > 1 and len(priority_ids) > 1) or (len(priority_ids) > 1)): - context_ids: list[int] = [] context_strings: list[str] = [s.replace(' ', '').replace('_', '').replace('-', '').replace( "'", '').replace('(', '').replace(')', '').replace('[', '').replace(']', '').lower() for s in context] for term in context: @@ -1833,16 +1804,16 @@ class Library: # Step [3/7]: # Remove ID -> cluster reference. - if tag_id in self._tag_id_to_cluster_map.keys(): + if tag_id in self._tag_id_to_cluster_map: del self._tag_id_to_cluster_map[tag.id] # Remove mentions of this ID in all clusters. - for key in self._tag_id_to_cluster_map.keys(): - if tag_id in self._tag_id_to_cluster_map[key]: - self._tag_id_to_cluster_map[key].remove(tag.id) + for key, values in self._tag_id_to_cluster_map.items(): + if tag_id in values: + values.remove(tag.id) # Step [4/7]: # Remove mapping of this ID to its index in the tags list. - if tag.id in self._tag_id_to_index_map.keys(): + if tag.id in self._tag_id_to_index_map: del self._tag_id_to_index_map[tag.id] # Step [5/7]: @@ -1921,7 +1892,7 @@ class Library: if data: # Add a Title Field if the data doesn't already exist. - if "title" in data.keys() and data["title"]: + if data.get("title"): field_id = 0 # Title Field ID if not self.does_field_content_exist(entry_id, field_id, data['title']): self.add_field_to_entry(entry_id, field_id) @@ -1929,7 +1900,7 @@ class Library: entry_id, -1, data["title"], 'replace') # Add an Author Field if the data doesn't already exist. - if "author" in data.keys() and data["author"]: + if data.get("author"): field_id = 1 # Author Field ID if not self.does_field_content_exist(entry_id, field_id, data['author']): self.add_field_to_entry(entry_id, field_id) @@ -1937,7 +1908,7 @@ class Library: entry_id, -1, data["author"], 'replace') # Add an Artist Field if the data doesn't already exist. - if "artist" in data.keys() and data["artist"]: + if data.get("artist"): field_id = 2 # Artist Field ID if not self.does_field_content_exist(entry_id, field_id, data['artist']): self.add_field_to_entry(entry_id, field_id) @@ -1945,7 +1916,7 @@ class Library: entry_id, -1, data["artist"], 'replace') # Add a Date Published Field if the data doesn't already exist. - if "date_published" in data.keys() and data["date_published"]: + if data.get("date_published"): field_id = 14 # Date Published Field ID date = str(datetime.datetime.strptime( data["date_published"], '%Y-%m-%d %H:%M:%S')) @@ -1955,7 +1926,7 @@ class Library: self.update_entry_field(entry_id, -1, date, 'replace') # Process String Tags if the data doesn't already exist. - if "tags" in data.keys() and data["tags"]: + if data.get("tags"): tags_field_id = 6 # Tags Field ID content_tags_field_id = 7 # Content Tags Field ID meta_tags_field_id = 8 # Meta Tags Field ID diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index fb247952..0f5457f0 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -137,12 +137,11 @@ class TagStudioCore: # # # print("Could not resolve URL.") # # pass - def match_conditions(self, entry_id: int) -> str: + def match_conditions(self, entry_id: int) -> None: """Matches defined conditions against a file to add Entry data.""" cond_file = os.path.normpath(f'{self.lib.library_dir}/{TS_FOLDER_NAME}/conditions.json') # TODO: Make this stored somewhere better instead of temporarily in this JSON file. - json_dump = {} entry: Entry = self.lib.get_entry(entry_id) try: if os.path.isfile(cond_file): @@ -155,8 +154,8 @@ class TagStudioCore: match = True break if match: - if 'fields' in c.keys() and c['fields']: - for field in c['fields']: + if fields := c.get('fields'): + for field in fields: field_id = self.lib.get_field_attr( field, 'id') diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 776691e9..9cfe206b 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -3805,7 +3805,7 @@ class QtDriver(QObject): 'Open/Create Library', '/', QFileDialog.ShowDirsOnly) - if dir != None and dir != '': + if dir not in (None, ''): self.open_library(dir) def signal_handler(self, sig, frame): @@ -4228,7 +4228,7 @@ class QtDriver(QObject): # sleep(5) # pb.deleteLater() - def run_macros(self, name: str, entry_ids: int): + def run_macros(self, name: str, entry_ids: list[int]): """Runs a specific Macro on a group of given entry_ids.""" for id in entry_ids: self.run_macro(name, id) @@ -4304,7 +4304,7 @@ class QtDriver(QObject): trimmed = False if len(self.nav_frames) > self.cur_frame_idx + 1: - if (frame_content != None): + if frame_content is not None: # Trim the nav stack if user is taking a new route. self.nav_frames = self.nav_frames[:self.cur_frame_idx+1] if self.nav_frames and not self.nav_frames[self.cur_frame_idx].contents: @@ -4316,7 +4316,7 @@ class QtDriver(QObject): self.nav_frames[self.cur_frame_idx].scrollbar_pos = sb_pos self.cur_frame_idx += 1 if not trimmed else 0 # Moving forward at the end of the stack with new content - elif (frame_content != None): + elif frame_content is not None: # If the current page is empty, don't include it in the new stack. if self.nav_frames and not self.nav_frames[self.cur_frame_idx].contents: self.nav_frames.pop() @@ -4327,7 +4327,7 @@ class QtDriver(QObject): self.cur_frame_idx += 1 if not trimmed else 0 # if self.nav_stack[self.cur_page_idx].contents: - if (self.cur_frame_idx != original_pos) or (frame_content != None): + if (self.cur_frame_idx != original_pos) or (frame_content is not None): self.update_thumbs() sb.verticalScrollBar().setValue( self.nav_frames[self.cur_frame_idx].scrollbar_pos) @@ -4601,7 +4601,7 @@ class QtDriver(QObject): for x in collation_entries]) # self.update_thumbs() - def get_frame_contents(self, index=0, query=str): + def get_frame_contents(self, index=0, query: str = None): return ([] if not self.frame_dict[query] else self.frame_dict[query][index], index, len(self.frame_dict[query])) def filter_items(self, query=''): From f0f8e0ea7ea96dd07cc6b9e542c662fd76adaa5a Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Sun, 28 Apr 2024 01:43:37 +0800 Subject: [PATCH 57/59] simplify bool conditions --- tagstudio/src/cli/ts_cli.py | 4 ++-- tagstudio/src/core/library.py | 18 +++++++++--------- tagstudio/src/qt/ts_qt.py | 3 +-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tagstudio/src/cli/ts_cli.py b/tagstudio/src/cli/ts_cli.py index df5dd58d..6605d7c2 100644 --- a/tagstudio/src/cli/ts_cli.py +++ b/tagstudio/src/cli/ts_cli.py @@ -1556,7 +1556,7 @@ class CliDriver: print(self.format_title(title)) - if len(self.filtered_entries) > 0: + if self.filtered_entries: # entry = self.lib.get_entry_from_index( # self.filtered_entries[index]) entry = self.lib.get_entry(self.filtered_entries[index][1]) @@ -1581,7 +1581,7 @@ class CliDriver: self.print_fields(self.filtered_entries[index][1]) else: - if len(self.lib.entries) > 0: + if self.lib.entries: print(self.format_h1('No Entry Results for Query', color=BRIGHT_RED_FG)) self.set_external_preview_default() else: diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 071fa4e9..230fffdd 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -1367,17 +1367,17 @@ class Library: query: str = query.strip().lower() query_words: list[str] = query.split(' ') all_tag_terms: list[str] = [] - only_untagged: bool = True if 'untagged' in query or 'no tags' in query else False - only_empty: bool = True if 'empty' in query or 'no fields' in query else False - only_missing: bool = True if 'missing' in query or 'no file' in query else False - allow_adv: bool = True if 'filename:' in query_words else False - tag_only: bool = True if 'tag_id:' in query_words else False + only_untagged: bool = ('untagged' in query or 'no tags' in query) + only_empty: bool = ('empty' in query or 'no fields' in query) + only_missing: bool = ('missing' in query or 'no file' in query) + allow_adv: bool = 'filename:' in query_words + tag_only: bool = 'tag_id:' in query_words if allow_adv: query_words.remove('filename:') if tag_only: query_words.remove('tag_id:') # TODO: Expand this to allow for dynamic fields to work. - only_no_author: bool = True if 'no author' in query or 'no artist' in query else False + only_no_author: bool = ('no author' in query or 'no artist' in query) # Preprocess the Tag terms. if query_words: @@ -1400,7 +1400,7 @@ class Library: # non_entry_count = 0 # Iterate over all Entries ============================================================= for entry in self.entries: - allowed_ext: bool = False if os.path.splitext(entry.filename)[1][1:].lower() in self.ignored_extensions else True + allowed_ext: bool = os.path.splitext(entry.filename)[1][1:].lower() not in self.ignored_extensions # try: # entry: Entry = self.entries[self.file_to_library_index_map[self._source_filenames[i]]] # print(f'{entry}') @@ -1522,7 +1522,7 @@ class Library: for entry in self.entries: added = False - allowed_ext: bool = False if os.path.splitext(entry.filename)[1][1:].lower() in self.ignored_extensions else True + allowed_ext: bool = os.path.splitext(entry.filename)[1][1:].lower() not in self.ignored_extensions if allowed_ext: for f in entry.fields: if self.get_field_attr(f, 'type') == 'collation': @@ -1963,7 +1963,7 @@ class Library: matching: list[int] = self.search_tags( tag.replace('_', ' ').replace('-', ' '), include_cluster=False, ignore_builtin=True, threshold=2, context=tags) priority_field_index = -1 - if len(matching) > 0: + if matching: # NOTE: The following commented-out code enables the ability # to prefer an existing built-in tag_box field to add to diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 9cfe206b..ba0f8b2f 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1421,10 +1421,9 @@ class FixDupeFilesModal(QWidget): os.path.normpath(self.lib.library_dir)) qfd.setFileMode(QFileDialog.FileMode.ExistingFile) qfd.setNameFilter("DupeGuru Files (*.dupeguru)") - filename = [] if qfd.exec_(): filename = qfd.selectedFiles() - if len(filename) > 0: + if filename: self.set_filename(filename[0]) def set_filename(self, filename:str): From e2aba7ddf76ef73d062f3c06bb2601796147bc9e Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Sat, 27 Apr 2024 21:13:57 +0200 Subject: [PATCH 58/59] Added Proper Visualls to show what would be added --- tagstudio/src/qt/ts_qt.py | 312 +++++++++++++++++++++++++++++++------- 1 file changed, 261 insertions(+), 51 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 42ef953b..6afb2ec7 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -33,7 +33,7 @@ from PySide6.QtUiTools import QUiLoader from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QPlainTextEdit, QLineEdit, QScrollArea, QFrame, QTextEdit, QComboBox, QProgressDialog, QFileDialog, QListView, QSplitter, QSizePolicy, QMessageBox, QBoxLayout, QCheckBox, QSplashScreen, - QMenu) + QMenu,QTreeWidget, QTreeWidgetItem) from humanfriendly import format_timespan, format_size from src.core.library import Collation, Entry, ItemType, Library, Tag @@ -3897,10 +3897,6 @@ class QtDriver(QObject): create_collage_action = QAction('Create Collage', menu_bar) create_collage_action.triggered.connect(lambda: self.create_collage()) tools_menu.addAction(create_collage_action) - - folders_to_tags_action = QAction('Folders to Tags', menu_bar) - folders_to_tags_action.triggered.connect(lambda: self.folders_to_tags(self.lib)) - tools_menu.addAction(folders_to_tags_action) # Macros Menu ========================================================== self.autofill_action = QAction('Autofill', menu_bar) @@ -3913,6 +3909,11 @@ class QtDriver(QObject): self.sort_fields_action.setToolTip('Alt+S') macros_menu.addAction(self.sort_fields_action) + folders_to_tags_action = QAction('Folders to Tags', menu_bar) + ftt_modal = FoldersToTagsModal(self.lib, self) + folders_to_tags_action.triggered.connect(lambda:ftt_modal.show()) + macros_menu.addAction(folders_to_tags_action) + self.set_macro_menu_viability() menu_bar.addMenu(file_menu) @@ -4739,52 +4740,6 @@ class QtDriver(QObject): ))) i = i+1 - def folders_to_tags(self,library:Library): - logging.info("Converting folders to Tags") - tree = dict(dirs={}) - - def add_tag_to_tree(list:list[Tag]): - branch = tree - for tag in list: - if tag.name not in branch["dirs"]: - branch["dirs"][tag.name] = dict(dirs={},tag=tag) - branch = branch["dirs"][tag.name] - - def add_folders_to_tree(list:list[str])->Tag: - branch = tree - for folder in list: - if folder not in branch["dirs"]: - new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"black") - library.add_tag_to_library(new_tag) - branch["dirs"][folder] = dict(dirs={},tag=new_tag) - branch = branch["dirs"][folder] - return branch["tag"] - - def reverse_tag(tag:Tag,list:list[Tag]): - if list != None: - list.append(tag) - else: - list = [tag] - - if len(tag.subtag_ids) == 0: - add_tag_to_tree(list) - else: - for subtag_id in tag.subtag_ids: - subtag = library.get_tag(subtag_id) - reverse_tag(subtag,list) - - for tag in library.tags: - reverse_tag(tag,None) - - for entry in library.entries: - folders = entry.path.split("\\") - tag = add_folders_to_tree(folders) - if tag: - if not entry.has_tag(library,tag.id): - entry.add_tag(library,tag.id,6) - - logging.info("Done") - def try_save_collage(self, increment_progress:bool): if increment_progress: self.completed += 1 @@ -4797,3 +4752,258 @@ class QtDriver(QObject): end_time = time.time() self.main_window.statusbar.showMessage(f'Collage Saved at "{filename}" ({format_timespan(end_time - self.collage_start_time)})') logging.info(f'Collage Saved at "{filename}" ({format_timespan(end_time - self.collage_start_time)})') + +class FoldersToTagsModal(QWidget): + # done = Signal(int) + def __init__(self, library:'Library', driver:'QtDriver'): + super().__init__() + self.library = library + self.driver:QtDriver = driver + self.count = -1 + self.filename = '' + + self.setWindowTitle(f'Folders To Tags') + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setMinimumSize(500, 800) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6,6,6,6) + + self.desc_widget = QLabel() + self.desc_widget.setObjectName('descriptionLabel') + self.desc_widget.setWordWrap(True) + self.desc_widget.setStyleSheet( + # 'background:blue;' + 'text-align:left;' + # 'font-weight:bold;' + 'font-size:18px;' + # 'padding-top: 6px' + '') + self.desc_widget.setText('''Creates tags based on the folder structure and applies them to entries.\n The Structure below shows all the tags that would be added and to which files they would be added. It being empty means that there are no Tag to be created or assigned''') + self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.scroll_contents = QWidget() + self.scroll_layout = QVBoxLayout(self.scroll_contents) + self.scroll_layout.setContentsMargins(6,0,6,0) + self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + self.scroll_area = QScrollArea() + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setFrameShadow(QFrame.Shadow.Plain) + self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) + self.scroll_area.setWidget(self.scroll_contents) + + self.Apply_button = QPushButton() + self.Apply_button.setText('&Apply') + self.Apply_button.clicked.connect(lambda: self.folders_to_tags(self.library)) + + self.showEvent = self.on_open + + self.root_layout.addWidget(self.desc_widget) + self.root_layout.addWidget(self.scroll_area) + self.root_layout.addWidget(self.Apply_button) + + def on_open(self,event): + for i in reversed(range(self.scroll_layout.count())): + self.scroll_layout.itemAt(i).widget().setParent(None) + + data = self.generate_preview_data(self.library) + + for folder in data["dirs"].values(): + test = self.TreeItemTest(folder,None) + self.scroll_layout.addWidget(test) + + + def generate_preview_data(self,library:Library): + tree = dict(dirs={},files=[]) + + def add_tag_to_tree(list:list[Tag]): + branch = tree + for tag in list: + if tag.name not in branch["dirs"]: + branch["dirs"][tag.name] = dict(dirs={},tag=tag,files=[]) + branch = branch["dirs"][tag.name] + + def add_folders_to_tree(list:list[str])->Tag: + branch = tree + for folder in list: + if folder not in branch["dirs"]: + new_tag = Tag(-1, folder,"",[],[],"green") + branch["dirs"][folder] = dict(dirs={},tag=new_tag,files=[]) + branch = branch["dirs"][folder] + return branch + + for tag in library.tags: + reversed_tag = self.reverse_tag(tag,None) + logging.info(set(map(lambda tag:tag.name ,reversed_tag))) + add_tag_to_tree(reversed_tag) + + for entry in library.entries: + folders = entry.path.split("\\") + if len(folders) == 1 and folders[0] == "": continue + branch = add_folders_to_tree(folders) + if branch: + field_indexes = library.get_field_index_in_entry(entry,6) + has_tag=False + for index in field_indexes: + content = library.get_field_attr(entry.fields[index],"content") + for tag_id in content: + tag = library.get_tag(tag_id) + if tag.name == branch["tag"].name: + has_tag=True + break + if not has_tag: + branch["files"].append(entry.filename) + + def cut_branches_adding_nothing(branch:dict): + folders = set(branch["dirs"].keys()) + for folder in folders: + logging.info(folder) + cut = cut_branches_adding_nothing(branch["dirs"][folder]) + if cut: + branch['dirs'].pop(folder) + + if not "tag" in branch: return + if branch["tag"].id == -1:#Needs to be first + return False + if len(branch["dirs"].keys()) == 0: + return True + + + cut_branches_adding_nothing(tree) + + return tree + + def folders_to_tags(self,library:Library): + logging.info("Converting folders to Tags") + tree = dict(dirs={}) + def add_tag_to_tree(list:list[Tag]): + branch = tree + for tag in list: + if tag.name not in branch["dirs"]: + branch["dirs"][tag.name] = dict(dirs={},tag=tag) + branch = branch["dirs"][tag.name] + + def add_folders_to_tree(list:list[str])->Tag: + branch = tree + for folder in list: + if folder not in branch["dirs"]: + new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"") + library.add_tag_to_library(new_tag) + branch["dirs"][folder] = dict(dirs={},tag=new_tag) + branch = branch["dirs"][folder] + return branch["tag"] + + + for tag in library.tags: + reversed_tag = self.reverse_tag(tag,None) + add_tag_to_tree(reversed_tag) + + for entry in library.entries: + folders = entry.path.split("\\") + if len(folders)== 1 and folders[0]=="": continue + tag = add_folders_to_tree(folders) + if tag: + if not entry.has_tag(library,tag.id): + entry.add_tag(library,tag.id,6) + + self.close() + + logging.info("Done") + + def reverse_tag(self,tag:Tag,list:list[Tag]) -> list[Tag]: + if list != None: + list.append(tag) + else: + list = [tag] + + if len(tag.subtag_ids) == 0: + list.reverse() + return list + else: + for subtag_id in tag.subtag_ids: + subtag = self.library.get_tag(subtag_id) + return self.reverse_tag(subtag,list) + + class ModifiedTagWidget(QWidget): # Needed to be modified because the original searched the display name in the library where it wasn't added yet + def __init__(self, tag:Tag,parentTag:Tag) -> None: + super().__init__() + self.tag = tag + + self.setCursor(Qt.CursorShape.PointingHandCursor) + self.base_layout = QVBoxLayout(self) + self.base_layout.setObjectName('baseLayout') + self.base_layout.setContentsMargins(0, 0, 0, 0) + + self.bg_button = QPushButton(self) + self.bg_button.setFlat(True) + if parentTag != None: + text = f"{tag.name} ({parentTag.name})".replace('&', '&&') + else: + text = tag.name.replace('&', '&&') + self.bg_button.setText(text) + self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + + self.inner_layout = QHBoxLayout() + self.inner_layout.setObjectName('innerLayout') + self.inner_layout.setContentsMargins(2, 2, 2, 2) + self.bg_button.setLayout(self.inner_layout) + self.bg_button.setMinimumSize(math.ceil(22*1.5), 22) + + self.bg_button.setStyleSheet( + f'QPushButton{{' + f'background: {get_tag_color(ColorType.PRIMARY, tag.color)};' + f"color: {get_tag_color(ColorType.TEXT, tag.color)};" + f'font-weight: 600;' + f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};" + f'border-radius: 6px;' + f'border-style:inset;' + f'border-width: {math.ceil(1*self.devicePixelRatio())}px;' + f'padding-right: 4px;' + f'padding-bottom: 1px;' + f'padding-left: 4px;' + f'font-size: 13px' + f'}}' + f'QPushButton::hover{{' + f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};" + f'}}') + + self.base_layout.addWidget(self.bg_button) + self.setMinimumSize(50,20) + class TreeItemTest(QWidget): + def __init__(self,data:dict,parentTag:Tag): + super().__init__() + + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(20,0,0,0) + self.root_layout.setSpacing(1) + + self.test = QWidget() + self.root_layout.addWidget(self.test) + + self.tag_layout = FlowLayout(self.test) + + self.tag_widget = FoldersToTagsModal.ModifiedTagWidget(data["tag"],parentTag) + self.tag_widget.bg_button.clicked.connect(lambda:self.hide_show()) + self.tag_layout.addWidget(self.tag_widget) + + self.children_widget = QWidget() + self.children_layout = QVBoxLayout(self.children_widget) + self.root_layout.addWidget(self.children_widget) + + self.populate(data) + + def hide_show(self): + self.children_widget.setHidden(not self.children_widget.isHidden()) + + def populate(self,data:dict): + for folder in data["dirs"].values(): + item = FoldersToTagsModal.TreeItemTest(folder,data["tag"]) + self.children_layout.addWidget(item) + for file in data["files"]: + label = QLabel() + label.setText(file) + self.children_layout.addWidget(label) + + if len(data["files"]) == 0 and len(data["dirs"].values()) == 0: + self.hide_show() \ No newline at end of file From 1f9a13fc4d15726466254ea730c555bce61921fa Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Sat, 27 Apr 2024 21:14:37 +0200 Subject: [PATCH 59/59] Removed unused imports --- tagstudio/src/qt/ts_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 6afb2ec7..b53cbd91 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -33,7 +33,7 @@ from PySide6.QtUiTools import QUiLoader from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QPlainTextEdit, QLineEdit, QScrollArea, QFrame, QTextEdit, QComboBox, QProgressDialog, QFileDialog, QListView, QSplitter, QSizePolicy, QMessageBox, QBoxLayout, QCheckBox, QSplashScreen, - QMenu,QTreeWidget, QTreeWidgetItem) + QMenu) from humanfriendly import format_timespan, format_size from src.core.library import Collation, Entry, ItemType, Library, Tag