Merge branch 'main' into test-support-2

This commit is contained in:
Vele George
2024-05-15 10:23:36 +03:00
committed by GitHub
9 changed files with 596 additions and 128 deletions

102
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
linux:
strategy:
matrix:
build-type: ['', portable]
include:
- build-type: ''
build-flag: ''
suffix: ''
- build-type: portable
build-flag: --portable
suffix: _portable
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -Ur requirements.txt pyinstaller
- run: pyinstaller tagstudio.spec -- ${{ matrix.build-flag }}
- run: tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio
- uses: actions/upload-artifact@v4
with:
name: tagstudio_linux_x86_64${{ matrix.suffix }}
path: dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz
macos:
strategy:
matrix:
os-version: ['11', '14']
include:
- os-version: '11'
arch: x86_64
- os-version: '14'
arch: aarch64
runs-on: macos-${{ matrix.os-version }}
env:
MACOSX_DEPLOYMENT_TARGET: '11.0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -Ur requirements.txt pyinstaller
- run: pyinstaller tagstudio.spec
- run: tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app
- uses: actions/upload-artifact@v4
with:
name: tagstudio_macos_${{ matrix.arch }}
path: dist/tagstudio_macos_${{ matrix.arch }}.tar.gz
windows:
strategy:
matrix:
build-type: ['', portable]
include:
- build-type: ''
build-flag: ''
suffix: ''
file-end: ''
- build-type: portable
build-flag: --portable
suffix: _portable
file-end: .exe
runs-on: windows-2019
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -Ur requirements.txt pyinstaller
- run: PyInstaller tagstudio.spec -- ${{ matrix.build-flag }}
- run: Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
- uses: actions/upload-artifact@v4
with:
name: tagstudio_windows_x86_64${{ matrix.suffix }}
path: dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
publish:
needs: [linux, macos, windows]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: softprops/action-gh-release@v2
with:
files: |
tagstudio_linux_x86_64/*
tagstudio_linux_x86_64_portable/*
tagstudio_macos_x86_64/*
tagstudio_macos_aarch64/*
tagstudio_windows_x86_64/*
tagstudio_windows_x86_64_portable/*

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ MANIFEST
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
!tagstudio.spec
# Installer logs
pip-log.txt

View File

@@ -5,7 +5,56 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [9.1.0-alpha] - 2024-04-22
## [9.2.0] - 2024-05-14
### Added
- Full macOS and Linux support
- Ability to apply tags to multiple selections at once
- Right-click context menu for opening files or their locations
- Support for all filetypes inside of the library
- Configurable filetype blacklist
- Option to automatically open last used library on startup
- Tool to convert folder structure to tag tree
- SIGTERM handling in console window
- Keyboard shortcuts for basic functions
- Basic support for plaintext thumbnails
- Default icon for files with no thumbnail support
- Menu action to close library
- All tags now show in the "Add Tag" panel by default
- Modal view to view and manage all library tags
- Build scripts for Windows and macOS
- Help menu option to visit the GitHub repository
- Toggleable "Recent Libraries" list in the entry side panel
### Fixed
- Fixed errors when performing actions with no library open
- Fixed bug where built-in tags were duplicated upon saving
- QThreads are now properly terminated on application exit
- Images with rotational EXIF data are now properly displayed
- Fixed "truncated" images causing errors
- Fixed images with large resolutions causing errors
### Changed
- Updated minimum Python version to 3.12
- Various UI improvements
- Improved legibility of the Light Theme (still a WIP)
- Updated Dark Theme
- Added hand cursor to several clickable elements
- Fixed network paths not being able to load
- Various code cleanup and refactoring
- New application icons
### Known Issues
- Using and editing multiple entry fields of the same type may result in incorrect field(s) being updated
- Adding Favorite or Archived tags via the thumbnail badges may apply the tag(s) to incorrect fields
- Searching for tag names with spaces does not currently function as intended
- A temporary workaround it to omit spaces in tag names when searching
- Sorting fields using the "Sort Fields" macro may result in edit icons being shown for incorrect fields
## [9.1.0] - 2024-04-22
### Added

142
README.md
View File

@@ -1,4 +1,4 @@
# TagStudio (Preview/Alpha): A User-Focused Document Management System
# TagStudio (Alpha): A User-Focused Document Management System
<p align="center">
<img width="60%" src="github_header.png">
@@ -15,6 +15,7 @@ TagStudio is a photo & file organization application with an underlying system t
</p>
## Contents
- [Goals](#goals)
- [Priorities](#priorities)
- [Current Features](#current-features)
@@ -45,72 +46,28 @@ TagStudio is a photo & file organization application with an underlying system t
- Description, Notes (Multiline Text Fields)
- Tags, Meta Tags, Content Tags (Tag Boxes)
- Create rich tags composed of a name, a list of aliases, and a list of “subtags” - being tags in which these tags inherit values from.
- Search for entries based on tags, metadata, or filename (using `filename: <query>`)
- Search for entries based on tags, ~~metadata~~ (TBA), or filenames/filetypes (using `filename: <query>`)
- Special search conditions for entries that are: `untagged`/`no tags` and `empty`/`no fields`.
> [!NOTE]
> For more information on the project itself, please see the [FAQ](#faq) section and other docs.
## Installation
> [!CAUTION]
> TagStudio is only currently verified to work on Windows. I've run into issues with the Qt code running on Linux, but I don't know how severe these issues are. There's also likely bugs regarding filenames and portability of the databases across different OSes.
### Prerequisites
- Python 3.12
### Creating the Virtual Environment
*Skip this step if launching from the .sh script on Linux.*
1. In the root repository directory, create a python virtual environment:
`python3 -m venv .venv`
2. Activate your environment:
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
- Linux/macOS: `source .venv/bin/activate`
3. Install the required packages:
- required to run the app: `pip install -r requirements.txt`
- required to develop: `pip install -r requirements-dev.txt`
To run all the tests use `python -m pytest tests/` from the `tagstudio` folder.
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
### Launching
To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around.
> [!NOTE]
> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3`.
> On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
#### Optional Arguments
Optional arguments to pass to the program.
> `--open <path>` / `-o <path>`
> Path to a TagStudio Library folder to open on start.
#### Windows
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\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/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.
> `--config-file <path>` / `-c <path>`
> Path to a TagStudio Library folder to open on start.
## Usage
@@ -202,11 +159,61 @@ Import JSON sidecar data generated by [gallery-dl](https://github.com/mikf/galle
> [!CAUTION]
> This feature is not supported or documented in any official capacity whatsoever. It will likely be rolled-in to a larger and more generalized sidecar importing feature in the future.
## Creating a Development Environment
If you're interested in contributing to TagStudio or just wish to poke around the live codebase, here are instructions for setting up the Python project.
### Prerequisites
- Python 3.12
### Creating a Python Virtual Environment
> [!NOTE]
> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3`. You can check to see which alias you system uses and if it's for the correct Python version by typing `python3 --version` (or whichever alias) into your terminal.
_Skip these steps if launching from the .sh script on Linux/macOS._
1. In the root repository directory, create a python virtual environment:
`python3 -m venv .venv`
2. Activate your environment:
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
- Linux/macOS: `source .venv/bin/activate`
3. Install the required packages:
- required to run the app: `pip install -r requirements.txt`
- required to develop: `pip install -r requirements-dev.txt`
To run all the tests use `python -m pytest tests/` from the `tagstudio` folder.
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
### Launching Development Environment
- **Windows** (start_win.bat)
- 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.
- **Linux/macOS** (TagStudio.sh)
- Run the "TagStudio.sh" script, and the program should launch! (Make sure that the script is marked as executable if on Linux). 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 `./TagStudio.sh`.
- **NixOS** (TagStudio.sh)
- Use the provided `flake.nix` file to create and enter a working environment by running `nix develop`. Then, run the `TagStudio.sh` script.
- **Any** (No Scripts)
- 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`.
## FAQ
### What State Is the Project Currently In?
As of writing (Alpha v9.1.0) the project is in a useable state, however it lacks proper testing and quality of life features. Currently the program has only been verified to work properly on Windows, and is unlikely to properly run on Linux or macOS in its current state, however this functionality is a priority going forward with testers.
As of writing (Alpha v9.2.0) the project is in a useable state, however it lacks proper testing and quality of life features.
### What Features Are You Planning on Adding?
@@ -219,18 +226,16 @@ Of the several features I have planned for the project, these are broken up into
- Boolean Search
- Coexisting Text + Tag Search
- Searchable File Metadata
- Tag management view
- Applying metadata via multi-selection
- Comprehensive Tag management tab
- Easier ways to apply tags in bulk
- Tag Search Panel
- Recent Tags Panel
- Top Tags Panel
- Pinned Tags Panel
- Apply tags based on system folders
- Better (stable, performant) library grid view
- Improved entry relinking
- Cached thumbnails
- Collations
- Tag-like Groups
- Resizable thumbnail grid
- User-defined metadata fields
- Multiple directory support
@@ -240,13 +245,13 @@ Of the several features I have planned for the project, these are broken up into
- Better internal API for accessing Entries, Tags, Fields, etc. from the library.
- Proper testing workflow
- Continued code cleanup and modularization
- Reassessment of save file structure in order to prioritize portability (leading to exportable tags, presets, etc)
- Exportable/importable library data including "Tag Packs"
#### Future Features
- Support for multiple simultaneous users/clients
- Draggable files outside the program
- Ability to ignore specific files
- Comprehensive filetype whitelist
- A finished “macro system” for automatic tagging based on predetermined criteria.
- Different library views
- Date and time fields
@@ -254,20 +259,20 @@ Of the several features I have planned for the project, these are broken up into
- Audio waveform previews
- 3D object previews
- Additional previews for miscellaneous file types
- Exportable/sharable tags and settings
- Optional global tags and settings, spanning across libraries
- Importing & exporting libraries to/from other programs
- Port to a more performant language and modern frontend (Rust?, Tauri?, etc.)
- Plugin system
- Local OCR search
- Support for local machine learning-based tag suggestions for images
- Mobile version
- Mobile version _(FAR future)_
#### Features I Likely Wont Add/Pull
- Native Cloud Integration
- Native Cloud Integration
- There are plenty of services already (native or third-party) that allow you to mount your cloud drives as virtual drives on your system. Pointing TagStudio to one of these mounts should function similarly to what native integration would look like.
- Native ChatGPT/Non-Local LLM Integration
- This could mean different things depending on what you're intending. Whether it's trying to use an LLM to replace the native search, or to trying to use a model for image recognition, I'm not interested in hooking people's TagStudio libraries into non-local LLMs such as ChatGPT and/or turn the program into a "chatbot" interface (see: [Goals/Privacy](#goals)). I wouldn't, however, mind using **locally** hosted models to provide the *optional* ability for additional searching and tagging methods (especially when it comes to facial recognition).
- This could mean different things depending on what you're intending. Whether it's trying to use an LLM to replace the native search, or to trying to use a model for image recognition, I'm not interested in hooking people's TagStudio libraries into non-local LLMs such as ChatGPT and/or turn the program into a "chatbot" interface (see: [Goals/Privacy](#goals)). I wouldn't, however, mind using **locally** hosted models to provide the _optional_ ability for additional searching and tagging methods (especially when it comes to facial recognition).
### Why Is the Version Already v9?
@@ -275,14 +280,19 @@ Ive been developing this project over several years in private, and have gone
### Wait, Is There a CLI Version?
As of right now, no. However, I _did_ have a CLI version in the recent past before dedicating my efforts to the Qt GUI version. Ive left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, its just a bunch of glorified print statements (_the outlook for some form of curses on Windows didnt look great at the time, and I just needed a driver for the newly refactored code...)._
As of right now, **no**. However, I _did_ have a CLI version in the recent past before dedicating my efforts to the Qt GUI version. Ive left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, its just a bunch of glorified print statements (_the outlook for some form of curses on Windows didnt look great at the time, and I just needed a driver for the newly refactored code...)._
### Can I Contribute?
**Yes!!** I recommend taking a look at the [Priority Features](#priority-features), [Future Features](#future-features), and [Features I Won't Pull](#features-i-likely-wont-addpull) lists, as well as the project issues to see whats currently being worked on. Please do not submit pull requests with new feature additions without opening up an issue with a feature request first.
Code formatting is automatically checked via [ruff](https://docs.astral.sh/ruff/).
Code formatting is automatically checked via [Ruff](https://docs.astral.sh/ruff/).
To format the code manually, install ruff via `pip install -r requirements-dev.txt` and then run `ruff format`
To format the code manually, install ruff via `pip install -r requirements-dev.txt` and then run `ruff format`
To format the code automatically before each commit, there's a configured action available for `pre-commit` hook. Install it by running `pre-commit install`. The hook will be executed each time on running `git commit`.
More structured documentation on contribution requirements is on its way, but for now:
- Use `pathlib` in favor of `os.path`
- Try to make new UI additions match the existing style of the application

86
tagstudio.spec Normal file
View File

@@ -0,0 +1,86 @@
# -*- mode: python ; coding: utf-8 -*-
# vi: ft=python
from argparse import ArgumentParser
import sys
from PyInstaller.building.api import COLLECT, EXE, PYZ
from PyInstaller.building.build_main import Analysis
from PyInstaller.building.osx import BUNDLE
parser = ArgumentParser()
parser.add_argument('--portable', action='store_true')
options = parser.parse_args()
name = 'TagStudio' if sys.platform == 'win32' else 'tagstudio'
icon = None
if sys.platform == 'win32':
icon = 'tagstudio/resources/icon.ico'
elif sys.platform == 'darwin':
icon = 'tagstudio/resources/icon.icns'
a = Analysis(
['tagstudio/tag_studio.py'],
pathex=[],
binaries=[],
datas=[('tagstudio/resources', 'resources')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
excludes=[],
runtime_hooks=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
include = [a.scripts]
if options.portable:
include += (a.binaries, a.datas)
exe = EXE(
pyz,
*include,
[],
bootloader_ignore_signals=False,
console=False,
hide_console='hide-early',
disable_windowed_traceback=False,
debug=False,
name=name,
exclude_binaries=not options.portable,
icon=icon,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None
)
coll = None if options.portable else COLLECT(
exe,
a.binaries,
a.datas,
name=name,
strip=False,
upx=True,
upx_exclude=[],
)
app = BUNDLE(
exe if coll is None else coll,
name='TagStudio.app',
icon=icon,
bundle_identifier='com.github.tagstudiodev',
version='0.0.0',
info_plist={
'NSAppleScriptEnabled': False,
'NSPrincipalClass': 'NSApplication',
}
)

View File

@@ -0,0 +1,16 @@
import enum
class SettingItems(str, enum.Enum):
"""List of setting item names."""
START_LOAD_LAST = "start_load_last"
LAST_LIBRARY = "last_library"
LIBS_LIST = "libs_list"
WINDOW_SHOW_LIBS = "window_show_libs"
class Theme(str, enum.Enum):
COLOR_BG = "#65000000"
COLOR_HOVER = "#65AAAAAA"
COLOR_PRESSED = "#65EEEEEE"

View File

@@ -16,7 +16,7 @@ import time
import webbrowser
from datetime import datetime as dt
from pathlib import Path
from queue import Empty, Queue
from queue import Queue
from typing import Optional
from PIL import Image
@@ -46,6 +46,7 @@ from PySide6.QtWidgets import (
)
from humanfriendly import format_timespan
from src.core.enums import SettingItems
from src.core.library import ItemType
from src.core.ts_core import TagStudioCore
from src.core.constants import (
@@ -88,7 +89,9 @@ from src.qt.modals.file_extension import FileExtensionModal
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
from src.qt.modals.fix_dupes import FixDupeFilesModal
from src.qt.modals.folders_to_tags import FoldersToTagsModal
import src.qt.resources_rc
# this import has side-effect of import PySide resources
import src.qt.resources_rc # pylint: disable=unused-import
# SIGQUIT is not defined on Windows
if sys.platform == "win32":
@@ -104,9 +107,6 @@ 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())
class NavigationState:
"""Represents a state of the Library grid view."""
@@ -186,9 +186,21 @@ class QtDriver(QObject):
self.SIGTERM.connect(self.handleSIGTERM)
self.settings = QSettings(
QSettings.IniFormat, QSettings.UserScope, "tagstudio", "TagStudio"
)
if self.args.config_file:
path = Path(self.args.config_file)
if not path.exists():
logging.warning(
f"[QT DRIVER] Config File does not exist creating {str(path)}"
)
logging.info(f"[QT DRIVER] Using Config File {str(path)}")
self.settings = QSettings(str(path), QSettings.IniFormat)
else:
self.settings = QSettings(
QSettings.IniFormat, QSettings.UserScope, "TagStudio", "TagStudio"
)
logging.info(
f"[QT DRIVER] Config File not specified, defaulting to {self.settings.fileName()}"
)
max_threads = os.cpu_count()
if args.ci:
@@ -282,6 +294,7 @@ class QtDriver(QObject):
edit_menu = QMenu("&Edit", menu_bar)
tools_menu = QMenu("&Tools", menu_bar)
macros_menu = QMenu("&Macros", menu_bar)
window_menu = QMenu("&Window", menu_bar)
help_menu = QMenu("&Help", menu_bar)
# File Menu ============================================================
@@ -376,6 +389,18 @@ class QtDriver(QObject):
tag_database_action.triggered.connect(lambda: self.show_tag_database())
edit_menu.addAction(tag_database_action)
check_action = QAction("Open library on start", self)
check_action.setCheckable(True)
check_action.setChecked(
self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool)
)
check_action.triggered.connect(
lambda checked: self.settings.setValue(
SettingItems.START_LOAD_LAST, checked
)
)
window_menu.addAction(check_action)
# Tools Menu ===========================================================
fix_unlinked_entries_action = QAction("Fix &Unlinked Entries", menu_bar)
fue_modal = FixUnlinkedEntriesModal(self.lib, self)
@@ -423,6 +448,20 @@ class QtDriver(QObject):
macros_menu.addAction(self.sort_fields_action)
folders_to_tags_action = QAction("Create Tags From Folders", menu_bar)
show_libs_list_action = QAction("Show Recent Libraries", menu_bar)
show_libs_list_action.setCheckable(True)
show_libs_list_action.setChecked(
self.settings.value(SettingItems.WINDOW_SHOW_LIBS, True, type=bool)
)
show_libs_list_action.triggered.connect(
lambda checked: (
self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked),
self.toggle_libs_list(checked),
)
)
window_menu.addAction(show_libs_list_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)
@@ -440,6 +479,7 @@ class QtDriver(QObject):
menu_bar.addMenu(edit_menu)
menu_bar.addMenu(tools_menu)
menu_bar.addMenu(macros_menu)
menu_bar.addMenu(window_menu)
menu_bar.addMenu(help_menu)
self.preview_panel = PreviewPanel(self.lib, self)
@@ -458,6 +498,31 @@ class QtDriver(QObject):
self.thumb_renderers: list[ThumbRenderer] = []
self.collation_thumb_size = math.ceil(self.thumb_size * 2)
self.init_library_window()
lib = None
if self.args.open:
lib = self.args.open
elif self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool):
lib = self.settings.value(SettingItems.LAST_LIBRARY)
if lib:
self.splash.showMessage(
f'Opening Library "{lib}"...',
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter),
QColor("#9782ff"),
)
self.open_library(lib)
if self.args.ci:
# gracefully terminate the app in CI environment
self.thumb_job_queue.put((self.SIGTERM.emit, []))
app.exec()
self.shutdown()
def init_library_window(self):
self._init_thumb_grid()
# TODO: Put this into its own method that copies the font file(s) into memory
@@ -510,31 +575,12 @@ class QtDriver(QObject):
self.splash.finish(self.main_window)
self.preview_panel.update_widgets()
# 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")
and os.path.isdir(self.settings.value("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)
if self.args.ci:
# gracefully terminate the app in CI environment
self.thumb_job_queue.put((self.SIGTERM.emit, []))
app.exec()
self.shutdown()
def toggle_libs_list(self, value: bool):
if value:
self.preview_panel.libs_flow_container.show()
else:
self.preview_panel.libs_flow_container.hide()
self.preview_panel.update()
def callback_library_needed_check(self, func):
"""Check if loaded library has valid path before executing the button function"""
@@ -548,7 +594,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)
self.settings.setValue(SettingItems.LAST_LIBRARY, self.lib.library_dir)
self.settings.sync()
logging.info("[SHUTDOWN] Ending Thumbnail Threads...")
for _ in self.thumb_threads:
@@ -597,7 +643,7 @@ class QtDriver(QObject):
self.main_window.statusbar.showMessage(f"Closing & Saving Library...")
start_time = time.time()
self.save_library(show_status=False)
self.settings.setValue("last_library", self.lib.library_dir)
self.settings.setValue(SettingItems.LAST_LIBRARY, self.lib.library_dir)
self.settings.sync()
self.lib.clear_internal_vars()
@@ -709,7 +755,7 @@ class QtDriver(QObject):
iterator.value.connect(lambda x: pw.update_progress(x + 1))
iterator.value.connect(
lambda x: pw.update_label(
f'Scanning Directories for New Files...\n{x+1} File{"s" if x+1 != 1 else ""} Searched, {len(self.lib.files_not_in_library)} New Files Found'
f'Scanning Directories for New Files...\n{x + 1} File{"s" if x + 1 != 1 else ""} Searched, {len(self.lib.files_not_in_library)} New Files Found'
)
)
r = CustomRunnable(lambda: iterator.run())
@@ -760,7 +806,7 @@ class QtDriver(QObject):
iterator.value.connect(lambda x: pw.update_progress(x + 1))
iterator.value.connect(
lambda x: pw.update_label(
f"Running Configured Macros on {x+1}/{len(new_ids)} New Entries"
f"Running Configured Macros on {x + 1}/{len(new_ids)} New Entries"
)
)
r = CustomRunnable(lambda: iterator.run())
@@ -1297,6 +1343,38 @@ class QtDriver(QObject):
# self.update_thumbs()
def remove_recent_library(self, item_key: str):
self.settings.beginGroup(SettingItems.LIBS_LIST)
self.settings.remove(item_key)
self.settings.endGroup()
self.settings.sync()
def update_libs_list(self, path: str | Path):
"""add library to list in SettingItems.LIBS_LIST"""
ITEMS_LIMIT = 5
path = Path(path)
self.settings.beginGroup(SettingItems.LIBS_LIST)
all_libs = {str(time.time()): str(path)}
for item_key in self.settings.allKeys():
item_path = self.settings.value(item_key)
if Path(item_path) != path:
all_libs[item_key] = item_path
# sort items, most recent first
all_libs = sorted(all_libs.items(), key=lambda item: item[0], reverse=True)
# remove previously saved items
self.settings.clear()
for item_key, item_value in all_libs[:ITEMS_LIMIT]:
self.settings.setValue(item_key, item_value)
self.settings.endGroup()
self.settings.sync()
def open_library(self, path):
"""Opens a TagStudio library."""
if self.lib.library_dir:
@@ -1314,7 +1392,7 @@ class QtDriver(QObject):
# self.lib.refresh_missing_files()
# title_text = f'{self.base_title} - Library \'{self.lib.library_dir}\''
# self.main_window.setWindowTitle(title_text)
pass
self.update_libs_list(path)
else:
logging.info(

View File

@@ -28,6 +28,7 @@ from PySide6.QtWidgets import (
)
from humanfriendly import format_size
from src.core.enums import SettingItems, Theme
from src.core.library import Entry, ItemType, Library
from src.core.constants import VIDEO_TYPES, IMAGE_TYPES
from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file
@@ -41,6 +42,7 @@ from src.qt.widgets.text_box_edit import EditTextBox
from src.qt.widgets.text_line_edit import EditTextLine
from src.qt.widgets.item_thumb import ItemThumb
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
@@ -74,17 +76,10 @@ class PreviewPanel(QWidget):
self.img_button_size: tuple[int, int] = (266, 266)
self.image_ratio: float = 1.0
root_layout = QHBoxLayout(self)
root_layout.setContentsMargins(0, 0, 0, 0)
self.image_container = QWidget()
image_layout = QHBoxLayout(self.image_container)
image_layout.setContentsMargins(0, 0, 0, 0)
splitter = QSplitter()
splitter.setOrientation(Qt.Orientation.Vertical)
splitter.setHandleWidth(12)
self.open_file_action = QAction("Open file", self)
self.open_explorer_action = QAction("Open file in explorer", self)
@@ -111,16 +106,6 @@ class PreviewPanel(QWidget):
)
)
splitter.splitterMoved.connect(
lambda: self.update_image_size(
(
self.image_container.size().width(),
self.image_container.size().height(),
)
)
)
splitter.addWidget(self.image_container)
image_layout.addWidget(self.preview_img)
image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter)
@@ -137,7 +122,7 @@ class PreviewPanel(QWidget):
# Qt.TextInteractionFlag.TextSelectableByMouse)
properties_style = (
f"background-color:#65000000;"
f"background-color:{Theme.COLOR_BG.value};"
f"font-family:Oxanium;"
f"font-weight:bold;"
f"font-size:12px;"
@@ -177,19 +162,48 @@ class PreviewPanel(QWidget):
# rounded corners are maintained when scrolling. I was unable to
# find the right trick to only select that particular element.
scroll_area.setStyleSheet(
f"QWidget#entryScrollContainer{{"
"background:#65000000;"
"QWidget#entryScrollContainer{"
f"background: {Theme.COLOR_BG.value};"
"border-radius:6px;"
f"}}"
"}"
)
scroll_area.setWidget(scroll_container)
info_layout.addWidget(self.file_label)
info_layout.addWidget(self.dimensions_label)
info_layout.addWidget(scroll_area)
splitter.addWidget(info_section)
root_layout.addWidget(splitter)
# keep list of rendered libraries to avoid needless re-rendering
self.render_libs = set()
self.libs_layout = QVBoxLayout()
self.fill_libs_widget(self.libs_layout)
self.libs_flow_container: QWidget = QWidget()
self.libs_flow_container.setObjectName("librariesList")
self.libs_flow_container.setLayout(self.libs_layout)
self.libs_flow_container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
# set initial visibility based on settings
if not self.driver.settings.value(
SettingItems.WINDOW_SHOW_LIBS, True, type=bool
):
self.libs_flow_container.hide()
splitter = QSplitter()
splitter.setOrientation(Qt.Orientation.Vertical)
splitter.setHandleWidth(12)
splitter.splitterMoved.connect(
lambda: self.update_image_size(
(
self.image_container.size().width(),
self.image_container.size().height(),
)
)
)
splitter.addWidget(self.image_container)
splitter.addWidget(info_section)
splitter.addWidget(self.libs_flow_container)
splitter.setStretchFactor(1, 2)
self.afb_container = QWidget()
@@ -208,6 +222,107 @@ class PreviewPanel(QWidget):
(self.image_container.size().width(), self.image_container.size().height())
)
root_layout = QHBoxLayout(self)
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.addWidget(splitter)
def fill_libs_widget(self, layout: QVBoxLayout):
settings = self.driver.settings
settings.beginGroup(SettingItems.LIBS_LIST)
lib_items: dict[str, tuple[str, str]] = {}
for item_tstamp in settings.allKeys():
val = settings.value(item_tstamp)
cut_val = val
if len(val) > 45:
cut_val = f"{val[0:10]} ... {val[-10:]}"
lib_items[item_tstamp] = (val, cut_val)
settings.endGroup()
new_keys = set(lib_items.keys())
if new_keys == self.render_libs:
# no need to re-render
return
# sort lib_items by the key
libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True)
self.render_libs = new_keys
self._fill_libs_widget(libs_sorted, layout)
def _fill_libs_widget(
self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout
):
def clear_layout(layout_item: QVBoxLayout):
for i in reversed(range(layout_item.count())):
child = layout_item.itemAt(i)
if child.widget() is not None:
child.widget().deleteLater()
elif child.layout() is not None:
clear_layout(child.layout())
# remove any potential previous items
clear_layout(layout)
label = QLabel("Recent Libraries")
label.setAlignment(Qt.AlignCenter)
row_layout = QHBoxLayout()
row_layout.addWidget(label)
layout.addLayout(row_layout)
def set_button_style(btn: QPushButton, extras: list[str] | None = None):
base_style = [
f"background-color:{Theme.COLOR_BG.value};",
"border-radius:6px;",
"text-align: left;",
"padding-top: 3px;",
"padding-left: 6px;",
"padding-bottom: 4px;",
]
full_style_rows = base_style + (extras or [])
btn.setStyleSheet(
(
"QPushButton{"
f"{''.join(full_style_rows)}"
"}"
f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER.value};}}"
f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED.value};}}"
)
)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
for item_key, (full_val, cut_val) in libraries:
button = QPushButton(text=cut_val)
button.setObjectName(f"path{item_key}")
def open_library_button_clicked(path):
return lambda: self.driver.open_library(path)
button.clicked.connect(open_library_button_clicked(full_val))
set_button_style(button)
button_remove = QPushButton("")
button_remove.setCursor(Qt.CursorShape.PointingHandCursor)
button_remove.setFixedWidth(30)
set_button_style(button_remove)
def remove_recent_library_clicked(key: str):
return lambda: (
self.driver.remove_recent_library(key),
self.fill_libs_widget(self.libs_layout),
)
button_remove.clicked.connect(remove_recent_library_clicked(item_key))
row_layout = QHBoxLayout()
row_layout.addWidget(button)
row_layout.addWidget(button_remove)
layout.addLayout(row_layout)
def resizeEvent(self, event: QResizeEvent) -> None:
self.update_image_size(
(self.image_container.size().width(), self.image_container.size().height())
@@ -309,6 +424,9 @@ class PreviewPanel(QWidget):
# self.tag_callback = tag_callback if tag_callback else None
window_title = ""
# update list of libraries
self.fill_libs_widget(self.libs_layout)
# 0 Selected Items
if not self.driver.selected:
if self.selected or not self.initialized:

View File

@@ -29,6 +29,14 @@ def main():
type=str,
help="Path to a TagStudio Library folder to open on start.",
)
parser.add_argument(
"-c",
"--config-file",
dest="config_file",
type=str,
help="Path to a TagStudio .ini or .plist config file to use.",
)
# parser.add_argument('--browse', dest='browse', action='store_true',
# help='Jumps to entry browsing on startup.')
# parser.add_argument('--external_preview', dest='external_preview', action='store_true',