Merge remote-tracking branch 'upstream/main'

This commit is contained in:
DrRetro
2024-04-25 09:46:22 -04:00
15 changed files with 227 additions and 129 deletions

2
.vscode/launch.json vendored
View File

@@ -8,7 +8,7 @@
"name": "TagStudio",
"type": "debugpy",
"request": "launch",
"program": "${workspaceRoot}\\TagStudio\\tagstudio.py",
"program": "${workspaceRoot}/tagstudio/tag_studio.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": ["--debug"]

View File

@@ -57,7 +57,7 @@ TagStudio is a photo & file organization application with an underlying system t
### Prerequisites
- Python 3.9.6 - ~3.10 *(Not working on 3.12)*
- Python 3.12
### Creating the Virtual Environment
@@ -90,19 +90,23 @@ _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
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

View File

@@ -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
python tagstudio/tag_studio.py

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1712473363,
"narHash": "sha256-TIScFAVdI2yuybMxxNjC4YZ/j++c64wwuKbpnZnGiyU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e89cf1c932006531f454de7d652163a9a5c86668",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

70
flake.nix Normal file
View File

@@ -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.qt6.full
pkgs.qt6.qtbase
pkgs.zstd
];
buildInputs = with pkgs; [
cmake
gdb
zstd
qt6.qtbase
qt6.full
qt6.qtwayland
qtcreator
python312Packages.pip
python312Full
python312Packages.virtualenv # run virtualenv .
python312Packages.pyusb # fixes the pyusb 'No backend available' when installed directly via pip
libgcc
makeWrapper
bashInteractive
glib
libxkbcommon
freetype
binutils
dbus
coreutils
libGL
libGLU
fontconfig
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"
'';
};
};
}

View File

@@ -1,9 +1,9 @@
humanfriendly==10.0
opencv_python==4.8.0.74
opencv_python>=4.8.0.74,<=4.9.0.80
Pillow==10.3.0
pillow_avif_plugin==1.3.1
PySide6==6.5.1.1
PySide6_Addons==6.5.1.1
PySide6_Essentials==6.5.1.1
typing_extensions==3.10.0.0
ujson==5.8.0
pillow_avif_plugin>=1.3.1,<=1.4.3
PySide6>=6.5.1.1,<=6.6.3.1
PySide6_Addons>=6.5.1.1,<=6.6.3.1
PySide6_Essentials>=6.5.1.1,<=6.6.3.1
typing_extensions>=3.10.0.0,<=4.11.0
ujson>=5.8.0,<=5.9.0

View File

@@ -1,2 +1,2 @@
@echo off
.venv\Scripts\python.exe .\TagStudio\tagstudio.py --ui qt %* --debug
.venv\Scripts\python.exe .\TagStudio\tag_studio.py --ui qt %*

View File

@@ -5,24 +5,22 @@
"""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 os
import sys
import time
import logging
import traceback
import xml.etree.ElementTree as ET
from enum import Enum
import ujson
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'])
class ItemType(Enum):
@@ -576,7 +574,7 @@ class Library:
if not os.path.isdir(full_collage_path):
os.mkdir(full_collage_path)
def verify_default_tags(self, tag_list: list) -> dict:
def verify_default_tags(self, tag_list: list) -> list:
"""
Ensures that the default builtin tags are present in the Library's
save file. Takes in and returns the tag dictionary from the JSON file.
@@ -630,41 +628,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:
@@ -856,10 +858,10 @@ class Library:
}
print('[LIBRARY] Formatting Tags to JSON...')
file_to_save['tags'] = self.verify_default_tags(file_to_save['tags'])
for tag in self.tags:
file_to_save["tags"].append(tag.compressed_dict())
file_to_save['tags'] = self.verify_default_tags(file_to_save['tags'])
print('[LIBRARY] Formatting Entries to JSON...')
for entry in self.entries:
file_to_save["entries"].append(entry.compressed_dict())

View File

@@ -4,16 +4,10 @@
"""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 requests
# from bs4 import BeautifulSoup as bs
from src.core.library import *
from src.core.field_template import FieldTemplate
import os
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
@@ -37,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']

View File

@@ -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 = "<>:\"/\\|?*."

View File

@@ -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):

View File

@@ -12,23 +12,14 @@
# 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)
from src.qt.pagination import Pagination
# from src.qt.qtacrylic.qtacrylic import WindowEffect
# from qframelesswindow import FramelessMainWindow, StandardTitleBar
class Ui_MainWindow(QMainWindow):

View File

@@ -5,10 +5,10 @@
"""A pagination widget created for TagStudio."""
# I never want to see this code again.
from PySide6 import QtCore
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtCore import QFile, QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QMimeData
from PySide6.QtCore import QObject, Signal, QSize
from PySide6.QtGui import QIntValidator
from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel, QLineEdit, QSizePolicy
# class NumberEdit(QLineEdit):
# def __init__(self, parent=None) -> None:

View File

@@ -7,40 +7,46 @@
"""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 os
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
import traceback
import shutil
import subprocess
from types import FunctionType
from datetime import datetime as dt
from src.core.ts_core import *
# from src.core.utils.web import *
# from src.core.utils.fs import *
from src.core.library import *
from pathlib import Path
from queue import Empty, Queue
from time import sleep
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.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 (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 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 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
# from typing_extensions import deprecated
from humanfriendly import format_timespan
# from src.qt.qtacrylic.qtacrylic import WindowEffect
# SIGQUIT is not defined on Windows
if sys.platform == "win32":
@@ -56,11 +62,20 @@ INFO = f'[INFO]'
logging.basicConfig(format="%(message)s", level=logging.INFO)
def open_file(path):
def open_file(path: str):
try:
os.startfile(path)
except FileNotFoundError:
logging.info('File Not Found! (Imagine this as a popup)')
if sys.platform == "win32":
subprocess.Popen(["start", path], shell=True, close_fds=True, creationflags=subprocess.DETACHED_PROCESS)
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()
@@ -2131,12 +2146,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):
@@ -4534,7 +4549,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

View File

@@ -9,7 +9,6 @@ from src.cli.ts_cli import CliDriver
from src.qt.ts_qt import QtDriver
import argparse
import traceback
# import ctypes
def main():