Alpha v9.1.0

Initial public release
This commit is contained in:
Travis Abendshien 2024-04-22 11:52:22 -07:00
parent bb5dff7daa
commit d63a978fb0
59 changed files with 44943 additions and 4 deletions

105
.gitignore vendored
View file

@ -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
View 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
View 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
View file

@ -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. Im 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 users 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. Its 2024, not 1994.
## Priorities
1. **The concept.** Even if TagStudio as a project or application fails, Id hope that the idea lives on in a superior project. The [goals](#goals) outlined above dont 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 whats possible when it comes to user file management.
4. (The name.) I think its fine for an app or client, but it doesnt 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 librarys 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, youll 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 youd 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 youve 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, its 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 theres 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?
Ive 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 couldnt released this as “Version 0” or “Version 1.0”, but Ive 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” Ive outlined in the [previous](#what-features-are-you-planning-on-adding) section. Ive 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. Ive left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, its just a bunch of glorified print statements (_the outlook for some form of curses on Windows didnt look great at the time, and I just needed a driver for the newly refactored code...)._
### Can I Contribute?
**Yes!!** I recommend taking a look at the [Priority Features](#priority-features) list, as well as the project issues to see whats currently being worked on. Please do not submit pull requests with new feature additions without opening up an issue with a feature request first.
As of writing I dont 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

12
requirements.txt Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

2
start_win.bat Normal file
View file

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

0
tagstudio/__init__.py Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

3300
tagstudio/src/cli/ts_cli.py Normal file

File diff suppressed because it is too large Load diff

View file

View 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

File diff suppressed because it is too large Load diff

View 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'

View 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 ''

View file

View 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

View 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(' ', '')

View 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

View file

View 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())

View 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)

View 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

File diff suppressed because it is too large Load diff

View 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>

File diff suppressed because it is too large Load diff

4475
tagstudio/src/qt/ts_qt.py Normal file

File diff suppressed because it is too large Load diff

212
tagstudio/src/qt/ui/home.ui Normal file
View 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>&lt;</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>&gt;</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>

View 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
View 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()

View file

View file

View 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)