mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
Merge branch 'main' into test-support-2
This commit is contained in:
102
.github/workflows/release.yml
vendored
Normal file
102
.github/workflows/release.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -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
|
||||
|
||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -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
142
README.md
@@ -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 Won’t 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 @@ I’ve 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. I’ve left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, it’s just a bunch of glorified print statements (_the outlook for some form of curses on Windows didn’t 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. I’ve left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, it’s just a bunch of glorified print statements (_the outlook for some form of curses on Windows didn’t 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 what’s 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
86
tagstudio.spec
Normal 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',
|
||||
}
|
||||
)
|
||||
16
tagstudio/src/core/enums.py
Normal file
16
tagstudio/src/core/enums.py
Normal 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"
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user