Alpha v9.1.0
Initial public release
105
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,qt
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python,qt
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -101,7 +105,15 @@ ipython_config.py
|
|||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
|
@ -145,8 +157,97 @@ dmypy.json
|
|||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
### Qt ###
|
||||
# C++ objects and libs
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.a
|
||||
*.la
|
||||
*.lai
|
||||
*.so.*
|
||||
*.dll
|
||||
*.dylib
|
||||
|
||||
# Qt-es
|
||||
object_script.*.Release
|
||||
object_script.*.Debug
|
||||
*_plugin_import.cpp
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*.qbs.user
|
||||
*.qbs.user.*
|
||||
*.moc
|
||||
moc_*.cpp
|
||||
moc_*.h
|
||||
qrc_*.cpp
|
||||
ui_*.h
|
||||
*.qmlc
|
||||
*.jsc
|
||||
Makefile*
|
||||
*build-*
|
||||
*.qm
|
||||
*.prl
|
||||
|
||||
# Qt unit tests
|
||||
target_wrapper.*
|
||||
|
||||
# QtCreator
|
||||
*.autosave
|
||||
|
||||
# QtCreator Qml
|
||||
*.qmlproject.user
|
||||
*.qmlproject.user.*
|
||||
|
||||
# QtCreator CMake
|
||||
CMakeLists.txt.user*
|
||||
|
||||
# QtCreator 4.8< compilation database
|
||||
compile_commands.json
|
||||
|
||||
# QtCreator local machine specific files for imported projects
|
||||
*creator.user*
|
||||
|
||||
*_qmlcache.qrc
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# TagStudio
|
||||
.TagStudio
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,qt
|
||||
|
|
17
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "TagStudio",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}\\TagStudio\\tagstudio.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
12
CHANGELOG.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# TagStudio Changelog
|
||||
|
||||
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
|
||||
|
||||
### Added
|
||||
|
||||
- Initial public release
|
262
README.md
|
@ -1,2 +1,260 @@
|
|||
# TagStudio
|
||||
A file and photo management application and system.
|
||||
# TagStudio (Preview/Alpha): A User-Focused Document Management System
|
||||
|
||||
<p align="center">
|
||||
<img width="60%" src="github_header.png">
|
||||
</p>
|
||||
|
||||
> [!CAUTION]
|
||||
> This is still a **_very_** rough personal project of mine in its infancy. I’m open-sourcing it now in order to accept contributors sooner and to better facilitate the direction of the project from an earlier stage.
|
||||
> There **_are_** bugs, and there will **_very likely_** be breaking changes!
|
||||
|
||||
TagStudio is a photo & file organization application with an underlying system that focuses on giving freedom and flexibility to the user. No proprietary programs or formats, no sea of sidecar files, and no complete upheaval of your filesystem structure.
|
||||
|
||||
<p align="center">
|
||||
<img width="80%" src="screenshot.jpg">
|
||||
</p>
|
||||
|
||||
## Contents
|
||||
- [Goals](#goals)
|
||||
- [Priorities](#priorities)
|
||||
- [Current Features](#current-features)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [FAQ](#faq)
|
||||
|
||||
## Goals
|
||||
|
||||
- To achieve a portable, privacy-oriented, open, extensible, and feature-rich system of organizing and rediscovering files.
|
||||
- To provide powerful methods for organization, notably the concept of tag composition, or “taggable tags”.
|
||||
- To create an implementation of such a system that is resilient against a user’s actions outside the program (modifying, moving, or renaming files) while also not burdening the user with mandatory sidecar files or otherwise requiring them to change their existing file structures and workflows.
|
||||
- To support a wide range of users spanning across different platforms, multi-user setups, and those with large (several terabyte) libraries.
|
||||
- To make the darn thing look like nice, too. It’s 2024, not 1994.
|
||||
|
||||
## Priorities
|
||||
|
||||
1. **The concept.** Even if TagStudio as a project or application fails, I’d hope that the idea lives on in a superior project. The [goals](#goals) outlined above don’t reference TagStudio once - _TagStudio_ is what references the _goals._
|
||||
2. **The system.** Frontends and implementations can vary, as they should. The core underlying metadata management system is what should be interoperable between different frontends, programs, and operating systems. A standard implementation for this should settle as development continues. This opens up the doors for improved and varied clients, integration with third-party applications, and more.
|
||||
3. **The application.** If nothing else, TagStudio the application serves as the first (and so far only) implementation for this system of metadata management. This has the responsibility of doing the idea justice and showing just what’s possible when it comes to user file management.
|
||||
4. (The name.) I think it’s fine for an app or client, but it doesn’t really make sense for a system or standard. I suppose this will evolve with time.
|
||||
|
||||
## Current Features
|
||||
|
||||
- Create libraries/vaults centered around a system directory. Libraries contain a series of entries: the representations of your files combined with metadata fields. Each entry represents a file in your library’s directory, and is linked to its location.
|
||||
- Add metadata to your library entries, including:
|
||||
- Name, Author, Artist (Single-Line Text Fields)
|
||||
- Description, Notes (Multiline Text Fields)
|
||||
- Tags, Meta Tags, Content Tags (Tag Boxes)
|
||||
- Crete rich tags composed of a name, a list of aliases, and a list of “subtags” - being tags which this tag inherits values from.
|
||||
- Search for entries based on tags, metadata, or filename (using `filename: <query>`
|
||||
- Special search conditions for entries that are: `untagged`/`no tags` and `empty`/`no fields`.
|
||||
|
||||
> [!INFO]
|
||||
> For more information on the project itself, please see the [FAQ](#faq) section and other docs.
|
||||
|
||||
## Installation
|
||||
> [!CAUTION]
|
||||
> TagStudio is currently only verified to be working on Windows. I've run into issues with the Qt code running on Linux, but I don't currently know how severe these issues are. In addition, there's likely to be bugs regarding filenames and portability of the databases across different OSes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.9.6 or higher
|
||||
|
||||
### Creating the Virtual Environment
|
||||
|
||||
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:
|
||||
`pip install -r requirements.txt`
|
||||
|
||||
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
|
||||
|
||||
### Launching
|
||||
|
||||
#### Optional Arguments
|
||||
|
||||
> `--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\tagstudio.py` from your terminal.
|
||||
|
||||
#### Linux & macOS
|
||||
|
||||
> [!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!
|
||||
|
||||
With the virtual environment loaded, run the python file at "tagstudio/tagstudio.py" from your terminal. If in the project's root directory, simply run `python3 tagstudio/tagstudio.py`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating/Opening a Library
|
||||
|
||||
With TagStudio launched, start by creating a new library or opening an existing one using File -> Open/Create Library from the menu. TagStudio will automatically create a new library from the chosen directory if one does not already exist. Upon creating a new library, TagStudio will automatically scan your folders for files and add those to your library.
|
||||
|
||||
### Refreshing the Library
|
||||
|
||||
In order to scan for new files or file changes, you’ll need to manually go to File -> Refresh Directories.
|
||||
|
||||
> [!INFO]
|
||||
> In the future, library refreshing will also be automatically done in the background, or additionally on app startup.
|
||||
|
||||
### Adding Metadata to Entries
|
||||
|
||||
To add a metadata field to a file entry, start by clicking the “Add Field” button under the file preview in the right-hand preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.
|
||||
|
||||
### Editing Metadata Fields
|
||||
|
||||
#### Text Line / Text Box
|
||||
|
||||
Hover over the field and click the pencil icon. From there, add or edit text in the dialog box popup.
|
||||
|
||||
#### Tag Box
|
||||
|
||||
Click the “+” button at the end of the Tags list, and search for tags to add inside the new dialog popup. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the add the first item in the list. Press Enter/Return once more to close the dialog box
|
||||
|
||||
> [!WARNING]
|
||||
> Keyboard control and navigation is currently _very_ buggy, but will be improved in future versions.
|
||||
|
||||
### Creating Tags
|
||||
|
||||
To create a new tag, click on Edit -> New Tag from the menu bar. From there, enter a tag name, shorthand name, any tag aliases separated by newlines, any subtags, and an optional color.
|
||||
|
||||
- The tag **shorthand** is a type of alias that displays in situations when screen space is more valuable (ex. as a subtag for other tags).
|
||||
- **Aliases** are alternate names for a tag. These let you search for terms other than the exact tag name in order to find the tag again.
|
||||
- **Subtags** are tags in which this tag is a child tag of. In other words, tags under this section are parents of this tag. For example, if you had a tag for a character from a show, you would make the show a subtag of this character. This would display as “Character (Show)” in most areas of the app. The first tag in this list is used as the tag shown in parentheses for specification.
|
||||
- The **color** dropdown lets you select an optional color for this tag to display as.
|
||||
|
||||
### Editing Tags
|
||||
|
||||
To edit a tag, right-click the tag in the tag field of the preview pane and select “Edit Tag”
|
||||
|
||||
> [!WARNING]
|
||||
> There is currently no method to view all tags that you’ve created in your library. This is a top priority for future releases.
|
||||
|
||||
### Relinking Renamed/Moved Files
|
||||
|
||||
Inevitably, some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red tag with a cross through it _(this icon is also used for items with broken thumbnails)._ To relink moved files or delete these entries, go to Tools -> Manage Unlinked Entries. Click the “Refresh” button to scan your library for unlinked entries. Once complete, you can attempt to “Search & Relink” any unlinked entries to their respective files, or “Delete Unlinked Entries” in the event the original files have been deleted and you no longer wish to keep their metadata entries inside your library.
|
||||
|
||||
> [!WARNING]
|
||||
> There is currently no method to relink entries to files that have been renamed - only moved or deleted. This is a top priority for future releases.
|
||||
|
||||
> [!WARNING]
|
||||
> If multiple matches for a moved file are found (matches are currently defined as files with a matching filename as the original), TagStudio will currently ignore the match groups. Adding a GUI for manual selection, as well as smarter automated relinking, are top priorities for future versions.
|
||||
|
||||
### Saving the Library
|
||||
|
||||
Libraries are saved upon exiting the program. To manually save, select File -> Save Library from the menu bar. To save a backup of your library, select File -> Save Library Backup from the menu bar.
|
||||
|
||||
### Half-Implemented Features
|
||||
|
||||
#### Fix Duplicate Files
|
||||
|
||||
Load in a .dupeguru file generated by [dupeGuru](https://github.com/arsenetar/dupeguru/) and mirror metadata across entries marked as duplicates. After mirroring, return to dupeGuru to manage deletion of the duplicate files. After deletion, use the “Fix Unlinked Entries” feature in TagStudio to delete the duplicate set of entries for the now-deleted files
|
||||
|
||||
> [!CAUTION]
|
||||
> While this feature is functional, it’s a pretty roundabout process and can be streamlined in the future.
|
||||
|
||||
#### Image Collage
|
||||
|
||||
Create an image collage of your photos and videos.
|
||||
|
||||
> [!CAUTION]
|
||||
> Collage sizes and options are hardcoded.
|
||||
|
||||
#### Macros
|
||||
|
||||
Apply tags and other metadata depending on certain criteria. Set specific macros to run when the files are added to the library. Part of this including applying tags automatically based on parent folders.
|
||||
|
||||
> [!CAUTION]
|
||||
> Macro options are hardcoded, and there’s currently no way for the user to interface with this (still incomplete) system at all.
|
||||
|
||||
#### Gallery-dl Sidecar Importing
|
||||
|
||||
Import JSON sidecar data generated by [gallery-dl](https://github.com/mikf/gallery-dl).
|
||||
|
||||
> [!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.
|
||||
|
||||
## FAQ
|
||||
|
||||
### What State Is the Project Currently In?
|
||||
|
||||
As of writing (v9.1.0 Alpha) the project is in a “useable” state, however it lacks proper testing and quality of life features. Currently the program has only been tested 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.
|
||||
|
||||
### What Features Are You Planning on Adding?
|
||||
|
||||
Of the several features I have planned for the project, these are broken up into “[priority](#priority-features)” features and “[future](#future-features)” features. Priority features were originally intended for the first public release, however are currently absent from the v9.x.x Alpha builds.
|
||||
|
||||
#### Priority Features
|
||||
|
||||
- Improved search
|
||||
- Sortable Search
|
||||
- Boolean Search
|
||||
- Coexisting Text + Tag Search
|
||||
- Searchable File Metadata
|
||||
- Tag management view
|
||||
- Applying metadata via multi-selection
|
||||
- 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
|
||||
- Resizable thumbnail grid
|
||||
- User-defined metadata fields
|
||||
- Multiple directory support
|
||||
- SQLite (or similar) save files
|
||||
- Reading and writing of EXIF and XMP fields
|
||||
- Improved UI/UX
|
||||
- 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)
|
||||
|
||||
#### Future Features
|
||||
|
||||
- Support for multiple simultaneous users/clients
|
||||
- Draggable files outside the program
|
||||
- Ability to ignore specific files
|
||||
- A finished “macro system” for automatic tagging based on predetermined criteria.
|
||||
- Different library views
|
||||
- Date and time fields
|
||||
- Entry linking/referencing
|
||||
- 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
|
||||
|
||||
### Why Is the Version Already v9?
|
||||
|
||||
I’ve been developing this project over several years in private, and have gone through several major iterations and rewrites in that time. This “major version” is just a number at the end of the day, and if I wanted to I couldn’t released this as “Version 0” or “Version 1.0”, but I’ve decided to stick to my original version numbers to avoid needing to go in and change existing documentation and code comments. Version 10 is intended to include all of the “Priority Features” I’ve outlined in the [previous](#what-features-are-you-planning-on-adding) section. I’ve also labeled this version as an Alpha, and will likely reset the numbers when a feature-complete beta is reached.
|
||||
|
||||
### 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...)._
|
||||
|
||||
### Can I Contribute?
|
||||
|
||||
**Yes!!** I recommend taking a look at the [Priority Features](#priority-features) list, 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.
|
||||
|
||||
As of writing I don’t have a concrete style guide, just try to stay within or close enough to the PEP 8 style guide and/or match the style of the existing code.
|
||||
|
|
189
doc/documentation.md
Normal file
|
@ -0,0 +1,189 @@
|
|||
# TagStudio Documentation (Alpha v9.1.0)
|
||||
|
||||
## _A User-Focused Document Management System_
|
||||
|
||||
> [!WARNING]
|
||||
> This documentation is still a work in progress, and is intended to aide with deconstructing and understanding of the core mechanics of TagStudio and how it operates.
|
||||
|
||||
## Contents
|
||||
- [Library](#library)
|
||||
- [Fields](#fields)
|
||||
- [Entries](#entries)
|
||||
- [Tags](#tags)
|
||||
- [Retrieving Entries](#retrieving-entries-based-on-tag-cluster)
|
||||
- [Missing File Resolution](#missing-file-resolution)
|
||||
|
||||
## Library
|
||||
|
||||
The Library is how TagStudio represents your chosen directory. In this Library or Vault system, all files within this directory are represented by Entries, which then contain metadata Fields. All TagStudio data for a Library is stored within a `.TagStudio` folder at the root of the Library's directory. Internal Library objects include:
|
||||
|
||||
- Fields (v9+)
|
||||
- Text Line (Title, Author, Artist, URL)
|
||||
- Text Box (Description, Notes)
|
||||
- Tag Box (Tags, Content Tags, Meta Tags)
|
||||
- Datetime (Date Created, Date Modified, Date Taken) [WIP]
|
||||
- Collation (Collation) [WIP]
|
||||
- `name: str`: Collation Name
|
||||
- `page: int`: Page #
|
||||
- Checkbox (Archive, Favorite) [WIP]
|
||||
- Drop Down (Group of Tags to select one from) [WIP]
|
||||
- Entries (v1+)
|
||||
- Tags (v7+)
|
||||
- Macros (v9/10+)
|
||||
|
||||
## Fields
|
||||
|
||||
Fields are the the building blocks of metadata stored in Entires. Fields have several base types for representing different types of information, including:
|
||||
|
||||
- `text_line`
|
||||
- A string of text, displayed as a single line.
|
||||
- Useful for Titles, Authors, URLs, etc.
|
||||
- `text_box`
|
||||
- A long string of text displayed as a box of text.
|
||||
- Useful for descriptions, notes, etc.
|
||||
- `datetime` [WIP]
|
||||
- A date and time value.
|
||||
- `tag_box`
|
||||
- A box of tags added by the user.
|
||||
- Multiple tag boxes can be used to separate classifications of tags, ex. 'Content Tags' and 'Meta Tags'.
|
||||
- `checkbox` [WIP]
|
||||
- A two-state checkbox.
|
||||
- Can be associated with a tag for quick organization.
|
||||
- `collation` [WIP]
|
||||
- A collation is a collection of files that are intended to be displayed and retrieved together. Examples may include pages of a book or document that are spread out across several individual files. If you're intention is to associate files across multiple 'collations', use Tags instead!
|
||||
|
||||
## Entries
|
||||
|
||||
Entries are the representations of your files within the Library. They consist of a reference to the file on your drive, as well as the metadata associated with it.
|
||||
|
||||
### Entry Object Structure (v9):
|
||||
|
||||
- `id`:
|
||||
- ID for the Entry.
|
||||
- Int, Unique, Required
|
||||
- Used for internal processing
|
||||
- `filename`:
|
||||
- The filename with extension of the referenced media file.
|
||||
- String, Required
|
||||
- `path`:
|
||||
- The folder path in which the media file is located in.
|
||||
- String, Required, OS Agnostic
|
||||
- `fields`:
|
||||
- A list of Field ID/Value dicts.
|
||||
- List of dicts, Optional
|
||||
|
||||
NOTE: _Entries currently have several unused optional fields intended for later features._
|
||||
|
||||
## Tags
|
||||
|
||||
**Tags** are small data objects that represent an attribute of something. A person, place, thing, concept, you name it! Tags in TagStudio allow for more sophisticated Entry organization and searching thanks to their ability to contain alternate names and spellings via `aliases`, relational organization thanks to inherent `subtags`, and more! Tags can be as simple or as powerful as you want to make them, and TagStudio aims to provide as much power to you as possible.
|
||||
|
||||
### Tag Object Structure (v9):
|
||||
|
||||
- `id`:
|
||||
- ID for the Tag.
|
||||
- Int, Unique, Required
|
||||
- Used for internal processing
|
||||
- `name`:
|
||||
- The normal name of the Tag, with no shortening or specification.
|
||||
- String, Required
|
||||
- Doesn't have to be unique
|
||||
- Each word analyzed individually
|
||||
- Used for display, searching, and storing
|
||||
- `shorthand`:
|
||||
- The shorthand name for the Tag.
|
||||
- String, Optional
|
||||
- Doesn't have to be unique
|
||||
- Entire string analyzed as-is
|
||||
- Used for display and searching
|
||||
- `aliases`:
|
||||
- Alternate names for the Tag.
|
||||
- List of Strings, Optional
|
||||
- Recommended to be unique to this Tag
|
||||
- Entire string analyzed as-is
|
||||
- Used for searching
|
||||
- `subtags`:
|
||||
- Other Tags that make up properties of this Tag.
|
||||
- List of Strings, Optional
|
||||
- Used for display (first subtag only) and searching.
|
||||
- `color`:
|
||||
- A hex code value for customizing the Tag's display color
|
||||
- String, Optional
|
||||
- Used for display
|
||||
|
||||
### Tag Examples:
|
||||
|
||||
#### League of Legends
|
||||
|
||||
- `name`: "League of Legends"
|
||||
- `shorthand`: "LoL"
|
||||
- `aliases`: ["League"]
|
||||
- `subtags`: ["Game", "Fantasy"]
|
||||
|
||||
#### Arcane
|
||||
|
||||
- `name`: "Arcane"
|
||||
- `shorthand`: ""
|
||||
- `aliases`: []
|
||||
- `subtags`: ["League of Legends", "Cartoon"]
|
||||
|
||||
#### Jinx (LoL)
|
||||
|
||||
- `name`: "Jinx Piltover"
|
||||
- `shorthand`: "Jinx"
|
||||
- `aliases`: ["Jinxy", "Jinxy Poo"]
|
||||
- `subtags`: ["League of Legends", "Arcane", "Character"]
|
||||
|
||||
#### Zander (Arcane)
|
||||
|
||||
- `name`: "Zander Zanderson"
|
||||
- `shorthand`: "Zander"
|
||||
- `aliases`: []
|
||||
- `subtags`: ["Arcane", "Character"]
|
||||
|
||||
#### Mr. Legend (LoL)
|
||||
|
||||
- `name`: "Mr. Legend"
|
||||
- `shorthand`: ""
|
||||
- `aliases`: []
|
||||
- `subtags`: ["League of Legends", "Character"]
|
||||
|
||||
### Query "League of Legends" returns results for:
|
||||
|
||||
- League of Legends [because of "League of Legend"'s name]
|
||||
- Arcane [because of "Arcane"'s subtag]
|
||||
- Jinx (LoL) [because of "Jinx Piltover"'s subtag]
|
||||
- Mr. Legend (LoL) [because of "Mr. Legned (LoL)'s subtag"]
|
||||
- Zander (Arcane) [because of "Zander Zanderson"'s subtag ("Arcane")'s subtag]
|
||||
|
||||
### Query "LoL" returns results for:
|
||||
|
||||
- League of Legends [because of "League of Legend"'s shorthand]
|
||||
- LoL [because of "League of Legend"'s shorthand]
|
||||
- Arcane [because of "Arcane"'s subtag]
|
||||
- Jinx (LoL) [because of "Jinx Piltover"'s subtag]
|
||||
- Mr. Legend (LoL) [because of "Mr. Legned (LoL)'s subtag"]
|
||||
- Zander (Arcane) [because of "Zander Zanderson"'s subtag ("Arcane")'s subtag]
|
||||
|
||||
### Query "Arcane" returns results for:
|
||||
|
||||
- Arcane [because of "Arcane"'s name]
|
||||
- Jinx (LoL) [because of "Jinx Piltover"'s subtag "Arcane"]
|
||||
- Zander (Arcane) [because of "Zander Zanderson"'s subtag]
|
||||
|
||||
## Retrieving Entries based on Tag Cluster
|
||||
|
||||
By default when querying Entries, each Entry's `tags` list (stored in the form of Tag `id`s) is compared against the Tag `id`s in a given Tag cluster (list of Tag `id`s) or appended clusters in the case of multi-term queries. The type of comparison depends on the type of query and whether or not it is an inclusive or exclusive query, or a combination of both. This default searching behavior is done in _O(n)_ time, but can be sped up in the future by building indexes on certain search terms. These indexes can be stored on disk and loaded back into memory in future sessions. These indexes will also need to be updated as new Tags and Entries are added or edited.
|
||||
|
||||
## Missing File Resolution
|
||||
|
||||
1. Refresh missing file list (`refresh missing`) (Automatically run if library has few entries)
|
||||
2. Fix missing files screen (`fix missing`)
|
||||
|
||||
### Fix Missing Files Screen
|
||||
|
||||
0. **Match Search** (Determines if entries can be fixed) Scans for filename in library directory
|
||||
1. **Quick Fixes** (one match found, no existing entry)
|
||||
2. **Match Selection** (multiple matches found)
|
||||
3. **Merge Conflict Resolution** (match has existing entry)
|
||||
Any remaining missing files can be listed, but they probably really are missing at this point. You can update the path and filename to point to new files if you know where they should actually be pointing to.
|
BIN
github_header.png
Normal file
After Width: | Height: | Size: 859 KiB |
12
requirements.txt
Normal file
|
@ -0,0 +1,12 @@
|
|||
click==8.1.3
|
||||
climage==0.1.3
|
||||
humanfriendly==10.0
|
||||
opencv_python==4.8.0.74
|
||||
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
|
||||
Requests==2.31.0
|
||||
typing_extensions==3.10.0.0
|
||||
ujson==5.8.0
|
BIN
screenshot.jpg
Normal file
After Width: | Height: | Size: 531 KiB |
2
start_win.bat
Normal file
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
.venv\Scripts\python.exe .\TagStudio\tagstudio.py --ui qt %*
|
0
tagstudio/__init__.py
Normal file
BIN
tagstudio/resources/icon.ico
Normal file
After Width: | Height: | Size: 628 KiB |
BIN
tagstudio/resources/icon.png
Normal file
After Width: | Height: | Size: 992 KiB |
BIN
tagstudio/resources/qt/fonts/Oxanium-Bold.ttf
Normal file
BIN
tagstudio/resources/qt/images/box_icon_empty_128 - Copy.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
tagstudio/resources/qt/images/box_icon_empty_128.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
tagstudio/resources/qt/images/box_icon_filled_128 - Copy.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
tagstudio/resources/qt/images/box_icon_filled_128.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
tagstudio/resources/qt/images/clipboard_icon_128.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
tagstudio/resources/qt/images/collation_icon_128.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
tagstudio/resources/qt/images/edit_icon_128.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
tagstudio/resources/qt/images/splash.png
Normal file
After Width: | Height: | Size: 229 KiB |
BIN
tagstudio/resources/qt/images/splitter_handle_128.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
tagstudio/resources/qt/images/star_icon_empty_128 - Copy.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
tagstudio/resources/qt/images/star_icon_empty_128.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
tagstudio/resources/qt/images/star_icon_filled_128 - Copy.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
tagstudio/resources/qt/images/star_icon_filled_128.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
tagstudio/resources/qt/images/tag_group_icon_128.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
tagstudio/resources/qt/images/thumb_border_512.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
tagstudio/resources/qt/images/thumb_broken_512.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
tagstudio/resources/qt/images/thumb_loading_512.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
tagstudio/resources/qt/images/thumb_mask_128.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
tagstudio/resources/qt/images/thumb_mask_512.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
tagstudio/resources/qt/images/thumb_mask_hl_512.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
tagstudio/resources/qt/images/trash_icon_128.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
0
tagstudio/src/cli/__init__.py
Normal file
3300
tagstudio/src/cli/ts_cli.py
Normal file
0
tagstudio/src/core/__init__.py
Normal file
27
tagstudio/src/core/field_template.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
class FieldTemplate:
|
||||
"""A TagStudio Library Field Template object."""
|
||||
|
||||
def __init__(self, id: int, name: str, type: str) -> None:
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'\nID: {self.id}\nName: {self.name}\nType: {self.type}\n'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def to_compressed_obj(self) -> dict:
|
||||
"""An alternative to __dict__ that only includes fields containing non-default data."""
|
||||
obj = {}
|
||||
# All Field fields (haha) are mandatory, so no value checks are done.
|
||||
obj['id'] = self.id
|
||||
obj['name'] = self.name
|
||||
obj['type'] = self.type
|
||||
|
||||
return obj
|
2252
tagstudio/src/core/library.py
Normal file
252
tagstudio/src/core/palette.py
Normal file
|
@ -0,0 +1,252 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ColorType(Enum):
|
||||
PRIMARY = 0
|
||||
TEXT = 1
|
||||
BORDER = 2
|
||||
LIGHT_ACCENT = 3
|
||||
DARK_ACCENT = 4
|
||||
|
||||
|
||||
_TAG_COLORS = {
|
||||
'': {ColorType.PRIMARY: '#1E1A33',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#2B2547',
|
||||
ColorType.LIGHT_ACCENT: '#CDA7F7',
|
||||
ColorType.DARK_ACCENT: '#1E1A33',
|
||||
},
|
||||
'black': {ColorType.PRIMARY: '#111018',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#18171e',
|
||||
ColorType.LIGHT_ACCENT: '#b7b6be',
|
||||
ColorType.DARK_ACCENT: '#03020a',
|
||||
},
|
||||
'dark gray': {ColorType.PRIMARY: '#24232a',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#2a2930',
|
||||
ColorType.LIGHT_ACCENT: '#bdbcc4',
|
||||
ColorType.DARK_ACCENT: '#07060e',
|
||||
},
|
||||
'gray': {ColorType.PRIMARY: '#53525a',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#5b5a62',
|
||||
ColorType.LIGHT_ACCENT: '#cbcad2',
|
||||
ColorType.DARK_ACCENT: '#191820',
|
||||
},
|
||||
'light gray': {ColorType.PRIMARY: '#aaa9b0',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b6b4bc',
|
||||
ColorType.LIGHT_ACCENT: '#cbcad2',
|
||||
ColorType.DARK_ACCENT: '#191820',
|
||||
},
|
||||
'white': {ColorType.PRIMARY: '#f2f1f8',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#fefeff',
|
||||
ColorType.LIGHT_ACCENT: '#ffffff',
|
||||
ColorType.DARK_ACCENT: '#302f36',
|
||||
},
|
||||
'light pink': {ColorType.PRIMARY: '#ff99c4',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ffaad0',
|
||||
ColorType.LIGHT_ACCENT: '#ffcbe7',
|
||||
ColorType.DARK_ACCENT: '#6c2e3b',
|
||||
},
|
||||
'pink': {ColorType.PRIMARY: '#ff99c4',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ffaad0',
|
||||
ColorType.LIGHT_ACCENT: '#ffcbe7',
|
||||
ColorType.DARK_ACCENT: '#6c2e3b',
|
||||
},
|
||||
'magenta': {ColorType.PRIMARY: '#f6466f',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f7587f',
|
||||
ColorType.LIGHT_ACCENT: '#fba4bf',
|
||||
ColorType.DARK_ACCENT: '#61152f',
|
||||
},
|
||||
'red': {ColorType.PRIMARY: '#e22c3c',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b21f2d',
|
||||
# ColorType.BORDER: '#e54252',
|
||||
ColorType.LIGHT_ACCENT: '#f39caa',
|
||||
ColorType.DARK_ACCENT: '#440d12',
|
||||
},
|
||||
'red orange': {ColorType.PRIMARY: '#e83726',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ea4b3b',
|
||||
ColorType.LIGHT_ACCENT: '#f5a59d',
|
||||
ColorType.DARK_ACCENT: '#61120b',
|
||||
},
|
||||
'salmon': {ColorType.PRIMARY: '#f65848',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f76c5f',
|
||||
ColorType.LIGHT_ACCENT: '#fcadaa',
|
||||
ColorType.DARK_ACCENT: '#6f1b16',
|
||||
},
|
||||
'orange': {ColorType.PRIMARY: '#ed6022',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#ef7038',
|
||||
ColorType.LIGHT_ACCENT: '#f7b79b',
|
||||
ColorType.DARK_ACCENT: '#551e0a',
|
||||
},
|
||||
'yellow orange': {ColorType.PRIMARY: '#fa9a2c',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#fba94b',
|
||||
ColorType.LIGHT_ACCENT: '#fdd7ab',
|
||||
ColorType.DARK_ACCENT: '#66330d',
|
||||
},
|
||||
'yellow': {ColorType.PRIMARY: '#ffd63d',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
# ColorType.BORDER: '#ffe071',
|
||||
ColorType.BORDER: '#e8af31',
|
||||
ColorType.LIGHT_ACCENT: '#fff3c4',
|
||||
ColorType.DARK_ACCENT: '#754312',
|
||||
},
|
||||
'mint': {ColorType.PRIMARY: '#4aed90',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#79f2b1',
|
||||
ColorType.LIGHT_ACCENT: '#c8fbe9',
|
||||
ColorType.DARK_ACCENT: '#164f3e',
|
||||
},
|
||||
'lime': {ColorType.PRIMARY: '#92e649',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b2ed72',
|
||||
ColorType.LIGHT_ACCENT: '#e9f9b7',
|
||||
ColorType.DARK_ACCENT: '#405516',
|
||||
},
|
||||
'light green': {ColorType.PRIMARY: '#85ec76',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#a3f198',
|
||||
ColorType.LIGHT_ACCENT: '#e7fbe4',
|
||||
ColorType.DARK_ACCENT: '#2b5524',
|
||||
},
|
||||
'green': {ColorType.PRIMARY: '#28bb48',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#43c568',
|
||||
ColorType.LIGHT_ACCENT: '#93e2c8',
|
||||
ColorType.DARK_ACCENT: '#0d3828',
|
||||
},
|
||||
'teal': {ColorType.PRIMARY: '#1ad9b2',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#4de3c7',
|
||||
ColorType.LIGHT_ACCENT: '#a0f3e8',
|
||||
ColorType.DARK_ACCENT: '#08424b',
|
||||
},
|
||||
'cyan': {ColorType.PRIMARY: '#49e4d5',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#76ebdf',
|
||||
ColorType.LIGHT_ACCENT: '#bff5f0',
|
||||
ColorType.DARK_ACCENT: '#0f4246',
|
||||
},
|
||||
'light blue': {ColorType.PRIMARY: '#55bbf6',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#70c6f7',
|
||||
ColorType.LIGHT_ACCENT: '#bbe4fb',
|
||||
ColorType.DARK_ACCENT: '#122541',
|
||||
},
|
||||
'blue': {ColorType.PRIMARY: '#3b87f0',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#4e95f2',
|
||||
ColorType.LIGHT_ACCENT: '#aedbfa',
|
||||
ColorType.DARK_ACCENT: '#122948',
|
||||
},
|
||||
'blue violet': {ColorType.PRIMARY: '#5948f2',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#6258f3',
|
||||
ColorType.LIGHT_ACCENT: '#9cb8fb',
|
||||
ColorType.DARK_ACCENT: '#1b1649',
|
||||
},
|
||||
'violet': {ColorType.PRIMARY: '#874ff5',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#9360f6',
|
||||
ColorType.LIGHT_ACCENT: '#c9b0fa',
|
||||
ColorType.DARK_ACCENT: '#3a1860',
|
||||
},
|
||||
'purple': {ColorType.PRIMARY: '#bb4ff0',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#c364f2',
|
||||
ColorType.LIGHT_ACCENT: '#dda7f7',
|
||||
ColorType.DARK_ACCENT: '#531862',
|
||||
},
|
||||
'peach': {ColorType.PRIMARY: '#f1c69c',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f4d4b4',
|
||||
ColorType.LIGHT_ACCENT: '#fbeee1',
|
||||
ColorType.DARK_ACCENT: '#613f2f',
|
||||
},
|
||||
'brown': {ColorType.PRIMARY: '#823216',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#8a3e22',
|
||||
ColorType.LIGHT_ACCENT: '#cd9d83',
|
||||
ColorType.DARK_ACCENT: '#3a1804',
|
||||
},
|
||||
'lavender': {ColorType.PRIMARY: '#ad8eef',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#b99ef2',
|
||||
ColorType.LIGHT_ACCENT: '#d5c7fa',
|
||||
ColorType.DARK_ACCENT: '#492b65',
|
||||
},
|
||||
'blonde': {ColorType.PRIMARY: '#efc664',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#f3d387',
|
||||
ColorType.LIGHT_ACCENT: '#faebc6',
|
||||
ColorType.DARK_ACCENT: '#6d461e',
|
||||
},
|
||||
'auburn': {ColorType.PRIMARY: '#a13220',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#aa402f',
|
||||
ColorType.LIGHT_ACCENT: '#d98a7f',
|
||||
ColorType.DARK_ACCENT: '#3d100a',
|
||||
},
|
||||
'light brown': {ColorType.PRIMARY: '#be5b2d',
|
||||
ColorType.TEXT: ColorType.DARK_ACCENT,
|
||||
ColorType.BORDER: '#c4693d',
|
||||
ColorType.LIGHT_ACCENT: '#e5b38c',
|
||||
ColorType.DARK_ACCENT: '#4c290e',
|
||||
},
|
||||
'dark brown': {ColorType.PRIMARY: '#4c2315',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#542a1c',
|
||||
ColorType.LIGHT_ACCENT: '#b78171',
|
||||
ColorType.DARK_ACCENT: '#211006',
|
||||
},
|
||||
'cool gray': {ColorType.PRIMARY: '#515768',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#5b6174',
|
||||
ColorType.LIGHT_ACCENT: '#9ea1c3',
|
||||
ColorType.DARK_ACCENT: '#181a37',
|
||||
},
|
||||
'warm gray': {ColorType.PRIMARY: '#625550',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#6c5e57',
|
||||
ColorType.LIGHT_ACCENT: '#c0a392',
|
||||
ColorType.DARK_ACCENT: '#371d18',
|
||||
},
|
||||
'olive': {ColorType.PRIMARY: '#4c652e',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#586f36',
|
||||
ColorType.LIGHT_ACCENT: '#b4c17a',
|
||||
ColorType.DARK_ACCENT: '#23300e',
|
||||
},
|
||||
'berry': {ColorType.PRIMARY: '#9f2aa7',
|
||||
ColorType.TEXT: ColorType.LIGHT_ACCENT,
|
||||
ColorType.BORDER: '#aa43b4',
|
||||
ColorType.LIGHT_ACCENT: '#cc8fdc',
|
||||
ColorType.DARK_ACCENT: '#41114a',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_tag_color(type: ColorType, color: str):
|
||||
color = color.lower()
|
||||
try:
|
||||
if type == ColorType.TEXT:
|
||||
return get_tag_color(_TAG_COLORS[color][type], color)
|
||||
else:
|
||||
return _TAG_COLORS[color][type]
|
||||
except KeyError:
|
||||
return '#FF00FF'
|
233
tagstudio/src/core/ts_core.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
"""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
|
||||
|
||||
VERSION: str = '9.1.0' # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = 'Alpha' # 'Alpha', 'Beta', or '' for Full Release
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
TS_FOLDER_NAME: str = '.TagStudio'
|
||||
BACKUP_FOLDER_NAME: str = 'backups'
|
||||
COLLAGE_FOLDER_NAME: str = 'collages'
|
||||
LIBRARY_FILENAME: str = 'ts_library.json'
|
||||
|
||||
IMAGE_TYPES: list[str] = ['png', 'jpg', 'jpeg', 'jpg_large', 'jpeg_large',
|
||||
'jfif', 'gif', 'tif', 'tiff', 'heic', 'heif', 'webp',
|
||||
'bmp', 'svg', 'avif', 'apng', 'jp2', 'j2k', 'jpg2']
|
||||
VIDEO_TYPES: list[str] = ['mp4', 'webm', 'mov', 'hevc', 'mkv', 'avi', 'wmv',
|
||||
'flv', 'gifv', 'm4p', 'm4v', '3gp']
|
||||
AUDIO_TYPES: list[str] = ['mp3', 'mp4', 'mpeg4', 'm4a', 'aac', 'wav', 'flac',
|
||||
'alac', 'wma', 'ogg', 'aiff']
|
||||
TEXT_TYPES: list[str] = ['txt', 'rtf', 'md',
|
||||
'doc', 'docx', 'pdf', 'tex', 'odt', 'pages']
|
||||
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']
|
||||
|
||||
ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES
|
||||
|
||||
BOX_FIELDS = ['tag_box', 'text_box']
|
||||
TEXT_FIELDS = ['text_line', 'text_box']
|
||||
DATE_FIELDS = ['datetime']
|
||||
|
||||
TAG_COLORS = ['', 'black', 'dark gray', 'gray', 'light gray', 'white', 'light pink',
|
||||
'pink', 'red', 'red orange', 'orange', 'yellow orange', 'yellow',
|
||||
'lime', 'light green', 'mint', 'green','teal', 'cyan', 'light blue',
|
||||
'blue', 'blue violet', 'violet', 'purple', 'lavender', 'berry',
|
||||
'magenta', 'salmon', 'auburn', 'dark brown', 'brown', 'light brown',
|
||||
'blonde', 'peach', 'warm gray', 'cool gray', 'olive']
|
||||
|
||||
|
||||
class TagStudioCore:
|
||||
"""
|
||||
Instantiate this to establish a TagStudio session.
|
||||
Holds all TagStudio session data and provides methods to manage it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.lib: Library = Library()
|
||||
|
||||
|
||||
def get_gdl_sidecar(self, filepath: str, source: str = '') -> dict:
|
||||
"""
|
||||
Attempts to open and dump a Gallery-DL Sidecar sidecar file for
|
||||
the filepath.\n Returns a formatted object with notable values or an
|
||||
empty object if none is found.
|
||||
"""
|
||||
json_dump = {}
|
||||
info = {}
|
||||
|
||||
# NOTE: This fixes an unknown (recent?) bug in Gallery-DL where Instagram sidecar
|
||||
# files may be downloaded with indices starting at 1 rather than 0, unlike the posts.
|
||||
# This may only occur with sidecar files that are downloaded separate from posts.
|
||||
if source == 'instagram':
|
||||
if not os.path.isfile(os.path.normpath(filepath + ".json")):
|
||||
filepath = filepath[:-16] + '1' + filepath[-15:]
|
||||
|
||||
try:
|
||||
with open(os.path.normpath(filepath + ".json"), "r", encoding="utf8") as f:
|
||||
json_dump = json.load(f)
|
||||
|
||||
if json_dump:
|
||||
if source == "twitter":
|
||||
info["content"] = json_dump["content"].strip()
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "instagram":
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "artstation":
|
||||
info["title"] = json_dump["title"].strip()
|
||||
info["artist"] = json_dump["user"]["full_name"].strip()
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["tags"] = json_dump["tags"]
|
||||
# info["tags"] = [x for x in json_dump["mediums"]["name"]]
|
||||
info["date_published"] = json_dump["date"]
|
||||
elif source == "newgrounds":
|
||||
# info["title"] = json_dump["title"]
|
||||
# info["artist"] = json_dump["artist"]
|
||||
# info["description"] = json_dump["description"]
|
||||
info["tags"] = json_dump["tags"]
|
||||
info["date_published"] = json_dump["date"]
|
||||
info["artist"] = json_dump["user"].strip()
|
||||
info["description"] = json_dump["description"].strip()
|
||||
info["source"] = json_dump["post_url"].strip()
|
||||
# else:
|
||||
# print(
|
||||
# f'[INFO]: TagStudio does not currently support sidecar files for "{source}"')
|
||||
|
||||
# except FileNotFoundError:
|
||||
except:
|
||||
# print(
|
||||
# f'[INFO]: No sidecar file found at "{os.path.normpath(file_path + ".json")}"')
|
||||
pass
|
||||
|
||||
return info
|
||||
|
||||
# def scrape(self, entry_id):
|
||||
# entry = self.lib.get_entry(entry_id)
|
||||
# if entry.fields:
|
||||
# urls: list[str] = []
|
||||
# if self.lib.get_field_index_in_entry(entry, 21):
|
||||
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
|
||||
# for x in self.lib.get_field_index_in_entry(entry, 21)])
|
||||
# if self.lib.get_field_index_in_entry(entry, 3):
|
||||
# urls.extend([self.lib.get_field_attr(entry.fields[x], 'content')
|
||||
# for x in self.lib.get_field_index_in_entry(entry, 3)])
|
||||
# # try:
|
||||
# if urls:
|
||||
# for url in urls:
|
||||
# url = "https://" + url if 'https://' not in url else url
|
||||
# html_doc = requests.get(url).text
|
||||
# soup = bs(html_doc, "html.parser")
|
||||
# print(soup)
|
||||
# input()
|
||||
|
||||
# # except:
|
||||
# # # print("Could not resolve URL.")
|
||||
# # pass
|
||||
|
||||
def match_conditions(self, entry_id: int) -> str:
|
||||
"""Matches defined conditions against a file to add Entry data."""
|
||||
|
||||
cond_file = os.path.normpath(f'{self.lib.library_dir}/{TS_FOLDER_NAME}/conditions.json')
|
||||
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
|
||||
json_dump = {}
|
||||
entry: Entry = self.lib.get_entry(entry_id)
|
||||
try:
|
||||
if os.path.isfile(cond_file):
|
||||
with open(cond_file, "r", encoding="utf8") as f:
|
||||
json_dump = json.load(f)
|
||||
for c in json_dump['conditions']:
|
||||
match: bool = False
|
||||
for path_c in c['path_conditions']:
|
||||
if os.path.normpath(path_c) in entry.path:
|
||||
match = True
|
||||
break
|
||||
if match:
|
||||
if 'fields' in c.keys() and c['fields']:
|
||||
for field in c['fields']:
|
||||
|
||||
field_id = self.lib.get_field_attr(
|
||||
field, 'id')
|
||||
content = field[field_id]
|
||||
|
||||
if self.lib.get_field_obj(int(field_id))['type'] == 'tag_box':
|
||||
existing_fields: list[int] = self.lib.get_field_index_in_entry(
|
||||
entry, field_id)
|
||||
if existing_fields:
|
||||
self.lib.update_entry_field(
|
||||
entry_id, existing_fields[0], content, 'append')
|
||||
else:
|
||||
self.lib.add_field_to_entry(
|
||||
entry_id, field_id)
|
||||
self.lib.update_entry_field(
|
||||
entry_id, -1, content, 'append')
|
||||
|
||||
if self.lib.get_field_obj(int(field_id))['type'] in TEXT_FIELDS:
|
||||
if not self.lib.does_field_content_exist(entry_id, field_id, content):
|
||||
self.lib.add_field_to_entry(
|
||||
entry_id, field_id)
|
||||
self.lib.update_entry_field(
|
||||
entry_id, -1, content, 'replace')
|
||||
except:
|
||||
print('Error in match_conditions...')
|
||||
# input()
|
||||
pass
|
||||
|
||||
def build_url(self, entry_id: int, source: str) -> str:
|
||||
"""Tries to rebuild a source URL given a specific filename structure."""
|
||||
|
||||
source = source.lower().replace('-', ' ').replace('_', ' ')
|
||||
if 'twitter' in source:
|
||||
return self._build_twitter_url(entry_id)
|
||||
elif 'instagram' in source:
|
||||
return self._build_instagram_url(entry_id)
|
||||
|
||||
def _build_twitter_url(self, entry_id: int):
|
||||
"""
|
||||
Builds an Twitter URL given a specific filename structure.
|
||||
Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD'
|
||||
"""
|
||||
try:
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
stubs = entry.filename.rsplit('_', 3)
|
||||
# print(stubs)
|
||||
# source, author = os.path.split(entry.path)
|
||||
url = f"www.twitter.com/{stubs[0]}/status/{stubs[-3]}/photo/{stubs[-2]}"
|
||||
return url
|
||||
except:
|
||||
return ''
|
||||
|
||||
def _build_instagram_url(self, entry_id: int):
|
||||
"""
|
||||
Builds an Instagram URL given a specific filename structure.
|
||||
Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD'
|
||||
"""
|
||||
try:
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
stubs = entry.filename.rsplit('_', 2)
|
||||
# stubs[0] = stubs[0].replace(f"{author}_", '', 1)
|
||||
# print(stubs)
|
||||
# NOTE: Both Instagram usernames AND their ID can have underscores in them,
|
||||
# so unless you have the exact username (which can change) on hand to remove,
|
||||
# your other best bet is to hope that the ID is only 11 characters long, which
|
||||
# seems to more or less be the case... for now...
|
||||
url = f"www.instagram.com/p/{stubs[-3][-11:]}"
|
||||
return url
|
||||
except:
|
||||
return ''
|
0
tagstudio/src/core/utils/__init__.py
Normal file
13
tagstudio/src/core/utils/fs.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# 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 = "<>:\"/\\|?*."
|
||||
for char in invalid_chars:
|
||||
cleaned_name = cleaned_name.replace(char, '_')
|
||||
return cleaned_name
|
11
tagstudio/src/core/utils/str.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
def strip_punctuation(string: str) -> str:
|
||||
"""Returns a given string stripped of all punctuation characters."""
|
||||
return string.replace('(', '').replace(')', '').replace('[', '') \
|
||||
.replace(']', '').replace('{', '').replace('}', '').replace("'", '') \
|
||||
.replace('`', '').replace('’', '').replace('‘', '').replace('"', '') \
|
||||
.replace('“', '').replace('”', '').replace('_', '').replace('-', '') \
|
||||
.replace(' ', '').replace(' ', '')
|
12
tagstudio/src/core/utils/web.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
def strip_web_protocol(string: str) -> str:
|
||||
"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string."""
|
||||
new_str = string
|
||||
new_str = new_str.removeprefix('https://')
|
||||
new_str = new_str.removeprefix('http://')
|
||||
new_str = new_str.removeprefix('www.')
|
||||
new_str = new_str.removeprefix('www2.')
|
||||
return new_str
|
0
tagstudio/src/qt/__init__.py
Normal file
164
tagstudio/src/qt/flowlayout.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
# Copyright (C) 2013 Riverbank Computing Limited.
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
"""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
|
||||
|
||||
|
||||
# class Window(QWidget):
|
||||
# def __init__(self):
|
||||
# super().__init__()
|
||||
|
||||
# flow_layout = FlowLayout(self)
|
||||
# flow_layout.addWidget(QPushButton("Short"))
|
||||
# flow_layout.addWidget(QPushButton("Longer"))
|
||||
# flow_layout.addWidget(QPushButton("Different text"))
|
||||
# flow_layout.addWidget(QPushButton("More text"))
|
||||
# flow_layout.addWidget(QPushButton("Even longer button text"))
|
||||
|
||||
# self.setWindowTitle("Flow Layout")
|
||||
|
||||
class FlowWidget(QWidget):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.ignore_size: bool = False
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
if parent is not None:
|
||||
self.setContentsMargins(QMargins(0, 0, 0, 0))
|
||||
|
||||
self._item_list = []
|
||||
self.grid_efficiency = False
|
||||
|
||||
def __del__(self):
|
||||
item = self.takeAt(0)
|
||||
while item:
|
||||
item = self.takeAt(0)
|
||||
|
||||
def addItem(self, item):
|
||||
self._item_list.append(item)
|
||||
|
||||
def count(self):
|
||||
return len(self._item_list)
|
||||
|
||||
def itemAt(self, index):
|
||||
if 0 <= index < len(self._item_list):
|
||||
return self._item_list[index]
|
||||
|
||||
return None
|
||||
|
||||
def takeAt(self, index):
|
||||
if 0 <= index < len(self._item_list):
|
||||
return self._item_list.pop(index)
|
||||
|
||||
return None
|
||||
|
||||
def expandingDirections(self):
|
||||
return Qt.Orientation(0)
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
height = self._do_layout(QRect(0, 0, width, 0), True)
|
||||
return height
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super(FlowLayout, self).setGeometry(rect)
|
||||
self._do_layout(rect, False)
|
||||
|
||||
def setGridEfficiency(self, bool):
|
||||
"""
|
||||
Enables or Disables efficiencies when all objects are equally sized.
|
||||
"""
|
||||
self.grid_efficiency = bool
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
def minimumSize(self):
|
||||
if self.grid_efficiency:
|
||||
if self._item_list:
|
||||
return self._item_list[0].minimumSize()
|
||||
else:
|
||||
return QSize()
|
||||
else:
|
||||
size = QSize()
|
||||
|
||||
for item in self._item_list:
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
|
||||
size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
|
||||
return size
|
||||
|
||||
|
||||
|
||||
def _do_layout(self, rect, test_only):
|
||||
x = rect.x()
|
||||
y = rect.y()
|
||||
line_height = 0
|
||||
spacing = self.spacing()
|
||||
item = None
|
||||
style = None
|
||||
layout_spacing_x = None
|
||||
layout_spacing_y = None
|
||||
|
||||
if self.grid_efficiency:
|
||||
if self._item_list:
|
||||
item = self._item_list[0]
|
||||
style = item.widget().style()
|
||||
layout_spacing_x = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
layout_spacing_y = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
for i, item in enumerate(self._item_list):
|
||||
# print(issubclass(type(item.widget()), FlowWidget))
|
||||
# print(item.widget().ignore_size)
|
||||
skip_count = 0
|
||||
if (issubclass(type(item.widget()), FlowWidget) and item.widget().ignore_size):
|
||||
skip_count += 1
|
||||
|
||||
if (issubclass(type(item.widget()), FlowWidget) and not item.widget().ignore_size) or (not issubclass(type(item.widget()), FlowWidget)):
|
||||
# print(f'Item {i}')
|
||||
if not self.grid_efficiency:
|
||||
style = item.widget().style()
|
||||
layout_spacing_x = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||
)
|
||||
layout_spacing_y = style.layoutSpacing(
|
||||
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||
)
|
||||
space_x = spacing + layout_spacing_x
|
||||
space_y = spacing + layout_spacing_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
if next_x - space_x > rect.right() and line_height > 0:
|
||||
x = rect.x()
|
||||
y = y + line_height + space_y
|
||||
next_x = x + item.sizeHint().width() + space_x
|
||||
line_height = 0
|
||||
|
||||
if not test_only:
|
||||
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||
|
||||
x = next_x
|
||||
line_height = max(line_height, item.sizeHint().height())
|
||||
|
||||
# print(y + line_height - rect.y() * ((len(self._item_list) - skip_count) / len(self._item_list)))
|
||||
# print(y + line_height - rect.y()) * ((len(self._item_list) - skip_count) / len(self._item_list))
|
||||
return y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list))
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app = QApplication(sys.argv)
|
||||
# main_win = Window()
|
||||
# main_win.show()
|
||||
# sys.exit(app.exec())
|
276
tagstudio/src/qt/main_window.py
Normal file
|
@ -0,0 +1,276 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
# Form generated from reading UI file 'home.ui'
|
||||
##
|
||||
# Created by: Qt User Interface Compiler version 6.5.1
|
||||
##
|
||||
# WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# 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,
|
||||
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):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
# self.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True)
|
||||
# self.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False)
|
||||
# # self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
|
||||
# self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
# self.windowFX = WindowEffect()
|
||||
# self.windowFX.setAcrylicEffect(self.winId(), isEnableShadow=False)
|
||||
|
||||
# # self.setStyleSheet(
|
||||
# # 'background:#EE000000;'
|
||||
# # )
|
||||
|
||||
|
||||
|
||||
def setupUi(self, MainWindow):
|
||||
if not MainWindow.objectName():
|
||||
MainWindow.setObjectName(u"MainWindow")
|
||||
MainWindow.resize(1300, 720)
|
||||
|
||||
# self._createMenuBar(MainWindow)
|
||||
|
||||
print(type(MainWindow))
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.gridLayout = QGridLayout(self.centralwidget)
|
||||
self.gridLayout.setObjectName(u"gridLayout")
|
||||
# self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||
|
||||
|
||||
|
||||
# tb = StandardTitleBar(MainWindow)
|
||||
# tb.setObjectName('TitleBar')
|
||||
# # # self.setTitleBar(tb)
|
||||
# hor = QVBoxLayout()
|
||||
# self.gridLayout.setContentsMargins(0,0,0,0)
|
||||
# self.gridLayout.addLayout(hor, 0, 0, 1, 1)
|
||||
|
||||
|
||||
|
||||
# hor.addWidget(tb)
|
||||
|
||||
self.splitter = QSplitter()
|
||||
self.splitter.setObjectName(u"splitter")
|
||||
self.splitter.setHandleWidth(12)
|
||||
|
||||
self.frame_container = QWidget()
|
||||
# self.frame_container.setStyleSheet('background:red;')
|
||||
self.frame_layout = QVBoxLayout(self.frame_container)
|
||||
# self.frame_container.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
|
||||
|
||||
self.frame_layout.setSpacing(0)
|
||||
|
||||
|
||||
|
||||
self.scrollArea = QScrollArea()
|
||||
# self.scrollArea.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
# self.scrollArea.setStyleSheet('background:green;')
|
||||
self.scrollArea.setObjectName(u"scrollArea")
|
||||
self.scrollArea.setFocusPolicy(Qt.WheelFocus)
|
||||
self.scrollArea.setFrameShape(QFrame.NoFrame)
|
||||
self.scrollArea.setFrameShadow(QFrame.Plain)
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollAreaWidgetContents = QWidget()
|
||||
self.scrollAreaWidgetContents.setObjectName(
|
||||
u"scrollAreaWidgetContents")
|
||||
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590))
|
||||
self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents)
|
||||
self.gridLayout_2.setSpacing(8)
|
||||
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 8)
|
||||
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
|
||||
self.frame_layout.addWidget(self.scrollArea)
|
||||
|
||||
self.scrollArea.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||
# self.scrollArea.setWindowFlag(Qt.WindowType.FramelessWindowHint)
|
||||
self.scrollArea.setAttribute(
|
||||
Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.scrollArea.setStyleSheet('background:#00000000;')
|
||||
|
||||
# self.page_bar_controls = QWidget()
|
||||
# self.page_bar_controls.setStyleSheet('background:blue;')
|
||||
# self.page_bar_controls.setMinimumHeight(32)
|
||||
|
||||
self.pagination = Pagination()
|
||||
self.frame_layout.addWidget(self.pagination)
|
||||
|
||||
# self.frame_layout.addWidget(self.page_bar_controls)
|
||||
# self.frame_layout.addWidget(self.page_bar_controls)
|
||||
|
||||
# self.horizontalLayout.addWidget(self.scrollArea)
|
||||
self.horizontalLayout.addWidget(self.splitter)
|
||||
self.splitter.addWidget(self.frame_container)
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
|
||||
self.gridLayout.addLayout(self.horizontalLayout, 10, 0, 1, 1)
|
||||
|
||||
self.horizontalLayout_2 = QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||
self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize)
|
||||
self.backButton = QPushButton(self.centralwidget)
|
||||
self.backButton.setObjectName(u"backButton")
|
||||
self.backButton.setMinimumSize(QSize(0, 32))
|
||||
self.backButton.setMaximumSize(QSize(32, 16777215))
|
||||
font = QFont()
|
||||
font.setPointSize(14)
|
||||
font.setBold(True)
|
||||
self.backButton.setFont(font)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.backButton)
|
||||
|
||||
self.forwardButton = QPushButton(self.centralwidget)
|
||||
self.forwardButton.setObjectName(u"forwardButton")
|
||||
self.forwardButton.setMinimumSize(QSize(0, 32))
|
||||
self.forwardButton.setMaximumSize(QSize(32, 16777215))
|
||||
font1 = QFont()
|
||||
font1.setPointSize(14)
|
||||
font1.setBold(True)
|
||||
font1.setKerning(True)
|
||||
self.forwardButton.setFont(font1)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.forwardButton)
|
||||
|
||||
self.searchField = QLineEdit(self.centralwidget)
|
||||
self.searchField.setObjectName(u"searchField")
|
||||
self.searchField.setMinimumSize(QSize(0, 32))
|
||||
self.searchField.setStyleSheet(
|
||||
'background:#55000000;'
|
||||
'border-radius:6px;'
|
||||
'border-style:solid;'
|
||||
'border-width:1px;'
|
||||
'border-color:#11FFFFFF;'
|
||||
)
|
||||
font2 = QFont()
|
||||
font2.setPointSize(11)
|
||||
font2.setBold(False)
|
||||
self.searchField.setFont(font2)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.searchField)
|
||||
|
||||
self.searchButton = QPushButton(self.centralwidget)
|
||||
self.searchButton.setObjectName(u"searchButton")
|
||||
self.searchButton.setMinimumSize(QSize(0, 32))
|
||||
self.searchButton.setFont(font2)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.searchButton)
|
||||
|
||||
self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
|
||||
|
||||
self.comboBox = QComboBox(self.centralwidget)
|
||||
self.comboBox.setObjectName(u"comboBox")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.comboBox.sizePolicy().hasHeightForWidth())
|
||||
self.comboBox.setSizePolicy(sizePolicy)
|
||||
self.comboBox.setMinimumWidth(128)
|
||||
self.comboBox.setMaximumWidth(128)
|
||||
|
||||
self.gridLayout.addWidget(self.comboBox, 4, 0, 1, 1, Qt.AlignRight)
|
||||
|
||||
self.gridLayout_2.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
# self.menubar = QMenuBar(MainWindow)
|
||||
# self.menubar.setObjectName(u"menubar")
|
||||
# self.menubar.setGeometry(QRect(0, 0, 1280, 22))
|
||||
# MainWindow.setMenuBar(self.menubar)
|
||||
self.statusbar = QStatusBar(MainWindow)
|
||||
self.statusbar.setObjectName(u"statusbar")
|
||||
sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
||||
sizePolicy1.setHorizontalStretch(0)
|
||||
sizePolicy1.setVerticalStretch(0)
|
||||
sizePolicy1.setHeightForWidth(
|
||||
self.statusbar.sizePolicy().hasHeightForWidth())
|
||||
self.statusbar.setSizePolicy(sizePolicy1)
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
|
||||
menu_bar = self.menuBar()
|
||||
self.setMenuBar(menu_bar)
|
||||
# self.gridLayout.addWidget(menu_bar, 4, 0, 1, 1, Qt.AlignRight)
|
||||
self.frame_layout.addWidget(menu_bar)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
# self.dumpObjectTree()
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindow)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate(
|
||||
"MainWindow", u"MainWindow", None))
|
||||
self.backButton.setText(
|
||||
QCoreApplication.translate("MainWindow", u"<", None))
|
||||
self.forwardButton.setText(
|
||||
QCoreApplication.translate("MainWindow", u">", None))
|
||||
self.searchField.setPlaceholderText(
|
||||
QCoreApplication.translate("MainWindow", u"Search Entries", None))
|
||||
self.searchButton.setText(
|
||||
QCoreApplication.translate("MainWindow", u"Search", None))
|
||||
self.comboBox.setCurrentText("")
|
||||
self.comboBox.setPlaceholderText(
|
||||
QCoreApplication.translate("MainWindow", u"Thumbnail Size", None))
|
||||
# retranslateUi
|
||||
|
||||
def moveEvent(self, event) -> None:
|
||||
# time.sleep(0.02) # sleep for 20ms
|
||||
pass
|
||||
|
||||
def resizeEvent(self, event) -> None:
|
||||
# time.sleep(0.02) # sleep for 20ms
|
||||
pass
|
||||
|
||||
def _createMenuBar(self, main_window):
|
||||
menu_bar = QMenuBar(main_window)
|
||||
file_menu = QMenu('&File', main_window)
|
||||
edit_menu = QMenu('&Edit', main_window)
|
||||
tools_menu = QMenu('&Tools', main_window)
|
||||
macros_menu = QMenu('&Macros', main_window)
|
||||
help_menu = QMenu('&Help', main_window)
|
||||
|
||||
file_menu.addAction(QAction('&New Library', main_window))
|
||||
file_menu.addAction(QAction('&Open Library', main_window))
|
||||
file_menu.addAction(QAction('&Save Library', main_window))
|
||||
file_menu.addAction(QAction('&Close Library', main_window))
|
||||
|
||||
file_menu.addAction(QAction('&Refresh Directories', main_window))
|
||||
file_menu.addAction(QAction('&Add New Files to Library', main_window))
|
||||
|
||||
menu_bar.addMenu(file_menu)
|
||||
menu_bar.addMenu(edit_menu)
|
||||
menu_bar.addMenu(tools_menu)
|
||||
menu_bar.addMenu(macros_menu)
|
||||
menu_bar.addMenu(help_menu)
|
||||
|
||||
main_window.setMenuBar(menu_bar)
|
551
tagstudio/src/qt/pagination.py
Normal file
|
@ -0,0 +1,551 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
"""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
|
||||
|
||||
# class NumberEdit(QLineEdit):
|
||||
# def __init__(self, parent=None) -> None:
|
||||
# super().__init__(parent)
|
||||
# self.textChanged
|
||||
|
||||
class Pagination(QWidget, QObject):
|
||||
"""Widget containing controls for navigating between pages of items."""
|
||||
index = Signal(int)
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.page_count: int = 0
|
||||
self.current_page_index: int = 0
|
||||
self.buffer_page_count: int = 4
|
||||
self.button_size = QSize(32, 24)
|
||||
|
||||
# ------------ UI EXAMPLE --------------
|
||||
# [<] [1]...[3][4] [5] [6][7]...[42] [>]
|
||||
# ^^^^ <-- 2 Buffer Pages
|
||||
# Center Page Number is Editable Text
|
||||
# --------------------------------------
|
||||
|
||||
# [----------- ROOT LAYOUT ------------]
|
||||
self.setHidden(True)
|
||||
self.root_layout = QHBoxLayout(self)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
|
||||
self.root_layout.setContentsMargins(0,6,0,0)
|
||||
self.root_layout.setSpacing(3)
|
||||
# self.setMinimumHeight(32)
|
||||
|
||||
# [<] ----------------------------------
|
||||
self.prev_button = QPushButton()
|
||||
self.prev_button.setText('<')
|
||||
self.prev_button.setMinimumSize(self.button_size)
|
||||
self.prev_button.setMaximumSize(self.button_size)
|
||||
|
||||
# --- [1] ------------------------------
|
||||
self.start_button = QPushButton()
|
||||
self.start_button.setMinimumSize(self.button_size)
|
||||
self.start_button.setMaximumSize(self.button_size)
|
||||
# self.start_button.setStyleSheet('background:cyan;')
|
||||
# self.start_button.setMaximumHeight(self.button_size.height())
|
||||
|
||||
# ------ ... ---------------------------
|
||||
self.start_ellipses = QLabel()
|
||||
self.start_ellipses.setMinimumSize(self.button_size)
|
||||
self.start_ellipses.setMaximumSize(self.button_size)
|
||||
# self.start_ellipses.setMaximumHeight(self.button_size.height())
|
||||
self.start_ellipses.setText('. . .')
|
||||
|
||||
# --------- [3][4] ---------------------
|
||||
self.start_buffer_container = QWidget()
|
||||
self.start_buffer_layout = QHBoxLayout(self.start_buffer_container)
|
||||
self.start_buffer_layout.setContentsMargins(0,0,0,0)
|
||||
self.start_buffer_layout.setSpacing(3)
|
||||
# self.start_buffer_container.setStyleSheet('background:blue;')
|
||||
|
||||
# ---------------- [5] -----------------
|
||||
self.current_page_field = QLineEdit()
|
||||
self.current_page_field.setMinimumSize(self.button_size)
|
||||
self.current_page_field.setMaximumSize(self.button_size)
|
||||
self.validator = Validator(1, self.page_count)
|
||||
self.current_page_field.setValidator(self.validator)
|
||||
self.current_page_field.returnPressed.connect(lambda: self._goto_page(int(self.current_page_field.text())-1))
|
||||
# self.current_page_field.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
|
||||
# self.current_page_field.setMaximumHeight(self.button_size.height())
|
||||
# self.current_page_field.setMaximumWidth(self.button_size.width())
|
||||
|
||||
# -------------------- [6][7] ----------
|
||||
self.end_buffer_container = QWidget()
|
||||
self.end_buffer_layout = QHBoxLayout(self.end_buffer_container)
|
||||
self.end_buffer_layout.setContentsMargins(0,0,0,0)
|
||||
self.end_buffer_layout.setSpacing(3)
|
||||
# self.end_buffer_container.setStyleSheet('background:orange;')
|
||||
|
||||
# -------------------------- ... -------
|
||||
self.end_ellipses = QLabel()
|
||||
self.end_ellipses.setMinimumSize(self.button_size)
|
||||
self.end_ellipses.setMaximumSize(self.button_size)
|
||||
# self.end_ellipses.setMaximumHeight(self.button_size.height())
|
||||
self.end_ellipses.setText('. . .')
|
||||
|
||||
# ----------------------------- [42] ---
|
||||
self.end_button = QPushButton()
|
||||
self.end_button.setMinimumSize(self.button_size)
|
||||
self.end_button.setMaximumSize(self.button_size)
|
||||
# self.end_button.setMaximumHeight(self.button_size.height())
|
||||
# self.end_button.setStyleSheet('background:red;')
|
||||
|
||||
# ---------------------------------- [>]
|
||||
self.next_button = QPushButton()
|
||||
self.next_button.setText('>')
|
||||
self.next_button.setMinimumSize(self.button_size)
|
||||
self.next_button.setMaximumSize(self.button_size)
|
||||
|
||||
# Add Widgets to Root Layout
|
||||
self.root_layout.addStretch(1)
|
||||
self.root_layout.addWidget(self.prev_button)
|
||||
self.root_layout.addWidget(self.start_button)
|
||||
self.root_layout.addWidget(self.start_ellipses)
|
||||
self.root_layout.addWidget(self.start_buffer_container)
|
||||
self.root_layout.addWidget(self.current_page_field)
|
||||
self.root_layout.addWidget(self.end_buffer_container)
|
||||
self.root_layout.addWidget(self.end_ellipses)
|
||||
self.root_layout.addWidget(self.end_button)
|
||||
self.root_layout.addWidget(self.next_button)
|
||||
self.root_layout.addStretch(1)
|
||||
|
||||
self._populate_buffer_buttons()
|
||||
# self.update_buttons(page_count=9, index=0)
|
||||
|
||||
|
||||
def update_buttons(self, page_count:int, index:int, emit:bool=True):
|
||||
|
||||
# Screw it
|
||||
for i in range(0, 10):
|
||||
if self.start_buffer_layout.itemAt(i):
|
||||
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
if page_count <= 1:
|
||||
# Hide everything if there are only one or less pages.
|
||||
# [-------------- HIDDEN --------------]
|
||||
self.setHidden(True)
|
||||
# elif page_count > 1 and page_count < 7:
|
||||
# # Only show Next/Prev, current index field, and both start and end
|
||||
# # buffers (the end may be odd).
|
||||
# # [<] [1][2][3][4][5][6] [>]
|
||||
# self.start_button.setHidden(True)
|
||||
# self.start_ellipses.setHidden(True)
|
||||
# self.end_ellipses.setHidden(True)
|
||||
# self.end_button.setHidden(True)
|
||||
# elif page_count > 1:
|
||||
# self.start_button.setHidden(False)
|
||||
# self.start_ellipses.setHidden(False)
|
||||
# self.end_ellipses.setHidden(False)
|
||||
# self.end_button.setHidden(False)
|
||||
|
||||
# self.start_button.setText('1')
|
||||
# self.assign_click(self.start_button, 0)
|
||||
# self.end_button.setText(str(page_count))
|
||||
# self.assign_click(self.end_button, page_count-1)
|
||||
|
||||
elif page_count > 1:
|
||||
|
||||
# Enable/Disable Next+Prev Buttons
|
||||
if index == 0:
|
||||
self.prev_button.setDisabled(True)
|
||||
# self.start_buffer_layout.setContentsMargins(0,0,0,0)
|
||||
else:
|
||||
# self.start_buffer_layout.setContentsMargins(3,0,3,0)
|
||||
self._assign_click(self.prev_button, index-1)
|
||||
self.prev_button.setDisabled(False)
|
||||
if index == page_count-1:
|
||||
self.next_button.setDisabled(True)
|
||||
# self.end_buffer_layout.setContentsMargins(0,0,0,0)
|
||||
else:
|
||||
# self.end_buffer_layout.setContentsMargins(3,0,3,0)
|
||||
self._assign_click(self.next_button, index+1)
|
||||
self.next_button.setDisabled(False)
|
||||
|
||||
# Set Ellipses Sizes
|
||||
if page_count == 8:
|
||||
if index == 0:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width())
|
||||
if index == page_count-1:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width())
|
||||
elif page_count == 9:
|
||||
if index == 0:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == 1:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width())
|
||||
if index == page_count-1:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == page_count-2:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width())
|
||||
elif page_count == 10:
|
||||
if index == 0:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*4 + 9)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*4 + 9)
|
||||
elif index == 1:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == 2:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width())
|
||||
if index == page_count-1:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*4 + 9)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*4 + 9)
|
||||
elif index == page_count-2:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == page_count-3:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width())
|
||||
elif page_count == 11:
|
||||
if index == 0:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*5 + 12)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*5 + 12)
|
||||
elif index == 1:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*4 + 9)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*4 + 9)
|
||||
elif index == 2:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == 3:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width())
|
||||
if index == page_count-1:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*5 + 12)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*5 + 12)
|
||||
elif index == page_count-2:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*4 + 9)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*4 + 9)
|
||||
elif index == page_count-3:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == page_count-4:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width())
|
||||
elif page_count > 11:
|
||||
if index == 0:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*7 + 18)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*7 + 18)
|
||||
elif index == 1:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*6 + 15)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*6 + 15)
|
||||
elif index == 2:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*5 + 12)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*5 + 12)
|
||||
elif index == 3:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*4 + 9)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*4 + 9)
|
||||
elif index == 4:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == 5:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.end_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.end_ellipses.setMaximumWidth(self.button_size.width())
|
||||
if index == page_count-1:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*7 + 18)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*7 + 18)
|
||||
elif index == page_count-2:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*6 + 15)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*6 + 15)
|
||||
elif index == page_count-3:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*5 + 12)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*5 + 12)
|
||||
elif index == page_count-4:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*4 + 9)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*4 + 9)
|
||||
elif index == page_count-5:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*3 + 6)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*3 + 6)
|
||||
elif index == page_count-6:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width()*2 + 3)
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width()*2 + 3)
|
||||
else:
|
||||
self.start_ellipses.setMinimumWidth(self.button_size.width())
|
||||
self.start_ellipses.setMaximumWidth(self.button_size.width())
|
||||
|
||||
# Enable/Disable Ellipses
|
||||
# if index <= max(self.buffer_page_count, 5)+1:
|
||||
if index <= self.buffer_page_count+1:
|
||||
self.start_ellipses.setHidden(True)
|
||||
# self.start_button.setHidden(True)
|
||||
else:
|
||||
self.start_ellipses.setHidden(False)
|
||||
# self.start_button.setHidden(False)
|
||||
# self.start_button.setText('1')
|
||||
self._assign_click(self.start_button, 0)
|
||||
# if index >=(page_count-max(self.buffer_page_count, 5)-2):
|
||||
if index >= (page_count-self.buffer_page_count-2):
|
||||
self.end_ellipses.setHidden(True)
|
||||
# self.end_button.setHidden(True)
|
||||
else:
|
||||
self.end_ellipses.setHidden(False)
|
||||
# self.end_button.setHidden(False)
|
||||
# self.end_button.setText(str(page_count))
|
||||
# self.assign_click(self.end_button, page_count-1)
|
||||
|
||||
# Hide/Unhide Start+End Buttons
|
||||
if index != 0:
|
||||
self.start_button.setText('1')
|
||||
self._assign_click(self.start_button, 0)
|
||||
self.start_button.setHidden(False)
|
||||
# self.start_buffer_layout.setContentsMargins(3,0,0,0)
|
||||
else:
|
||||
self.start_button.setHidden(True)
|
||||
# self.start_buffer_layout.setContentsMargins(0,0,0,0)
|
||||
if index != page_count-1:
|
||||
self.end_button.setText(str(page_count))
|
||||
self._assign_click(self.end_button, page_count-1)
|
||||
self.end_button.setHidden(False)
|
||||
# self.end_buffer_layout.setContentsMargins(0,0,3,0)
|
||||
else:
|
||||
self.end_button.setHidden(True)
|
||||
# self.end_buffer_layout.setContentsMargins(0,0,0,0)
|
||||
|
||||
if index == 0 or index == 1:
|
||||
self.start_buffer_container.setHidden(True)
|
||||
else:
|
||||
self.start_buffer_container.setHidden(False)
|
||||
|
||||
if index == page_count-1 or index == page_count-2:
|
||||
self.end_buffer_container.setHidden(True)
|
||||
else:
|
||||
self.end_buffer_container.setHidden(False)
|
||||
|
||||
# for i in range(0, self.buffer_page_count):
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
# Current Field and Buffer Pages
|
||||
sbc = 0
|
||||
# for i in range(0, max(self.buffer_page_count*2, 11)):
|
||||
for i in range(0, page_count):
|
||||
# for j in range(0, self.buffer_page_count+1):
|
||||
# self.start_buffer_layout.itemAt(j).widget().setHidden(True)
|
||||
# if i == 1:
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
# elif i == page_count-2:
|
||||
# self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
# Set Field
|
||||
if i == index:
|
||||
# print(f'Current Index: {i}')
|
||||
if self.start_buffer_layout.itemAt(i):
|
||||
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
sbc += 1
|
||||
self.current_page_field.setText((str(i+1)))
|
||||
# elif index == page_count-1:
|
||||
# self.start_button.setText(str(page_count))
|
||||
|
||||
start_offset = max(0, (index-4)-4)
|
||||
end_offset = min(page_count-1, (index+4)-4)
|
||||
if i < index:
|
||||
# if i != 0 and ((i-self.buffer_page_count) >= 0 or i <= self.buffer_page_count):
|
||||
if (i != 0) and i >= index-4:
|
||||
# print(f' Start i: {i}')
|
||||
# print(f'Start Offset: {start_offset}')
|
||||
# print(f' Requested i: {i-start_offset}')
|
||||
# print(f'Setting Text "{str(i+1)}" for Local Start i:{i-start_offset}, Global i:{i}')
|
||||
self.start_buffer_layout.itemAt(i-start_offset).widget().setHidden(False)
|
||||
self.start_buffer_layout.itemAt(i-start_offset).widget().setText(str(i+1))
|
||||
self._assign_click(self.start_buffer_layout.itemAt(i-start_offset).widget(), i)
|
||||
sbc += 1
|
||||
else:
|
||||
if self.start_buffer_layout.itemAt(i):
|
||||
# print(f'Removing S-Start {i}')
|
||||
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
# print(f'Removing S-End {i}')
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
elif i > index:
|
||||
# if i != page_count-1:
|
||||
if i != page_count-1 and i <= index+4:
|
||||
# print(f'End Buffer: {i}')
|
||||
# print(f' End i: {i}')
|
||||
# print(f' End Offset: {end_offset}')
|
||||
# print(f'Requested i: {i-end_offset}')
|
||||
# print(f'Requested i: {end_offset-sbc-i}')
|
||||
# if self.start_buffer_layout.itemAt(i):
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
# print(f'Setting Text "{str(i+1)}" for Local End i:{i-end_offset}, Global i:{i}')
|
||||
self.end_buffer_layout.itemAt(i-end_offset).widget().setHidden(False)
|
||||
self.end_buffer_layout.itemAt(i-end_offset).widget().setText(str(i+1))
|
||||
self._assign_click(self.end_buffer_layout.itemAt(i-end_offset).widget(), i)
|
||||
else:
|
||||
# if self.start_buffer_layout.itemAt(i-1):
|
||||
# print(f'Removing E-Start {i-1}')
|
||||
# self.start_buffer_layout.itemAt(i-1).widget().setHidden(True)
|
||||
# if self.start_buffer_layout.itemAt(i-start_offset):
|
||||
# print(f'Removing E-Start Offset {i-end_offset}')
|
||||
# self.start_buffer_layout.itemAt(i-end_offset).widget().setHidden(True)
|
||||
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
# print(f'Removing E-End {i}')
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
for j in range(0,self.buffer_page_count):
|
||||
if self.end_buffer_layout.itemAt(i-end_offset+j):
|
||||
# print(f'Removing E-End-Offset {i-end_offset+j}')
|
||||
self.end_buffer_layout.itemAt(i-end_offset+j).widget().setHidden(True)
|
||||
|
||||
# if self.end_buffer_layout.itemAt(i+1):
|
||||
# print(f'Removing T-End {i+1}')
|
||||
# self.end_buffer_layout.itemAt(i+1).widget().setHidden(True)
|
||||
|
||||
if self.start_buffer_layout.itemAt(i-1):
|
||||
# print(f'Removing T-Start {i-1}')
|
||||
self.start_buffer_layout.itemAt(i-1).widget().setHidden(True)
|
||||
|
||||
|
||||
# if index == 0 or index == 1:
|
||||
# print(f'Removing Start i: {i}')
|
||||
# if self.start_buffer_layout.itemAt(i):
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
# elif index == page_count-1 or index == page_count-2 or index == page_count-3 or index == page_count-4:
|
||||
# print(f' Removing End i: {i}')
|
||||
# if self.end_buffer_layout.itemAt(i):
|
||||
# self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
|
||||
|
||||
# else:
|
||||
# print(f'Truncate: {i}')
|
||||
# if self.start_buffer_layout.itemAt(i):
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
# if self.end_buffer_layout.itemAt(i):
|
||||
# self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
# if i < self.buffer_page_count:
|
||||
# print(f'start {i}')
|
||||
# if i == 0:
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
# self.current_page_field.setText((str(i+1)))
|
||||
# else:
|
||||
# self.start_buffer_layout.itemAt(i).widget().setHidden(False)
|
||||
# self.start_buffer_layout.itemAt(i).widget().setText(str(i+1))
|
||||
# elif i >= self.buffer_page_count and i < count:
|
||||
# print(f'end {i}')
|
||||
# self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setHidden(False)
|
||||
# self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setText(str(i+1))
|
||||
# else:
|
||||
# self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setHidden(True)
|
||||
|
||||
self.setHidden(False)
|
||||
# elif page_count >= 7:
|
||||
# # Show everything, except truncate the buffers as needed.
|
||||
# # [<] [1]...[3] [4] [5]...[7] [>]
|
||||
# self.start_button.setHidden(False)
|
||||
# self.start_ellipses.setHidden(False)
|
||||
# self.end_ellipses.setHidden(False)
|
||||
# self.end_button.setHidden(False)
|
||||
|
||||
# if index == 0:
|
||||
# self.prev_button.setDisabled(True)
|
||||
# self.start_buffer_layout.setContentsMargins(0,0,3,0)
|
||||
# else:
|
||||
# self.start_buffer_layout.setContentsMargins(3,0,3,0)
|
||||
# self.assign_click(self.prev_button, index-1)
|
||||
# self.prev_button.setDisabled(False)
|
||||
|
||||
# if index == page_count-1:
|
||||
# self.next_button.setDisabled(True)
|
||||
# self.end_buffer_layout.setContentsMargins(3,0,0,0)
|
||||
# else:
|
||||
# self.end_buffer_layout.setContentsMargins(3,0,3,0)
|
||||
# self.assign_click(self.next_button, index+1)
|
||||
# self.next_button.setDisabled(False)
|
||||
|
||||
# self.start_button.setText('1')
|
||||
# self.assign_click(self.start_button, 0)
|
||||
# self.end_button.setText(str(page_count))
|
||||
# self.assign_click(self.end_button, page_count-1)
|
||||
|
||||
# self.setHidden(False)
|
||||
|
||||
self.validator.setTop(page_count)
|
||||
# if self.current_page_index != index:
|
||||
if emit:
|
||||
print(f'[PAGINATION] Emitting {index}')
|
||||
self.index.emit(index)
|
||||
self.current_page_index = index
|
||||
self.page_count = page_count
|
||||
|
||||
|
||||
def _goto_page(self, index:int):
|
||||
# print(f'GOTO PAGE: {index}')
|
||||
self.update_buttons(self.page_count, index)
|
||||
|
||||
def _assign_click(self, button:QPushButton, index):
|
||||
try:
|
||||
button.clicked.disconnect()
|
||||
except RuntimeError:
|
||||
pass
|
||||
button.clicked.connect(lambda checked=False, i=index: self._goto_page(i))
|
||||
|
||||
def _populate_buffer_buttons(self):
|
||||
for i in range(max(self.buffer_page_count*2, 5)):
|
||||
button = QPushButton()
|
||||
button.setMinimumSize(self.button_size)
|
||||
button.setMaximumSize(self.button_size)
|
||||
button.setHidden(True)
|
||||
# button.setMaximumHeight(self.button_size.height())
|
||||
self.start_buffer_layout.addWidget(button)
|
||||
|
||||
for i in range(max(self.buffer_page_count*2, 5)):
|
||||
button = QPushButton()
|
||||
button.setMinimumSize(self.button_size)
|
||||
button.setMaximumSize(self.button_size)
|
||||
button.setHidden(True)
|
||||
# button.setMaximumHeight(self.button_size.height())
|
||||
self.end_buffer_layout.addWidget(button)
|
||||
|
||||
class Validator(QIntValidator):
|
||||
def __init__(self, bottom: int, top: int, parent=None) -> None:
|
||||
super().__init__(bottom, top, parent)
|
||||
|
||||
def fixup(self, input: str) -> str:
|
||||
# print(input)
|
||||
input = input.strip('0')
|
||||
print(input)
|
||||
return super().fixup(str(self.top()) if input else '1')
|
15428
tagstudio/src/qt/resources.py
Normal file
20
tagstudio/src/qt/resources.qrc
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!-- TO COMPILE: pyside6-rcc resources.qrc -o resources.py (in \tagstudio\src\qt)-->
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file alias = "images/star_icon_empty_128.png">../../resources/qt/images/star_icon_empty_128.png</file>
|
||||
<file alias = "images/star_icon_filled_128.png">../../resources/qt/images/star_icon_filled_128.png</file>
|
||||
<file alias = "images/box_icon_empty_128.png">../../resources/qt/images/box_icon_empty_128.png</file>
|
||||
<file alias = "images/box_icon_filled_128.png">../../resources/qt/images/box_icon_filled_128.png</file>
|
||||
<file alias = "images/edit_icon_128.png">../../resources/qt/images/edit_icon_128.png</file>
|
||||
<file alias = "images/trash_icon_128.png">../../resources/qt/images/trash_icon_128.png</file>
|
||||
<file alias = "images/clipboard_icon_128.png">../../resources/qt/images/clipboard_icon_128.png</file>
|
||||
<file alias = "images/splash.png">../../resources/qt/images/splash.png</file>
|
||||
<!-- <file>../../CommonClasses/PNGWriter/report_text/GothamBlackRegular.otf</file>
|
||||
<file>../../CommonClasses/PNGWriter/report_text/GothamBold.otf</file>
|
||||
<file>../../CommonClasses/PNGWriter/report_text/GothamBook.otf</file>
|
||||
<file>../../CommonClasses/PNGWriter/report_text/GothamLight.otf</file>
|
||||
<file>../../CommonClasses/PNGWriter/report_text/GothamMedium.otf</file>
|
||||
<file>../../CommonClasses/PNGWriter/report_text/Spanish</file>
|
||||
<file>../../CommonClasses/PNGWriter/report_text/viewmind.png</file> -->
|
||||
</qresource>
|
||||
</RCC>
|
16910
tagstudio/src/qt/resources_rc.py
Normal file
4475
tagstudio/src/qt/ts_qt.py
Normal file
212
tagstudio/src/qt/ui/home.ui
Normal file
|
@ -0,0 +1,212 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="5" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::WheelFocus</enum>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1260</width>
|
||||
<height>590</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="backButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>14</pointsize>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="forwardButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>14</pointsize>
|
||||
<bold>true</bold>
|
||||
<kerning>true</kerning>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="searchField">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Search Entries</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="searchButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0" alignment="Qt::AlignRight">
|
||||
<widget class="QComboBox" name="comboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>128</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>256</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Thumbnail Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1280</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
140
tagstudio/src/qt/ui/home_ui.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'home.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.5.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
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)
|
||||
from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout,
|
||||
QHBoxLayout, QLayout, QLineEdit, QMainWindow,
|
||||
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
|
||||
QStatusBar, QWidget)
|
||||
|
||||
class Ui_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
if not MainWindow.objectName():
|
||||
MainWindow.setObjectName(u"MainWindow")
|
||||
MainWindow.resize(1280, 720)
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.gridLayout = QGridLayout(self.centralwidget)
|
||||
self.gridLayout.setObjectName(u"gridLayout")
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||
self.scrollArea = QScrollArea(self.centralwidget)
|
||||
self.scrollArea.setObjectName(u"scrollArea")
|
||||
self.scrollArea.setFocusPolicy(Qt.WheelFocus)
|
||||
self.scrollArea.setFrameShape(QFrame.NoFrame)
|
||||
self.scrollArea.setFrameShadow(QFrame.Plain)
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollAreaWidgetContents = QWidget()
|
||||
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
|
||||
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590))
|
||||
self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents)
|
||||
self.gridLayout_2.setSpacing(8)
|
||||
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 8)
|
||||
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
|
||||
|
||||
self.horizontalLayout.addWidget(self.scrollArea)
|
||||
|
||||
|
||||
self.gridLayout.addLayout(self.horizontalLayout, 5, 0, 1, 1)
|
||||
|
||||
self.horizontalLayout_2 = QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||
self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize)
|
||||
self.backButton = QPushButton(self.centralwidget)
|
||||
self.backButton.setObjectName(u"backButton")
|
||||
self.backButton.setMinimumSize(QSize(0, 32))
|
||||
self.backButton.setMaximumSize(QSize(32, 16777215))
|
||||
font = QFont()
|
||||
font.setPointSize(14)
|
||||
font.setBold(True)
|
||||
self.backButton.setFont(font)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.backButton)
|
||||
|
||||
self.forwardButton = QPushButton(self.centralwidget)
|
||||
self.forwardButton.setObjectName(u"forwardButton")
|
||||
self.forwardButton.setMinimumSize(QSize(0, 32))
|
||||
self.forwardButton.setMaximumSize(QSize(32, 16777215))
|
||||
font1 = QFont()
|
||||
font1.setPointSize(14)
|
||||
font1.setBold(True)
|
||||
font1.setKerning(True)
|
||||
self.forwardButton.setFont(font1)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.forwardButton)
|
||||
|
||||
self.searchField = QLineEdit(self.centralwidget)
|
||||
self.searchField.setObjectName(u"searchField")
|
||||
self.searchField.setMinimumSize(QSize(0, 32))
|
||||
font2 = QFont()
|
||||
font2.setPointSize(11)
|
||||
font2.setBold(False)
|
||||
self.searchField.setFont(font2)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.searchField)
|
||||
|
||||
self.searchButton = QPushButton(self.centralwidget)
|
||||
self.searchButton.setObjectName(u"searchButton")
|
||||
self.searchButton.setMinimumSize(QSize(0, 32))
|
||||
self.searchButton.setFont(font2)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.searchButton)
|
||||
|
||||
|
||||
self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
|
||||
|
||||
self.comboBox = QComboBox(self.centralwidget)
|
||||
self.comboBox.setObjectName(u"comboBox")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.comboBox.sizePolicy().hasHeightForWidth())
|
||||
self.comboBox.setSizePolicy(sizePolicy)
|
||||
self.comboBox.setMinimumSize(QSize(128, 0))
|
||||
self.comboBox.setMaximumSize(QSize(256, 32))
|
||||
|
||||
self.gridLayout.addWidget(self.comboBox, 4, 0, 1, 1, Qt.AlignRight)
|
||||
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QMenuBar(MainWindow)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 1280, 22))
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
self.statusbar = QStatusBar(MainWindow)
|
||||
self.statusbar.setObjectName(u"statusbar")
|
||||
sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
||||
sizePolicy1.setHorizontalStretch(0)
|
||||
sizePolicy1.setVerticalStretch(0)
|
||||
sizePolicy1.setHeightForWidth(self.statusbar.sizePolicy().hasHeightForWidth())
|
||||
self.statusbar.setSizePolicy(sizePolicy1)
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindow)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
|
||||
self.backButton.setText(QCoreApplication.translate("MainWindow", u"<", None))
|
||||
self.forwardButton.setText(QCoreApplication.translate("MainWindow", u">", None))
|
||||
self.searchField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Search Entries", None))
|
||||
self.searchButton.setText(QCoreApplication.translate("MainWindow", u"Search", None))
|
||||
self.comboBox.setCurrentText("")
|
||||
self.comboBox.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Thumbnail Size", None))
|
||||
# retranslateUi
|
||||
|
60
tagstudio/tagstudio.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
"""TagStudio launcher."""
|
||||
|
||||
from src.core.ts_core import TagStudioCore
|
||||
from src.cli.ts_cli import CliDriver
|
||||
from src.qt.ts_qt import QtDriver
|
||||
import argparse
|
||||
import traceback
|
||||
# import ctypes
|
||||
|
||||
|
||||
def main():
|
||||
# appid = "cyanvoxel.tagstudio.9"
|
||||
# ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
|
||||
|
||||
# Parse arguments.
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--open', dest='open', type=str,
|
||||
help='Path to a TagStudio Library folder to open on start.')
|
||||
parser.add_argument('-o', dest='open', type=str,
|
||||
help='Path to a TagStudio Library folder to open on start.')
|
||||
# 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',
|
||||
# help='Outputs current preview thumbnail to a live-updating file.')
|
||||
parser.add_argument('--debug', dest='debug', action='store_true',
|
||||
help='Reveals additional internal data useful for debugging.')
|
||||
parser.add_argument('--ui', dest='ui', type=str,
|
||||
help='User interface option for TagStudio. Options: qt, cli (Default: qt)')
|
||||
args = parser.parse_args()
|
||||
|
||||
core = TagStudioCore() # The TagStudio Core instance. UI agnostic.
|
||||
driver = None # The UI driver instance.
|
||||
ui_name: str = 'unknown' # Display name for the UI, used in logs.
|
||||
|
||||
# Driver selection based on parameters.
|
||||
if args.ui and args.ui == 'qt':
|
||||
driver = QtDriver(core, args)
|
||||
ui_name='Qt'
|
||||
elif args.ui and args.ui == 'cli':
|
||||
driver = CliDriver(core, args)
|
||||
ui_name='CLI'
|
||||
else:
|
||||
driver = QtDriver(core, args)
|
||||
ui_name='Qt'
|
||||
|
||||
# Run the chosen frontend driver.
|
||||
try:
|
||||
driver.start()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print(f'\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...')
|
||||
input()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
0
tagstudio/tests/__init__.py
Normal file
0
tagstudio/tests/core/__init__.py
Normal file
12
tagstudio/tests/core/test_tags.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from src.core.library import Tag
|
||||
|
||||
|
||||
class TestTags:
|
||||
def test_construction(self):
|
||||
tag = Tag(id=1, name='Tag Name', shorthand='TN', aliases=[
|
||||
'First A', 'Second A'], subtags_ids=[2, 3, 4], color='')
|
||||
assert (tag)
|
||||
|
||||
def test_empty_construction(self):
|
||||
tag = Tag(id=1, name='', shorthand='', aliases=[], subtags_ids=[], color='')
|
||||
assert (tag)
|