Compare commits
27 Commits
v9.5.0-pr3
...
v9.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf3b2f96b | ||
|
|
d94befed5d | ||
|
|
d91ee5dbf7 | ||
|
|
ea8d074c51 | ||
|
|
186c6e2139 | ||
|
|
c695f1765f | ||
|
|
077f91af88 | ||
|
|
accf50ccf9 | ||
|
|
9f9a5ccf44 | ||
|
|
ee23ef3451 | ||
|
|
2a787592b6 | ||
|
|
152c2b20ba | ||
|
|
1b5b40792c | ||
|
|
4b381e78ba | ||
|
|
b1126d5313 | ||
|
|
f9ca743b64 | ||
|
|
04bd1bc027 | ||
|
|
ecc0bb57fb | ||
|
|
d11b514bab | ||
|
|
fe94d84b94 | ||
|
|
db408f09c4 | ||
|
|
61b9fcf764 | ||
|
|
28de21ade7 | ||
|
|
2173d1d4f4 | ||
|
|
c1ec8a6650 | ||
|
|
69d3a6ed09 | ||
|
|
e481ab64c9 |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
patreon: cyanvoxel
|
||||
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
^^^ Summarize the changes done and why they were done above.
|
||||
|
||||
By submitting this pull request, you certify that you have read the
|
||||
[CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md).
|
||||
|
||||
IMPORTANT FOR FEATURES: Please verify that a feature request or some other form
|
||||
of communication with maintainers was already conducted in terms of approving.
|
||||
|
||||
Thank you for your eagerness to contribute!
|
||||
-->
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
<!-- No requirements, just context for reviewers. -->
|
||||
|
||||
- Tested on:
|
||||
- [ ] Windows x86
|
||||
- [ ] Windows ARM
|
||||
- [ ] macOS x86
|
||||
- [ ] macOS ARM
|
||||
- [ ] Linux x86
|
||||
- [ ] Linux ARM
|
||||
<!-- If an unspecified platform was tested, please add it here! -->
|
||||
- Tested:
|
||||
- [ ] Basic functionality
|
||||
- [ ] PyInstaller executable
|
||||
BIN
docs/assets/create_namespace.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/assets/custom_color_border.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/assets/custom_color_no_border.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/assets/custom_color_primary_only.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/assets/custom_tag_color_selection.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/assets/tag_color_manager.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 105 KiB |
@@ -10,7 +10,8 @@ Pre-built binaries from trusted sources are available on the [FFmpeg website](ht
|
||||
|
||||

|
||||
|
||||
!!! note
|
||||
<!-- prettier-ignore -->
|
||||
!!! warning
|
||||
Do NOT download the source code by mistake!
|
||||
|
||||
To Install:
|
||||
@@ -19,10 +20,10 @@ To Install:
|
||||
2. Move extracted contents to a unique folder (i.e; `c:\ffmpeg` or `c:\Program Files\ffmpeg`)
|
||||
3. Add FFmpeg to your system PATH
|
||||
|
||||
1. In Windows, search for or go to "Edit the system environment variables" under the Control Panel
|
||||
2. Under "User Variables", select "Path" then edit
|
||||
3. Click new and add `<Your folder>\bin` (e.g; `c:\ffmpeg\bin` or `c:\Program Files\ffmpeg\bin`)
|
||||
4. Click "Okay"
|
||||
1. In Windows, search for or go to "Edit the system environment variables" under the Control Panel
|
||||
2. Under "User Variables", select "Path" then edit
|
||||
3. Click new and add `<Your folder>\bin` (e.g; `c:\ffmpeg\bin` or `c:\Program Files\ffmpeg\bin`)
|
||||
4. Click "Okay"
|
||||
|
||||
### Package Managers
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@ To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagS
|
||||
|
||||
**We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub releases page are _unofficial_ and not maintained by us.** Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk.
|
||||
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! info "For macOS Users"
|
||||
On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! info "For Linux Users"
|
||||
On Linux with non-Qt based Desktop Environments you may be unable to open TagStudio. You need to make sure that "xcb-cursor0" or "libxcb-cursor0" packages are installed. For more info check [Missing linux dependencies](https://github.com/TagStudioDev/TagStudio/discussions/182#discussioncomment-9452896)
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
- For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](/docs/help/ffmpeg.md) guide.
|
||||
- For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide.
|
||||
|
||||
## Optional Arguments
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Library Search
|
||||
|
||||
TagStudio provides various methods to search your library, ranging from TagStudio data such as tags to inherent file data such as paths or media types.
|
||||
|
||||
## Boolean Operators
|
||||
|
||||
TagStudio allows you to use common [boolean search](https://en.wikipedia.org/wiki/Full-text_search#Boolean_queries) operators when searching your library, along with [grouping](#grouping-and-nesting), [nesting](#grouping-and-nesting), and [character escaping](#escaping-characters). Note that you may need to use grouping in order to get the desired results you're looking for.
|
||||
@@ -8,29 +10,37 @@ TagStudio allows you to use common [boolean search](https://en.wikipedia.org/wik
|
||||
|
||||
The `AND` operator will only return results that match **both** sides of the operator. `AND` is used implicitly when no boolean operators are given. To use the `AND` operator explicitly, simply type "and" (case insensitive) in-between items of your search.
|
||||
|
||||
> For example, searching for "Tag1 Tag2" will be treated the same as "Tag1 `AND` Tag2" and will only return results that contain both Tag1 and Tag2.
|
||||
<!-- prettier-ignore -->
|
||||
!!! example
|
||||
Searching for "Tag1 Tag2" will be treated the same as "Tag1 `AND` Tag2" and will only return results that contain both Tag1 and Tag2.
|
||||
|
||||
### OR
|
||||
|
||||
The `OR` operator will return results that match **either** the left or right side of the operator. To use the `OR` operator simply type "or" (case insensitive) in-between items of your search.
|
||||
|
||||
> For example, searching for "Tag1 `OR` Tag2" will return results that contain either "Tag1", "Tag2", or both.
|
||||
<!-- prettier-ignore -->
|
||||
!!! example
|
||||
Searching for "Tag1 `OR` Tag2" will return results that contain either "Tag1", "Tag2", or both.
|
||||
|
||||
### NOT
|
||||
|
||||
The `NOT` operator will returns results where the condition on the right is **false.** To use the `NOT` operator simply type "not" (case insensitive) in-between items of your search. You can also begin your search with `NOT` to only view results that do not contain the next term that follows.
|
||||
|
||||
> For example, searching for "Tag1 `NOT` Tag2" will only return results that contain "Tag1" while also not containing "Tag2".
|
||||
<!-- prettier-ignore -->
|
||||
!!! example
|
||||
Searching for "Tag1 `NOT` Tag2" will only return results that contain "Tag1" while also not containing "Tag2".
|
||||
|
||||
### Grouping and Nesting
|
||||
|
||||
Searches can be grouped and nested by using parentheses to surround parts of your search query.
|
||||
|
||||
> For example, searching for "(Tag1 `OR` Tag2) `AND` Tag3" will return results any results that contain Tag3, plus one or the other (or both) of Tag1 and Tag2.
|
||||
<!-- prettier-ignore -->
|
||||
!!! example
|
||||
Searching for "(Tag1 `OR` Tag2) `AND` Tag3" will return results any results that contain Tag3, plus one or the other (or both) of Tag1 and Tag2.
|
||||
|
||||
### Escaping Characters
|
||||
|
||||
Sometimes search queries have ambiguous characters and need to be "escaped". This is most common with tag names which contain spaces, or overlap with existing search keywords such as "[path:](#filename--filepath) of exile". To escape most search terms, surround the section of your search in plain quotes. Alternatively, spaces in tag names can be replaced by underscores.
|
||||
Sometimes search queries have ambiguous characters and need to be "escaped". This is most common with tag names which contain spaces, or overlap with existing search keywords such as "[path:](#filename-and-path) of exile". To escape most search terms, surround the section of your search in plain quotes. Alternatively, spaces in tag names can be replaced by underscores.
|
||||
|
||||
#### Valid Escaped Tag Searches
|
||||
|
||||
@@ -54,40 +64,58 @@ _[Field](field.md) search is currently not in the program, however is coming in
|
||||
|
||||
## File Entry Search
|
||||
|
||||
### Filename + Filepath
|
||||
### Filename and Path
|
||||
|
||||
Currently (v9.5.0-PR1) the filepath search uses [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) syntax, meaning you'll likely have to wrap your filename or partial filepath inside asterisks for results to appear. This search is also currently case sensitive. Use the `path:` keyword prefix followed by the filename or path, with asterisks surrounding partial names.
|
||||
Filename and path search is available via the `path:` keyword and comes in a few different styles. By default, any string that follows the `path:` keyword will be searched as a substring inside a file's complete filepath. This means that given a file `folder/my_file.txt`, searching for `path: my_file` or `path: folder` will both return results for that file.
|
||||
|
||||
#### Case Sensitivity
|
||||
|
||||
TagStudio uses a "[smartcase](https://neovim.io/doc/user/options.html#'smartcase')"-like system for case sensitivity. This means that a search term typed in `lowercase` will be treated as **case-insensitive**, while a term typed in any `MixedCase` will be treated as **case-sensitive**. This makes it quicker to type searches when case sensitivity isn't required, while also providing a simple option to leverage case sensitivity when desired. Note that this means there's technically no way to currently search for a lowercase term while respecting case sensitivity.
|
||||
|
||||
#### Glob Syntax
|
||||
|
||||
Optionally, you may use [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) syntax to search filepaths.
|
||||
|
||||
#### Examples
|
||||
|
||||
Given a file "artwork/piece.jpg", these searches will return results with it:
|
||||
|
||||
- `path: artwork/piece.jpg` _(Note how no asterisks are required if the full path is given)_
|
||||
- `path: *piece.jpg*`
|
||||
- `path: *artwork*`
|
||||
- `path: *rtwor*`
|
||||
- `path: *ece.jpg*`
|
||||
- `path: *iec*`
|
||||
|
||||
And these (currently) won't:
|
||||
Given a file "Artwork/Piece.jpg", the following searches will return results for it:
|
||||
|
||||
- `path: artwork/piece.jpg`
|
||||
- `path: Artwork/Piece.jpg`
|
||||
- `path: piece.jpg`
|
||||
- `path: piece.jpg`
|
||||
- `path: Piece.jpg`
|
||||
- `path: artwork`
|
||||
- `path: rtwor`
|
||||
- `path: ece.jpg`
|
||||
- `path: iec`
|
||||
- `path: artwork/*`
|
||||
- `path: Artwork/*`
|
||||
- `path: *piece.jpg*`
|
||||
- `path: *Piece.jpg*`
|
||||
- `path: *artwork*`
|
||||
- `path: *Artwork*`
|
||||
- `path: *rtwor*`
|
||||
- `path: *ece.jpg*`
|
||||
- `path: *iec*`
|
||||
- `path: *.jpg`
|
||||
|
||||
While the following searches will **NOT:**
|
||||
|
||||
- `path: ARTWORK/Piece.jpg` _(Reason: Mismatched case)_
|
||||
- `path: *aRtWoRk/Piece*` _(Reason: Mismatched case)_
|
||||
- `path: PieCe.jpg` _(Reason: Mismatched case)_
|
||||
- `path: *PieCe.jpg*` _(Reason: Mismatched case)_
|
||||
|
||||
## Special Searches
|
||||
|
||||
"Special" searches use the `special:` keyword prefix and give quick results for certain special search queries.
|
||||
Some predefined searches use the `special:` keyword prefix and give quick results for certain special search queries.
|
||||
|
||||
### Untagged
|
||||
|
||||
To see all your file entries which don't contain any tags, use the `special:untagged` search.
|
||||
To see all your file entries which don't contain any tags, use the `special: untagged` search.
|
||||
|
||||
### Empty
|
||||
|
||||
**_NOTE:_** _Currently unavailable in v9.5.0-PR1_
|
||||
**_NOTE:_** _Currently unavailable in v9.5.0_
|
||||
|
||||
To see all your file entries which don't contain any tags _and_ any fields, use the `special:empty` search.
|
||||
To see all your file entries which don't contain any tags _and_ any fields, use the `special: empty` search.
|
||||
|
||||
@@ -32,7 +32,7 @@ Given a tag named "Freddy", we may confuse it with other "Freddy" tags in our li
|
||||
|
||||

|
||||
|
||||
So if the "Five Night's at Fredddy's" tag is added as a parent tag on the "Freddy" tag, and the disambiguation box next to it is checked, then our tag name will automatically be displayed as "Freddy (Five Nights at Freddy's)". Better yet, if the "Five Night's at Fredddy's" tag has a shorthand such as "FNAF", then our "Freddy" tag will be displayed as "Freddy (FNAF)". This process preserves our base tag name ("Freddy") and provides an option to get a clean and consistent method to display disambiguating parent categories, rather than having to type this information in manually for each applicable tag.
|
||||
So if the "Five Night's at Freddy's" tag is added as a parent tag on the "Freddy" tag, and the disambiguation box next to it is checked, then our tag name will automatically be displayed as "Freddy (Five Nights at Freddy's)". Better yet, if the "Five Night's at Freddy's" tag has a shorthand such as "FNAF", then our "Freddy" tag will be displayed as "Freddy (FNAF)". This process preserves our base tag name ("Freddy") and provides an option to get a clean and consistent method to display disambiguating parent categories, rather than having to type this information in manually for each applicable tag.
|
||||
|
||||
## Tag Relationships
|
||||
|
||||
@@ -60,7 +60,7 @@ Lastly, when searching your files with broader categories such as `Character` or
|
||||
|
||||
### Component Tags
|
||||
|
||||
**_[Coming in version 9.6](../updates/roadmap.md#96-alpha)_**
|
||||
**_[Coming in version 9.6](../updates/roadmap.md#v96)_**
|
||||
|
||||
Component tags will be built from a composition-based, or "HAS" type relationship between tags. This takes care of instances where an attribute may "have" another attribute, but doesn't inherit from it. Shrek may be an `Orge`, he may be a `Character`, but he is NOT a `Leather Vest` - even if he's commonly seen _with_ it. Component tags, along with the upcoming [Tag Override](tag_overrides.md) feature, are built to handle these cases in a way that still simplifies the tagging process without adding too much undue complexity for the user.
|
||||
|
||||
@@ -68,15 +68,19 @@ Component tags will be built from a composition-based, or "HAS" type relationshi
|
||||
|
||||
### Color
|
||||
|
||||
Tags use a default uncolored appearance by default, however can take on a number of built-in and user-created\* colors and color palettes! Tag color palettes can be based on a single color value (see: TagStudio Standard, TagStudio Shades, TagStudio Pastels) or use an optional secondary color to override the border and text colors (see: TagStudio Neon).
|
||||
Tags use a default uncolored appearance by default, however can take on a number of built-in and user-created colors and color palettes! Tag color palettes can be based on a single color value (see: TagStudio Standard, TagStudio Shades, TagStudio Pastels) or use an optional secondary color use for the text and optionally the tag border (e.g. TagStudio Neon).
|
||||
|
||||

|
||||
|
||||
\*_Coming in the full version 9.5.0 release_
|
||||
#### User-Created Colors
|
||||
|
||||
Custom palettes and colors can be created via the [Tag Color Manager](./tag_color.md). These colors will display alongside the built-in colors inside the tag selection window and are separated by their namespace names. Colors which use the secondary color for the tag border will be outlined in that color, otherwise they will only display the secondary color on the bottom of the swatch to indicate at a glance that the text colors are different.
|
||||
|
||||

|
||||
|
||||
### Icon
|
||||
|
||||
**_[Coming in version 9.6](../updates/roadmap.md#96-alpha)_**
|
||||
**_[Coming in version 9.6](../updates/roadmap.md#v96)_**
|
||||
|
||||
## Tag Properties
|
||||
|
||||
@@ -90,7 +94,7 @@ When the "Is Category" property is checked, this tag now acts as a category sepa
|
||||
|
||||
#### Is Hidden
|
||||
|
||||
**_[Coming in version 9.6](../updates/roadmap.md#96-alpha)_**
|
||||
**_[Coming in version 9.6](../updates/roadmap.md#v96)_**
|
||||
|
||||
When the "Is Hidden" property is checked, any file entries tagged with this tag will not show up in searches by default. This property comes by default with the built-in "Archived" tag.
|
||||
|
||||
|
||||
71
docs/library/tag_color.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Tag Colors
|
||||
|
||||
TagStudio features a variety of built-in tag colors, alongside the ability for users to create their own custom tag color palettes.
|
||||
|
||||
## Tag Color Manager
|
||||
|
||||
The Tag Color Manager is where you can create and manage your custom tag colors and associated namespaces. To open the Tag Color Manager, go to "File -> Manage Tag Colors" option in the menu bar.
|
||||
|
||||

|
||||
|
||||
## Creating a Namespace
|
||||
|
||||
TagStudio uses namespaces to group colors into palettes. Namespaces are a way for you to use the same color name across multiple palettes without having to worry about [name collision](https://en.wikipedia.org/wiki/Name_collision) with other palettes. This is especially useful when sharing your color palettes with others!\*
|
||||
|
||||
_\* Color pack sharing coming in a future update_
|
||||
|
||||
To create your first namespace, either click the "New Namespace" button or the large button prompt underneath the built-in colors.
|
||||
|
||||

|
||||
|
||||
### Name
|
||||
|
||||
The display name of the namespace, used for presentation.
|
||||
|
||||
### ID Slug
|
||||
|
||||
An internal ID for the namespace which is automatically derived from the namespace name.
|
||||
|
||||
Namespaces beginning with "tagstudio" are reserved by TagStudio and will automatically have their text changed.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! note
|
||||
It's currently not possible to manually edit the Namespace ID Slug. This will be possible once sharable color packs are added.
|
||||
|
||||
## Creating a Color
|
||||
|
||||
Once you've created your first namespace, click the "+" button inside the namespace section to create a color. To edit a color that you've previously created, either click on the color name or right click and select "Edit Color" from the context menu.
|
||||
|
||||

|
||||
|
||||
### Name
|
||||
|
||||
The display name for the color, used for presentation. You may occasionally see the color name followed by the [namespace name](#name) in parentheses to disambiguate it from other colors with the same name.
|
||||
|
||||
### ID Slug
|
||||
|
||||
Similar to [Namespace ID Slugs](#id-slug), the ID Slug is used as an internal ID and is automatically derived from the tag color name.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! note
|
||||
It's currently not possible to manually edit the Color ID Slug. This will be possible once sharable color packs are added.
|
||||
|
||||
### Primary Color
|
||||
|
||||
The primary color is used as the main tag color and by default is used as the background color with the text and border colors being derived from this color.
|
||||
|
||||
### Secondary Color
|
||||
|
||||
By default, the secondary color is only used as an optional override for the tag text color. This color can be cleared by clicking the adjacent "Reset" button.
|
||||
|
||||

|
||||
|
||||
The secondary color can also be used as the tag border color by checking the "Use Secondary Color for Border" box.
|
||||
|
||||

|
||||
|
||||
## Using Colors
|
||||
|
||||
When editing a tag, click the tag color button to bring up the tag color selection panel. From here you can choose any built-in TagStudio color as well as any of your custom colors.
|
||||
|
||||

|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
tags:
|
||||
- Upcoming Feature
|
||||
- Upcoming Feature
|
||||
---
|
||||
|
||||
# Tag Overrides
|
||||
|
||||
Tag overrides are the ability to add or remove [parent tags](tag.md#subtags) from a [tag](tag.md) on a per- [entry](entry.md) basis.
|
||||
Tag overrides are the ability to add or remove [parent tags](tag.md#parent-tags) from a [tag](tag.md) on a per- [entry](entry.md) basis.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -10,218 +10,122 @@ Features are broken up into the following priority levels, with nested prioritie
|
||||
- [MEDIUM] - Important but not necessary
|
||||
- [LOW] - Just nice to have
|
||||
|
||||
## Core Feature List
|
||||
|
||||
- [ ] Tags [HIGH]
|
||||
- [x] ID-based, not string based [HIGH]
|
||||
- [x] Tag name [HIGH]
|
||||
- [x] Tag alias list, aka alternate names [HIGH]
|
||||
- [x] Tag shorthand (specific short alias for displaying) [HIGH]
|
||||
- [x] Parent/Inheritance subtags [HIGH]
|
||||
- [ ] Composition/HAS subtags [HIGH]
|
||||
- [x] Deleting Tags [HIGH]
|
||||
- [ ] Merging Tags [HIGH]
|
||||
- [ ] Tag Icons [HIGH]
|
||||
- [ ] Small Icons [HIGH]
|
||||
- [ ] Large Icons for Profiles [MEDIUM]
|
||||
- [ ] Built-in Icon Packs (i.e. Boxicons) [HIGH]
|
||||
- [ ] User Defined Icons [HIGH]
|
||||
- [ ] Multiple Languages for Tag Strings [MEDIUM]
|
||||
- [ ] User-defined tag colors [HIGH]
|
||||
- [x] ID based, not string or hex [HIGH]
|
||||
- [x] Color name [HIGH]
|
||||
- [x] Color value (hex) [HIGH]
|
||||
- [x] Existing colors are now a set of base colors [HIGH]
|
||||
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
|
||||
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
|
||||
- [x] Title is tag name [HIGH]
|
||||
- [ ] [Tag Overrides](../library/tag_overrides.md) [MEDIUM]
|
||||
- [ ] Per-file overrides of subtags [HIGH]
|
||||
- [ ] Tag Packs [MEDIUM]
|
||||
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Conflict resolution [HIGH]
|
||||
- [ ] Color Packs [MEDIUM]
|
||||
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Exportable Library Data [HIGH]
|
||||
- [ ] Standard notation format (i.e. JSON) contacting all library data [HIGH]
|
||||
- [ ] [Macros](../utilities/macro.md) [HIGH]
|
||||
- [ ] Sharable Macros [MEDIUM]
|
||||
- [ ] Standard notation format (i.e. JSON) contacting macro instructions [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Triggers [HIGH]
|
||||
- [ ] On new file [HIGH]
|
||||
- [ ] On library refresh [HIGH]
|
||||
- [...]
|
||||
- [ ] Actions [HIGH]
|
||||
- [ ] Add tag(s) [HIGH]
|
||||
- [ ] Add field(s) [HIGH]
|
||||
- [ ] Set field content [HIGH]
|
||||
- [ ] [...]
|
||||
- [ ] Settings Menu [HIGH]
|
||||
- [ ] Application Settings [HIGH]
|
||||
- [ ] Stored in system user folder/designated folder [HIGH]
|
||||
- [ ] Library Settings [HIGH]
|
||||
- [ ] Stored in `.TagStudio` folder [HIGH]
|
||||
- [ ] Multiple Root Directories per Library [HIGH]
|
||||
- [ ] [Entry groups](../library/entry_groups.md) [HIGH]
|
||||
- [ ] Groups for files/entries where the same entry can be in multiple groups [HIGH]
|
||||
- [ ] Ability to number entries within group [HIGH]
|
||||
- [ ] Ability to set sorting method for group [HIGH]
|
||||
- [ ] Ability to set custom thumbnail for group [HIGH]
|
||||
- [ ] Group is treated as entry with tags and metadata [HIGH]
|
||||
- [ ] Nested groups [MEDIUM]
|
||||
- [ ] Fields [HIGH]
|
||||
- [x] Text Boxes [HIGH]
|
||||
- [x] Text Lines [HIGH]
|
||||
- [ ] Dates [HIGH]
|
||||
- [ ] Custom field names [HIGH]
|
||||
- [ ] Search engine [HIGH]
|
||||
- [x] Boolean operators [HIGH]
|
||||
- [ ] Tag objects + autocomplete [HIGH]
|
||||
- [x] Filename search [HIGH]
|
||||
- [x] Filetype search [HIGH]
|
||||
- [x] Search by extension (e.g. ".jpg", ".png") [HIGH]
|
||||
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW]
|
||||
- [x] Search by media type (e.g. "image", "video", "document") [MEDIUM]
|
||||
- [ ] Field content search [HIGH]
|
||||
- [ ] HAS operator for composition tags [HIGH]
|
||||
- [ ] OCR search [LOW]
|
||||
- [ ] Fuzzy Search [LOW]
|
||||
- [ ] Sortable results [HIGH]
|
||||
- [ ] Sort by relevance [HIGH]
|
||||
- [x] Sort by date added [HIGH]
|
||||
- [ ] Sort by date created [HIGH]
|
||||
- [ ] Sort by date modified [HIGH]
|
||||
- [ ] Sort by date taken (photos) [MEDIUM]
|
||||
- [ ] Sort by file size [HIGH]
|
||||
- [ ] Sort by file dimension (images/video) [LOW]
|
||||
- [ ] Automatic Entry Relinking [HIGH] [#36](https://github.com/TagStudioDev/TagStudio/issues/36)
|
||||
- [ ] Detect Renames [HIGH]
|
||||
- [ ] Detect Moves [HIGH]
|
||||
- [ ] Detect Deletions [HIGH]
|
||||
- [ ] Image Collages [LOW] [#91](https://github.com/TagStudioDev/TagStudio/issues/91)
|
||||
- [ ] UI [HIGH]
|
||||
- [ ] Tagging Panel [HIGH]
|
||||
- [ ] Top Tags [HIGH]
|
||||
- [ ] Recent Tags [HIGH]
|
||||
- [ ] Tag Search [HIGH]
|
||||
- [ ] Pinned Tags [HIGH]
|
||||
- [ ] Configurable Thumbnails [MEDIUM]
|
||||
- [ ] Custom thumbnail override [HIGH]
|
||||
- [ ] Toggle File Extension Label [MEDIUM]
|
||||
- [ ] Toggle Duration Label [MEDIUM]
|
||||
- [ ] Custom Tag Badges [LOW]
|
||||
- [ ] Thumbnails [HIGH]
|
||||
- [ ] File Duration Label [HIGH]
|
||||
- [ ] 3D Model Previews [MEDIUM]
|
||||
- [ ] STL Previews [HIGH]
|
||||
- [x] Drag and Drop [HIGH]
|
||||
- [x] Drag files _to_ other programs [HIGH]
|
||||
- [x] Drag files _to_ file explorer windows [MEDIUM]
|
||||
- [x] Drag files _from_ file explorer windows [MEDIUM]
|
||||
- [x] Drag files _from_ other programs [LOW]
|
||||
- [ ] File Preview Panel [HIGH]
|
||||
- [ ] Video Playback [HIGH]
|
||||
- [x] Play/Pause [HIGH]
|
||||
- [x] Loop [HIGH]
|
||||
- [x] Toggle Autoplay [MEDIUM]
|
||||
- [ ] Volume Control [HIGH]
|
||||
- [x] Toggle Mute [HIGH]
|
||||
- [ ] Timeline scrubber [HIGH]
|
||||
- [ ] Fullscreen [MEDIUM]
|
||||
- [x] Audio Playback [HIGH]
|
||||
- [x] Play/Pause [HIGH]
|
||||
- [ ] Loop [HIGH]
|
||||
- [ ] Toggle Autoplay [MEDIUM]
|
||||
- [x] Volume Control [HIGH]
|
||||
- [x] Toggle Mute [HIGH]
|
||||
- [x] Timeline scrubber [HIGH]
|
||||
- [ ] Optimizations [HIGH]
|
||||
- [x] Thumbnail caching [HIGH]
|
||||
- [ ] File property caching/indexes [HIGH]
|
||||
|
||||
## Version Milestones
|
||||
|
||||
These version milestones are rough estimations for when the previous core features will be added. For a more definitive idea for when features are coming, please reference the current GitHub [milestones](https://github.com/TagStudioDev/TagStudio/milestones).
|
||||
|
||||
_(This list was created after the release of version 9.4)_
|
||||
<!-- prettier-ignore -->
|
||||
!!! note
|
||||
This list was created after the release of version 9.4
|
||||
|
||||
### 9.5 (Alpha)
|
||||
### v9.5
|
||||
|
||||
#### Core
|
||||
|
||||
- [x] SQL backend [HIGH]
|
||||
- [x] Translations _(Any applicable)_ [MEDIUM]
|
||||
- [ ] Tags [HIGH]
|
||||
- [x] Deleting Tags [HIGH]
|
||||
- [ ] User-defined tag colors [HIGH]
|
||||
- [x] ID based, not string or hex [HIGH]
|
||||
- [x] Color name [HIGH]
|
||||
- [x] Color value (hex) [HIGH]
|
||||
- [x] Existing colors are now a set of base colors [HIGH]
|
||||
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
|
||||
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
|
||||
- [ ] Search engine [HIGH]
|
||||
- [x] Boolean operators [HIGH]
|
||||
- [ ] Tag objects + autocomplete [HIGH]
|
||||
- [x] Filename search [HIGH]
|
||||
- [x] Filetype search [HIGH]
|
||||
- [x] Search by extension (e.g. ".jpg", ".png") [HIGH]
|
||||
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW]
|
||||
- [x] Search by media type (e.g. "image", "video", "document") [MEDIUM]
|
||||
- [ ] Field content search [HIGH]
|
||||
- [ ] Sortable results [HIGH]
|
||||
- [x] Sort by date added [HIGH]
|
||||
- [ ] Sort by date created [HIGH]
|
||||
- [ ] Sort by date modified [HIGH]
|
||||
- [ ] Settings Menu [HIGH]
|
||||
- [ ] Application Settings [HIGH]
|
||||
- [ ] Stored in system user folder/designated folder [HIGH]
|
||||
- [ ] Library Settings [HIGH]
|
||||
- [ ] Stored in `.TagStudio` folder [HIGH]
|
||||
- [ ] Optimizations [HIGH]
|
||||
- [x] Thumbnail caching [HIGH]
|
||||
|
||||
### 9.6 (Alpha)
|
||||
#### Tags
|
||||
|
||||
- [x] Deleting Tags [HIGH]
|
||||
- [ ] User-defined tag colors [HIGH]
|
||||
- [x] ID based, not string or hex [HIGH]
|
||||
- [x] Color name [HIGH]
|
||||
- [x] Color value (hex) [HIGH]
|
||||
- [x] Existing colors are now a set of base colors [HIGH]
|
||||
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
|
||||
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
|
||||
|
||||
#### Search
|
||||
|
||||
- [x] Boolean operators [HIGH]
|
||||
- [x] Filename search [HIGH]
|
||||
- [x] Filetype search [HIGH]
|
||||
- [x] Search by extension (e.g. ".jpg", ".png") [HIGH]
|
||||
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW]
|
||||
- [x] Search by media type (e.g. "image", "video", "document") [MEDIUM]
|
||||
- [x] Sort by date added [HIGH]
|
||||
|
||||
#### UI
|
||||
|
||||
- [ ] Translations _(Any applicable)_ [MEDIUM]
|
||||
|
||||
#### Performance
|
||||
|
||||
- [x] Thumbnail caching [HIGH]
|
||||
|
||||
### v9.6
|
||||
|
||||
#### Core
|
||||
|
||||
- [ ] Cached file property table (media duration, word count, dimensions, etc.) [MEDIUM]
|
||||
|
||||
#### Library
|
||||
|
||||
- [ ] Tags [HIGH]
|
||||
- [ ] Merging Tags [HIGH]
|
||||
- [ ] Composition/HAS subtags [HIGH]
|
||||
- [ ] Tag Icons [HIGH]
|
||||
- [ ] Small Icons [HIGH]
|
||||
- [ ] Large Icons for Profiles [MEDIUM]
|
||||
- [ ] Built-in Icon Packs (i.e. Boxicons) [HIGH]
|
||||
- [ ] User Defined Icons [HIGH]
|
||||
- [ ] Multiple Languages for Tag Strings [MEDIUM]
|
||||
- [ ] Title is tag name [HIGH]
|
||||
- [ ] Title has tag color [MEDIUM]
|
||||
- [ ] Tag marked as category does not display as a tag itself [HIGH]
|
||||
- [ ] [Tag Overrides](../library/tag_overrides.md) [MEDIUM]
|
||||
- [ ] Per-file overrides of subtags [HIGH]
|
||||
- [ ] Fields [HIGH]
|
||||
- [ ] Dates [HIGH]
|
||||
- [ ] Custom field names [HIGH]
|
||||
- [ ] Multiple Root Directories per Library [HIGH]
|
||||
- [ ] `.ts_ignore` (`.gitignore`-style glob ignoring) [HIGH]
|
||||
- [ ] Sharable Color Packs [MEDIUM]
|
||||
- [ ] Human-readable (TOML) files containing tag data [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
|
||||
#### Tags
|
||||
|
||||
- [ ] Merging Tags [HIGH]
|
||||
- [ ] [Component/HAS](../library/tag.md#component-tags) subtags [HIGH]
|
||||
- [ ] Tag Icons [HIGH]
|
||||
- [ ] Small Icons [HIGH]
|
||||
- [ ] Large Icons for Profiles [MEDIUM]
|
||||
- [ ] Built-in Icon Packs (i.e. Boxicons) [HIGH]
|
||||
- [ ] User Defined Icons [HIGH]
|
||||
- [ ] Multiple Languages for Tag Strings [MEDIUM]
|
||||
- [ ] Title is tag name [HIGH]
|
||||
- [ ] Title has tag color [MEDIUM]
|
||||
- [ ] Tag marked as category does not display as a tag itself [HIGH]
|
||||
- [ ] [Tag Overrides](../library/tag_overrides.md) [MEDIUM]
|
||||
- [ ] Per-file overrides of subtags [HIGH]
|
||||
|
||||
#### Fields
|
||||
|
||||
- [ ] Datetime fields [HIGH]
|
||||
- [ ] Custom field names [HIGH]
|
||||
|
||||
#### Search
|
||||
|
||||
- [ ] Field content search [HIGH]
|
||||
- [ ] Sort by date created [HIGH]
|
||||
- [ ] Sort by date modified [HIGH]
|
||||
- [ ] Sort by filename [HIGH]
|
||||
- [ ] HAS operator for composition tags [HIGH]
|
||||
- [ ] Search bar rework
|
||||
- [ ] Improved tag autocomplete [HIGH]
|
||||
- [ ] Tags appear as widgets in search bar [HIGH]
|
||||
|
||||
#### UI
|
||||
|
||||
- [ ] File duration on video thumbnails [HIGH]
|
||||
- [ ] 3D Model Previews [MEDIUM]
|
||||
- [ ] STL Previews [HIGH]
|
||||
- [ ] Word count/line count on text thumbnails [LOW]
|
||||
- [ ] Settings Menu [HIGH]
|
||||
- [ ] Application Settings [HIGH]
|
||||
- [ ] Stored in system user folder/designated folder [HIGH]
|
||||
- [ ] Library Settings [HIGH]
|
||||
- [ ] Stored in `.TagStudio` folder [HIGH]
|
||||
- [ ] Tagging Panel [HIGH]
|
||||
|
||||
Togglebale persistent main window panel or popout. Replaces the current tag manager.
|
||||
|
||||
- [ ] Top Tags [HIGH]
|
||||
- [ ] Recent Tags [HIGH]
|
||||
- [ ] Tag Search [HIGH]
|
||||
- [ ] Pinned Tags [HIGH]
|
||||
- [ ] Search engine [HIGH]
|
||||
- [ ] HAS operator for composition tags [HIGH]
|
||||
|
||||
### 9.7 (Alpha)
|
||||
- [ ] New tabbed tag building UI to support the new tag features [HIGH]
|
||||
|
||||
### v9.7
|
||||
|
||||
#### Library
|
||||
|
||||
- [ ] Configurable Thumbnails [MEDIUM]
|
||||
- [ ] Toggle File Extension Label [MEDIUM]
|
||||
- [ ] Toggle Duration Label [MEDIUM]
|
||||
- [ ] Custom Tag Badges [LOW]
|
||||
- [ ] Thumbnails [HIGH]
|
||||
- [ ] File Duration Label [HIGH]
|
||||
- [ ] [Entry groups](../library/entry_groups.md) [HIGH]
|
||||
- [ ] Groups for files/entries where the same entry can be in multiple groups [HIGH]
|
||||
- [ ] Ability to number entries within group [HIGH]
|
||||
@@ -230,46 +134,84 @@ _(This list was created after the release of version 9.4)_
|
||||
- [ ] Group is treated as entry with tags and metadata [HIGH]
|
||||
- [ ] Nested groups [MEDIUM]
|
||||
|
||||
### 9.8 (Possible Beta)
|
||||
#### Search
|
||||
|
||||
- [ ] Sort by relevance [HIGH]
|
||||
- [ ] Sort by date taken (photos) [MEDIUM]
|
||||
- [ ] Sort by file size [HIGH]
|
||||
- [ ] Sort by file dimension (images/video) [LOW]
|
||||
|
||||
#### [Macros](../utilities/macro.md)
|
||||
|
||||
- [ ] Sharable Macros [MEDIUM]
|
||||
- [ ] Standard notation format (TOML) contacting macro instructions [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Triggers [HIGH]
|
||||
- [ ] On new file [HIGH]
|
||||
- [ ] On library refresh [HIGH]
|
||||
- [ ] [...]
|
||||
- [ ] Actions [HIGH]
|
||||
- [ ] Add tag(s) [HIGH]
|
||||
- [ ] Add field(s) [HIGH]
|
||||
- [ ] Set field content [HIGH]
|
||||
- [ ] [...]
|
||||
|
||||
#### UI
|
||||
|
||||
- [ ] Custom thumbnail overrides [MEDIUM]
|
||||
- [ ] Toggle File Extension Label [MEDIUM]
|
||||
- [ ] Toggle Duration Label [MEDIUM]
|
||||
- [ ] Custom Tag Badges [LOW]
|
||||
- [ ] Unified Media Player [HIGH]
|
||||
- [ ] Auto-hiding player controls
|
||||
- [x] Play/Pause [HIGH]
|
||||
- [x] Loop [HIGH]
|
||||
- [x] Toggle Autoplay [MEDIUM]
|
||||
- [ ] Volume Control [HIGH]
|
||||
- [x] Toggle Mute [HIGH]
|
||||
- [ ] Timeline scrubber [HIGH]
|
||||
- [ ] Fullscreen [MEDIUM]
|
||||
- [ ] Library list view [HIGH]
|
||||
- [ ] Configurable page size [HIGH]
|
||||
|
||||
### v9.8
|
||||
|
||||
#### Library
|
||||
|
||||
- [ ] Automatic Entry Relinking [HIGH]
|
||||
- [ ] Detect Renames [HIGH]
|
||||
- [ ] Detect Moves [HIGH]
|
||||
- [ ] Detect Deletions [HIGH]
|
||||
- [ ] [Macros](../utilities/macro.md) [HIGH]
|
||||
- [ ] Sharable Macros [MEDIUM]
|
||||
- [ ] Standard notation format (i.e. JSON) contacting macro instructions [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Triggers [HIGH]
|
||||
- [ ] On new file [HIGH]
|
||||
- [ ] On library refresh [HIGH]
|
||||
- [...]
|
||||
- [ ] Actions [HIGH]
|
||||
- [ ] Add tag(s) [HIGH]
|
||||
- [ ] Add field(s) [HIGH]
|
||||
- [ ] Set field content [HIGH]
|
||||
- [ ] [...]
|
||||
|
||||
### 9.9 (Possible Beta)
|
||||
#### Search
|
||||
|
||||
- [ ] OCR search [LOW]
|
||||
- [ ] Fuzzy Search [LOW]
|
||||
|
||||
### v9.9
|
||||
|
||||
#### Library
|
||||
|
||||
- [ ] Tag Packs [MEDIUM]
|
||||
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Conflict resolution [HIGH]
|
||||
- [ ] Color Packs [MEDIUM]
|
||||
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Exportable Library Data [HIGH]
|
||||
- [ ] Standard notation format (i.e. JSON) contacting all library data [HIGH]
|
||||
|
||||
### 10.0 (Possible Beta/Full Release)
|
||||
#### Tags
|
||||
|
||||
- [ ] Tag Packs [MEDIUM]
|
||||
- [ ] Human-readable (TOML) files containing tag data [HIGH]
|
||||
- [ ] Multiple Languages for Tag Strings [MEDIUM]
|
||||
- [ ] Importable [HIGH]
|
||||
- [ ] Exportable [HIGH]
|
||||
- [ ] Conflict resolution [HIGH]
|
||||
|
||||
### v10.0
|
||||
|
||||
- [ ] All remaining [HIGH] and optional [MEDIUM] features
|
||||
|
||||
### Post 10.0
|
||||
### Post v10.0
|
||||
|
||||
#### Core
|
||||
|
||||
- [ ] Core Library/API
|
||||
- [ ] Plugin Support
|
||||
|
||||
46
docs/updates/schema_changes.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Save Format Changes
|
||||
|
||||
This page outlines the various changes made the TagStudio save file format over time, sometimes referred to as the "database" or "database file".
|
||||
|
||||
## JSON
|
||||
|
||||
| First Used | Last Used | Format | Location |
|
||||
| ---------- | ----------------------------------------------------------------------- | ------ | --------------------------------------------- |
|
||||
| v1.0.0 | [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2) | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
|
||||
|
||||
The legacy database format for public TagStudio releases [v9.1](https://github.com/TagStudioDev/TagStudio/tree/Alpha-v9.1) through [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2). Variations of this format had been used privately since v1.0.0.
|
||||
|
||||
Replaced by the new SQLite format introduced in TagStudio [v9.5.0 Pre-Release 1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1).
|
||||
|
||||
## DB_VERSION 6
|
||||
|
||||
| First Used | Last Used | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-PR1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | [v9.5.0-PR1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
The first public version of the SQLite save file format.
|
||||
|
||||
Migration from the legacy JSON format is provided via a walkthrough when opening a legacy library in TagStudio [v9.5.0 Pre-Release 1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) or later.
|
||||
|
||||
## DB_VERSION 7
|
||||
|
||||
| First Used | Last Used | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-PR2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | [v9.5.0-PR3](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr3) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
### Changes
|
||||
|
||||
- Repairs "Description" fields to use a TEXT_LINE key instead of a TEXT_BOX key.
|
||||
- Repairs tags that may have a disambiguation_id pointing towards a deleted tag.
|
||||
|
||||
## DB_VERSION 8
|
||||
|
||||
| First Used | Last Used | Format | Location |
|
||||
| ------------------------------------------------------------------------------- | --------- | ------ | ----------------------------------------------- |
|
||||
| [v9.5.0-PR4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | _Current_ | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
### Changes
|
||||
|
||||
- Adds the `color_border` column to `tag_colors` table. Used for instructing the [secondary color](../library/tag_color.md#secondary-color) to apply to a tag's border as a new optional behavior.
|
||||
- Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)".
|
||||
- Updates Neon colors to use the the new `color_border` property.
|
||||
@@ -41,7 +41,7 @@ Create a new tag by accessing the "New Tag" option from the Edit menu or by pres
|
||||
|
||||
### Tag Manager
|
||||
|
||||
You can manage your library of tags from opening the "Tag Manager" panel from Edit -> "Tag Manager". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
|
||||
You can manage your library of tags from opening the "Tag Manager" panel from Edit -> "Manage Tags". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
|
||||
|
||||
## Editing Tags
|
||||
|
||||
@@ -51,9 +51,11 @@ To edit a tag, click on it inside the preview panel or right-click the tag and s
|
||||
|
||||
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 broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to “Search & Relink” any unlinked file 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 entries inside your library.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! warning
|
||||
There is currently no method to relink entries to files that have been renamed - only moved or deleted. This is a high priority for future releases.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! 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 high priorities for future versions.
|
||||
|
||||
|
||||
@@ -43,4 +43,4 @@ Tool is in development, will allow for user-defined sorting of [fields](../libra
|
||||
|
||||
### Folders to Tags
|
||||
|
||||
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](../library/tag.md#subtags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.
|
||||
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](../library/tag.md#parent-tags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "TagStudio"
|
||||
description = "A User-Focused Photo & File Management System."
|
||||
version = "9.5.0-pre3"
|
||||
version = "9.5.0"
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -20,6 +20,17 @@ convention = "google"
|
||||
select = ["B", "D", "E", "F", "FBT003", "I", "N", "SIM", "T20", "UP"]
|
||||
ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"]
|
||||
|
||||
[tool.pyright]
|
||||
ignore = [".venv/**"]
|
||||
include = ["tagstudio/**"]
|
||||
reportAny = false
|
||||
reportImplicitStringConcatenation = false
|
||||
# reportOptionalMemberAccess = false
|
||||
reportUnannotatedClassAttribute = false
|
||||
reportUnknownArgumentType = false
|
||||
reportUnknownMemberType = false
|
||||
reportUnusedCallResult = false
|
||||
|
||||
[tool.mypy]
|
||||
strict_optional = false
|
||||
disable_error_code = ["func-returns-value", "import-untyped"]
|
||||
|
||||
@@ -78,7 +78,7 @@ app = BUNDLE(
|
||||
name='TagStudio.app',
|
||||
icon=icon,
|
||||
bundle_identifier='com.cyanvoxel.tagstudio',
|
||||
version='9.5.0-pr3',
|
||||
version='9.5.0',
|
||||
info_plist={
|
||||
'NSAppleScriptEnabled': False,
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
{
|
||||
"about.content": "<h2>TagStudio Alpha {version} ({branch})</h2><p>TagStudio ist eine Anwendung zum organisieren von Fotos & Dateien mit einem zugrunde liegendem Tag-basierten System, welches sich darauf konzentriert, dem Nutzer Freiraum und Flexibilität zu bieten. Keine proprietären Programme oder Formate, kein Meer an Hilfsdateien und keine komplette Umwälzung deiner Dateisystemstruktur.</p>Lizenz: GPLv3<br>Konfigurations-Pfad: {config_path}<br>FFmpeg: {ffmpeg}<br>FFprobe: {ffprobe}<p><a href=\"https://github.com/TagStudioDev/TagStudio\">GitHub</a> | <a href=\"https://docs.tagstud.io\">Dokumentation</a> | <a href=\"https://discord.com/invite/hRNnVKhF2G\">Discord</a></p>",
|
||||
"about.title": "Über",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Bibliothek '{library_dir}'",
|
||||
"drop_import.description": "Die folgenden Dateinamen existieren bereits in der Bibliothek",
|
||||
"drop_import.duplicates_choice.plural": "Die folgenden {count} Dateinamen existieren bereits in der Bibliothek.",
|
||||
"drop_import.duplicates_choice.singular": "Der folgende Dateiname existiert bereits in der Bibliothek.",
|
||||
"color.color_border": "Benutze Sekundärfarbe für die Umrandung",
|
||||
"color.confirm_delete": "Soll die Farbe \"{color_name}\" wirklich gelöscht werden?",
|
||||
"color.delete": "Tag löschen",
|
||||
"color.import_pack": "Farb-Paket importieren",
|
||||
"color.name": "Name",
|
||||
"color.namespace.delete.prompt": "Soll dieser Farb-Namensraum wirklich gelöscht werden? Diese aktion wird neben dem Namensraum ALLE darin enthaltenen Farben löschen!",
|
||||
"color.namespace.delete.title": "Farb-Namensraum löschen",
|
||||
"color.new": "Neue Farbe",
|
||||
"color.placeholder": "Farbe",
|
||||
"color.primary": "Primärfarbe",
|
||||
"color.primary_required": "Primärfarbe (erforderlich)",
|
||||
"color.secondary": "Sekundärfarbe",
|
||||
"color.title.no_color": "Keine Farbe",
|
||||
"color_manager.title": "Tag-Farben verwalten",
|
||||
"drop_import.description": "Die folgenden Dateien passen zu Dateipfaden, welche bereits in der Bibliothek existieren",
|
||||
"drop_import.duplicates_choice.plural": "Die folgenden {count} Dateien passen zu Dateipfaden, welche bereits in der Bibliothek existieren.",
|
||||
"drop_import.duplicates_choice.singular": "Die folgende Datei passt zu einem Dateipfad, welcher bereits in der Bibliothek existiert.",
|
||||
"drop_import.progress.label.initial": "Neue Dateien werden importiert...",
|
||||
"drop_import.progress.label.plural": "Neue Dateien werden importiert...\n{count} Dateien importiert.{suffix}",
|
||||
"drop_import.progress.label.singular": "Neue Dateien werden importiert...\n1 Datei importiert.{suffix}",
|
||||
"drop_import.progress.window_title": "Dateien Importieren",
|
||||
"drop_import.title": "Dateikollisionen",
|
||||
"drop_import.title": "Dateikollision(en)",
|
||||
"edit.color_manager": "Tag-Farben verwalten",
|
||||
"edit.copy_fields": "Felder kopieren",
|
||||
"edit.paste_fields": "Felder einfügen",
|
||||
"edit.tag_manager": "Tags Verwalten",
|
||||
"entries.duplicate.merge": "Doppelte Einträge zusammenführen",
|
||||
"entries.duplicate.merge.label": "Führe doppelte Einträge zusammen…",
|
||||
"entries.duplicate.refresh": "Doppelte Einträge aktualisieren",
|
||||
"entries.duplicates.description": "Doppelte Einträge sind definiert als mehrere Einträge, die auf dieselbe Datei auf der Festplatte verweisen. Durch das Zusammenführen dieser Einträge werden die Tags und Metadaten aller Duplikate zu einem einzigen konsolidierten Eintrag zusammengefasst. Diese sind nicht zu verwechseln mit „doppelten Dateien“, die Duplikate Ihrer Dateien selbst außerhalb von TagStudio sind.",
|
||||
"entries.mirror": "Kopieren",
|
||||
"entries.mirror.confirmation": "Sind Sie sich sicher, dass Sie die folgenden {count} Einträge kopieren wollen?",
|
||||
"entries.mirror.label": "Kopiere {idx}/{total} Einträge...",
|
||||
"entries.mirror.title": "Einträge werden kopiert",
|
||||
"entries.mirror.window_title": "Einträge duplizieren",
|
||||
"entries.mirror": "Spiegeln",
|
||||
"entries.mirror.confirmation": "Sind Sie sich sicher, dass Sie die folgenden {count} Einträge spiegeln wollen?",
|
||||
"entries.mirror.label": "Spiegele {idx}/{total} Einträge...",
|
||||
"entries.mirror.title": "Einträge werden gespiegelt",
|
||||
"entries.mirror.window_title": "Einträge spiegeln",
|
||||
"entries.running.dialog.new_entries": "Füge {total} neue Dateieinträge hinzu...",
|
||||
"entries.running.dialog.title": "Füge neue Dateieinträge hinzu",
|
||||
"entries.tags": "Tags",
|
||||
"entries.unlinked.delete": "Unverknüpfte Einträge löschen",
|
||||
"entries.unlinked.delete.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?",
|
||||
@@ -84,12 +105,14 @@
|
||||
"generic.filename": "Dateiname",
|
||||
"generic.navigation.back": "Zurück",
|
||||
"generic.navigation.next": "Weiter",
|
||||
"generic.none": "Kein(e)",
|
||||
"generic.overwrite": "Überschreibem",
|
||||
"generic.overwrite_alt": "Überschreiben",
|
||||
"generic.paste": "Einfügen",
|
||||
"generic.recent_libraries": "Aktuelle Bibliotheken",
|
||||
"generic.rename": "Umbenennen",
|
||||
"generic.rename_alt": "Umbenennen",
|
||||
"generic.reset": "Zurücksetzen",
|
||||
"generic.save": "Speichern",
|
||||
"generic.skip": "Überspringen",
|
||||
"generic.skip_alt": "Über&springen",
|
||||
@@ -123,11 +146,12 @@
|
||||
"json_migration.heading.fields": "Felder:",
|
||||
"json_migration.heading.file_extension_list": "Liste der Dateiendungen:",
|
||||
"json_migration.heading.match": "Übereinstimmend",
|
||||
"json_migration.heading.names": "Namen:",
|
||||
"json_migration.heading.parent_tags": "Übergeordnete Tags:",
|
||||
"json_migration.heading.paths": "Pfade:",
|
||||
"json_migration.heading.shorthands": "Kurzformen:",
|
||||
"json_migration.heading.tags": "Tags:",
|
||||
"json_migration.info.description": "Bibliotheksdaten, welche mit TagStudio Versionen <b>9.4 und niedriger</b> erstellt wurden, müssen in das neue Format <b>v9.5+</b> migriert werden.<br><h2>Was du wissen solltest:</h2><ul><li>Deine bestehenden Bibliotheksdaten werden<b><i>NICHT</i></b> gelöscht.</li><li>Deine persönlichen Dateien werden <b><i>NICHT</i></b> gelöscht, verschoben oder verändert.</li><li>Das neue Format v9.5+ kann nicht von früheren TagStudio Versionen geöffnet werden.</li></ul>",
|
||||
"json_migration.info.description": "Bibliotheksdaten, welche mit TagStudio-Versionen <b>9.4 und niedriger</b> erstellt wurden, müssen in das neue Format <b>v9.5+</b> migriert werden.<br><h2>Was du wissen solltest:</h2><ul><li>Deine bestehenden Bibliotheksdaten werden<b><i>NICHT</i></b> gelöscht.</li><li>Deine persönlichen Dateien werden <b><i>NICHT</i></b> gelöscht, verschoben oder verändert.</li><li>Das neue Format v9.5+ kann nicht von früheren TagStudio-Versionen geöffnet werden.</li></ul>",
|
||||
"json_migration.migrating_files_entries": "Migriere {entries:,d} Dateieinträge...",
|
||||
"json_migration.migration_complete": "Migration abgeschlossen!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Migration abgeschlossen, Diskrepanzen gefunden",
|
||||
@@ -147,9 +171,16 @@
|
||||
"library.refresh.scanning_preparing": "Überprüfe Verzeichnisse auf neue Dateien...\nBereite vor...",
|
||||
"library.refresh.title": "Verzeichnisse werden aktualisiert",
|
||||
"library.scan_library.title": "Bibliothek wird scannen",
|
||||
"macros.running.dialog.new_entries": "Führe konfigurierte Makros für {count}/{total} neue Einträge aus",
|
||||
"library_object.name": "Name",
|
||||
"library_object.name_required": "Name (erforderlich)",
|
||||
"library_object.slug": "ID Schlüssel",
|
||||
"library_object.slug_required": "ID Schlüssel (erforderlich)",
|
||||
"macros.running.dialog.new_entries": "Führe konfigurierte Makros für {count}/{total} neue Dateieinträge aus...",
|
||||
"macros.running.dialog.title": "Ausführen von Makros bei neuen Einträgen",
|
||||
"media_player.autoplay": "Autoplay",
|
||||
"menu.delete_selected_files_ambiguous": "Datei(en) nach {trash_item} verschieben",
|
||||
"menu.delete_selected_files_plural": "Dateien nach {trash_item} verschieben",
|
||||
"menu.delete_selected_files_singular": "Datei nach {trash_item} verschieben",
|
||||
"menu.edit": "Bearbeiten",
|
||||
"menu.edit.ignore_list": "Dateien und Verzeichnisse ignorieren",
|
||||
"menu.edit.manage_file_extensions": "Dateiendungen verwalten",
|
||||
@@ -166,39 +197,66 @@
|
||||
"menu.file.save_backup": "Bibliotheksbackup speichern",
|
||||
"menu.file.save_library": "Bibliothek speichern",
|
||||
"menu.help": "&Hilfe",
|
||||
"menu.help.about": "Über",
|
||||
"menu.macros": "&Makros",
|
||||
"menu.macros.folders_to_tags": "Verzeichnisse zu Tags",
|
||||
"menu.select": "Auswählen",
|
||||
"menu.settings": "Optionen...",
|
||||
"menu.tools": "Werkzeuge",
|
||||
"menu.tools.fix_duplicate_files": "Duplizierte &Dateien reparieren",
|
||||
"menu.tools.fix_unlinked_entries": "&Unverknüpfte Einträge reparieren",
|
||||
"menu.view": "Ansicht",
|
||||
"menu.window": "Fenster",
|
||||
"namespace.create.description": "Namespaces werden von Tagstudio verwendet, um Gruppen von Objekten (bspw. Tags oder Farben) so darzustellen, dass sie einfach exportiert und geteilt werden können. Namespaces, die mit \"tagstudio\" beginnen sind für interne Vorgänge von Tagstudio reserviert.",
|
||||
"namespace.create.description_color": "Tagfarben nutzen Namespaces als Farbpalettengruppen. Alle benutzerdefinierten Farben müssen erst einer Namespacegruppe zugeordnet werden.",
|
||||
"namespace.create.title": "Namensraum erstellen",
|
||||
"namespace.new.button": "Neuer Namensraum",
|
||||
"namespace.new.prompt": "Erstelle einen neuen Namensraum um eigene Farben hinzuzufügen!",
|
||||
"preview.multiple_selection": "<b>{count}</b> Elemente ausgewählt",
|
||||
"preview.no_selection": "Keine Elemente ausgewählt",
|
||||
"select.add_tag_to_selected": "Tag zu Ausgewähltem hinzufügen",
|
||||
"select.all": "Alle auswählen",
|
||||
"select.clear": "Auswahl leeren",
|
||||
"settings.clear_thumb_cache.title": "Vorschaubild-Zwischenspeicher leeren",
|
||||
"settings.language": "Sprache",
|
||||
"settings.open_library_on_start": "Bibliothek zum Start öffnen",
|
||||
"settings.restart_required": "Bitte TagStudio neustarten, um Änderungen anzuwenden.",
|
||||
"settings.show_filenames_in_grid": "Dateinamen in Raster darstellen",
|
||||
"settings.show_recent_libraries": "Zuletzt verwendete Bibliotheken anzeigen",
|
||||
"settings.title": "Einstellungen",
|
||||
"sorting.direction.ascending": "Aufsteigend",
|
||||
"sorting.direction.descending": "Absteigend",
|
||||
"splash.opening_library": "Öffne Bibliothek \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "{count} Dateien gelöscht!",
|
||||
"status.deleted_file_singular": "1 Datei gelöscht!",
|
||||
"status.deleted_none": "Keine Dateien gelöscht.",
|
||||
"status.deleted_partial_warning": "Es wurden nur {count} Datei(en) gelöscht! Bitte Überprüfen, ob eine der Dateien derzeit fehlt oder in verwendet wird.",
|
||||
"status.deleting_file": "Lösche Datei [{i}/{count}]: \"{path}\"...",
|
||||
"status.library_backup_in_progress": "Bibliotheksbackup wird gespeichert...",
|
||||
"status.library_backup_success": "Bibliotheks-Backup gespeichert unter: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "Bibliothek geschlossen ({time_span})",
|
||||
"status.library_closing": "Bibliothek wird geschlossen...",
|
||||
"status.library_save_success": "Bibliothek gespeichert und geschlossen!",
|
||||
"status.library_search_query": "Durchsuche die Bibliothek...",
|
||||
"status.library_version_expected": "Erwartet:",
|
||||
"status.library_version_found": "Gefunden:",
|
||||
"status.library_version_mismatch": "BIbliotheksversion stimmt nicht überein!",
|
||||
"status.results": "Ergebnisse",
|
||||
"status.results.invalid_syntax": "Ungültige Such-Syntax:",
|
||||
"status.results_found": "{count} Ergebnisse gefunden ({time_span})",
|
||||
"tag.add": "Tag hinzufügen",
|
||||
"tag.add.plural": "Tags hinzufügen",
|
||||
"tag.add_to_search": "Zur Suche hinzufügen",
|
||||
"tag.aliases": "Aliase",
|
||||
"tag.all_tags": "Alle Tags",
|
||||
"tag.choose_color": "Tag-Farbe auswählen",
|
||||
"tag.color": "Farbe",
|
||||
"tag.confirm_delete": "Sind Sie sich sicher, dass Sie den Tag \"{tag_name}\" löschen wollen?",
|
||||
"tag.create": "Tag erstellen",
|
||||
"tag.create_add": "Erstellen && Hinzufügen \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Diesen Tag zur Unterscheidung verwenden",
|
||||
"tag.edit": "Tag bearbeiten",
|
||||
"tag.is_category": "Ist Kategorie",
|
||||
"tag.name": "Name",
|
||||
"tag.new": "Neuer Tag",
|
||||
"tag.parent_tags": "Übergeordnete Tags",
|
||||
@@ -208,7 +266,19 @@
|
||||
"tag.search_for_tag": "Nach Tag suchen",
|
||||
"tag.shorthand": "Kürzel",
|
||||
"tag.tag_name_required": "Tag Name (Pflichtfeld)",
|
||||
"tag.view_limit": "Anzeige-Limit:",
|
||||
"tag_manager.title": "Bibliothek Tags",
|
||||
"trash.context.ambiguous": "Datei(en) nach {trash_term} verschieben",
|
||||
"trash.context.plural": "Dateien nach {trash_term} verschieben",
|
||||
"trash.context.singular": "Datei nach {trash_term} verschieben",
|
||||
"trash.dialog.disambiguation_warning.plural": "Dies wird sie aus TagStudio <i>UND</i> vom Dateisystem entfernen!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Dies wird sie aus TagStudio <i>UND</i> vom Dateisystem entfernen!",
|
||||
"trash.dialog.move.confirmation.plural": "Bist du sicher, dass du diese {count} Dateien in den {trash_term} verschieben möchtest?",
|
||||
"trash.dialog.move.confirmation.singular": "Bist du sicher, dass du diese Datei in den {trash_term} verschieben möchtest?",
|
||||
"trash.dialog.title.plural": "Dateien löschen",
|
||||
"trash.dialog.title.singular": "Datei löschen",
|
||||
"trash.name.generic": "Mülleimer",
|
||||
"trash.name.windows": "Papierkorb",
|
||||
"view.size.0": "Mini",
|
||||
"view.size.1": "Klein",
|
||||
"view.size.2": "Mittel",
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Library '{library_dir}'",
|
||||
"color_manager.title": "Manage Tag Colors",
|
||||
"color.color_border": "Use Secondary Color for Border",
|
||||
"color.confirm_delete": "Are you sure you want to delete the color \"{color_name}\"?",
|
||||
"color.delete": "Delete Tag",
|
||||
"color.import_pack": "Import Color Pack",
|
||||
"color.name": "Name",
|
||||
"color.namespace.delete.prompt": "Are you sure you want to delete this color namespace? This will delete ALL colors in the namespace along with it!",
|
||||
"color.namespace.delete.title": "Delete Color Namespace",
|
||||
"color.new": "New Color",
|
||||
"color.placeholder": "Color",
|
||||
"color.primary_required": "Primary Color (Required)",
|
||||
"color.primary": "Primary Color",
|
||||
"color.secondary": "Secondary Color",
|
||||
"color.title.no_color": "No Color",
|
||||
"drop_import.description": "The following files match file paths that already exist in the library",
|
||||
"drop_import.duplicates_choice.plural": "The following {count} files match file paths that already exist in the library.",
|
||||
@@ -11,6 +24,7 @@
|
||||
"drop_import.progress.label.singular": "Importing New Files...\n1 File imported.{suffix}",
|
||||
"drop_import.progress.window_title": "Import Files",
|
||||
"drop_import.title": "Conflicting File(s)",
|
||||
"edit.color_manager": "Manage Tag Colors",
|
||||
"edit.tag_manager": "Manage Tags",
|
||||
"entries.duplicate.merge.label": "Merging Duplicate Entries...",
|
||||
"entries.duplicate.merge": "Merge Duplicate Entries",
|
||||
@@ -96,6 +110,7 @@
|
||||
"generic.recent_libraries": "Recent Libraries",
|
||||
"generic.rename_alt": "&Rename",
|
||||
"generic.rename": "Rename",
|
||||
"generic.reset": "Reset",
|
||||
"generic.save": "Save",
|
||||
"generic.skip_alt": "&Skip",
|
||||
"generic.skip": "Skip",
|
||||
@@ -143,6 +158,10 @@
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Library</h2>",
|
||||
"json_migration.title": "Save Format Migration: \"{path}\"",
|
||||
"landing.open_create_library": "Open/Create Library {shortcut}",
|
||||
"library_object.name_required": "Name (Required)",
|
||||
"library_object.name": "Name",
|
||||
"library_object.slug_required": "ID Slug (Required)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library.field.add": "Add Field",
|
||||
"library.field.confirm_remove": "Are you sure you want to remove this \"{name}\" field?",
|
||||
"library.field.mixed_data": "Mixed Data",
|
||||
@@ -175,16 +194,23 @@
|
||||
"menu.file.save_backup": "&Save Library Backup",
|
||||
"menu.file.save_library": "Save Library",
|
||||
"menu.file": "&File",
|
||||
"menu.help": "&Help",
|
||||
"menu.help.about": "About",
|
||||
"menu.help": "&Help",
|
||||
"menu.macros.folders_to_tags": "Folders to Tags",
|
||||
"menu.macros": "&Macros",
|
||||
"menu.select": "Select",
|
||||
"menu.settings": "Settings...",
|
||||
"menu.tools.fix_duplicate_files": "Fix Duplicate &Files",
|
||||
"menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries",
|
||||
"menu.tools": "&Tools",
|
||||
"menu.view": "&View",
|
||||
"menu.window": "Window",
|
||||
"namespace.create.description_color": "Tag colors use namespaces as color palette groups. All custom colors must be under a namespace group first.",
|
||||
"namespace.create.description": "Namespaces are used by TagStudio to separate groups of items such as tags and colors in a way that makes them easy to export and share. Namespaces starting with \"tagstudio\" are reserved by TagStudio for internal use.",
|
||||
"namespace.create.title": "Create Namespace",
|
||||
"namespace.new.button": "New Namespace",
|
||||
"namespace.new.prompt": "Create a New Namespace to Start Adding Custom Colors!",
|
||||
"preview.multiple_selection": "<b>{count}</b> Items Selected",
|
||||
"preview.no_selection": "No Items Selected",
|
||||
"select.add_tag_to_selected": "Add Tag to Selected",
|
||||
"select.all": "Select All",
|
||||
@@ -192,9 +218,12 @@
|
||||
"edit.copy_fields": "Copy Fields",
|
||||
"edit.paste_fields": "Paste Fields",
|
||||
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
|
||||
"settings.language": "Language",
|
||||
"settings.open_library_on_start": "Open Library on Start",
|
||||
"settings.restart_required": "Please restart TagStudio for changes to take effect.",
|
||||
"settings.show_filenames_in_grid": "Show Filenames in Grid",
|
||||
"settings.show_recent_libraries": "Show Recent Libraries",
|
||||
"settings.title": "Settings",
|
||||
"sorting.direction.ascending": "Ascending",
|
||||
"sorting.direction.descending": "Descending",
|
||||
"splash.opening_library": "Opening Library \"{library_path}\"...",
|
||||
@@ -228,6 +257,7 @@
|
||||
"tag.create": "Create Tag",
|
||||
"tag.disambiguation.tooltip": "Use this tag for disambiguation",
|
||||
"tag.edit": "Edit Tag",
|
||||
"tag.is_category": "Is Category",
|
||||
"tag.name": "Name",
|
||||
"tag.new": "New Tag",
|
||||
"tag.parent_tags.add": "Add Parent Tag(s)",
|
||||
|
||||
@@ -2,14 +2,29 @@
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Previas al lanzamiento",
|
||||
"app.title": "{base_title} - Biblioteca '{library_dir}'",
|
||||
"drop_import.description": "Los siguientes archivos tienen nombres de archivo que ya existen en la biblioteca",
|
||||
"drop_import.duplicates_choice.plural": "Los siguientes {count} archivos tienen nombres de archivo que ya existen en la biblioteca.",
|
||||
"drop_import.duplicates_choice.singular": "El siguiente archivo tiene un nombre de archivo que ya existe en la biblioteca.",
|
||||
"color.color_border": "Usar color secundario para la cenefa",
|
||||
"color.confirm_delete": "¿Estás seguro de que quieres eliminar el color \"{color_name}\"?",
|
||||
"color.delete": "Eliminar la etiqueta",
|
||||
"color.import_pack": "Importar paquete de colores",
|
||||
"color.name": "Nombre",
|
||||
"color.namespace.delete.prompt": "¿Estás seguro de que quieres eliminar el espacio de nombres de este color? ¡Esto eliminará todos los colores en el espacio de nombres junto con él!",
|
||||
"color.namespace.delete.title": "Eliminar el espacio de nombres de color",
|
||||
"color.new": "Nuevo color",
|
||||
"color.placeholder": "Color",
|
||||
"color.primary": "Color primario",
|
||||
"color.primary_required": "Color primario (Obligatorio)",
|
||||
"color.secondary": "Color secundario",
|
||||
"color.title.no_color": "Sin color",
|
||||
"color_manager.title": "Administrar los colores de las etiquetas",
|
||||
"drop_import.description": "Los siguientes archivos igualan con las rutas de archivos que ya existen en la biblioteca",
|
||||
"drop_import.duplicates_choice.plural": "Los siguientes {count} archivos igualan con las rutas de archivos que ya existen en la biblioteca.",
|
||||
"drop_import.duplicates_choice.singular": "El siguiente archivo iguala con la ruta de archivo que ya existe en la biblioteca.",
|
||||
"drop_import.progress.label.initial": "Importando archivos nuevos...",
|
||||
"drop_import.progress.label.plural": "Importando archivos nuevos...\n{count} Archivos importado.{suffix}",
|
||||
"drop_import.progress.label.singular": "Importando archivos nuevos...\n1 Archivo importado.{suffix}",
|
||||
"drop_import.progress.window_title": "Importar archivos",
|
||||
"drop_import.title": "Conflictos de archivos",
|
||||
"edit.color_manager": "Administrar los colores de las etiquetas",
|
||||
"edit.tag_manager": "Administrar etiquetas",
|
||||
"entries.duplicate.merge": "Fusionar entradas duplicadas",
|
||||
"entries.duplicate.merge.label": "Fusionando entradas duplicadas...",
|
||||
@@ -20,6 +35,8 @@
|
||||
"entries.mirror.label": "Reflejando {idx}/{total} Entradas...",
|
||||
"entries.mirror.title": "Reflejando entradas",
|
||||
"entries.mirror.window_title": "Reflejar entradas",
|
||||
"entries.running.dialog.new_entries": "Añadiendo {total} nuevas entradas de archivos...",
|
||||
"entries.running.dialog.title": "Añadiendo las nuevas entradas de archivos",
|
||||
"entries.tags": "Etiquetas",
|
||||
"entries.unlinked.delete": "Eliminar entradas no vinculadas",
|
||||
"entries.unlinked.delete.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?",
|
||||
@@ -45,7 +62,7 @@
|
||||
"file.dimensions": "Dimensiones",
|
||||
"file.duplicates.description": "TagStudio es compatible con Importación de resultados de DupeGuru para administrar archivos duplicados.",
|
||||
"file.duplicates.dupeguru.advice": "Después de la duplicación, puede utilizar DupeGuru para eliminar los archivos no deseados. Luego, utilice la función \"Reparar entradas no vinculadas\" de TagStudio en el menú Herramientas para eliminar las entradas no vinculadas.",
|
||||
"file.duplicates.dupeguru.file_extension": "Archivos DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.file_extension": "Archivos de DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&Cargar archivo DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "No se ha seleccionado ningún archivo DupeGuru",
|
||||
"file.duplicates.dupeguru.open_file": "Abrir el archivo de resultados de DupeGuru",
|
||||
@@ -98,7 +115,7 @@
|
||||
"home.search_entries": "Buscar entradas",
|
||||
"home.search_library": "Buscar el biblioteca",
|
||||
"home.search_tags": "Buscar etiquetas",
|
||||
"home.thumbnail_size": "Tamaño de las imágenes",
|
||||
"home.thumbnail_size": "Tamaño de la vista previa",
|
||||
"home.thumbnail_size.extra_large": "Imágenes extra grandes",
|
||||
"home.thumbnail_size.large": "Imágenes grandes",
|
||||
"home.thumbnail_size.medium": "Imágenes medianas",
|
||||
@@ -111,6 +128,20 @@
|
||||
"ignore_list.title": "Extensiones del archivo",
|
||||
"json_migration.checking_for_parity": "Revisando paridad...",
|
||||
"json_migration.creating_database_tables": "Creando tablas en la base de datos de SQL...",
|
||||
"json_migration.heading.fields": "Campos:",
|
||||
"json_migration.heading.file_extension_list": "Lista de extensiones de archivos:",
|
||||
"json_migration.heading.match": "Igualado",
|
||||
"json_migration.heading.names": "Nombres:",
|
||||
"json_migration.heading.parent_tags": "Etiquetas principales:",
|
||||
"json_migration.heading.paths": "Rutas:",
|
||||
"json_migration.heading.shorthands": "Abreviaturas:",
|
||||
"json_migration.heading.tags": "Etiquetas:",
|
||||
"json_migration.migrating_files_entries": "Migrando {entries;.d} entradas de archivos...",
|
||||
"json_migration.migration_complete": "¡La migración es terminado!",
|
||||
"json_migration.migration_complete_with_discrepancies": "La migración es terminado, discrepancias descubierto",
|
||||
"json_migration.start_and_preview": "Comenzar y preestrenar",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ biblioteca</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 biblioteca</h2>",
|
||||
"library.field.add": "Añadir campo",
|
||||
"library.field.confirm_remove": "¿Está seguro de que desea eliminar este campo \"%{self.lib.get_field_attr(field, \"name\")}\"?",
|
||||
"library.field.mixed_data": "Datos variados",
|
||||
|
||||
@@ -1,29 +1,104 @@
|
||||
{
|
||||
"entries.duplicate.merge.label": "Sinasama ang mga Duplicate na Entry",
|
||||
"entries.mirror": "Salamin",
|
||||
"about.content": "<h2>TagStudio Alpha {version} {{branch}}</h2><p>Ang TagStudio ay isang application ng pagsasaayos ng file at larawan na may pinagbabatayan na tag-based na sistema na nakatutok sa pagbibigay ng kalayaan at kakayahang umangkop sa user. Walang mga proprietary na format o program, walang dagat ng mga sidecar file, at walang kaguluhan ng iyong estruktura ng filesystem.</p>Lisensya: GPLv3<br>Path ng config: {config_path}<br>FFMpeg: {ffmpeg}<br>FFProbe: {ffprobe}<p><a href=\"https://github.com/TagStudioDev/TagStudio\">GitHub</a> | <a href=\"https://docs.tagstud.io\">Dokumentasyon</a> | <a href=\"https://discord.gg/invite/hRNnVKhF2G\">Discord</a></p>",
|
||||
"about.title": "Tungkol sa",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Library '{library_dir}'",
|
||||
"color.color_border": "Gumamit ng Pangalawang Kulay para sa Border",
|
||||
"color.confirm_delete": "Sigurado ka ba gusto mong burahin ang kulay na \"{color_name}\"?",
|
||||
"color.delete": "Burahin ang Tag",
|
||||
"color.import_pack": "Mag-import ng Color Pack",
|
||||
"color.name": "Pangalan",
|
||||
"color.namespace.delete.prompt": "Sigurado ka ba gusto mong burahin ang color namespace na ito? Buburahin nito ang LAHAT ng mga kulay sa namespace kasama nito!",
|
||||
"color.namespace.delete.title": "Burahin ang Color Namespace",
|
||||
"color.new": "Bagong Kulay",
|
||||
"color.placeholder": "Kulay",
|
||||
"color.primary": "Pangunahing Kulay",
|
||||
"color.primary_required": "Pangunahing Kulay (Kinakailangan)",
|
||||
"color.secondary": "Pangalawang Kulay",
|
||||
"color.title.no_color": "Walang Kulay",
|
||||
"color_manager.title": "Ipamahala ang Mga Kulay ng Tag",
|
||||
"drop_import.description": "Ang mga sumusunod na file ay tumutugma sa mga file path na umiiral na sa library",
|
||||
"drop_import.duplicates_choice.plural": "Ang sumusunod na {count} mga file ay tumutugma sa mga file path na umiiral sa library.",
|
||||
"drop_import.duplicates_choice.singular": "Ang sumusunod na file ay tumutugma sa file path na umiiral na sa library.",
|
||||
"drop_import.progress.label.initial": "Ini-import Ang Mga Bagong File…",
|
||||
"drop_import.progress.label.plural": "Ini-import Ang Mga Bagong File…\nNa-import ang {count} Mga File.{suffix}",
|
||||
"drop_import.progress.label.singular": "Ini-import Ang Mga Bagong File…\nNa-import ang 1 File.{suffix}",
|
||||
"drop_import.progress.window_title": "I-import ang Mga File",
|
||||
"drop_import.title": "(Mga) Sumasalungat na File",
|
||||
"edit.color_manager": "Ipamahala ang Mga Kulay ng Tag",
|
||||
"edit.tag_manager": "Ipamahala ang Mga Tag",
|
||||
"entries.duplicate.merge": "Isama ang Mga Duplicate na Entry",
|
||||
"entries.duplicate.merge.label": "Sinasama ang mga Duplicate na Entry…",
|
||||
"entries.duplicate.refresh": "I-refresh ang Mga Duplicate na Entry",
|
||||
"entries.duplicates.description": "Ang mga duplicate na entry ay tinukoy bilang maramihang mga entry na tumuturo sa parehong file sa disk. Ang pagsasama-sama ng mga ito ay pagsasama-samahin ang mga tag at metadata mula sa lahat ng mga duplicate sa isang solong pinagsama-samang entry. Ang mga ito ay hindi dapat ipagkamali sa \"mga duplicate na file\", na mga duplicate ng iyong mga file mismo sa labas ng TagStudio.",
|
||||
"entries.mirror": "&Mirror",
|
||||
"entries.mirror.confirmation": "Sigurado ka ba gusto mong i-mirror ang sumusunod na {count} Mga Entry?",
|
||||
"entries.mirror.label": "Mini-mirror ang {idx}/{total} Mga Entry…",
|
||||
"entries.mirror.title": "Mini-mirror ang Mga Entry",
|
||||
"entries.mirror.window_title": "I-mirror ang Mga Entry",
|
||||
"entries.running.dialog.new_entries": "Dinadagdag ang {total} Mga Bagong Entry ng File…",
|
||||
"entries.running.dialog.title": "Dinadagdag ang Mga Bagong Entry ng File",
|
||||
"entries.tags": "Mga Tag",
|
||||
"entries.unlinked.delete": "Burahin ang Mga Hindi Naka-link na Entry",
|
||||
"entries.unlinked.delete.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na %{len(self.lib.missing_files)} entry?",
|
||||
"entries.unlinked.delete.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?",
|
||||
"entries.unlinked.delete.deleting": "Binubura ang Mga Entry",
|
||||
"entries.unlinked.delete.deleting_count": "Binubura ang %{x[0]+1}/{len(self.lib.missing_files)} (mga) Naka-unlink na Entry",
|
||||
"entries.unlinked.refresh_all": "I-refresh Lahat",
|
||||
"entries.unlinked.delete.deleting_count": "Binubura ang {idx}/{count} (mga) Naka-unlink na Entry",
|
||||
"entries.unlinked.delete_alt": "Burahin ang Mga Naka-unlink na Entry",
|
||||
"entries.unlinked.description": "Ang bawat entry sa library ay naka-link sa isang file sa isa sa iyong mga direktoryo. Kung ang isang file na naka-link sa isang entry ay inilipat o binura sa labas ng TagStudio, ito ay isinasaalang-alang na naka-unlink.<br><br>Ang mga naka-unlink na entry ay maaring i-link muli sa pamamagitan ng paghahanap sa iyong mga direktoryo o buburahin kung ninanais.",
|
||||
"entries.unlinked.missing_count.none": "Mga Naka-unlink na Entry: N/A",
|
||||
"entries.unlinked.missing_count.some": "Mga Naka-unlink na Entry: {count}",
|
||||
"entries.unlinked.refresh_all": "&I-refresh Lahat",
|
||||
"entries.unlinked.relink.attempting": "Sinusubukang i-link muli ang {idx}/{missing_count} Mga Entry, {fixed_count} Matagumpay na na-link muli",
|
||||
"entries.unlinked.relink.manual": "&Manwal na Pag-link Muli",
|
||||
"entries.unlinked.relink.title": "Nili-link muli ang Mga Entry",
|
||||
"entries.unlinked.scanning": "Sina-scan ang Library para sa Mga Naka-unlink na Entry…",
|
||||
"entries.unlinked.search_and_relink": "&Maghanap at Mag-link muli",
|
||||
"entries.unlinked.title": "Ayusin ang Mga Naka-unlink na Entry",
|
||||
"field.copy": "Kopyahin ang Field",
|
||||
"field.edit": "I-edit ang Field",
|
||||
"field.paste": "I-paste ang Field",
|
||||
"file.date_added": "Petsang Dinagdag",
|
||||
"file.date_created": "Petsa na Ginawa",
|
||||
"file.date_modified": "Binago Noong",
|
||||
"file.dimensions": "Laki",
|
||||
"file.duplicates.dupeguru.load_file": "Mag-load ng DupeGuru File",
|
||||
"file.duplicates.description": "Sinusuportahan ng TagStudio ang pag-import ng mga DupeGuru na resulta para ipamahala ang mga duplicate na file.",
|
||||
"file.duplicates.dupeguru.advice": "Pagkatapos ng pag-mirror, malaya kang gamitin ang DupeGuru para magbura ng mga hindi gustong file. Pagkatapos, gamitin ang \"Ayusin ang Mga Naka-unlink na Entry\" feature ng TagStudio sa Tools menu para burahin ang mga hindi naka-link na Entry.",
|
||||
"file.duplicates.dupeguru.file_extension": "Mga DupeGuru File (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&Mag-load ng DupeGuru File",
|
||||
"file.duplicates.dupeguru.no_file": "Walang DupeGuru na File na Napili",
|
||||
"file.duplicates.dupeguru.open_file": "Buksan ang DupeGuru Results File",
|
||||
"file.duplicates.fix": "Ayusin ang Mga Duplicate na File",
|
||||
"file.duplicates.matches": "Mga Tumutugmang Duplicate File: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Mga Tumutugmang Duplicate File: N/A",
|
||||
"file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.",
|
||||
"file.duplicates.mirror_entries": "Mga Entry ng Salamin",
|
||||
"file.duplicates.mirror_entries": "&I-mirror ang mga Entry",
|
||||
"file.duration": "Haba",
|
||||
"file.not_found": "Hindi nahanap ang file:",
|
||||
"file.open_file": "Buksan ang file",
|
||||
"file.open_location.generic": "Buksan ang file sa explorer",
|
||||
"file.open_file_with": "Buksan ang file gamit ang",
|
||||
"file.open_location.generic": "Ipakita ang file sa file explorer",
|
||||
"file.open_location.mac": "Ipakita sa Finder",
|
||||
"file.open_location.windows": "Ipakita sa File Explorer",
|
||||
"folders_to_tags.close_all": "Isara Lahat",
|
||||
"folders_to_tags.converting": "Kino-convert ang mga folder sa Tag",
|
||||
"folders_to_tags.description": "Gumawa ng mga tag base sa iyong estruktura ng folder at ia-apply sa iyong mga entry.\nAng istraktura sa ibaba ay pinapakita ang lahat ng mga tag na gagawin at anong mga entry ang ia-apply sa.",
|
||||
"folders_to_tags.open_all": "Buksan Lahat",
|
||||
"folders_to_tags.title": "Gumawa ng Mga Tag Mula Sa Mga Folder",
|
||||
"generic.add": "Magdagdag",
|
||||
"generic.apply": "I-apply",
|
||||
"generic.apply_alt": "&I-apply",
|
||||
"generic.cancel": "Kanselahin",
|
||||
"generic.cancel_alt": "&Kanselahin",
|
||||
"generic.close": "Isara",
|
||||
"generic.continue": "Magpatuloy",
|
||||
"generic.copy": "Kopyahin",
|
||||
"generic.cut": "I-cut",
|
||||
"generic.delete": "Burahin",
|
||||
"generic.delete_alt": "&Burahin",
|
||||
"generic.done": "Tapos na",
|
||||
"generic.done_alt": "&Tapos na",
|
||||
"generic.edit_alt": "&I-edit",
|
||||
"generic.recent_libraries": "Mga Kamakailang Library",
|
||||
"home.search": "Maghanap",
|
||||
"home.search_entries": "Mga Entry sa Paghahanap",
|
||||
|
||||
@@ -3,19 +3,35 @@
|
||||
"about.title": "À propos",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Version Préliminaire",
|
||||
"app.title": "ase_title} - Bibliothèque '{library_dir}'",
|
||||
"app.title": "{base_title} - Bibliothèque '{library_dir}'",
|
||||
"color.color_border": "Utiliser la couleur secondaire sur la bordure",
|
||||
"color.confirm_delete": "Voulez vous vraiment supprimer la couleur \"{color_name}\"?",
|
||||
"color.delete": "Supprimer le Tag",
|
||||
"color.import_pack": "Importer un Pack de Couleur",
|
||||
"color.name": "Nom",
|
||||
"color.namespace.delete.prompt": "Voulez vous vraiment supprimer ce namespace de couleur? Cela supprimera TOUTES les couleur membre du namespace!",
|
||||
"color.namespace.delete.title": "Supprimer le namespace de couleur",
|
||||
"color.new": "Nouvelle couleur",
|
||||
"color.placeholder": "Couleur",
|
||||
"color.primary": "Couleur Primaire",
|
||||
"color.primary_required": "Couleur Primaire (Requis)",
|
||||
"color.secondary": "Couleur Secondaire",
|
||||
"color.title.no_color": "Aucune couleur",
|
||||
"color_manager.title": "Gérer la Couleur des Tags",
|
||||
"drop_import.description": "Les fichiers suivants correspondent à des chemins de fichiers déjà existant dans la bibliothèque",
|
||||
"drop_import.duplicates_choice.plural": "Les noms des {count} fichiers suivants existent déjà dans la Bibliothèque.",
|
||||
"drop_import.duplicates_choice.singular": "Le fichier suivant a un nom déjà existant dans la bibliothèque.",
|
||||
"drop_import.progress.label.initial": "Import des Nouveaux Fichiers...",
|
||||
"drop_import.progress.label.plural": "Import des Nouveaux Fichiers...\n{count} Fichiers Importés.{suffix}",
|
||||
"drop_import.progress.label.singular": "Import des Nouveaux Fichiers...\n1 Fichier Importé.{suffix}",
|
||||
"drop_import.duplicates_choice.plural": "Les chemins d'accès des {count} fichiers suivants existent déjà dans la Bibliothèque.",
|
||||
"drop_import.duplicates_choice.singular": "Le fichier suivant correspond a un chemin d'accès déjà existant dans la bibliothèque.",
|
||||
"drop_import.progress.label.initial": "Importer des Nouveaux Fichiers...",
|
||||
"drop_import.progress.label.plural": "Importation des Nouveaux Fichiers...\n{count} Fichiers Importés.{suffix}",
|
||||
"drop_import.progress.label.singular": "Importation des Nouveaux Fichiers...\n1 Fichier Importé.{suffix}",
|
||||
"drop_import.progress.window_title": "Importer des Fichiers",
|
||||
"drop_import.title": "Fichier(s) en Conflit",
|
||||
"edit.color_manager": "Gérer la Couleur des Tags",
|
||||
"edit.copy_fields": "Copier les Fields",
|
||||
"edit.paste_fields": "Coller les Fields",
|
||||
"edit.tag_manager": "Gérer les Tags",
|
||||
"entries.duplicate.merge": "Fusion des Duplicatas",
|
||||
"entries.duplicate.merge.label": "Fusionner les duplicatas...",
|
||||
"entries.duplicate.merge": "Fusion des entrées dupliquées",
|
||||
"entries.duplicate.merge.label": "Fusionner les entrées dupliquées...",
|
||||
"entries.duplicate.refresh": "Rafraichir les Entrées en Doublon",
|
||||
"entries.duplicates.description": "Les entrées dupliquées sont définies comme des entrées multiple qui pointent vers le même fichier sur le disque. Les fusionner va combiner les labels et metadatas de tous les duplicatas vers une seule entrée consolidée. Elles ne doivent pas être confondues avec les \"fichiers en doublon\", qui sont des doublons de vos fichiers en dehors de TagStudio.",
|
||||
"entries.mirror": "&Refléter",
|
||||
@@ -25,7 +41,7 @@
|
||||
"entries.mirror.window_title": "Entrée Miroir",
|
||||
"entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...",
|
||||
"entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier",
|
||||
"entries.tags": "Labels",
|
||||
"entries.tags": "Tags",
|
||||
"entries.unlinked.delete": "Supprimer les Entrées non Liées",
|
||||
"entries.unlinked.delete.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?",
|
||||
"entries.unlinked.delete.deleting": "Suppression des Entrées",
|
||||
@@ -51,14 +67,14 @@
|
||||
"file.duplicates.description": "TagStudio supporte l'importation de résultats DupeGuru pour gérer les doublons de fichier.",
|
||||
"file.duplicates.dupeguru.advice": "Après réplication, vous êtes libre d'utiliser DupeGuru pour supprimer des fichiers non désirés. Ensuite, utilisez la fonctionnalité \"Réparation des Entrées non Liées\" de TagStudio dans le menu Outils pour supprimer les Entrées non liées.",
|
||||
"file.duplicates.dupeguru.file_extension": "Fichiers DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "Charger un Fichier DupeGuru",
|
||||
"file.duplicates.dupeguru.load_file": "&Charger un Fichier DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "Aucun Fichier DupeGuru Sélectionné",
|
||||
"file.duplicates.dupeguru.open_file": "Ouvrire les Fichiers de Résultats de DupeGuru",
|
||||
"file.duplicates.fix": "Réparer les Fichiers en Double",
|
||||
"file.duplicates.matches": "Dupliquer les Correspondances de Fichier : %{count}",
|
||||
"file.duplicates.matches": "Dupliquer les Correspondances de Fichier: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Dupliquer les Correspondances de Fichier : N/A",
|
||||
"file.duplicates.mirror.description": "Repliquer les données d'entrée dans chaque jeu de correspondances en double, en combinant toutes les données sans supprimer ni dupliquer de champs. Cette opération ne supprime aucun fichier ni aucune donnée.",
|
||||
"file.duplicates.mirror_entries": "Répliquer les Entrées",
|
||||
"file.duplicates.mirror_entries": "&Répliquer les Entrées",
|
||||
"file.duration": "Durée",
|
||||
"file.not_found": "Fichier non trouvé",
|
||||
"file.open_file": "Ouvrir un Fichier",
|
||||
@@ -96,6 +112,7 @@
|
||||
"generic.recent_libraries": "Bibliothèques Récentes",
|
||||
"generic.rename": "Renommer",
|
||||
"generic.rename_alt": "&Renommer",
|
||||
"generic.reset": "Réinitialiser",
|
||||
"generic.save": "Sauvegarder",
|
||||
"generic.skip": "Passer",
|
||||
"generic.skip_alt": "&Passer",
|
||||
@@ -110,13 +127,14 @@
|
||||
"home.thumbnail_size.medium": "Miniatures Moyennes",
|
||||
"home.thumbnail_size.mini": "Mini Miniatures",
|
||||
"home.thumbnail_size.small": "Petites Miniatures",
|
||||
"ignore_list.add_extension": "Ajouter une Extension",
|
||||
"ignore_list.add_extension": "&Ajouter une Extension",
|
||||
"ignore_list.mode.exclude": "Exclure",
|
||||
"ignore_list.mode.include": "Inclure",
|
||||
"ignore_list.mode.label": "Mode Liste",
|
||||
"ignore_list.mode.label": "Mode de la liste:",
|
||||
"ignore_list.title": "Extensions de Fichiers",
|
||||
"json_migration.checking_for_parity": "Vérification de la Parité...",
|
||||
"json_migration.creating_database_tables": "Création des Tables de Base de Données SQL...",
|
||||
"json_migration.description": "<br>Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie <i>ne</i> sera utilisée que si vous cliquez sur \"Terminer la migration\". <br><br>Les données de la bibliothèque doivent soit avoir des valeurs correspondantes, soit comporter un label \"Matched\". Les valeurs qui ne correspondent pas seront affichées en rouge et comporteront un symbole \"<b>(!)</b>\" à côté d'elles.<br><center><i>Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.</i></center>",
|
||||
"json_migration.discrepancies_found": "Divergence Détectées dans la Bibliothèque",
|
||||
"json_migration.discrepancies_found.description": "Des divergences ont été détectées entre le format d'origine et le format converti de la bibliothèque. Veuillez les examiner et choisir de poursuivre la migration ou de l'annuler.",
|
||||
"json_migration.finish_migration": "Terminer la Migration",
|
||||
@@ -124,67 +142,150 @@
|
||||
"json_migration.heading.colors": "Couleurs:",
|
||||
"json_migration.heading.differ": "Divergence",
|
||||
"json_migration.heading.entires": "Entrées:",
|
||||
"json_migration.heading.extension_list_type": "Type de liste d'extension:",
|
||||
"json_migration.heading.fields": "Champs:",
|
||||
"json_migration.heading.file_extension_list": "Liste des extensions de fichiers:",
|
||||
"json_migration.heading.match": "Correspondant",
|
||||
"json_migration.heading.names": "Noms:",
|
||||
"json_migration.heading.parent_tags": "Tags Parents:",
|
||||
"json_migration.heading.paths": "Chemins:",
|
||||
"json_migration.heading.shorthands": "Abréviations:",
|
||||
"json_migration.heading.tags": "Tags:",
|
||||
"json_migration.info.description": "Les fichiers de sauvegarde de bibliothèque créés avec les versions <b>9.4 et inférieures</b> de TagStudio devront être migrés vers le nouveau format <b>v9.5+</b>.<br><h2>Ce que vous devez savoir:</h2><ul><li>Votre fichier de sauvegarde de bibliothèque existant ne sera <b><i>PAS</i></b> supprimé</li><li>Vos fichiers personnels ne seront <b><i>PAS</i></b> supprimés, déplacés ou modifié</li><li>Le nouveau format de sauvegarde v9.5+ ne peut pas être ouvert dans les versions antérieures de TagStudio</li></ul><h3>Ce qui a changé:</h3><ul><li>Les \"Tag Fields\" ont été remplacés par \"Tag Categories\". Au lieu d’ajouter d’abord des Tags aux fields, les Tags sont désormais ajoutées directement aux entrées de fichier. Ils sont ensuite automatiquement organisés en catégories basées sur les tags parent marquées avec la nouvelle propriété \"Is Category\" dans le menu d'édition des tags. N'importe quelle tag peut être marquée comme catégorie, et les tags enfants seront triées sous les tags parents marquées comme catégories. Les tags « Favoris » et « Archivés » héritent désormais d'un nouveaux tag \"Meta Tags\" qui est marquée comme catégorie par défaut.</li><li>La couleur des tags à été modifiées et développées. Certaines couleurs ont été renommées ou consolidées, mais toutes les couleurs des tags seront toujours converties en correspondances exactes ou proches dans la version 9.5.</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "Migration de {entries:,d} entrées de fichier.",
|
||||
"json_migration.migration_complete": "Migration Terminée!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Migration Terminée, Divergences Trouvées",
|
||||
"json_migration.start_and_preview": "Commencer et Prévisualiser",
|
||||
"json_migration.title": "Migration du format d'enregistrement: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Library</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Library</h2>",
|
||||
"landing.open_create_library": "Ouvrir/Créer une Bibliothèque {shortcut}",
|
||||
"library.field.add": "Ajouter un Champ",
|
||||
"library.field.confirm_remove": "Êtes-vous sûr de vouloir supprimer le champ \"%{self.lib.get_field_attr(field, \"name\")}\" ?",
|
||||
"library.field.confirm_remove": "Êtes-vous sûr de vouloir supprimer le champ \"{name}\"?",
|
||||
"library.field.mixed_data": "Données Mélangées",
|
||||
"library.field.remove": "Supprimer un Champ",
|
||||
"library.missing": "Emplacement Manquant",
|
||||
"library.name": "Bibliothèque",
|
||||
"library.refresh.scanning.plural": "Analyse du Répertoire pour de Nouveaux Fichiers...\n{searched_count} Fichiers Trouvées, {found_count} Nouveaux Fichiers",
|
||||
"library.refresh.scanning.singular": "Analyse du Répertoire pour de Nouveaux Fichiers...\n{searched_count} Fichier Trouvé, {found_count} Nouveaux Fichiers",
|
||||
"library.refresh.scanning_preparing": "Recherche de Nouveaux Fichiers dans les Dossiers...\nPréparation...",
|
||||
"library.refresh.title": "Rafraîchissement des Dossiers",
|
||||
"library.scan_library.title": "Balayage de la Bibliothèque",
|
||||
"macros.running.dialog.new_entries": "Éxectution des Macros Configurées sur %{x + 1}/%{len(new_ids)} Nouvelles Entrées",
|
||||
"library_object.name": "Nom",
|
||||
"library_object.name_required": "Nom (Requis)",
|
||||
"library_object.slug": "Nom d'affichage",
|
||||
"library_object.slug_required": "ID Slug (Requis)",
|
||||
"macros.running.dialog.new_entries": "Exécution des Macros Configurées sur {count}/{total} Nouvelles Entrées de Fichiers...",
|
||||
"macros.running.dialog.title": "Exécution des Macros sur les Nouvelles Entrées",
|
||||
"media_player.autoplay": "Lecture automatique",
|
||||
"menu.delete_selected_files_ambiguous": "Déplacer les Fichier(s) vers {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Déplacer les Fichiers vers {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Déplacer le Fichier vers {trash_term}",
|
||||
"menu.edit": "Édition",
|
||||
"menu.edit.ignore_list": "Ignorer les Fichiers et Dossiers",
|
||||
"menu.file": "Fichier",
|
||||
"menu.edit.manage_file_extensions": "Gérer les extensions de fichier",
|
||||
"menu.edit.manage_tags": "Gérer les Tags",
|
||||
"menu.edit.new_tag": "Nouveaux &Tag",
|
||||
"menu.file": "&Fichier",
|
||||
"menu.file.clear_recent_libraries": "Supprimer l'historique de Bibliothèque récente",
|
||||
"menu.file.close_library": "&Fermer la Bibliothèque",
|
||||
"menu.file.new_library": "Nouvelle Bibliothéque",
|
||||
"menu.file.open_create_library": "Ouvrir/Créer une Bibliothèque",
|
||||
"menu.file.open_create_library": "&Ouvrir/Créer une Bibliothèque",
|
||||
"menu.file.open_library": "Ouvrir la Bibliothèque",
|
||||
"menu.file.open_recent_library": "Ouvrir la Bibliothèque récente",
|
||||
"menu.file.refresh_directories": "&Rafraichir les Répertoires",
|
||||
"menu.file.save_backup": "&Sauvegarde de la Bibliothèque",
|
||||
"menu.file.save_library": "Enregistrer la Bibliothèque",
|
||||
"menu.help": "Aide",
|
||||
"menu.macros": "Macros",
|
||||
"menu.help": "&Aide",
|
||||
"menu.help.about": "À propos",
|
||||
"menu.macros": "&Macros",
|
||||
"menu.macros.folders_to_tags": "Répertoires à Tags",
|
||||
"menu.select": "Sélectionner",
|
||||
"menu.tools": "Outils",
|
||||
"menu.view": "Vues",
|
||||
"menu.settings": "Paramètres...",
|
||||
"menu.tools": "&Outils",
|
||||
"menu.tools.fix_duplicate_files": "Réparer les entrées de fichiers en double",
|
||||
"menu.tools.fix_unlinked_entries": "Réparer les entrées de fichier non liée",
|
||||
"menu.view": "&Vues",
|
||||
"menu.window": "Fenêtre",
|
||||
"namespace.create.description": "Les namespaces sont utilisés par TagStudio pour séparer les groupes d'éléments tels que les Tags et les couleurs de manière à faciliter leur exportation et leur partage. Les namespace avec des noms commençant par « tagstudio » sont réservés par TagStudio pour un usage interne.",
|
||||
"namespace.create.description_color": "Les tags utilise les namespace pour regrouper plusieurs couleurs. Toutes les couleurs personnalisées doivent être ajoutées à un namespace.",
|
||||
"namespace.create.title": "Créer une Namespace",
|
||||
"namespace.new.button": "Nouvelle Namespace",
|
||||
"namespace.new.prompt": "Commencer par créer une nouvelle namespace pour pouvoir créer des couleurs personnalisées!",
|
||||
"preview.multiple_selection": "<b>{count}</b> Éléments Sélectionner",
|
||||
"preview.no_selection": "Pas d'Objet Selectionné",
|
||||
"select.add_tag_to_selected": "Ajouter un Tag à la sélection",
|
||||
"select.all": "Tout Sélectionner",
|
||||
"select.clear": "Effacer la Sélection",
|
||||
"settings.clear_thumb_cache.title": "Effacer le cache des vignettes",
|
||||
"settings.language": "Langage",
|
||||
"settings.open_library_on_start": "Ouvrir la Bibliothèque au Démarrage",
|
||||
"settings.restart_required": "Veuillez redémarré TagStudio pour que les changements prenne effet.",
|
||||
"settings.show_filenames_in_grid": "Afficher les Noms de Fichiers en Grille",
|
||||
"settings.show_recent_libraries": "Afficher les Bibliothèques Récentes",
|
||||
"settings.title": "Paramètres",
|
||||
"sorting.direction.ascending": "Croissant",
|
||||
"sorting.direction.descending": "Décroissant",
|
||||
"splash.opening_library": "Ouverture de la Bibliothèque",
|
||||
"status.library_backup_success": "Bibliothèque sauvegardée au chemin :",
|
||||
"status.library_save_success": "Bibliothèque Sauvegardée et Fermée !",
|
||||
"status.library_search_query": "Recherche dans la Bibliothèque pour",
|
||||
"splash.opening_library": "Ouverture de la Bibliothèque \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "Suppression de {count} fichiers!",
|
||||
"status.deleted_file_singular": "Suppression de 1 fichier!",
|
||||
"status.deleted_none": "Aucun fichiers supprimer.",
|
||||
"status.deleted_partial_warning": "Seulement {count} fichier(s) on été supprimé! Vérifier si les fichiers restant ne serais pas manquant ou en cours d'utilisation.",
|
||||
"status.deleting_file": "Suppression de fichier(s) [{i}/{count}]: \"{path}\"...",
|
||||
"status.library_backup_in_progress": "Création d'une Sauvegarde de la Bibliothèque...",
|
||||
"status.library_backup_success": "Bibliothèque sauvegardée au chemin: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "Bibliothèque fermée ({time_span})",
|
||||
"status.library_closing": "Fermeture de la Bibliothèque...",
|
||||
"status.library_save_success": "Bibliothèque Sauvegardée et Fermée!",
|
||||
"status.library_search_query": "Rechercher dans la Bibliothèque...",
|
||||
"status.library_version_expected": "Exceptée:",
|
||||
"status.library_version_found": "Trouvée:",
|
||||
"status.library_version_mismatch": "La version de la library ne correspond pas!",
|
||||
"status.results": "Résultats",
|
||||
"status.results_found": "{results.total_count} Résultats Trouvés",
|
||||
"tag.add": "Ajouter un Label",
|
||||
"status.results.invalid_syntax": "Syntaxe de recherche invalide:",
|
||||
"status.results_found": "{count} Résultats Trouvés({time_span})",
|
||||
"tag.add": "Ajouter un Tag",
|
||||
"tag.add.plural": "Ajouter des Tags",
|
||||
"tag.add_to_search": "Ajouter à la Recherche",
|
||||
"tag.aliases": "Alias",
|
||||
"tag.all_tags": "Tout les Tags",
|
||||
"tag.choose_color": "Choisir une couleur de Tag",
|
||||
"tag.color": "Couleur",
|
||||
"tag.create_add": "Créer && Adjouter \"{query}\"",
|
||||
"tag.confirm_delete": "Voulez vous vraiment supprimer le tag \"{tag_name}\"?",
|
||||
"tag.create": "Créer un Tag",
|
||||
"tag.create_add": "Créer && Ajouter \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Utilisez ce Tag pour définir une ambiguïté",
|
||||
"tag.edit": "Modifier un Tag",
|
||||
"tag.is_category": "Est une Catégorie",
|
||||
"tag.name": "Nom",
|
||||
"tag.new": "Nouveau Label",
|
||||
"tag.parent_tags": "Labels Parent",
|
||||
"tag.parent_tags.add": "Ajouter des Labels Parents",
|
||||
"tag.new": "Nouveau Tag",
|
||||
"tag.parent_tags": "Tags Parent",
|
||||
"tag.parent_tags.add": "Ajouter des Tags Parents",
|
||||
"tag.parent_tags.description": "Ce Tag peut être utilisé en replacement de tous ces Tags Parents dans les recherches.",
|
||||
"tag.remove": "Supprimer un Tag",
|
||||
"tag.search_for_tag": "Recherche de Label",
|
||||
"tag.shorthand": "Abrégé",
|
||||
"tag.tag_name_required": "Nom du Tag (Requis)",
|
||||
"tag.view_limit": "Limite d'affichage:",
|
||||
"tag_manager.title": "Labels de la Bibliothèque",
|
||||
"trash.context.ambiguous": "Déplacer les fichier(s) vers {trash_term}",
|
||||
"trash.context.plural": "Déplacer les fichiers vers {trash_term}",
|
||||
"trash.context.singular": "Déplacer le fichier vers {trash_term}",
|
||||
"trash.dialog.disambiguation_warning.plural": "Cela les retirera de TagStudio <i>ET</i> de votre système de fichier!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Cela le retirera de TagStudio <i>ET</i> de votre système de fichier!",
|
||||
"trash.dialog.move.confirmation.plural": "Voulez vous vraiment déplacer {count} fichiers vers la {trash_term}?",
|
||||
"trash.dialog.move.confirmation.singular": "Voulez vous vraiment déplacer ce fichier vers la {trash_term}?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>ATTENTION!</b> Si le fichier ne peut pas être déplacer vers la {trash_term}, <b>elle sera<b>supprimer de manière permanente!</b>",
|
||||
"trash.dialog.title.plural": "Supprimer les Fichiers",
|
||||
"trash.dialog.title.singular": "Supprimer le Fichier",
|
||||
"trash.name.generic": "Poubelle",
|
||||
"trash.name.windows": "Corbeille",
|
||||
"view.size.0": "Mini",
|
||||
"view.size.1": "Petit",
|
||||
"view.size.2": "Moyen",
|
||||
"view.size.3": "Grand",
|
||||
"view.size.4": "Très Grand"
|
||||
"view.size.4": "Très Grand",
|
||||
"window.message.error_opening_library": "Une erreur est survenue lors de l'ouverture de la bibliothèque.",
|
||||
"window.title.error": "Erreur",
|
||||
"window.title.open_create_library": "Ouvrir/Créer une Bibliothèque"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,20 @@
|
||||
"app.git": "Git-véglegesítés",
|
||||
"app.pre_release": "Kísérleti verzió",
|
||||
"app.title": "{base_title} – Könyvtár: „{library_dir}”",
|
||||
"color.color_border": "Másodlagos szín használata keretszínként",
|
||||
"color.confirm_delete": "Biztosan törölni akarja a(z) „{color_name}”-színt?",
|
||||
"color.delete": "Címke törlése",
|
||||
"color.import_pack": "Színcsomag importálása",
|
||||
"color.name": "Megnevezés",
|
||||
"color.namespace.delete.prompt": "Biztosan törölni akarja ezt a színnévteret? Ezzel a névtér ÖSSZES színét törölni fogja!",
|
||||
"color.namespace.delete.title": "Színnévtér törlése",
|
||||
"color.new": "Új szín",
|
||||
"color.placeholder": "Szín",
|
||||
"color.primary": "Elsődleges szín",
|
||||
"color.primary_required": "Elsődleges szín (kötelező)",
|
||||
"color.secondary": "Másodlagos szín",
|
||||
"color.title.no_color": "Színtelen",
|
||||
"color_manager.title": "Színek kezelése",
|
||||
"drop_import.description": "Az alábbi fájlok elérési útvonala már foglaltak a könyvtárban",
|
||||
"drop_import.duplicates_choice.plural": "Az alábbi {count} fájl elérési útvonala már szerepel a könyvtárban.",
|
||||
"drop_import.duplicates_choice.singular": "Az alábbi fájl elérési útvonala már szerepel a könyvtárban.",
|
||||
@@ -13,6 +26,7 @@
|
||||
"drop_import.progress.label.singular": "Új fájlok importálása folyamatban…\n1 fájl importálva.{suffix}",
|
||||
"drop_import.progress.window_title": "Fájlok importálása",
|
||||
"drop_import.title": "Fájlütközés",
|
||||
"edit.color_manager": "Színek kezelése",
|
||||
"edit.copy_fields": "Mezők másolása",
|
||||
"edit.paste_fields": "Mezők beillesztése",
|
||||
"edit.tag_manager": "Címkék kezelése",
|
||||
@@ -98,6 +112,7 @@
|
||||
"generic.recent_libraries": "Legutóbbi könyvtárak",
|
||||
"generic.rename": "Átnevezés",
|
||||
"generic.rename_alt": "&Átnevezés",
|
||||
"generic.reset": "Alaphelyzet",
|
||||
"generic.save": "Mentés",
|
||||
"generic.skip": "Kihagyás",
|
||||
"generic.skip_alt": "&Kihagyás",
|
||||
@@ -156,9 +171,16 @@
|
||||
"library.refresh.scanning_preparing": "Új fájlok keresése a mappákban…\nElőkészítés…",
|
||||
"library.refresh.title": "Mappák frissítése",
|
||||
"library.scan_library.title": "Könyvtár vizsgálata",
|
||||
"library_object.name": "Megnevezés",
|
||||
"library_object.name_required": "Megnevezés (kötelező)",
|
||||
"library_object.slug": "Azonosító-helyőrző",
|
||||
"library_object.slug_required": "Azonosító-helyőrző (kötelező)",
|
||||
"macros.running.dialog.new_entries": "Korábban beállított makrók futtatása {total}/{count} új elemen…",
|
||||
"macros.running.dialog.title": "Makrók futtatása az új elemeken",
|
||||
"media_player.autoplay": "Automatikus lejátszás",
|
||||
"menu.delete_selected_files_ambiguous": "Fájl(ok) {trash_term} helyezése",
|
||||
"menu.delete_selected_files_plural": "Fájlok {trash_term} helyezése",
|
||||
"menu.delete_selected_files_singular": "Fájl {trash_term} helyezése",
|
||||
"menu.edit": "S&zerkesztés",
|
||||
"menu.edit.ignore_list": "Fájlok és mappák figyelmen kívül hagyása",
|
||||
"menu.edit.manage_file_extensions": "&Fájlkiterjesztések kezelése",
|
||||
@@ -179,22 +201,37 @@
|
||||
"menu.macros": "&Makrók",
|
||||
"menu.macros.folders_to_tags": "Mappák &címkékké alakítása",
|
||||
"menu.select": "Kijelölés",
|
||||
"menu.settings": "Beállítások…",
|
||||
"menu.tools": "&Eszközök",
|
||||
"menu.tools.fix_duplicate_files": "&Egyező fájlok egyesítése",
|
||||
"menu.tools.fix_unlinked_entries": "Kapcsolat &nélküli elemek javítása",
|
||||
"menu.view": "&Nézet",
|
||||
"menu.window": "&Ablak",
|
||||
"namespace.create.description": "A TagStudio névterekkel különíti el az adatcsoportokat, mint a címkék és a színek, így azok könnyen exportálhatóak és megoszthatóak. A „tagstudio”-val kezdődő névterek belső használatra vannak lefoglalva.",
|
||||
"namespace.create.description_color": "Minden szín névterekbe van foglalva, amelyek színpalettaként viselkednek. Minden egyéni színt névtérbe kell foglalni.",
|
||||
"namespace.create.title": "Névtér létrehozása",
|
||||
"namespace.new.button": "Új névtér",
|
||||
"namespace.new.prompt": "Az egyéni színek használatához először hozzon létre egy névteret!",
|
||||
"preview.multiple_selection": "<b>{count}</b> kijelölt elem",
|
||||
"preview.no_selection": "Nincs kijelölt elem",
|
||||
"select.add_tag_to_selected": "Címke hozzáadása a kijelölt elemekhez",
|
||||
"select.all": "&Az összes kijelölése",
|
||||
"select.clear": "&Kijelölés megszüntetése",
|
||||
"settings.clear_thumb_cache.title": "Miniatűr-gyorsítótár ürítése",
|
||||
"settings.language": "Nyelv",
|
||||
"settings.open_library_on_start": "Könyvtár megnyitása a program indulásakor",
|
||||
"settings.restart_required": "A módosítások érvénybeléptetéséhez újra kell indítani a TagStudiót.",
|
||||
"settings.show_filenames_in_grid": "Fájlnevek megjelenítése rácsnézetben",
|
||||
"settings.show_recent_libraries": "&Legutóbbi könyvtárak megjelenítése",
|
||||
"settings.title": "Beállítások",
|
||||
"sorting.direction.ascending": "Növekvő sorrend",
|
||||
"sorting.direction.descending": "Csökkenő sorrend",
|
||||
"splash.opening_library": "Könyvtár megnyitása folyamatban: „{library_path}”…",
|
||||
"status.deleted_file_plural": "{count} fájl törlése befejeződött.",
|
||||
"status.deleted_file_singular": "1 fájl törlése befejeződött.",
|
||||
"status.deleted_none": "Egy fájl sem került törlésre.",
|
||||
"status.deleted_partial_warning": "Csak {count} fájl került törlésre. Lehetséges, hogy a többi fájl hiányzik vagy használatban van.",
|
||||
"status.deleting_file": "{count}/{i}. fájl törlése folyamatban: „{path}”…",
|
||||
"status.library_backup_in_progress": "Könyvtár biztonsági mentése folyamatban…",
|
||||
"status.library_backup_success": "A biztonsági mentés létrehozása megtörtént az alábbi elérési úton: „{path}” ({time_span})",
|
||||
"status.library_closed": "Könyvtár bezárása ({time_span}) sikeresen megtörtént.",
|
||||
@@ -211,6 +248,7 @@
|
||||
"tag.add.plural": "Címkék hozzáadása",
|
||||
"tag.add_to_search": "Keresési kifejezés kiegészítése",
|
||||
"tag.aliases": "Áljelek",
|
||||
"tag.all_tags": "Minden címke",
|
||||
"tag.choose_color": "Címkeszín",
|
||||
"tag.color": "Szín",
|
||||
"tag.confirm_delete": "Biztosan törölni akarja a(z) „{tag_name}” címkét?",
|
||||
@@ -218,6 +256,7 @@
|
||||
"tag.create_add": "„{query}”-címke létrehozása és alkalmazása",
|
||||
"tag.disambiguation.tooltip": "Címke használata egyértelműsítéshez",
|
||||
"tag.edit": "Címke szerkesztése",
|
||||
"tag.is_category": "Kategória",
|
||||
"tag.name": "Név",
|
||||
"tag.new": "Új címke",
|
||||
"tag.parent_tags": "Szülőcímkék",
|
||||
@@ -226,8 +265,21 @@
|
||||
"tag.remove": "Címke eltávolítása",
|
||||
"tag.search_for_tag": "Címke keresése",
|
||||
"tag.shorthand": "Rövidítés",
|
||||
"tag.tag_name_required": "Címkenév (Kötelező)",
|
||||
"tag.tag_name_required": "Címkenév (kötelező)",
|
||||
"tag.view_limit": "Megtekintési korlát:",
|
||||
"tag_manager.title": "Könyvtárcímkék",
|
||||
"trash.context.ambiguous": "Fájl(ok) {trash_term} helyezése",
|
||||
"trash.context.plural": "Fájlok {trash_term} helyezése",
|
||||
"trash.context.singular": "Fájl {trash_term} helyezése",
|
||||
"trash.dialog.disambiguation_warning.plural": "Ezzel a fájlok nem csak a TagStudióból, hanem a fájlrendszerből <i>is</i> el lesznek távolítva!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Ezzel a fájl nem csak a TagStudióból, hanem a fájlrendszerből <i>is</i> el lesz távolítva!",
|
||||
"trash.dialog.move.confirmation.plural": "Biztosan a {trash_term} akarod helyezni ezt a(z) {count} elemet?",
|
||||
"trash.dialog.move.confirmation.singular": "Biztosan a {trash_term} akarod helyezni ezt az elemet?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>FIGYELMEZTETÉS:</b> Ha a fájlt nem lehet a {trash_term} helyezni, akkor <b>véglegesen <b>törlésre kerül!</b>",
|
||||
"trash.dialog.title.plural": "Fájlok törlése",
|
||||
"trash.dialog.title.singular": "Fájl törlése",
|
||||
"trash.name.generic": "Kukába",
|
||||
"trash.name.windows": "Lomtárba",
|
||||
"view.size.0": "Apró",
|
||||
"view.size.1": "Kicsi",
|
||||
"view.size.2": "Közepes",
|
||||
|
||||
106
tagstudio/resources/translations/nl.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"color.title.no_color": "Geen Kleur",
|
||||
"drop_import.progress.label.initial": "Nieuwe bestanden importeren…",
|
||||
"drop_import.progress.label.plural": "Nieuwe bestanden importeren…\n{count} bestanden geïmporteerd.{suffix}",
|
||||
"drop_import.progress.label.singular": "Nieuwe bestanden importeren…\n1 bestand geïmporteerd.{suffix}",
|
||||
"drop_import.progress.window_title": "Importeer bestanden",
|
||||
"edit.copy_fields": "Velden Kopiëren",
|
||||
"edit.paste_fields": "Velden Plakken",
|
||||
"edit.tag_manager": "Beheer Labels",
|
||||
"entries.tags": "Labels",
|
||||
"field.copy": "Veld Kopiëren",
|
||||
"field.edit": "Veld Aanpassen",
|
||||
"field.paste": "Veld Plakken",
|
||||
"file.date_added": "Datum Toegevoegd",
|
||||
"file.date_created": "Datum Aangemaakt",
|
||||
"file.date_modified": "Datum Aangepast",
|
||||
"file.dimensions": "Dimensies",
|
||||
"file.duration": "Speelduur",
|
||||
"file.not_found": "Bestand Niet Gevonden",
|
||||
"file.open_file": "Bestand openen",
|
||||
"file.open_file_with": "Bestand openen met",
|
||||
"folders_to_tags.close_all": "Alles sluiten",
|
||||
"folders_to_tags.open_all": "Alles openen",
|
||||
"generic.add": "Toevoegen",
|
||||
"generic.apply": "Toepassen",
|
||||
"generic.apply_alt": "&Toepassen",
|
||||
"generic.cancel": "Annuleren",
|
||||
"generic.cancel_alt": "&Annuleren",
|
||||
"generic.close": "Sluiten",
|
||||
"generic.continue": "Doorgaan",
|
||||
"generic.copy": "Kopiëren",
|
||||
"generic.cut": "Knippen",
|
||||
"generic.delete": "Verwijderen",
|
||||
"generic.delete_alt": "&Verwijderen",
|
||||
"generic.done": "Klaar",
|
||||
"generic.done_alt": "&klaar",
|
||||
"generic.edit": "Aanpassen",
|
||||
"generic.edit_alt": "&Aanpassen",
|
||||
"generic.filename": "Bestandsnaam",
|
||||
"generic.navigation.back": "Terug",
|
||||
"generic.navigation.next": "Volgende",
|
||||
"generic.none": "Niks",
|
||||
"generic.overwrite": "Overschrijven",
|
||||
"generic.overwrite_alt": "&Overschrijven",
|
||||
"generic.paste": "Plakken",
|
||||
"generic.rename": "Hernoemen",
|
||||
"generic.rename_alt": "&Hernoemen",
|
||||
"generic.save": "Opslaan",
|
||||
"generic.skip": "Overslaan",
|
||||
"generic.skip_alt": "&Overslaan",
|
||||
"home.search": "Zoeken",
|
||||
"home.search_tags": "Labels Zoeken",
|
||||
"home.thumbnail_size": "Miniatuur Grootte",
|
||||
"home.thumbnail_size.extra_large": "Extra Grote Miniaturen",
|
||||
"home.thumbnail_size.large": "Grote Miniaturen",
|
||||
"home.thumbnail_size.medium": "Gemiddelde Minituren",
|
||||
"home.thumbnail_size.mini": "Mini Miniaturen",
|
||||
"home.thumbnail_size.small": "Kleine Miniaturen",
|
||||
"ignore_list.mode.exclude": "Uitsluiten",
|
||||
"json_migration.finish_migration": "Migratie Afronden",
|
||||
"json_migration.heading.aliases": "Aliassen:",
|
||||
"json_migration.heading.colors": "Kleuren:",
|
||||
"json_migration.heading.fields": "Velden:",
|
||||
"json_migration.heading.names": "Namen:",
|
||||
"json_migration.heading.paths": "Paden:",
|
||||
"json_migration.heading.shorthands": "Afkortingen:",
|
||||
"json_migration.heading.tags": "Labels:",
|
||||
"json_migration.migration_complete": "Migratie Afgerond!",
|
||||
"json_migration.title": "Migratie Formaat Opslaan: \"{path}\"",
|
||||
"library.field.add": "Veld Toevoegen",
|
||||
"library.field.mixed_data": "Gemixte Data",
|
||||
"library.field.remove": "Veld Verwijderen",
|
||||
"menu.delete_selected_files_ambiguous": "Bestand(en) verplaatsen naar {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Bestanden verplaatsen naar {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Bestand verplaatsen naar {trash_term}",
|
||||
"menu.edit": "Aanpassen",
|
||||
"menu.edit.ignore_list": "Bestanden en Mappen Negeren",
|
||||
"menu.edit.manage_tags": "Labels Beheren",
|
||||
"menu.edit.new_tag": "Nieuw &Label",
|
||||
"menu.file": "&Bestand",
|
||||
"menu.help": "&Help",
|
||||
"menu.macros.folders_to_tags": "Mappen naar Labels",
|
||||
"menu.select": "Selecteren",
|
||||
"menu.window": "Venster",
|
||||
"select.all": "Alles Selecteren",
|
||||
"sorting.direction.ascending": "Oplopend",
|
||||
"sorting.direction.descending": "Aflopend",
|
||||
"status.deleted_file_plural": "{count} Bestanden Verwijderd!",
|
||||
"status.deleted_file_singular": "1 Bestand Verwijderd!",
|
||||
"status.deleted_none": "Geen bestanden verwijderd.",
|
||||
"status.deleting_file": "Bestand [{i}/{count}] verwijderen: \"{path}\"…",
|
||||
"status.library_version_found": "Gevonden:",
|
||||
"status.results": "Resultaten",
|
||||
"tag.add": "Label toevoegen",
|
||||
"tag.add.plural": "Labels toevoegen",
|
||||
"tag.aliases": "Aliassen",
|
||||
"tag.all_tags": "Alle Labels",
|
||||
"tag.choose_color": "Kies Label Kleur",
|
||||
"tag.color": "Kleur",
|
||||
"tag.edit": "Label Aanpassen",
|
||||
"tag.name": "Naam",
|
||||
"tag.new": "Nieuw Label",
|
||||
"tag.remove": "Verwijder Label",
|
||||
"trash.dialog.title.plural": "Bestanden Verwijderen",
|
||||
"trash.dialog.title.singular": "Bestand Verwijden"
|
||||
}
|
||||
@@ -1,14 +1,33 @@
|
||||
{
|
||||
"about.title": "O programie",
|
||||
"app.git": "Migawki Git",
|
||||
"app.git": "Migawka Git",
|
||||
"app.pre_release": "Przedpremiera",
|
||||
"app.title": "{base_title} - Biblioteka '{library_dir}'",
|
||||
"color.color_border": "Użyj koloru pochodnego na ramkę",
|
||||
"color.confirm_delete": "Czy na pewno chcesz usunąć kolor \"{color_name}\"?",
|
||||
"color.delete": "Usuń tag",
|
||||
"color.import_pack": "Importuj paczkę kolorów",
|
||||
"color.name": "Nazwa",
|
||||
"color.namespace.delete.prompt": "Czy na pewno chcesz usunąć tę przestrzeń kolorów? Zostaną usunięte WSZYSTKIE kolory w przestrzeni!",
|
||||
"color.namespace.delete.title": "Usuń przestrzeń kolorów",
|
||||
"color.new": "Nowy kolor",
|
||||
"color.placeholder": "Kolor",
|
||||
"color.primary": "Kolor podstawowy",
|
||||
"color.primary_required": "Kolor podstawowy (wymagany)",
|
||||
"color.secondary": "Kolor pochodny",
|
||||
"color.title.no_color": "Brak koloru",
|
||||
"color_manager.title": "Zarządzaj kolorami tagów",
|
||||
"drop_import.description": "Następujące pliki pasują ścieżkami do już istniejących w bibliotece",
|
||||
"drop_import.duplicates_choice.plural": "Następujące {count} pliki pasują ścieżkami do już istniejących w bibliotece.",
|
||||
"drop_import.duplicates_choice.singular": "Następujący plik pasuje ścieżką do już istniejącego w bibliotece.",
|
||||
"drop_import.progress.label.initial": "Importowanie nowych plików...",
|
||||
"drop_import.progress.label.plural": "Importowanie nowych plików...\nZaimportowano {count} plików.{suffix}",
|
||||
"drop_import.progress.label.singular": "Importowanie nowych plików...\nZaimportowano 1 plik.{suffix}",
|
||||
"drop_import.progress.window_title": "Importuj pliki",
|
||||
"drop_import.title": "Konfliktujące pliki",
|
||||
"edit.color_manager": "Zarządzaj kolorami tagów",
|
||||
"edit.copy_fields": "Skopiuj pola",
|
||||
"edit.paste_fields": "Wklej pola",
|
||||
"edit.tag_manager": "Zarządzaj tagami",
|
||||
"entries.duplicate.merge": "Złącz zduplikowane wpisy",
|
||||
"entries.duplicate.merge.label": "Łączenie zduplikowanych wpisów...",
|
||||
@@ -92,6 +111,7 @@
|
||||
"generic.recent_libraries": "Ostatnie biblioteki",
|
||||
"generic.rename": "Zmień nazwę",
|
||||
"generic.rename_alt": "&Zmień nazwę",
|
||||
"generic.reset": "Resetuj",
|
||||
"generic.save": "Zapisz",
|
||||
"generic.skip": "Pomiń",
|
||||
"generic.skip_alt": "&Pomiń",
|
||||
@@ -116,14 +136,18 @@
|
||||
"json_migration.discrepancies_found": "Znaleziono niezgodności biblioteki",
|
||||
"json_migration.discrepancies_found.description": "Znaleziono niezgodności pomiędzy oryginalną a skonwertowaną biblioteką. Proszę sprawdzić i wybrać czy chcesz kontynuować z migracją czy anulować.",
|
||||
"json_migration.finish_migration": "Ukończ migrację",
|
||||
"json_migration.heading.aliases": "Zastępcze nazwy:",
|
||||
"json_migration.heading.colors": "Kolory:",
|
||||
"json_migration.heading.differ": "Niezgodność",
|
||||
"json_migration.heading.entires": "Wpisy:",
|
||||
"json_migration.heading.extension_list_type": "Typ listy rozszerzeń:",
|
||||
"json_migration.heading.fields": "Pola:",
|
||||
"json_migration.heading.file_extension_list": "Lista rozszerzeń plików:",
|
||||
"json_migration.heading.match": "Dopasowane",
|
||||
"json_migration.heading.names": "Nazwy:",
|
||||
"json_migration.heading.parent_tags": "Tagi nadrzędne:",
|
||||
"json_migration.heading.paths": "Ścieżki:",
|
||||
"json_migration.heading.shorthands": "Skróty:",
|
||||
"json_migration.heading.tags": "Tagi:",
|
||||
"json_migration.migrating_files_entries": "Migrowanie {entries:,d} wpisów plików...",
|
||||
"json_migration.migration_complete": "Migrowanie skończone!",
|
||||
@@ -139,12 +163,18 @@
|
||||
"library.field.remove": "Usuń pole",
|
||||
"library.missing": "Brak lokalizacji",
|
||||
"library.name": "Biblioteka",
|
||||
"library.refresh.scanning.plural": "Skanowanie folderów w poszukiwaniu nowych plików...\nPrzeszukano {searched_count} plików, Znaleziono {found_count} nowych plików",
|
||||
"library.refresh.scanning_preparing": "Skanowanie katalogów w poszukiwaniu nowych plików\nPrzygotowywanie...",
|
||||
"library.refresh.title": "Odświeżanie katalogów",
|
||||
"library.scan_library.title": "Skanowanie biblioteki",
|
||||
"library_object.name": "Nazwa",
|
||||
"library_object.name_required": "Nazwa (wymagana)",
|
||||
"macros.running.dialog.new_entries": "Stosowanie skonfigurowanych makr na {count}/{total} nowych wpisach plików...",
|
||||
"macros.running.dialog.title": "Stosowanie makr na nowych wpisach",
|
||||
"media_player.autoplay": "Automatyczne odtwarzanie",
|
||||
"menu.delete_selected_files_ambiguous": "Przenieś plik(i) do {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Przenieś pliki do {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Przenieś plik do {trash_term}",
|
||||
"menu.edit": "Edytuj",
|
||||
"menu.edit.ignore_list": "Ignoruj pliki i foldery",
|
||||
"menu.edit.manage_file_extensions": "Zarządzaj rozszerzeniami plików",
|
||||
@@ -165,22 +195,36 @@
|
||||
"menu.macros": "&Makra",
|
||||
"menu.macros.folders_to_tags": "Foldery na Tagi",
|
||||
"menu.select": "Zaznacz",
|
||||
"menu.settings": "Ustawienia...",
|
||||
"menu.tools": "&Narzędzia",
|
||||
"menu.tools.fix_duplicate_files": "Napraw zduplikowane &pliki",
|
||||
"menu.tools.fix_unlinked_entries": "Napraw &odłączone wpisy",
|
||||
"menu.view": "&Widok",
|
||||
"menu.window": "Okno",
|
||||
"namespace.create.description_color": "Kolory tagów używają przestrzeni jako grup palet. Wszystkie niestandardowe kolory muszą najpierw znajdować się w grupie przestrzeni.",
|
||||
"namespace.create.title": "Stwórz przestrzeń",
|
||||
"namespace.new.button": "Nowa przestrzeń",
|
||||
"namespace.new.prompt": "Stwórz nową przestrzeń żeby zacząć dodawać niestandardowe kolory!",
|
||||
"preview.multiple_selection": "<b>{count}</b> pozycji zaznaczonych",
|
||||
"preview.no_selection": "Nie wybrano żadnych pozycji",
|
||||
"select.add_tag_to_selected": "Dodaj tag do zaznaczonych",
|
||||
"select.all": "Zaznacz wszystko",
|
||||
"select.clear": "Odznacz zaznaczenie",
|
||||
"settings.clear_thumb_cache.title": "Wyczyść pamięć podręczną miniaturek",
|
||||
"settings.language": "Język",
|
||||
"settings.open_library_on_start": "Otwieraj bibliotekę podczas startu",
|
||||
"settings.restart_required": "Zrestartuj TagStudio żeby zmiany zaczęły obowiązywać.",
|
||||
"settings.show_filenames_in_grid": "Pokazuj nazwy plików w siatce",
|
||||
"settings.show_recent_libraries": "Pokazuj ostatnie biblioteki",
|
||||
"settings.title": "Ustawienia",
|
||||
"sorting.direction.ascending": "Rosnąco",
|
||||
"sorting.direction.descending": "Malejąco",
|
||||
"splash.opening_library": "Otwieranie biblioteki \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "Usunięto {count} plików!",
|
||||
"status.deleted_file_singular": "Usunięto 1 plik!",
|
||||
"status.deleted_none": "Nie usunięto żadnych plików.",
|
||||
"status.deleted_partial_warning": "Usunięto tylko {count} plik(i/ów)! Sprawdź czy nie brakuje jakichś plików lub czy nie są obecnie w użyciu.",
|
||||
"status.deleting_file": "Usuwanie pliku [{i}/{count}]: \"{path}\"...",
|
||||
"status.library_backup_in_progress": "Zapisywanie kopii zapasowej biblioteki...",
|
||||
"status.library_backup_success": "Kopia zapasowa biblioteki zapisana w: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "Biblioteka zamknięta ({time_span})",
|
||||
@@ -191,17 +235,21 @@
|
||||
"status.library_version_found": "Znaleziono:",
|
||||
"status.library_version_mismatch": "Niezgodność wersji biblioteki!",
|
||||
"status.results": "Wyniki",
|
||||
"status.results.invalid_syntax": "Niepoprawna składnia zapytania:",
|
||||
"status.results_found": "Znaleziono {count} wyników ({time_span})",
|
||||
"tag.add": "Dodaj tag",
|
||||
"tag.add.plural": "Dodaj tagi",
|
||||
"tag.add_to_search": "Dodaj do wyszukiwania",
|
||||
"tag.aliases": "Aliasy",
|
||||
"tag.all_tags": "Wszystkie tagi",
|
||||
"tag.choose_color": "Wybierz kolor tagu",
|
||||
"tag.color": "Kolor",
|
||||
"tag.confirm_delete": "Jesteś pewien że chcesz usunąć tag \"{tag_name}\"?",
|
||||
"tag.create": "Stwórz tag",
|
||||
"tag.create_add": "Stwórz && dodaj \"zapytanie\"",
|
||||
"tag.disambiguation.tooltip": "Uzyj tego tagu dla uściślenia",
|
||||
"tag.edit": "Edytuj tag",
|
||||
"tag.is_category": "Jest kategorią",
|
||||
"tag.name": "Nazwa",
|
||||
"tag.new": "Nowy tag",
|
||||
"tag.parent_tags": "Tagi nadrzędne",
|
||||
@@ -211,7 +259,20 @@
|
||||
"tag.search_for_tag": "Szukaj dla tagu",
|
||||
"tag.shorthand": "Skrót",
|
||||
"tag.tag_name_required": "Nazwa tagu (wymagana)",
|
||||
"tag.view_limit": "Limit wyświetlania:",
|
||||
"tag_manager.title": "Biblioteka tagów",
|
||||
"trash.context.ambiguous": "Przenieś plik(i) do {trash_term}",
|
||||
"trash.context.plural": "Przenieś pliki do {trash_term}",
|
||||
"trash.context.singular": "Przenieś plik do {trash_term}",
|
||||
"trash.dialog.disambiguation_warning.plural": "To usunie je z TagStudio <i>ORAZ</i> z twojego systemu plików!",
|
||||
"trash.dialog.disambiguation_warning.singular": "To usunie go z TagStudio <i>ORAZ</i> z twojego systemu plików!",
|
||||
"trash.dialog.move.confirmation.plural": "Czy na pewno chcesz przenieść te {count} plików do {trash_term}?",
|
||||
"trash.dialog.move.confirmation.singular": "Czy na pewno chcesz przenieść ten plik do {trash_term}?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>WARNING!</b> Jeśli ten plik nie może być przeniesiony do {trash_term}, <b>Zostanie <b>usunięty na stałe!</b>",
|
||||
"trash.dialog.title.plural": "Usuń pliki",
|
||||
"trash.dialog.title.singular": "Usuń plik",
|
||||
"trash.name.generic": "Kosz",
|
||||
"trash.name.windows": "Kosz",
|
||||
"view.size.0": "Mini",
|
||||
"view.size.1": "Mały",
|
||||
"view.size.2": "Średni",
|
||||
|
||||
@@ -1,60 +1,142 @@
|
||||
{
|
||||
"entries.duplicate.merge.label": "Mesclando Entradas Duplicadas",
|
||||
"about.content": "<h2>TagStudio Alpha {version} ({branch})</h2><p>TagStudio é uma aplicação de organização de fotos e arquivos com um sistema de tags que tem como foco conceder liberdade e flexibilidade ao usuário. Sem programas ou formatos proprietários, sem imensidão de arquivos Sidecar, e sem total transtorno de sua estrutura de sistema de arquivos.</p>Licença: GPLv3<br>Diretório de Configuração: {config_path}<br>FFmpeg: {ffmpeg}<br>FFprobe: {ffprobe}<p><a href=\"https://github.com/TagStudioDev/TagStudio\">GitHub</a> | <a href=\"https://docs.tagstud.io\">Documentação</a> | <a href=\"https://discord.com/invite/hRNnVKhF2G\">Discord</a></p>",
|
||||
"about.title": "Sobre",
|
||||
"app.git": "Confirmação do Git",
|
||||
"app.pre_release": "Pré-Lançamento",
|
||||
"app.title": "{base_title} - Biblioteca '{library_dir}'",
|
||||
"color.color_border": "Usar Cores Secundárias nas Bordas",
|
||||
"color.confirm_delete": "Tem certeza que você quer deletar a cor \"{color_name}\"?",
|
||||
"color.delete": "Deletar Tag",
|
||||
"color.import_pack": "Importar Pacote de Cores",
|
||||
"color.name": "Nome",
|
||||
"color.namespace.delete.prompt": "Tem certeza que você quer deletar o espaço de nome dessa cor? Isso irá deletar TODAS as cores do espaço de nome ao mesmo tempo!",
|
||||
"color.namespace.delete.title": "Deletar Espaço de Cor",
|
||||
"color.new": "Nova Cor",
|
||||
"color.placeholder": "Cor",
|
||||
"color.primary": "Cor Primária",
|
||||
"color.primary_required": "Cor Primária (Obrigatório)",
|
||||
"color.secondary": "Cor Secundária",
|
||||
"color.title.no_color": "Nenhuma Cor",
|
||||
"color_manager.title": "Gerenciar Tag Colors",
|
||||
"drop_import.description": "Os seguintes arquivos correspondem a caminhos de arquivos que já existem na biblioteca",
|
||||
"drop_import.duplicates_choice.plural": "Os seguintes arquivos {count} correspondem a caminhos de arquivo que já existem na biblioteca.",
|
||||
"drop_import.duplicates_choice.singular": "O arquivo a seguir corresponde a um caminho de arquivo que já existe na biblioteca.",
|
||||
"drop_import.progress.label.initial": "Importando Novos Arquivos...",
|
||||
"drop_import.progress.label.plural": "Importando Novos Arquivos...\n{count} Arquivos Importados.{suffix}",
|
||||
"drop_import.progress.label.singular": "Importando Novos Arquivos...\n1 Arquivo Importado.{suffix}",
|
||||
"drop_import.progress.window_title": "Importar Arquivos",
|
||||
"drop_import.title": "Arquivo(s) em Conflito",
|
||||
"edit.color_manager": "Gerenciar Cores de Tags",
|
||||
"edit.copy_fields": "Copiar Campos",
|
||||
"edit.paste_fields": "Colar Campos",
|
||||
"edit.tag_manager": "Gerenciar Tags",
|
||||
"entries.duplicate.merge": "Mesclar Entradas Duplicadas",
|
||||
"entries.duplicate.merge.label": "Mesclando Entradas Duplicadas...",
|
||||
"entries.duplicate.refresh": "Atualizar Entradas Duplicadas",
|
||||
"entries.duplicates.description": "Entradas duplicadas são definidas como multiplas entradas que levam ao mesmo arquivo no disco. Mergir essas entradas irá combinar as tags e metadados de todas as duplicatas em uma única entrada consolidada. Não confundir com \"Arquivos Duplicados\" que são duplicatas dos seus arquivos fora do TagStudio.",
|
||||
"entries.mirror": "Espelho",
|
||||
"entries.mirror.confirmation": "Tem certeza que você deseja espelhar os seguintes %{len(self.lib.dupe_files)} entradas?",
|
||||
"entries.mirror.label": "Espelhando 1/%{count} Entradas...",
|
||||
"entries.duplicates.description": "Entradas duplicadas são definidas como multiplas entradas que levam ao mesmo arquivo no disco. Mesclar essas entradas irá combinar as tags e metadados de todas as duplicatas em uma única entrada consolidada. Não confundir com \"Arquivos Duplicados\" que são duplicatas dos seus arquivos fora do TagStudio.",
|
||||
"entries.mirror": "&Espelho",
|
||||
"entries.mirror.confirmation": "Tem certeza que você deseja espelhar os seguintes {count} entradas?",
|
||||
"entries.mirror.label": "Espelhando {idx}/{total} Entradas...",
|
||||
"entries.mirror.title": "Espelhando Entradas",
|
||||
"entries.mirror.window_title": "Espelhar Entradas",
|
||||
"entries.running.dialog.new_entries": "Adicionando {total} Novas entradas de Arquivos...",
|
||||
"entries.running.dialog.title": "Adicionando Novas Entradas de Arquivos",
|
||||
"entries.tags": "Rótulos",
|
||||
"entries.unlinked.delete": "Deletar Entradas Não Linkada",
|
||||
"entries.unlinked.delete.confirm": "Tem certeza que deseja deletar as seguintes %{len(self.lib.missing_files)} entradas?",
|
||||
"entries.unlinked.delete": "Deletar Entradas Não Linkadas",
|
||||
"entries.unlinked.delete.confirm": "Tem certeza que deseja deletar as seguintes {count} entradas?",
|
||||
"entries.unlinked.delete.deleting": "Deletando Entradas",
|
||||
"entries.unlinked.delete.deleting_count": "Deletando %{x[0]+1}/{len(self.lib.missing_files)} Entradas Não Linkadas",
|
||||
"entries.unlinked.description": "Cada entrada na biblioteca está linkada a um arquivo em um dos seus diretórios. Se um arquivo linkado a uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não linkado. Entradas não linkadas podem ser automaticamente re-linkadas por buscas nos seus diretórios, manualmente re-linkadas pelo usuário, ou deletada se for desejada.",
|
||||
"entries.unlinked.refresh_all": "Atualizar_Tudo",
|
||||
"entries.unlinked.relink.attempting": "Tentando Relinkar %{x[0]+1}/%{len(self.lib.missing_files)} Entradas, %{self.fixed} Relinkadas com Sucesso",
|
||||
"entries.unlinked.delete.deleting_count": "Deletando {idx}/{count} Entradas Não Linkadas",
|
||||
"entries.unlinked.delete_alt": "De&letar Entradas Não Linkadas",
|
||||
"entries.unlinked.description": "Cada entrada na biblioteca está linkada a um arquivo em um dos seus diretórios. Se um arquivo linkado a uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não linkado.<br><br>Entradas não linkadas podem ser automaticamente re-linkadas por buscas nos seus diretórios, manualmente re-linkadas pelo usuário, ou deletada se for desejada.",
|
||||
"entries.unlinked.missing_count.none": "Entradas Não Linkadas: N/A",
|
||||
"entries.unlinked.missing_count.some": "Entradas Não Linkadas: {count}",
|
||||
"entries.unlinked.refresh_all": "&Atualizar Tudo",
|
||||
"entries.unlinked.relink.attempting": "Tentando Relinkar {idx}/{missing_count} Entradas, {fixed_count} Relinkadas com Sucesso",
|
||||
"entries.unlinked.relink.manual": "Relink Manual",
|
||||
"entries.unlinked.relink.title": "Relinkando Entradas",
|
||||
"entries.unlinked.scanning": "Escaneando Bibliotecada para Entradas Não Linkadas...",
|
||||
"entries.unlinked.search_and_relink": "Buscar && Relinkar",
|
||||
"entries.unlinked.search_and_relink": "&Buscar && Relinkar",
|
||||
"entries.unlinked.title": "Corrigir Entradas Não Linkadas",
|
||||
"field.copy": "Copiar Campo",
|
||||
"field.edit": "Editar Campo",
|
||||
"field.paste": "Colar Campo",
|
||||
"file.date_created": "Data de Criação",
|
||||
"file.date_modified": "Dada de Modificação",
|
||||
"file.date_modified": "Data de Modificação",
|
||||
"file.dimensions": "Dimensões",
|
||||
"file.duplicates.description": "TagStudio aceita resultados do DupeGuru pra gerenciar arquivos duplicados.",
|
||||
"file.duplicates.dupeguru.advice": "Após espelhagem, você estará livre para usar DupeGuru para deletar arquivos indesejados. Após, use a função \"Consertar Entradas Não Linkadas\" do TagStudio no menu de Ferramentas para deletar entradas não linkadas.",
|
||||
"file.duplicates.dupeguru.file_extension": "Arquivos DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "Carregar Aquivo DupeGuru",
|
||||
"file.duplicates.dupeguru.load_file": "&Carregar Aquivo DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "Nenhum Arquivo DupeGuru Selecionado",
|
||||
"file.duplicates.dupeguru.open_file": "Abrir Arquivo de Resultados do DupeGuru",
|
||||
"file.duplicates.fix": "Corrigir Arquivos Duplicados",
|
||||
"file.duplicates.matches": "Correspondências de Arquivos Duplicados: %{count}",
|
||||
"file.duplicates.matches": "Correspondências de Arquivos Duplicados: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Correspondências de Arquivos Duplicados: N/A",
|
||||
"file.duplicates.mirror.description": "Espelhe os ados de entrada em cada conjunto de correspondência duplicado, combinando todos os dados sem remover ou duplicar campos. Esta operação não excluirá nenhum arquivo ou dado.",
|
||||
"file.duplicates.mirror_entries": "Entradas Espelhadas",
|
||||
"file.not_found": "Arquivo não encontrado:",
|
||||
"file.duration": "Duração",
|
||||
"file.not_found": "Arquivo não encontrado",
|
||||
"file.open_file": "Abrir arquivo",
|
||||
"file.open_file_with": "Abrir arquivo com",
|
||||
"file.open_location.generic": "Abrir no explorador de arquivos",
|
||||
"file.open_location.mac": "Mostrar no Finder",
|
||||
"file.open_location.windows": "Mostrar no Explorador de Arquivos",
|
||||
"folders_to_tags.close_all": "Fechar Tudo",
|
||||
"folders_to_tags.converting": "Convertendo pastas para Rótulos",
|
||||
"folders_to_tags.description": "Cria rótulos baseado na sua estrutura de arquivos e aplica elas nas suas entradas\nA estrutura abaixo mostra todos os rótulos que irão ser criados e a quais entradas eles serão aplicados.",
|
||||
"folders_to_tags.open_all": "Abrir Tudo",
|
||||
"folders_to_tags.title": "Criar rótulos a partir das pastas",
|
||||
"generic.add": "Adicionar",
|
||||
"generic.apply": "Aplicar",
|
||||
"generic.apply_alt": "&Aplicar",
|
||||
"generic.cancel": "Cancelar",
|
||||
"generic.cancel_alt": "&Cancelar",
|
||||
"generic.close": "Fechar",
|
||||
"generic.continue": "Continuar",
|
||||
"generic.copy": "Copiar",
|
||||
"generic.cut": "Recortar",
|
||||
"generic.delete": "Deletar",
|
||||
"generic.delete_alt": "&Excluir",
|
||||
"generic.done": "Completo",
|
||||
"generic.edit": "Editar",
|
||||
"generic.edit_alt": "&Editar",
|
||||
"generic.filename": "Nome do Arquivo",
|
||||
"generic.navigation.back": "Anterior",
|
||||
"generic.navigation.next": "Próximo",
|
||||
"generic.none": "Nenhum",
|
||||
"generic.overwrite": "Sobrescrever",
|
||||
"generic.overwrite_alt": "&Sobrescrever",
|
||||
"generic.paste": "Colar",
|
||||
"generic.recent_libraries": "Bibliotecas recentes",
|
||||
"generic.rename": "Renomear",
|
||||
"generic.rename_alt": "&Renomear",
|
||||
"generic.save": "Salvar",
|
||||
"generic.skip": "Pular",
|
||||
"generic.skip_alt": "&Pular",
|
||||
"help.visit_github": "Visite o Repositório no GitHub",
|
||||
"home.search": "Buscar",
|
||||
"home.search_entries": "Buscar Entradas",
|
||||
"home.search_library": "Buscar na Biblioteca",
|
||||
"home.search_tags": "Buscar Rótulos",
|
||||
"home.thumbnail_size": "Tamanho de miniatura",
|
||||
"home.thumbnail_size.extra_large": "Miniaturas Extra Grandes",
|
||||
"home.thumbnail_size.large": "Miniaturas Grandes",
|
||||
"home.thumbnail_size.medium": "Miniaturas Médias",
|
||||
"home.thumbnail_size.mini": "Miniaturas Mini",
|
||||
"home.thumbnail_size.small": "Miniaturas Pequenas",
|
||||
"ignore_list.add_extension": "Adicionar Extensão",
|
||||
"ignore_list.mode.exclude": "Excluir",
|
||||
"ignore_list.mode.include": "Incluir",
|
||||
"ignore_list.mode.label": "Modo de Lista:",
|
||||
"ignore_list.title": "Extensões de Arquivo",
|
||||
"json_migration.creating_database_tables": "Criando Tabelas de Banco de Dados SQL...",
|
||||
"json_migration.discrepancies_found": "Encontradas Discrepâncias na biblioteca",
|
||||
"json_migration.discrepancies_found.description": "Discrepâncias foram encontradas entre os arquivos de Biblioteca originais e os convertidos. Por favor, revise e escolha continuar com a migração ou cancelar.",
|
||||
"json_migration.finish_migration": "Finalizar Migração",
|
||||
"json_migration.heading.colors": "Cores:",
|
||||
"json_migration.heading.differ": "Discrepância",
|
||||
"json_migration.heading.fields": "Campos:",
|
||||
"json_migration.heading.names": "Nomes:",
|
||||
"library.field.add": "Adicionar Campo",
|
||||
"library.field.confirm_remove": "Você tem certeza de que quer remover o campo \"%{self.lib.get_field_attr(field, \"name\")}\"?",
|
||||
"library.field.mixed_data": "Dados Mistos",
|
||||
@@ -64,7 +146,7 @@
|
||||
"library.refresh.scanning_preparing": "Escaneando Diretórios por Novos Arquivos...\nPreparando...",
|
||||
"library.refresh.title": "Atualizando Diretórios",
|
||||
"library.scan_library.title": "Escaneando Biblioteca",
|
||||
"macros.running.dialog.new_entries": "Executando Macros Configurados em %{x + 1}/%{len(new_ids)} Novas Entradas",
|
||||
"macros.running.dialog.new_entries": "Executando Macros Configurados nas {count}/{total} Novas Entradas de Arquivos...",
|
||||
"macros.running.dialog.title": "Executando Macros nas Novas Entradas",
|
||||
"menu.edit": "Editar",
|
||||
"menu.file": "Arquivo",
|
||||
@@ -73,19 +155,45 @@
|
||||
"menu.tools": "Ferramentas",
|
||||
"menu.window": "Janela",
|
||||
"preview.no_selection": "Nenhum Item Selecionado",
|
||||
"status.library_backup_success": "Backup da Biblioteca Salvo em:",
|
||||
"select.clear": "Limpar Seleção",
|
||||
"settings.clear_thumb_cache.title": "Limpar cache de Thumbnails",
|
||||
"status.library_backup_success": "Backup da Biblioteca Salvo em: \"{path}\" ({time_span})",
|
||||
"status.library_closing": "Fechando Biblioteca...",
|
||||
"status.library_save_success": "Biblioteca Salva e Fechada!",
|
||||
"status.library_search_query": "Procurando na Biblioteca por",
|
||||
"status.library_search_query": "Procurando na Biblioteca...",
|
||||
"status.library_version_expected": "Esperado:",
|
||||
"status.library_version_found": "Encontrado:",
|
||||
"status.library_version_mismatch": "Incompatibilidade de versão da biblioteca!",
|
||||
"status.results": "Resultados",
|
||||
"status.results.invalid_syntax": "Sintaxe de Pesquisa Inválida:",
|
||||
"status.results_found": "{count} Resultados Encontrados ({time_span})",
|
||||
"tag.add": "Adicionar Rótulo",
|
||||
"tag.add_to_search": "Adicionar a Pesquisa",
|
||||
"tag.add.plural": "Adicionar Tags",
|
||||
"tag.add_to_search": "Adicionar à Pesquisa",
|
||||
"tag.aliases": "Alias",
|
||||
"tag.all_tags": "Todas Tags",
|
||||
"tag.choose_color": "Escolha a cor da Tag",
|
||||
"tag.color": "Cor",
|
||||
"tag.confirm_delete": "Tem certeza que quer deletar a tag \"{tag_name}\"?",
|
||||
"tag.create": "Criar Tag",
|
||||
"tag.edit": "Editar Tag",
|
||||
"tag.name": "Nome",
|
||||
"tag.new": "Novo Rótulo",
|
||||
"tag.parent_tags": "Rótulos Pai",
|
||||
"tag.parent_tags.add": "Adicionar Rótulo Pai",
|
||||
"tag.search_for_tag": "Procurar por Rótulo",
|
||||
"tag.shorthand": "Taquigrafia",
|
||||
"tag_manager.title": "Rótulos da biblioteca"
|
||||
"tag_manager.title": "Rótulos da biblioteca",
|
||||
"trash.context.ambiguous": "Mover arquivo(s) para {trash_term}",
|
||||
"trash.context.plural": "Mover arquivos para {trash_term}",
|
||||
"trash.context.singular": "Mover arquivo para {trash_term}",
|
||||
"trash.dialog.move.confirmation.plural": "Tem certeza que quer remover esses {count} arquivos para o {trash_term}?",
|
||||
"trash.dialog.move.confirmation.singular": "Tem certeza que quer mover esse arquivo para o {trash_term}?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>AVISO!</b> Se esse arquivo não puder ser movido para o {trash_term}, <b> ele será <b>apagado permanentemente!</b>",
|
||||
"trash.dialog.title.plural": "Apagar Arquivos",
|
||||
"trash.dialog.title.singular": "Apagar Arquivo",
|
||||
"trash.name.windows": "Lixeira",
|
||||
"window.message.error_opening_library": "Erro ao abrir biblioteca.",
|
||||
"window.title.error": "Erro",
|
||||
"window.title.open_create_library": "Abrir/Criar Bilbioteca"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
VERSION: str = "9.5.0" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "Pre-Release 3" # Usually "" or "Pre-Release"
|
||||
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
TS_FOLDER_NAME: str = ".TagStudio"
|
||||
@@ -25,3 +25,5 @@ TAG_FAVORITE = 1
|
||||
TAG_META = 2
|
||||
RESERVED_TAG_START = 0
|
||||
RESERVED_TAG_END = 999
|
||||
|
||||
RESERVED_NAMESPACE_PREFIX = "tagstudio"
|
||||
|
||||
@@ -17,6 +17,7 @@ class SettingItems(str, enum.Enum):
|
||||
SHOW_FILENAMES = "show_filenames"
|
||||
AUTOPLAY = "autoplay_videos"
|
||||
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
|
||||
LANGUAGE = "language"
|
||||
|
||||
|
||||
class Theme(str, enum.Enum):
|
||||
@@ -71,4 +72,4 @@ class LibraryPrefs(DefaultEnum):
|
||||
IS_EXCLUDE_LIST = True
|
||||
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
|
||||
PAGE_SIZE: int = 500
|
||||
DB_VERSION: int = 7
|
||||
DB_VERSION: int = 8
|
||||
|
||||
@@ -307,6 +307,12 @@ def pastels() -> list[TagColorGroup]:
|
||||
|
||||
|
||||
def shades() -> list[TagColorGroup]:
|
||||
burgundy = TagColorGroup(
|
||||
slug="burgundy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Burgundy",
|
||||
primary="#6E1C24",
|
||||
)
|
||||
auburn = TagColorGroup(
|
||||
slug="auburn",
|
||||
namespace="tagstudio-shades",
|
||||
@@ -319,19 +325,31 @@ def shades() -> list[TagColorGroup]:
|
||||
name="Olive",
|
||||
primary="#4C652E",
|
||||
)
|
||||
dark_teal = TagColorGroup(
|
||||
slug="dark-teal",
|
||||
namespace="tagstudio-shades",
|
||||
name="Dark Teal",
|
||||
primary="#1F5E47",
|
||||
)
|
||||
navy = TagColorGroup(
|
||||
slug="navy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Navy",
|
||||
primary="#104B98",
|
||||
)
|
||||
dark_lavender = TagColorGroup(
|
||||
slug="dark_lavender",
|
||||
namespace="tagstudio-shades",
|
||||
name="Dark Lavender",
|
||||
primary="#3D3B6C",
|
||||
)
|
||||
berry = TagColorGroup(
|
||||
slug="berry",
|
||||
namespace="tagstudio-shades",
|
||||
name="Berry",
|
||||
primary="#9F2AA7",
|
||||
)
|
||||
return [auburn, olive, navy, berry]
|
||||
return [burgundy, auburn, olive, dark_teal, navy, dark_lavender, berry]
|
||||
|
||||
|
||||
def earth_tones() -> list[TagColorGroup]:
|
||||
@@ -421,6 +439,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Red",
|
||||
primary="#180607",
|
||||
secondary="#E22C3C",
|
||||
color_border=True,
|
||||
)
|
||||
neon_red_orange = TagColorGroup(
|
||||
slug="neon-red-orange",
|
||||
@@ -428,6 +447,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Red Orange",
|
||||
primary="#220905",
|
||||
secondary="#E83726",
|
||||
color_border=True,
|
||||
)
|
||||
neon_orange = TagColorGroup(
|
||||
slug="neon-orange",
|
||||
@@ -435,6 +455,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Orange",
|
||||
primary="#1F0D05",
|
||||
secondary="#ED6022",
|
||||
color_border=True,
|
||||
)
|
||||
neon_amber = TagColorGroup(
|
||||
slug="neon-amber",
|
||||
@@ -442,6 +463,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Amber",
|
||||
primary="#251507",
|
||||
secondary="#FA9A2C",
|
||||
color_border=True,
|
||||
)
|
||||
neon_yellow = TagColorGroup(
|
||||
slug="neon-yellow",
|
||||
@@ -449,6 +471,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Yellow",
|
||||
primary="#2B1C0B",
|
||||
secondary="#FFD63D",
|
||||
color_border=True,
|
||||
)
|
||||
neon_lime = TagColorGroup(
|
||||
slug="neon-lime",
|
||||
@@ -456,6 +479,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Lime",
|
||||
primary="#1B220C",
|
||||
secondary="#92E649",
|
||||
color_border=True,
|
||||
)
|
||||
neon_green = TagColorGroup(
|
||||
slug="neon-green",
|
||||
@@ -463,6 +487,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Green",
|
||||
primary="#091610",
|
||||
secondary="#45D649",
|
||||
color_border=True,
|
||||
)
|
||||
neon_teal = TagColorGroup(
|
||||
slug="neon-teal",
|
||||
@@ -470,6 +495,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Teal",
|
||||
primary="#09191D",
|
||||
secondary="#22D589",
|
||||
color_border=True,
|
||||
)
|
||||
neon_cyan = TagColorGroup(
|
||||
slug="neon-cyan",
|
||||
@@ -477,6 +503,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Cyan",
|
||||
primary="#0B191C",
|
||||
secondary="#3DDBDB",
|
||||
color_border=True,
|
||||
)
|
||||
neon_blue = TagColorGroup(
|
||||
slug="neon-blue",
|
||||
@@ -484,6 +511,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Blue",
|
||||
primary="#09101C",
|
||||
secondary="#3B87F0",
|
||||
color_border=True,
|
||||
)
|
||||
neon_indigo = TagColorGroup(
|
||||
slug="neon-indigo",
|
||||
@@ -491,6 +519,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Indigo",
|
||||
primary="#150B24",
|
||||
secondary="#874FF5",
|
||||
color_border=True,
|
||||
)
|
||||
neon_purple = TagColorGroup(
|
||||
slug="neon-purple",
|
||||
@@ -498,6 +527,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Purple",
|
||||
primary="#1E0B26",
|
||||
secondary="#BB4FF0",
|
||||
color_border=True,
|
||||
)
|
||||
neon_magenta = TagColorGroup(
|
||||
slug="neon-magenta",
|
||||
@@ -505,6 +535,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Magenta",
|
||||
primary="#220A13",
|
||||
secondary="#F64680",
|
||||
color_border=True,
|
||||
)
|
||||
neon_pink = TagColorGroup(
|
||||
slug="neon-pink",
|
||||
@@ -512,6 +543,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon Pink",
|
||||
primary="#210E15",
|
||||
secondary="#FF62AF",
|
||||
color_border=True,
|
||||
)
|
||||
neon_white = TagColorGroup(
|
||||
slug="neon-white",
|
||||
@@ -519,6 +551,7 @@ def neon() -> list[TagColorGroup]:
|
||||
name="Neon White",
|
||||
primary="#131315",
|
||||
secondary="#F2F1F8",
|
||||
color_border=True,
|
||||
)
|
||||
return [
|
||||
neon_red,
|
||||
|
||||
@@ -49,6 +49,7 @@ from src.qt.translations import Translations
|
||||
from ...constants import (
|
||||
BACKUP_FOLDER_NAME,
|
||||
LEGACY_TAG_FIELD_IDS,
|
||||
RESERVED_NAMESPACE_PREFIX,
|
||||
RESERVED_TAG_END,
|
||||
RESERVED_TAG_START,
|
||||
TAG_ARCHIVED,
|
||||
@@ -89,7 +90,16 @@ SELECT * FROM ChildTags;
|
||||
""") # noqa: E501
|
||||
|
||||
|
||||
def slugify(input_string: str) -> str:
|
||||
class ReservedNamespaceError(Exception):
|
||||
"""Raise during an unauthorized attempt to create or modify a reserved namespace value.
|
||||
|
||||
Reserved namespace prefix: "tagstudio".
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def slugify(input_string: str, allow_reserved: bool = False) -> str:
|
||||
# Convert to lowercase and normalize unicode characters
|
||||
slug = unicodedata.normalize("NFKD", input_string.lower())
|
||||
|
||||
@@ -99,6 +109,9 @@ def slugify(input_string: str) -> str:
|
||||
# Replace spaces with hyphens
|
||||
slug = re.sub(r"[-\s]+", "-", slug)
|
||||
|
||||
if not allow_reserved and slug.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
raise ReservedNamespaceError
|
||||
|
||||
return slug
|
||||
|
||||
|
||||
@@ -171,6 +184,7 @@ class LibraryStatus:
|
||||
success: bool
|
||||
library_path: Path | None = None
|
||||
message: str | None = None
|
||||
msg_description: str | None = None
|
||||
json_migration_req: bool = False
|
||||
|
||||
|
||||
@@ -179,7 +193,7 @@ class Library:
|
||||
|
||||
library_dir: Path | None = None
|
||||
storage_path: Path | str | None
|
||||
engine: Engine | None
|
||||
engine: Engine | None = None
|
||||
folder: Folder | None
|
||||
included_files: set[Path] = set()
|
||||
|
||||
@@ -353,14 +367,11 @@ class Library:
|
||||
if db_result:
|
||||
db_version = db_result.value # type: ignore
|
||||
|
||||
if db_version < 6: # NOTE: DB_VERSION 6 is the first supported SQL DB version.
|
||||
mismatch_text = Translations.translate_formatted(
|
||||
"status.library_version_mismatch"
|
||||
)
|
||||
found_text = Translations.translate_formatted("status.library_version_found")
|
||||
expected_text = Translations.translate_formatted(
|
||||
"status.library_version_expected"
|
||||
)
|
||||
# NOTE: DB_VERSION 6 is the first supported SQL DB version.
|
||||
if db_version < 6 or db_version > LibraryPrefs.DB_VERSION.default:
|
||||
mismatch_text = Translations["status.library_version_mismatch"]
|
||||
found_text = Translations["status.library_version_found"]
|
||||
expected_text = Translations["status.library_version_expected"]
|
||||
return LibraryStatus(
|
||||
success=False,
|
||||
message=(
|
||||
@@ -391,12 +402,13 @@ class Library:
|
||||
tag_colors += default_color_groups.grayscale()
|
||||
tag_colors += default_color_groups.earth_tones()
|
||||
tag_colors += default_color_groups.neon()
|
||||
try:
|
||||
session.add_all(tag_colors)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
logger.error("[Library] Couldn't add default tag colors", error=e)
|
||||
session.rollback()
|
||||
if is_new:
|
||||
try:
|
||||
session.add_all(tag_colors)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
logger.error("[Library] Couldn't add default tag colors", error=e)
|
||||
session.rollback()
|
||||
|
||||
# Add default tags.
|
||||
if is_new:
|
||||
@@ -446,25 +458,24 @@ class Library:
|
||||
|
||||
# Apply any post-SQL migration patches.
|
||||
if not is_new:
|
||||
# NOTE: DB_VERSION 6 was first used in v9.5.0-pr1
|
||||
if db_version < 8:
|
||||
self.apply_db8_schema_changes(session)
|
||||
if db_version == 6:
|
||||
self.apply_db6_patches(session)
|
||||
else:
|
||||
pass
|
||||
self.apply_repairs_for_db6(session)
|
||||
if db_version >= 6 and db_version < 8:
|
||||
self.apply_db8_default_data(session)
|
||||
|
||||
# Update DB_VERSION
|
||||
self.set_prefs(LibraryPrefs.DB_VERSION, LibraryPrefs.DB_VERSION.default)
|
||||
if LibraryPrefs.DB_VERSION.default > db_version:
|
||||
self.set_prefs(LibraryPrefs.DB_VERSION, LibraryPrefs.DB_VERSION.default)
|
||||
|
||||
# everything is fine, set the library path
|
||||
self.library_dir = library_dir
|
||||
return LibraryStatus(success=True, library_path=library_dir)
|
||||
|
||||
def apply_db6_patches(self, session: Session):
|
||||
"""Apply migration patches to a library with DB_VERSION 6.
|
||||
|
||||
DB_VERSION 6 was first used in v9.5.0-pr1.
|
||||
"""
|
||||
logger.info("[Library] Applying patches to DB_VERSION: 6 library...")
|
||||
def apply_repairs_for_db6(self, session: Session):
|
||||
"""Apply database repairs introduced in DB_VERSION 7."""
|
||||
logger.info("[Library][Migration] Applying patches to DB_VERSION: 6 library...")
|
||||
with session:
|
||||
# Repair "Description" fields with a TEXT_LINE key instead of a TEXT_BOX key.
|
||||
desc_stmd = (
|
||||
@@ -487,6 +498,74 @@ class Library:
|
||||
|
||||
session.commit()
|
||||
|
||||
def apply_db8_schema_changes(self, session: Session):
|
||||
"""Apply database schema changes introduced in DB_VERSION 8."""
|
||||
# TODO: Use Alembic for this part instead
|
||||
# Add the missing color_border column to the TagColorGroups table.
|
||||
color_border_stmt = text(
|
||||
"ALTER TABLE tag_colors ADD COLUMN color_border BOOLEAN DEFAULT FALSE NOT NULL"
|
||||
)
|
||||
try:
|
||||
session.execute(color_border_stmt)
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Added color_border column to tag_colors table")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"[Library][Migration] Could not create color_border column in tag_colors table!",
|
||||
error=e,
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
def apply_db8_default_data(self, session: Session):
|
||||
"""Apply default data changes introduced in DB_VERSION 8."""
|
||||
tag_colors: list[TagColorGroup] = default_color_groups.standard()
|
||||
tag_colors += default_color_groups.pastels()
|
||||
tag_colors += default_color_groups.shades()
|
||||
tag_colors += default_color_groups.grayscale()
|
||||
tag_colors += default_color_groups.earth_tones()
|
||||
# tag_colors += default_color_groups.neon() # NOTE: Neon is handled separately
|
||||
|
||||
# Add any new default colors introduced in DB_VERSION 8
|
||||
for color in tag_colors:
|
||||
try:
|
||||
session.add(color)
|
||||
logger.info(
|
||||
"[Library][Migration] Migrated tag color to DB_VERSION 8+",
|
||||
color_name=color.name,
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
|
||||
# Update Neon colors to use the the color_border property
|
||||
for color in default_color_groups.neon():
|
||||
try:
|
||||
neon_stmt = (
|
||||
update(TagColorGroup)
|
||||
.where(
|
||||
and_(
|
||||
TagColorGroup.namespace == color.namespace,
|
||||
TagColorGroup.slug == color.slug,
|
||||
)
|
||||
)
|
||||
.values(
|
||||
slug=color.slug,
|
||||
namespace=color.namespace,
|
||||
name=color.name,
|
||||
primary=color.primary,
|
||||
secondary=color.secondary,
|
||||
color_border=color.color_border,
|
||||
)
|
||||
)
|
||||
session.execute(neon_stmt)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
logger.error(
|
||||
"[Library] Could not migrate Neon colors to DB_VERSION 8+!",
|
||||
error=e,
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
@property
|
||||
def default_fields(self) -> list[BaseField]:
|
||||
with Session(self.engine) as session:
|
||||
@@ -1088,6 +1167,79 @@ class Library:
|
||||
session.commit()
|
||||
return tags
|
||||
|
||||
def add_namespace(self, namespace: Namespace) -> bool:
|
||||
"""Add a namespace value to the library.
|
||||
|
||||
Args:
|
||||
namespace(str): The namespace slug. No special characters
|
||||
"""
|
||||
with Session(self.engine) as session:
|
||||
if not namespace.namespace:
|
||||
logger.warning("[LIBRARY][add_namespace] Namespace slug must not be empty")
|
||||
return False
|
||||
|
||||
slug = namespace.namespace
|
||||
try:
|
||||
slug = slugify(namespace.namespace)
|
||||
except ReservedNamespaceError:
|
||||
logger.error(
|
||||
f"[LIBRARY][add_namespace] Will not add a namespace with the reserved prefix:"
|
||||
f"{RESERVED_NAMESPACE_PREFIX}",
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
namespace_obj = Namespace(
|
||||
namespace=slug,
|
||||
name=namespace.name,
|
||||
)
|
||||
|
||||
try:
|
||||
session.add(namespace_obj)
|
||||
session.commit()
|
||||
return True
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
logger.error("IntegrityError")
|
||||
return False
|
||||
|
||||
def delete_namespace(self, namespace: Namespace | str):
|
||||
"""Delete a namespace and any connected data from the library."""
|
||||
if isinstance(namespace, str):
|
||||
if namespace.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
raise ReservedNamespaceError
|
||||
else:
|
||||
if namespace.namespace.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
raise ReservedNamespaceError
|
||||
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
namespace_: Namespace | None = None
|
||||
if isinstance(namespace, str):
|
||||
namespace_ = session.scalar(
|
||||
select(Namespace).where(Namespace.namespace == namespace)
|
||||
)
|
||||
else:
|
||||
namespace_ = namespace
|
||||
|
||||
if not namespace_:
|
||||
raise Exception
|
||||
session.delete(namespace_)
|
||||
session.flush()
|
||||
|
||||
colors = session.scalars(
|
||||
select(TagColorGroup).where(TagColorGroup.namespace == namespace_.namespace)
|
||||
)
|
||||
for color in colors:
|
||||
session.delete(color)
|
||||
session.flush()
|
||||
|
||||
session.commit()
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.error(e)
|
||||
session.rollback()
|
||||
return None
|
||||
|
||||
def add_tag(
|
||||
self,
|
||||
tag: Tag,
|
||||
@@ -1165,6 +1317,33 @@ class Library:
|
||||
session.rollback()
|
||||
return False
|
||||
|
||||
def add_color(self, color_group: TagColorGroup) -> TagColorGroup | None:
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
session.add(color_group)
|
||||
session.commit()
|
||||
session.expunge(color_group)
|
||||
return color_group
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.error(
|
||||
"[Library] Could not add color, trying to update existing value instead.",
|
||||
error=e,
|
||||
)
|
||||
session.rollback()
|
||||
return None
|
||||
|
||||
def delete_color(self, color: TagColorGroup):
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
session.delete(color)
|
||||
session.commit()
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.error(e)
|
||||
session.rollback()
|
||||
return None
|
||||
|
||||
def save_library_backup_to_disk(self) -> Path:
|
||||
assert isinstance(self.library_dir, Path)
|
||||
makedirs(str(self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
|
||||
@@ -1280,6 +1459,55 @@ class Library:
|
||||
"""Edit a Tag in the Library."""
|
||||
self.add_tag(tag, parent_ids, alias_names, alias_ids)
|
||||
|
||||
def update_color(self, old_color_group: TagColorGroup, new_color_group: TagColorGroup) -> None:
|
||||
"""Update a TagColorGroup in the Library. If it doesn't already exist, create it."""
|
||||
with Session(self.engine) as session:
|
||||
existing_color = session.scalar(
|
||||
select(TagColorGroup).where(
|
||||
and_(
|
||||
TagColorGroup.namespace == old_color_group.namespace,
|
||||
TagColorGroup.slug == old_color_group.slug,
|
||||
)
|
||||
)
|
||||
)
|
||||
if existing_color:
|
||||
update_color_stmt = (
|
||||
update(TagColorGroup)
|
||||
.where(
|
||||
and_(
|
||||
TagColorGroup.namespace == old_color_group.namespace,
|
||||
TagColorGroup.slug == old_color_group.slug,
|
||||
)
|
||||
)
|
||||
.values(
|
||||
slug=new_color_group.slug,
|
||||
namespace=new_color_group.namespace,
|
||||
name=new_color_group.name,
|
||||
primary=new_color_group.primary,
|
||||
secondary=new_color_group.secondary,
|
||||
color_border=new_color_group.color_border,
|
||||
)
|
||||
)
|
||||
session.execute(update_color_stmt)
|
||||
session.flush()
|
||||
update_tags_stmt = (
|
||||
update(Tag)
|
||||
.where(
|
||||
and_(
|
||||
Tag.color_namespace == old_color_group.namespace,
|
||||
Tag.color_slug == old_color_group.slug,
|
||||
)
|
||||
)
|
||||
.values(
|
||||
color_namespace=new_color_group.namespace,
|
||||
color_slug=new_color_group.slug,
|
||||
)
|
||||
)
|
||||
session.execute(update_tags_stmt)
|
||||
session.commit()
|
||||
else:
|
||||
self.add_color(new_color_group)
|
||||
|
||||
def update_aliases(self, tag, alias_ids, alias_names, session):
|
||||
prev_aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)).all()
|
||||
|
||||
@@ -1379,7 +1607,28 @@ class Library:
|
||||
color_groups[color.namespace] = []
|
||||
color_groups[color.namespace].append(color)
|
||||
session.expunge(color)
|
||||
return color_groups
|
||||
|
||||
# Add empty namespaces that are available for use.
|
||||
empty_namespaces = session.scalars(
|
||||
select(Namespace)
|
||||
.where(Namespace.namespace.not_in(color_groups.keys()))
|
||||
.order_by(asc(Namespace.namespace))
|
||||
)
|
||||
for en in empty_namespaces:
|
||||
if not color_groups.get(en.namespace):
|
||||
color_groups[en.namespace] = []
|
||||
session.expunge(en)
|
||||
|
||||
return dict(
|
||||
sorted(color_groups.items(), key=lambda kv: self.get_namespace_name(kv[0]).lower())
|
||||
)
|
||||
|
||||
@property
|
||||
def namespaces(self) -> list[Namespace]:
|
||||
"""Return every Namespace in the library."""
|
||||
with Session(self.engine) as session:
|
||||
namespaces = session.scalars(select(Namespace).order_by(asc(Namespace.name)))
|
||||
return list(namespaces)
|
||||
|
||||
def get_namespace_name(self, namespace: str) -> str:
|
||||
with Session(self.engine) as session:
|
||||
|
||||
@@ -63,6 +63,7 @@ class TagColorGroup(Base):
|
||||
name: Mapped[str] = mapped_column()
|
||||
primary: Mapped[str] = mapped_column(nullable=False)
|
||||
secondary: Mapped[str | None]
|
||||
color_border: Mapped[bool] = mapped_column(nullable=False, default=False)
|
||||
|
||||
# TODO: Determine if slug and namespace can be optional and generated/added here if needed.
|
||||
def __init__(
|
||||
@@ -72,6 +73,7 @@ class TagColorGroup(Base):
|
||||
name: str,
|
||||
primary: str,
|
||||
secondary: str | None = None,
|
||||
color_border: bool = False,
|
||||
):
|
||||
self.slug = slug
|
||||
self.namespace = namespace
|
||||
@@ -79,6 +81,7 @@ class TagColorGroup(Base):
|
||||
self.primary = primary
|
||||
if secondary:
|
||||
self.secondary = secondary
|
||||
self.color_border = color_border
|
||||
super().__init__()
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class Ui_MainWindow(QMainWindow):
|
||||
# Thumbnail Size placeholder
|
||||
self.thumb_size_combobox = QComboBox(self.centralwidget)
|
||||
self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox")
|
||||
Translations.translate_with_setter(self.thumb_size_combobox.setPlaceholderText, "home.thumbnail_size")
|
||||
self.thumb_size_combobox.setPlaceholderText(Translations["home.thumbnail_size"])
|
||||
self.thumb_size_combobox.setCurrentText("")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -142,7 +142,7 @@ class Ui_MainWindow(QMainWindow):
|
||||
self.horizontalLayout_2.addWidget(self.forwardButton)
|
||||
|
||||
self.searchField = QLineEdit(self.centralwidget)
|
||||
Translations.translate_with_setter(self.searchField.setPlaceholderText, "home.search_entries")
|
||||
self.searchField.setPlaceholderText(Translations["home.search_entries"])
|
||||
self.searchField.setObjectName(u"searchField")
|
||||
self.searchField.setMinimumSize(QSize(0, 32))
|
||||
|
||||
@@ -152,8 +152,7 @@ class Ui_MainWindow(QMainWindow):
|
||||
self.searchField.setCompleter(self.searchFieldCompleter)
|
||||
self.horizontalLayout_2.addWidget(self.searchField)
|
||||
|
||||
self.searchButton = QPushButton(self.centralwidget)
|
||||
Translations.translate_qobject(self.searchButton, "home.search")
|
||||
self.searchButton = QPushButton(Translations["home.search"], self.centralwidget)
|
||||
self.searchButton.setObjectName(u"searchButton")
|
||||
self.searchButton.setMinimumSize(QSize(0, 32))
|
||||
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
from PIL import ImageQt
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
from src.core.constants import VERSION, VERSION_BRANCH
|
||||
from src.qt.modals.ffmpeg_checker import FfmpegChecker
|
||||
from src.qt.resource_manager import ResourceManager
|
||||
@@ -22,7 +16,7 @@ from src.qt.translations import Translations
|
||||
class AboutModal(QWidget):
|
||||
def __init__(self, config_path):
|
||||
super().__init__()
|
||||
Translations.translate_with_setter(self.setWindowTitle, "about.title")
|
||||
self.setWindowTitle(Translations["about.title"])
|
||||
|
||||
self.fc: FfmpegChecker = FfmpegChecker()
|
||||
self.rm: ResourceManager = ResourceManager()
|
||||
@@ -42,9 +36,6 @@ class AboutModal(QWidget):
|
||||
self.logo_widget.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
self.logo_widget.setContentsMargins(0, 0, 0, 20)
|
||||
|
||||
self.content_widget = QLabel()
|
||||
self.content_widget.setObjectName("contentLabel")
|
||||
self.content_widget.setWordWrap(True)
|
||||
ff_version = self.fc.version()
|
||||
ffmpeg = '<span style="color:red">Missing</span>'
|
||||
if ff_version["ffmpeg"] is not None:
|
||||
@@ -52,23 +43,25 @@ class AboutModal(QWidget):
|
||||
ffprobe = '<span style="color:red">Missing</span>'
|
||||
if ff_version["ffprobe"] is not None:
|
||||
ffprobe = '<span style="color:green">Found</span> (' + ff_version["ffprobe"] + ")"
|
||||
Translations.translate_qobject(
|
||||
self.content_widget,
|
||||
"about.content",
|
||||
version=VERSION,
|
||||
branch=VERSION_BRANCH,
|
||||
config_path=config_path,
|
||||
ffmpeg=ffmpeg,
|
||||
ffprobe=ffprobe,
|
||||
self.content_widget = QLabel(
|
||||
Translations["about.content"].format(
|
||||
version=VERSION,
|
||||
branch=VERSION_BRANCH,
|
||||
config_path=config_path,
|
||||
ffmpeg=ffmpeg,
|
||||
ffprobe=ffprobe,
|
||||
)
|
||||
)
|
||||
self.content_widget.setObjectName("contentLabel")
|
||||
self.content_widget.setWordWrap(True)
|
||||
self.content_widget.setOpenExternalLinks(True)
|
||||
self.content_widget.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
self.button_widget = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_widget)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.close_button = QPushButton()
|
||||
Translations.translate_qobject(self.close_button, "generic.close")
|
||||
self.close_button = QPushButton(Translations["generic.close"])
|
||||
self.close_button.clicked.connect(lambda: self.close())
|
||||
self.close_button.setMaximumWidth(80)
|
||||
|
||||
|
||||
@@ -31,17 +31,16 @@ class AddFieldModal(QWidget):
|
||||
# [Cancel] [Save]
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
Translations.translate_with_setter(self.setWindowTitle, "library.field.add")
|
||||
self.setWindowTitle(Translations["library.field.add"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.title_widget = QLabel()
|
||||
self.title_widget = QLabel(Translations["library.field.add"])
|
||||
self.title_widget.setObjectName("fieldTitle")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px;")
|
||||
Translations.translate_qobject(self.title_widget, "library.field.add")
|
||||
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.list_widget = QListWidget()
|
||||
@@ -51,13 +50,11 @@ class AddFieldModal(QWidget):
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
Translations.translate_qobject(self.cancel_button, "generic.cancel")
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel"])
|
||||
self.cancel_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.save_button = QPushButton()
|
||||
Translations.translate_qobject(self.save_button, "generic.add")
|
||||
self.save_button = QPushButton(Translations["generic.add"])
|
||||
self.save_button.setDefault(True)
|
||||
self.save_button.clicked.connect(self.hide)
|
||||
self.save_button.clicked.connect(
|
||||
|
||||
406
tagstudio/src/qt/modals/build_color.py
Normal file
@@ -0,0 +1,406 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import contextlib
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QColorDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core import palette
|
||||
from src.core.library import Library
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.library.alchemy.library import slugify
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
from src.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_text_color,
|
||||
)
|
||||
from src.qt.widgets.tag_color_preview import TagColorPreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class BuildColorPanel(PanelWidget):
|
||||
on_edit = Signal(TagColorGroup)
|
||||
|
||||
def __init__(self, library: Library, color_group: TagColorGroup):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.color_group: TagColorGroup = color_group
|
||||
self.tag_color_namespace: str | None
|
||||
self.tag_color_slug: str | None
|
||||
self.disambiguation_id: int | None
|
||||
|
||||
self.known_colors: set[str]
|
||||
self.update_known_colors()
|
||||
|
||||
self.setMinimumSize(340, 240)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.form_container = QWidget()
|
||||
self.form_layout = QFormLayout(self.form_container)
|
||||
self.form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.form_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Preview Tag ----------------------------------------------------------
|
||||
self.preview_widget = QWidget()
|
||||
self.preview_layout = QVBoxLayout(self.preview_widget)
|
||||
self.preview_layout.setStretch(1, 1)
|
||||
self.preview_layout.setContentsMargins(0, 0, 0, 6)
|
||||
self.preview_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.preview_button = TagColorPreview(self.lib, None)
|
||||
self.preview_button.setEnabled(False)
|
||||
self.preview_layout.addWidget(self.preview_button)
|
||||
|
||||
# Name -----------------------------------------------------------------
|
||||
self.name_title = QLabel(Translations["library_object.name"])
|
||||
self.name_field = QLineEdit()
|
||||
self.name_field.setFixedHeight(24)
|
||||
self.name_field.textChanged.connect(self.on_text_changed)
|
||||
self.name_field.setPlaceholderText(Translations["library_object.name_required"])
|
||||
self.form_layout.addRow(self.name_title, self.name_field)
|
||||
|
||||
# Slug -----------------------------------------------------------------
|
||||
self.slug_title = QLabel(Translations["library_object.slug"])
|
||||
self.slug_field = QLineEdit()
|
||||
self.slug_field.setEnabled(False)
|
||||
self.slug_field.setFixedHeight(24)
|
||||
self.slug_field.setPlaceholderText(Translations["library_object.slug_required"])
|
||||
self.form_layout.addRow(self.slug_title, self.slug_field)
|
||||
|
||||
# Primary --------------------------------------------------------------
|
||||
self.primary_title = QLabel(Translations["color.primary"])
|
||||
self.primary_button = QPushButton()
|
||||
self.primary_button.setMinimumSize(44, 22)
|
||||
self.primary_button.setMaximumHeight(22)
|
||||
self.edit_primary_modal = QColorDialog()
|
||||
self.primary_button.clicked.connect(self.primary_color_callback)
|
||||
self.form_layout.addRow(self.primary_title, self.primary_button)
|
||||
|
||||
# Secondary ------------------------------------------------------------
|
||||
self.secondary_widget = QWidget()
|
||||
self.secondary_layout = QHBoxLayout(self.secondary_widget)
|
||||
self.secondary_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.secondary_layout.setSpacing(6)
|
||||
self.secondary_title = QLabel(Translations["color.secondary"])
|
||||
self.secondary_button = QPushButton()
|
||||
self.secondary_button.setMinimumSize(44, 22)
|
||||
self.secondary_button.setMaximumHeight(22)
|
||||
self.edit_secondary_modal = QColorDialog()
|
||||
self.secondary_button.clicked.connect(self.secondary_color_callback)
|
||||
self.secondary_layout.addWidget(self.secondary_button)
|
||||
|
||||
self.secondary_reset_button = QPushButton(Translations["generic.reset"])
|
||||
self.secondary_reset_button.clicked.connect(self.update_secondary)
|
||||
self.secondary_layout.addWidget(self.secondary_reset_button)
|
||||
self.secondary_layout.setStretch(0, 3)
|
||||
self.secondary_layout.setStretch(1, 1)
|
||||
self.form_layout.addRow(self.secondary_title, self.secondary_widget)
|
||||
|
||||
# Color Border ---------------------------------------------------------
|
||||
self.border_widget = QWidget()
|
||||
self.border_layout = QHBoxLayout(self.border_widget)
|
||||
self.border_layout.setStretch(1, 1)
|
||||
self.border_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.border_layout.setSpacing(6)
|
||||
self.border_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.border_checkbox = QCheckBox()
|
||||
self.border_checkbox.setFixedSize(22, 22)
|
||||
self.border_checkbox.clicked.connect(
|
||||
lambda checked: self.update_secondary(
|
||||
color=QColor(self.preview_button.tag_color_group.secondary)
|
||||
if self.preview_button.tag_color_group.secondary
|
||||
else None,
|
||||
color_border=checked,
|
||||
)
|
||||
)
|
||||
self.border_layout.addWidget(self.border_checkbox)
|
||||
self.border_label = QLabel(Translations["color.color_border"])
|
||||
self.border_layout.addWidget(self.border_label)
|
||||
|
||||
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
|
||||
border_color = get_border_color(primary_color)
|
||||
highlight_color = get_highlight_color(primary_color)
|
||||
text_color: QColor = get_text_color(primary_color, highlight_color)
|
||||
self.border_checkbox.setStyleSheet(
|
||||
f"QCheckBox{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"}}"
|
||||
f"QCheckBox::indicator{{"
|
||||
f"width: 10px;"
|
||||
f"height: 10px;"
|
||||
f"border-radius: 2px;"
|
||||
f"margin: 4px;"
|
||||
f"}}"
|
||||
f"QCheckBox::indicator:checked{{"
|
||||
f"background: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QCheckBox::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QCheckBox::focus{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"outline:none;"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
# Add Widgets to Layout ================================================
|
||||
self.root_layout.addWidget(self.preview_widget)
|
||||
self.root_layout.addWidget(self.form_container)
|
||||
self.root_layout.addWidget(self.border_widget)
|
||||
|
||||
self.set_color(color_group or TagColorGroup("", "", Translations["color.new"], ""))
|
||||
self.update_primary(QColor(color_group.primary))
|
||||
self.update_secondary(None if not color_group.secondary else QColor(color_group.secondary))
|
||||
self.on_text_changed()
|
||||
|
||||
def set_color(self, color_group: TagColorGroup):
|
||||
logger.info("[BuildColorPanel] Setting Color", color=color_group)
|
||||
self.color_group = color_group
|
||||
|
||||
self.preview_button.set_tag_color_group(color_group)
|
||||
self.name_field.setText(color_group.name)
|
||||
self.primary_button.setText(color_group.primary)
|
||||
self.edit_primary_modal.setCurrentColor(color_group.primary)
|
||||
self.secondary_button.setText(
|
||||
Translations["color.title.no_color"]
|
||||
if not color_group.secondary
|
||||
else str(color_group.secondary)
|
||||
)
|
||||
self.edit_secondary_modal.setCurrentColor(color_group.secondary or QColor(0, 0, 0, 255))
|
||||
self.border_checkbox.setChecked(color_group.color_border)
|
||||
|
||||
def primary_color_callback(self) -> None:
|
||||
initial = (
|
||||
self.primary_button.text()
|
||||
if self.primary_button.text().startswith("#")
|
||||
else self.color_group.primary
|
||||
)
|
||||
color = self.edit_primary_modal.getColor(initial=initial)
|
||||
if color.isValid():
|
||||
self.update_primary(color)
|
||||
self.preview_button.set_tag_color_group(self.build_color()[1])
|
||||
else:
|
||||
logger.info("[BuildColorPanel] Primary color selection was cancelled!")
|
||||
|
||||
def secondary_color_callback(self) -> None:
|
||||
initial = (
|
||||
self.secondary_button.text()
|
||||
if self.secondary_button.text().startswith("#")
|
||||
else (self.color_group.secondary or QColor())
|
||||
)
|
||||
color = self.edit_secondary_modal.getColor(initial=initial)
|
||||
if color.isValid():
|
||||
self.update_secondary(color)
|
||||
self.preview_button.set_tag_color_group(self.build_color()[1])
|
||||
else:
|
||||
logger.info("[BuildColorPanel] Secondary color selection was cancelled!")
|
||||
|
||||
def update_primary(self, color: QColor):
|
||||
logger.info("[BuildColorPanel] Updating Primary", primary_color=color)
|
||||
|
||||
highlight_color = get_highlight_color(color)
|
||||
text_color = get_text_color(color, highlight_color)
|
||||
border_color = get_border_color(color)
|
||||
|
||||
hex_code = color.name().upper()
|
||||
self.primary_button.setText(hex_code)
|
||||
self.primary_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"background: rgba{color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 4px;"
|
||||
f"padding-bottom: 1px;"
|
||||
f"padding-left: 4px;"
|
||||
f"font-size: 13px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::pressed{{"
|
||||
f"background: rgba{highlight_color.toTuple()};"
|
||||
f"color: rgba{color.toTuple()};"
|
||||
f"border-color: rgba{color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"padding-right: 0px;"
|
||||
f"padding-left: 0px;"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 1px;"
|
||||
f"outline-radius: 4px;"
|
||||
f"outline-color: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
self.preview_button.set_tag_color_group(self.build_color()[1])
|
||||
|
||||
def update_secondary(self, color: QColor | None = None, color_border: bool = False):
|
||||
logger.info("[BuildColorPanel] Updating Secondary", color=color)
|
||||
|
||||
color_ = color or QColor(palette.get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
|
||||
|
||||
highlight_color = get_highlight_color(color_)
|
||||
text_color = get_text_color(color_, highlight_color)
|
||||
border_color = get_border_color(color_)
|
||||
|
||||
hex_code = "" if not color else color.name().upper()
|
||||
self.secondary_button.setText(
|
||||
Translations["color.title.no_color"] if not color else hex_code
|
||||
)
|
||||
self.secondary_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"background: rgba{color_.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 4px;"
|
||||
f"padding-bottom: 1px;"
|
||||
f"padding-left: 4px;"
|
||||
f"font-size: 13px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::pressed{{"
|
||||
f"background: rgba{highlight_color.toTuple()};"
|
||||
f"color: rgba{color_.toTuple()};"
|
||||
f"border-color: rgba{color_.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"padding-right: 0px;"
|
||||
f"padding-left: 0px;"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 1px;"
|
||||
f"outline-radius: 4px;"
|
||||
f"outline-color: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
self.preview_button.set_tag_color_group(self.build_color()[1])
|
||||
|
||||
def update_known_colors(self):
|
||||
groups = self.lib.tag_color_groups
|
||||
colors = groups.get(self.color_group.namespace, [])
|
||||
self.known_colors = {c.slug for c in colors}
|
||||
with contextlib.suppress(KeyError):
|
||||
self.known_colors.remove(self.color_group.slug)
|
||||
|
||||
def update_preview_text(self):
|
||||
self.preview_button.button.setText(
|
||||
f"{self.name_field.text().strip() or Translations["color.placeholder"]} "
|
||||
f"({self.lib.get_namespace_name(self.color_group.namespace)})"
|
||||
)
|
||||
self.preview_button.button.setMaximumWidth(self.preview_button.button.sizeHint().width())
|
||||
|
||||
def no_collide(self, slug: str) -> str:
|
||||
"""Return a slug name that's verified not to collide with other known color slugs."""
|
||||
if slug and slug in self.known_colors:
|
||||
split_slug: list[str] = slug.rsplit("-", 1)
|
||||
suffix: str = ""
|
||||
if len(split_slug) > 1:
|
||||
suffix = split_slug[1]
|
||||
|
||||
if suffix:
|
||||
try:
|
||||
suffix_num: int = int(suffix)
|
||||
return self.no_collide(f"{split_slug[0]}-{suffix_num+1}")
|
||||
except ValueError:
|
||||
return self.no_collide(f"{slug}-2")
|
||||
else:
|
||||
return self.no_collide(f"{slug}-2")
|
||||
return slug
|
||||
|
||||
def on_text_changed(self):
|
||||
slug = self.no_collide(slugify(self.name_field.text().strip(), allow_reserved=True))
|
||||
|
||||
is_name_empty = not self.name_field.text().strip()
|
||||
is_slug_empty = not slug
|
||||
is_invalid = False
|
||||
|
||||
self.name_field.setStyleSheet(
|
||||
f"border: 1px solid {get_ui_color(ColorType.PRIMARY, UiColor.RED)}; border-radius: 2px"
|
||||
if is_name_empty
|
||||
else ""
|
||||
)
|
||||
|
||||
self.slug_field.setStyleSheet(
|
||||
f"border: 1px solid {get_ui_color(ColorType.PRIMARY, UiColor.RED)}; border-radius: 2px"
|
||||
if is_slug_empty or is_invalid
|
||||
else ""
|
||||
)
|
||||
|
||||
self.slug_field.setText(slug)
|
||||
self.update_preview_text()
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_name_empty)
|
||||
|
||||
def build_color(self) -> tuple[TagColorGroup, TagColorGroup]:
|
||||
name = self.name_field.text()
|
||||
slug = self.slug_field.text()
|
||||
primary: str = self.primary_button.text()
|
||||
secondary: str | None = (
|
||||
self.secondary_button.text() if self.secondary_button.text().startswith("#") else None
|
||||
)
|
||||
color_border: bool = self.border_checkbox.isChecked()
|
||||
|
||||
new_color = TagColorGroup(
|
||||
slug=slug,
|
||||
namespace=self.color_group.namespace,
|
||||
name=name,
|
||||
primary=primary,
|
||||
secondary=secondary,
|
||||
color_border=color_border,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[BuildColorPanel] Built Color",
|
||||
slug=new_color.slug,
|
||||
namespace=new_color.namespace,
|
||||
name=new_color.name,
|
||||
primary=new_color.primary,
|
||||
secondary=new_color.secondary,
|
||||
color_border=new_color.color_border,
|
||||
)
|
||||
return (self.color_group, new_color)
|
||||
|
||||
def parent_post_init(self):
|
||||
# self.setTabOrder(self.name_field, self.shorthand_field)
|
||||
# self.setTabOrder(self.shorthand_field, self.aliases_add_button)
|
||||
# self.setTabOrder(self.aliases_add_button, self.parent_tags_add_button)
|
||||
# self.setTabOrder(self.parent_tags_add_button, self.color_button)
|
||||
# self.setTabOrder(self.color_button, self.panel_cancel_button)
|
||||
# self.setTabOrder(self.panel_cancel_button, self.panel_save_button)
|
||||
# self.setTabOrder(self.panel_save_button, self.aliases_table.cellWidget(0, 1))
|
||||
self.name_field.selectAll()
|
||||
self.name_field.setFocus()
|
||||
168
tagstudio/src/qt/modals/build_namespace.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import contextlib
|
||||
from uuid import uuid4
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from src.core.library import Library
|
||||
from src.core.library.alchemy.library import ReservedNamespaceError, slugify
|
||||
from src.core.library.alchemy.models import Namespace
|
||||
from src.core.palette import ColorType, UiColor, get_ui_color
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class BuildNamespacePanel(PanelWidget):
|
||||
on_edit = Signal(Namespace)
|
||||
|
||||
def __init__(self, library: Library, namespace: Namespace | None = None):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.namespace: Namespace | None = namespace
|
||||
|
||||
self.known_namespaces: set[str]
|
||||
self.update_known_namespaces()
|
||||
|
||||
self.setMinimumSize(360, 260)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Name -----------------------------------------------------------------
|
||||
self.name_widget = QWidget()
|
||||
self.name_layout = QVBoxLayout(self.name_widget)
|
||||
self.name_layout.setStretch(1, 1)
|
||||
self.name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.name_layout.setSpacing(0)
|
||||
self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.name_title = QLabel(Translations["library_object.name"])
|
||||
self.name_layout.addWidget(self.name_title)
|
||||
self.name_field = QLineEdit()
|
||||
self.name_field.setFixedHeight(24)
|
||||
self.name_field.textChanged.connect(self.on_text_changed)
|
||||
self.name_field.setPlaceholderText(Translations["library_object.name_required"])
|
||||
self.name_layout.addWidget(self.name_field)
|
||||
|
||||
# Slug -----------------------------------------------------------------
|
||||
self.slug_widget = QWidget()
|
||||
self.slug_layout = QVBoxLayout(self.slug_widget)
|
||||
self.slug_layout.setStretch(1, 1)
|
||||
self.slug_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.slug_layout.setSpacing(0)
|
||||
self.slug_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.slug_title = QLabel(Translations["library_object.slug"])
|
||||
self.slug_layout.addWidget(self.slug_title)
|
||||
self.slug_field = QLineEdit()
|
||||
self.slug_field.setFixedHeight(24)
|
||||
self.slug_field.setEnabled(False)
|
||||
self.slug_field.setPlaceholderText(Translations["library_object.slug_required"])
|
||||
self.slug_layout.addWidget(self.slug_field)
|
||||
|
||||
# Description ----------------------------------------------------------
|
||||
self.desc_label = QLabel(Translations["namespace.create.description"])
|
||||
self.desc_label.setWordWrap(True)
|
||||
self.desc_color_label = QLabel(Translations["namespace.create.description_color"])
|
||||
self.desc_color_label.setWordWrap(True)
|
||||
|
||||
# Add Widgets to Layout ================================================
|
||||
self.root_layout.addWidget(self.name_widget)
|
||||
self.root_layout.addWidget(self.slug_widget)
|
||||
self.root_layout.addSpacing(12)
|
||||
self.root_layout.addWidget(self.desc_label)
|
||||
self.root_layout.addSpacing(6)
|
||||
self.root_layout.addWidget(self.desc_color_label)
|
||||
|
||||
self.set_namespace(namespace)
|
||||
|
||||
def set_namespace(self, namespace: Namespace | None):
|
||||
logger.info("[BuildNamespacePanel] Setting Namespace", namespace=namespace)
|
||||
self.namespace = namespace
|
||||
|
||||
if namespace:
|
||||
self.name_field.setText(namespace.name)
|
||||
self.slug_field.setText(namespace.namespace)
|
||||
else:
|
||||
self.name_field.setText("User Colors")
|
||||
|
||||
def update_known_namespaces(self):
|
||||
namespaces = self.lib.namespaces
|
||||
self.known_namespaces = {n.namespace for n in namespaces}
|
||||
if self.namespace:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.known_namespaces.remove(self.namespace.namespace)
|
||||
|
||||
def on_text_changed(self):
|
||||
slug = ""
|
||||
try:
|
||||
slug = self.no_collide(slugify(self.name_field.text().strip()))
|
||||
except ReservedNamespaceError:
|
||||
raw_name = self.name_field.text().strip().lower()
|
||||
raw_name = raw_name.replace(RESERVED_NAMESPACE_PREFIX, str(uuid4()).split("-", 1)[0])
|
||||
slug = self.no_collide(slugify(raw_name))
|
||||
|
||||
is_name_empty = not self.name_field.text().strip()
|
||||
is_slug_empty = not slug
|
||||
is_invalid = False
|
||||
|
||||
self.name_field.setStyleSheet(
|
||||
f"border: 1px solid {get_ui_color(ColorType.PRIMARY, UiColor.RED)}; border-radius: 2px"
|
||||
if is_name_empty
|
||||
else ""
|
||||
)
|
||||
|
||||
self.slug_field.setStyleSheet(
|
||||
f"border: 1px solid {get_ui_color(ColorType.PRIMARY, UiColor.RED)}; border-radius: 2px"
|
||||
if is_slug_empty or is_invalid
|
||||
else ""
|
||||
)
|
||||
|
||||
self.slug_field.setText(slug)
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_name_empty)
|
||||
|
||||
def no_collide(self, slug: str) -> str:
|
||||
"""Return a slug name that's verified not to collide with other known namespace slugs."""
|
||||
if slug and slug in self.known_namespaces:
|
||||
split_slug: list[str] = slug.rsplit("-", 1)
|
||||
suffix: str = ""
|
||||
if len(split_slug) > 1:
|
||||
suffix = split_slug[1]
|
||||
|
||||
if suffix:
|
||||
try:
|
||||
suffix_num: int = int(suffix)
|
||||
return self.no_collide(f"{split_slug[0]}-{suffix_num+1}")
|
||||
except ValueError:
|
||||
return self.no_collide(f"{slug}-2")
|
||||
else:
|
||||
return self.no_collide(f"{slug}-2")
|
||||
return slug
|
||||
|
||||
def build_namespace(self) -> Namespace:
|
||||
name = self.name_field.text()
|
||||
slug_raw = self.slug_field.text()
|
||||
slug = slugify(slug_raw)
|
||||
|
||||
namespace = Namespace(namespace=slug, name=name)
|
||||
|
||||
logger.info("[BuildNamespacePanel] Built Namespace", slug=slug, name=name)
|
||||
return namespace
|
||||
|
||||
def parent_post_init(self):
|
||||
self.setTabOrder(self.name_field, self.slug_field)
|
||||
self.name_field.selectAll()
|
||||
self.name_field.setFocus()
|
||||
@@ -86,15 +86,12 @@ class BuildTagPanel(PanelWidget):
|
||||
self.name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.name_layout.setSpacing(0)
|
||||
self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.name_title = QLabel()
|
||||
Translations.translate_qobject(self.name_title, "tag.name")
|
||||
self.name_title = QLabel(Translations["tag.name"])
|
||||
self.name_layout.addWidget(self.name_title)
|
||||
self.name_field = QLineEdit()
|
||||
self.name_field.setFixedHeight(24)
|
||||
self.name_field.textChanged.connect(self.on_name_changed)
|
||||
Translations.translate_with_setter(
|
||||
self.name_field.setPlaceholderText, "tag.tag_name_required"
|
||||
)
|
||||
self.name_field.setPlaceholderText(Translations["tag.tag_name_required"])
|
||||
self.name_layout.addWidget(self.name_field)
|
||||
|
||||
# Shorthand ------------------------------------------------------------
|
||||
@@ -104,8 +101,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.shorthand_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.shorthand_layout.setSpacing(0)
|
||||
self.shorthand_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.shorthand_title = QLabel()
|
||||
Translations.translate_qobject(self.shorthand_title, "tag.shorthand")
|
||||
self.shorthand_title = QLabel(Translations["tag.shorthand"])
|
||||
self.shorthand_layout.addWidget(self.shorthand_title)
|
||||
self.shorthand_field = QLineEdit()
|
||||
self.shorthand_layout.addWidget(self.shorthand_field)
|
||||
@@ -117,8 +113,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.aliases_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.aliases_layout.setSpacing(0)
|
||||
self.aliases_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.aliases_title = QLabel()
|
||||
Translations.translate_qobject(self.aliases_title, "tag.aliases")
|
||||
self.aliases_title = QLabel(Translations["tag.aliases"])
|
||||
self.aliases_layout.addWidget(self.aliases_title)
|
||||
|
||||
self.aliases_table = QTableWidget(0, 2)
|
||||
@@ -144,8 +139,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.disam_button_group = QButtonGroup(self)
|
||||
self.disam_button_group.setExclusive(False)
|
||||
|
||||
self.parent_tags_title = QLabel()
|
||||
Translations.translate_qobject(self.parent_tags_title, "tag.parent_tags")
|
||||
self.parent_tags_title = QLabel(Translations["tag.parent_tags"])
|
||||
self.parent_tags_layout.addWidget(self.parent_tags_title)
|
||||
|
||||
self.scroll_contents = QWidget()
|
||||
@@ -173,8 +167,8 @@ class BuildTagPanel(PanelWidget):
|
||||
tsp = TagSearchPanel(self.lib, exclude_ids)
|
||||
tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x))
|
||||
self.add_tag_modal = PanelModal(tsp)
|
||||
Translations.translate_with_setter(self.add_tag_modal.setTitle, "tag.parent_tags.add")
|
||||
Translations.translate_with_setter(self.add_tag_modal.setWindowTitle, "tag.parent_tags.add")
|
||||
self.add_tag_modal.setTitle(Translations["tag.parent_tags.add"])
|
||||
self.add_tag_modal.setWindowTitle(Translations["tag.parent_tags.add"])
|
||||
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
|
||||
|
||||
# Color ----------------------------------------------------------------
|
||||
@@ -184,18 +178,17 @@ class BuildTagPanel(PanelWidget):
|
||||
self.color_layout.setContentsMargins(0, 0, 0, 6)
|
||||
self.color_layout.setSpacing(6)
|
||||
self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.color_title = QLabel()
|
||||
Translations.translate_qobject(self.color_title, "tag.color")
|
||||
self.color_title = QLabel(Translations["tag.color"])
|
||||
self.color_layout.addWidget(self.color_title)
|
||||
self.color_button: TagColorPreview
|
||||
try:
|
||||
self.color_button = TagColorPreview(tag.color)
|
||||
self.color_button = TagColorPreview(self.lib, tag.color)
|
||||
except Exception as e:
|
||||
# TODO: Investigate why this happens during tests
|
||||
logger.error("[BuildTag] Could not access Tag member attributes", error=e)
|
||||
self.color_button = TagColorPreview(None)
|
||||
self.color_button = TagColorPreview(self.lib, None)
|
||||
self.tag_color_selection = TagColorSelection(self.lib)
|
||||
chose_tag_color_title = Translations.translate_formatted("tag.choose_color")
|
||||
chose_tag_color_title = Translations["tag.choose_color"]
|
||||
self.choose_color_modal = PanelModal(
|
||||
self.tag_color_selection,
|
||||
chose_tag_color_title,
|
||||
@@ -214,8 +207,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.cat_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.cat_layout.setSpacing(6)
|
||||
self.cat_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.cat_title = QLabel()
|
||||
self.cat_title.setText("Is Category")
|
||||
self.cat_title = QLabel(Translations["tag.is_category"])
|
||||
self.cat_checkbox = QCheckBox()
|
||||
self.cat_checkbox.setFixedSize(22, 22)
|
||||
|
||||
@@ -245,6 +237,10 @@ class BuildTagPanel(PanelWidget):
|
||||
f"QCheckBox::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QCheckBox::focus{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"outline:none;"
|
||||
f"}}"
|
||||
)
|
||||
self.cat_layout.addWidget(self.cat_checkbox)
|
||||
self.cat_layout.addWidget(self.cat_title)
|
||||
@@ -372,7 +368,7 @@ class BuildTagPanel(PanelWidget):
|
||||
primary_color = get_primary_color(tag)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (tag.color and tag.color.secondary)
|
||||
if not (tag.color and tag.color.secondary and tag.color.color_border)
|
||||
else (QColor(tag.color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
@@ -400,7 +396,7 @@ class BuildTagPanel(PanelWidget):
|
||||
disam_button = QRadioButton()
|
||||
disam_button.setObjectName(f"disambiguationButton.{parent_id}")
|
||||
disam_button.setFixedSize(22, 22)
|
||||
disam_button.setToolTip(Translations.translate_formatted("tag.disambiguation.tooltip"))
|
||||
disam_button.setToolTip(Translations["tag.disambiguation.tooltip"])
|
||||
disam_button.setStyleSheet(
|
||||
f"QRadioButton{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
|
||||
@@ -7,14 +7,7 @@ from typing import TYPE_CHECKING, override
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt, QThreadPool, Signal
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListView,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget
|
||||
from src.core.utils.missing_files import MissingRegistry
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.translations import Translations
|
||||
@@ -32,20 +25,19 @@ class DeleteUnlinkedEntriesModal(QWidget):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.tracker = tracker
|
||||
Translations.translate_with_setter(self.setWindowTitle, "entries.unlinked.delete")
|
||||
self.setWindowTitle(Translations["entries.unlinked.delete"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(500, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.desc_widget = QLabel()
|
||||
self.desc_widget = QLabel(
|
||||
Translations["entries.unlinked.delete.confirm"].format(
|
||||
count=self.tracker.missing_file_entries_count,
|
||||
)
|
||||
)
|
||||
self.desc_widget.setObjectName("descriptionLabel")
|
||||
self.desc_widget.setWordWrap(True)
|
||||
Translations.translate_qobject(
|
||||
self.desc_widget,
|
||||
"entries.unlinked.delete.confirm",
|
||||
count=self.tracker.missing_file_entries_count,
|
||||
)
|
||||
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.list_view = QListView()
|
||||
@@ -57,14 +49,12 @@ class DeleteUnlinkedEntriesModal(QWidget):
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
Translations.translate_qobject(self.cancel_button, "generic.cancel_alt")
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel_alt"])
|
||||
self.cancel_button.setDefault(True)
|
||||
self.cancel_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.delete_button = QPushButton()
|
||||
Translations.translate_qobject(self.delete_button, "generic.delete_alt")
|
||||
self.delete_button = QPushButton(Translations["generic.delete_alt"])
|
||||
self.delete_button.clicked.connect(self.hide)
|
||||
self.delete_button.clicked.connect(lambda: self.delete_entries())
|
||||
self.button_layout.addWidget(self.delete_button)
|
||||
@@ -75,8 +65,8 @@ class DeleteUnlinkedEntriesModal(QWidget):
|
||||
|
||||
def refresh_list(self):
|
||||
self.desc_widget.setText(
|
||||
Translations.translate_formatted(
|
||||
"entries.unlinked.delete.confirm", count=self.tracker.missing_file_entries_count
|
||||
Translations["entries.unlinked.delete.confirm"].format(
|
||||
count=self.tracker.missing_file_entries_count
|
||||
)
|
||||
)
|
||||
|
||||
@@ -87,20 +77,13 @@ class DeleteUnlinkedEntriesModal(QWidget):
|
||||
self.model.appendRow(item)
|
||||
|
||||
def delete_entries(self):
|
||||
def displayed_text(x):
|
||||
return Translations.translate_formatted(
|
||||
"entries.unlinked.delete.deleting_count",
|
||||
idx=x,
|
||||
count=self.tracker.missing_file_entries_count,
|
||||
)
|
||||
|
||||
pw = ProgressWidget(
|
||||
cancel_button_text=None,
|
||||
minimum=0,
|
||||
maximum=0,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "entries.unlinked.delete.deleting")
|
||||
Translations.translate_with_setter(pw.update_label, "entries.unlinked.delete.deleting")
|
||||
pw.setWindowTitle(Translations["entries.unlinked.delete.deleting"])
|
||||
pw.update_label(Translations["entries.unlinked.delete.deleting"])
|
||||
pw.show()
|
||||
|
||||
r = CustomRunnable(self.tracker.execute_deletion)
|
||||
|
||||
@@ -11,14 +11,7 @@ import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListView,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
@@ -44,7 +37,7 @@ class DropImportModal(QWidget):
|
||||
self.driver: QtDriver = driver
|
||||
|
||||
# Widget ======================
|
||||
Translations.translate_with_setter(self.setWindowTitle, "drop_import.title")
|
||||
self.setWindowTitle(Translations["drop_import.title"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(500, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
@@ -67,26 +60,22 @@ class DropImportModal(QWidget):
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.skip_button = QPushButton()
|
||||
Translations.translate_qobject(self.skip_button, "generic.skip_alt")
|
||||
self.skip_button = QPushButton(Translations["generic.skip_alt"])
|
||||
self.skip_button.setDefault(True)
|
||||
self.skip_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.SKIP))
|
||||
self.button_layout.addWidget(self.skip_button)
|
||||
|
||||
self.overwrite_button = QPushButton()
|
||||
Translations.translate_qobject(self.overwrite_button, "generic.overwrite_alt")
|
||||
self.overwrite_button = QPushButton(Translations["generic.overwrite_alt"])
|
||||
self.overwrite_button.clicked.connect(
|
||||
lambda: self.begin_transfer(DuplicateChoice.OVERWRITE)
|
||||
)
|
||||
self.button_layout.addWidget(self.overwrite_button)
|
||||
|
||||
self.rename_button = QPushButton()
|
||||
Translations.translate_qobject(self.rename_button, "generic.rename_alt")
|
||||
self.rename_button = QPushButton(Translations["generic.rename_alt"])
|
||||
self.rename_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.RENAME))
|
||||
self.button_layout.addWidget(self.rename_button)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
Translations.translate_qobject(self.cancel_button, "generic.cancel_alt")
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel_alt"])
|
||||
self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL))
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
@@ -142,8 +131,8 @@ class DropImportModal(QWidget):
|
||||
self.desc_widget.setText(
|
||||
Translations["drop_import.duplicates_choice.singular"]
|
||||
if len(self.duplicate_files) == 1
|
||||
else Translations.translate_formatted(
|
||||
"drop_import.duplicates_choice.plural", count=len(self.duplicate_files)
|
||||
else Translations["drop_import.duplicates_choice.plural"].format(
|
||||
count=len(self.duplicate_files)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,10 +154,11 @@ class DropImportModal(QWidget):
|
||||
return
|
||||
|
||||
def displayed_text(x):
|
||||
return Translations.translate_formatted(
|
||||
return Translations[
|
||||
"drop_import.progress.label.singular"
|
||||
if x[0] + 1 == 1
|
||||
else "drop_import.progress.label.plural",
|
||||
else "drop_import.progress.label.plural"
|
||||
].format(
|
||||
count=x[0] + 1,
|
||||
suffix=f" {x[1]} {self.choice.value}" if self.choice else "",
|
||||
)
|
||||
@@ -178,8 +168,8 @@ class DropImportModal(QWidget):
|
||||
minimum=0,
|
||||
maximum=len(self.files),
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "drop_import.progress.window_title")
|
||||
Translations.translate_with_setter(pw.update_label, "drop_import.progress.label.initial")
|
||||
pw.setWindowTitle(Translations["drop_import.progress.window_title"])
|
||||
pw.update_label(Translations["drop_import.progress.label.initial"])
|
||||
|
||||
pw.from_iterable_function(
|
||||
self.copy_files,
|
||||
|
||||
@@ -36,7 +36,7 @@ class FileExtensionModal(PanelWidget):
|
||||
super().__init__()
|
||||
# Initialize Modal =====================================================
|
||||
self.lib = library
|
||||
Translations.translate_with_setter(self.setWindowTitle, "ignore_list.title")
|
||||
self.setWindowTitle(Translations["ignore_list.title"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(240, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
@@ -50,8 +50,7 @@ class FileExtensionModal(PanelWidget):
|
||||
self.table.setItemDelegate(FileExtensionItemDelegate())
|
||||
|
||||
# Create "Add Button" Widget -------------------------------------------
|
||||
self.add_button = QPushButton()
|
||||
Translations.translate_qobject(self.add_button, "ignore_list.add_extension")
|
||||
self.add_button = QPushButton(Translations["ignore_list.add_extension"])
|
||||
self.add_button.clicked.connect(self.add_item)
|
||||
self.add_button.setDefault(True)
|
||||
self.add_button.setMinimumWidth(100)
|
||||
@@ -61,18 +60,13 @@ class FileExtensionModal(PanelWidget):
|
||||
self.mode_layout = QHBoxLayout(self.mode_widget)
|
||||
self.mode_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.mode_layout.setSpacing(12)
|
||||
self.mode_label = QLabel()
|
||||
Translations.translate_qobject(self.mode_label, "ignore_list.mode.label")
|
||||
self.mode_label = QLabel(Translations["ignore_list.mode.label"])
|
||||
self.mode_combobox = QComboBox()
|
||||
self.mode_combobox.setEditable(False)
|
||||
self.mode_combobox.addItem("")
|
||||
self.mode_combobox.addItem("")
|
||||
Translations.translate_with_setter(
|
||||
lambda text: self.mode_combobox.setItemText(0, text), "ignore_list.mode.include"
|
||||
)
|
||||
Translations.translate_with_setter(
|
||||
lambda text: self.mode_combobox.setItemText(1, text), "ignore_list.mode.exclude"
|
||||
)
|
||||
self.mode_combobox.setItemText(0, Translations["ignore_list.mode.include"])
|
||||
self.mode_combobox.setItemText(1, Translations["ignore_list.mode.exclude"])
|
||||
|
||||
is_exclude_list = int(bool(self.lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST)))
|
||||
|
||||
|
||||
@@ -7,14 +7,7 @@ from typing import TYPE_CHECKING, override
|
||||
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QFileDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
from src.core.library import Library
|
||||
from src.core.utils.dupe_files import DupeRegistry
|
||||
from src.qt.modals.mirror_entities import MirrorEntriesModal
|
||||
@@ -32,7 +25,7 @@ class FixDupeFilesModal(QWidget):
|
||||
self.driver = driver
|
||||
self.count = -1
|
||||
self.filename = ""
|
||||
Translations.translate_with_setter(self.setWindowTitle, "file.duplicates.fix")
|
||||
self.setWindowTitle(Translations["file.duplicates.fix"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
@@ -40,11 +33,10 @@ class FixDupeFilesModal(QWidget):
|
||||
|
||||
self.tracker = DupeRegistry(library=self.lib)
|
||||
|
||||
self.desc_widget = QLabel()
|
||||
self.desc_widget = QLabel(Translations["file.duplicates.description"])
|
||||
self.desc_widget.setObjectName("descriptionLabel")
|
||||
self.desc_widget.setWordWrap(True)
|
||||
self.desc_widget.setStyleSheet("text-align:left;")
|
||||
Translations.translate_qobject(self.desc_widget, "file.duplicates.description")
|
||||
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.dupe_count = QLabel()
|
||||
@@ -52,35 +44,29 @@ class FixDupeFilesModal(QWidget):
|
||||
self.dupe_count.setStyleSheet("font-weight:bold;" "font-size:14px;" "")
|
||||
self.dupe_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.file_label = QLabel()
|
||||
self.file_label = QLabel(Translations["file.duplicates.dupeguru.no_file"])
|
||||
self.file_label.setObjectName("fileLabel")
|
||||
Translations.translate_qobject(self.file_label, "file.duplicates.dupeguru.no_file")
|
||||
|
||||
self.open_button = QPushButton()
|
||||
Translations.translate_qobject(self.open_button, "file.duplicates.dupeguru.load_file")
|
||||
self.open_button = QPushButton(Translations["file.duplicates.dupeguru.load_file"])
|
||||
self.open_button.clicked.connect(self.select_file)
|
||||
|
||||
self.mirror_modal = MirrorEntriesModal(self.driver, self.tracker)
|
||||
self.mirror_modal.done.connect(self.refresh_dupes)
|
||||
|
||||
self.mirror_button = QPushButton()
|
||||
Translations.translate_qobject(self.mirror_button, "file.duplicates.mirror_entries")
|
||||
self.mirror_button = QPushButton(Translations["file.duplicates.mirror_entries"])
|
||||
self.mirror_button.clicked.connect(self.mirror_modal.show)
|
||||
self.mirror_desc = QLabel()
|
||||
self.mirror_desc = QLabel(Translations["file.duplicates.mirror.description"])
|
||||
self.mirror_desc.setWordWrap(True)
|
||||
Translations.translate_qobject(self.mirror_desc, "file.duplicates.mirror.description")
|
||||
|
||||
self.advice_label = QLabel()
|
||||
self.advice_label = QLabel(Translations["file.duplicates.dupeguru.advice"])
|
||||
self.advice_label.setWordWrap(True)
|
||||
Translations.translate_qobject(self.advice_label, "file.duplicates.dupeguru.advice")
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.done_button = QPushButton()
|
||||
Translations.translate_qobject(self.done_button, "generic.done_alt")
|
||||
self.done_button = QPushButton(Translations["generic.done_alt"])
|
||||
self.done_button.setDefault(True)
|
||||
self.done_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
@@ -128,14 +114,10 @@ class FixDupeFilesModal(QWidget):
|
||||
self.dupe_count.setText(Translations["file.duplicates.matches_uninitialized"])
|
||||
elif count == 0:
|
||||
self.mirror_button.setDisabled(True)
|
||||
self.dupe_count.setText(
|
||||
Translations.translate_formatted("file.duplicates.matches", count=count)
|
||||
)
|
||||
self.dupe_count.setText(Translations["file.duplicates.matches"].format(count=count))
|
||||
else:
|
||||
self.mirror_button.setDisabled(False)
|
||||
self.dupe_count.setText(
|
||||
Translations.translate_formatted("file.duplicates.matches", count=count)
|
||||
)
|
||||
self.dupe_count.setText(Translations["file.duplicates.matches"].format(count=count))
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
|
||||
@@ -31,17 +31,16 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
|
||||
self.missing_count = -1
|
||||
self.dupe_count = -1
|
||||
Translations.translate_with_setter(self.setWindowTitle, "entries.unlinked.title")
|
||||
self.setWindowTitle(Translations["entries.unlinked.title"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.unlinked_desc_widget = QLabel()
|
||||
self.unlinked_desc_widget = QLabel(Translations["entries.unlinked.description"])
|
||||
self.unlinked_desc_widget.setObjectName("unlinkedDescriptionLabel")
|
||||
self.unlinked_desc_widget.setWordWrap(True)
|
||||
self.unlinked_desc_widget.setStyleSheet("text-align:left;")
|
||||
Translations.translate_qobject(self.unlinked_desc_widget, "entries.unlinked.description")
|
||||
|
||||
self.missing_count_label = QLabel()
|
||||
self.missing_count_label.setObjectName("missingCountLabel")
|
||||
@@ -53,15 +52,13 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.dupe_count_label.setStyleSheet("font-weight:bold;" "font-size:14px;")
|
||||
self.dupe_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.refresh_unlinked_button = QPushButton()
|
||||
Translations.translate_qobject(self.refresh_unlinked_button, "entries.unlinked.refresh_all")
|
||||
self.refresh_unlinked_button = QPushButton(Translations["entries.unlinked.refresh_all"])
|
||||
self.refresh_unlinked_button.clicked.connect(self.refresh_missing_files)
|
||||
|
||||
self.merge_class = MergeDuplicateEntries(self.lib, self.driver)
|
||||
self.relink_class = RelinkUnlinkedEntries(self.tracker)
|
||||
|
||||
self.search_button = QPushButton()
|
||||
Translations.translate_qobject(self.search_button, "entries.unlinked.search_and_relink")
|
||||
self.search_button = QPushButton(Translations["entries.unlinked.search_and_relink"])
|
||||
self.relink_class.done.connect(
|
||||
# refresh the grid
|
||||
lambda: (
|
||||
@@ -71,11 +68,10 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
)
|
||||
self.search_button.clicked.connect(self.relink_class.repair_entries)
|
||||
|
||||
self.manual_button = QPushButton()
|
||||
Translations.translate_qobject(self.manual_button, "entries.unlinked.relink.manual")
|
||||
self.manual_button = QPushButton(Translations["entries.unlinked.relink.manual"])
|
||||
self.manual_button.setHidden(True)
|
||||
|
||||
self.delete_button = QPushButton()
|
||||
self.delete_button = QPushButton(Translations["entries.unlinked.delete_alt"])
|
||||
self.delete_modal = DeleteUnlinkedEntriesModal(self.driver, self.tracker)
|
||||
self.delete_modal.done.connect(
|
||||
lambda: (
|
||||
@@ -84,7 +80,6 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.driver.filter_items(),
|
||||
)
|
||||
)
|
||||
Translations.translate_qobject(self.delete_button, "entries.unlinked.delete_alt")
|
||||
self.delete_button.clicked.connect(self.delete_modal.show)
|
||||
|
||||
self.button_container = QWidget()
|
||||
@@ -92,8 +87,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.done_button = QPushButton()
|
||||
Translations.translate_qobject(self.done_button, "generic.done_alt")
|
||||
self.done_button = QPushButton(Translations["generic.done_alt"])
|
||||
self.done_button.setDefault(True)
|
||||
self.done_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
@@ -116,8 +110,8 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
minimum=0,
|
||||
maximum=self.lib.entries_count,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "library.scan_library.title")
|
||||
Translations.translate_with_setter(pw.update_label, "entries.unlinked.scanning")
|
||||
pw.setWindowTitle(Translations["library.scan_library.title"])
|
||||
pw.update_label(Translations["entries.unlinked.scanning"])
|
||||
|
||||
pw.from_iterable_function(
|
||||
self.tracker.refresh_missing_files,
|
||||
@@ -141,9 +135,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.search_button.setDisabled(self.missing_count == 0)
|
||||
self.delete_button.setDisabled(self.missing_count == 0)
|
||||
self.missing_count_label.setText(
|
||||
Translations.translate_formatted(
|
||||
"entries.unlinked.missing_count.some", count=self.missing_count
|
||||
)
|
||||
Translations["entries.unlinked.missing_count.some"].format(count=self.missing_count)
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -166,17 +166,16 @@ class FoldersToTagsModal(QWidget):
|
||||
self.count = -1
|
||||
self.filename = ""
|
||||
|
||||
Translations.translate_with_setter(self.setWindowTitle, "folders_to_tags.title")
|
||||
self.setWindowTitle(Translations["folders_to_tags.title"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(640, 640)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.title_widget = QLabel()
|
||||
self.title_widget = QLabel(Translations["folders_to_tags.title"])
|
||||
self.title_widget.setObjectName("title")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px")
|
||||
Translations.translate_qobject(self.title_widget, "folders_to_tags.title")
|
||||
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.desc_widget = QLabel()
|
||||
@@ -191,11 +190,9 @@ class FoldersToTagsModal(QWidget):
|
||||
self.open_close_button_w = QWidget()
|
||||
self.open_close_button_layout = QHBoxLayout(self.open_close_button_w)
|
||||
|
||||
self.open_all_button = QPushButton()
|
||||
Translations.translate_qobject(self.open_all_button, "folders_to_tags.open_all")
|
||||
self.open_all_button = QPushButton(Translations["folders_to_tags.open_all"])
|
||||
self.open_all_button.clicked.connect(lambda: self.set_all_branches(False))
|
||||
self.close_all_button = QPushButton()
|
||||
Translations.translate_qobject(self.close_all_button, "folders_to_tags.close_all")
|
||||
self.close_all_button = QPushButton(Translations["folders_to_tags.close_all"])
|
||||
self.close_all_button.clicked.connect(lambda: self.set_all_branches(True))
|
||||
|
||||
self.open_close_button_layout.addWidget(self.open_all_button)
|
||||
@@ -213,8 +210,7 @@ class FoldersToTagsModal(QWidget):
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scroll_area.setWidget(self.scroll_contents)
|
||||
|
||||
self.apply_button = QPushButton()
|
||||
Translations.translate_qobject(self.apply_button, "generic.apply_alt")
|
||||
self.apply_button = QPushButton(Translations["generic.apply_alt"])
|
||||
self.apply_button.setMinimumWidth(100)
|
||||
self.apply_button.clicked.connect(self.on_apply)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class MergeDuplicateEntries(QObject):
|
||||
minimum=0,
|
||||
maximum=self.tracker.groups_count,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "entries.duplicate.merge.label")
|
||||
Translations.translate_with_setter(pw.update_label, "entries.duplicate.merge.label")
|
||||
pw.setWindowTitle(Translations["entries.duplicate.merge.label"])
|
||||
pw.update_label(Translations["entries.duplicate.merge.label"])
|
||||
|
||||
pw.from_iterable_function(self.tracker.merge_dupe_entries, None, self.done.emit)
|
||||
|
||||
@@ -8,14 +8,7 @@ from time import sleep
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListView,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget
|
||||
from src.core.utils.dupe_files import DupeRegistry
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
@@ -31,19 +24,18 @@ class MirrorEntriesModal(QWidget):
|
||||
def __init__(self, driver: "QtDriver", tracker: DupeRegistry):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
Translations.translate_with_setter(self.setWindowTitle, "entries.mirror.window_title")
|
||||
self.setWindowTitle(Translations["entries.mirror.window_title"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(500, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.tracker = tracker
|
||||
|
||||
self.desc_widget = QLabel()
|
||||
self.desc_widget = QLabel(
|
||||
Translations["entries.mirror.confirmation"].format(count=self.tracker.groups_count)
|
||||
)
|
||||
self.desc_widget.setObjectName("descriptionLabel")
|
||||
self.desc_widget.setWordWrap(True)
|
||||
Translations.translate_qobject(
|
||||
self.desc_widget, "entries.mirror.confirmation", count=self.tracker.groups_count
|
||||
)
|
||||
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.list_view = QListView()
|
||||
@@ -55,14 +47,12 @@ class MirrorEntriesModal(QWidget):
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
Translations.translate_qobject(self.cancel_button, "generic.cancel_alt")
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel_alt"])
|
||||
self.cancel_button.setDefault(True)
|
||||
self.cancel_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.mirror_button = QPushButton()
|
||||
Translations.translate_qobject(self.mirror_button, "entries.mirror")
|
||||
self.mirror_button = QPushButton(Translations["entries.mirror"])
|
||||
self.mirror_button.clicked.connect(self.hide)
|
||||
self.mirror_button.clicked.connect(self.mirror_entries)
|
||||
self.button_layout.addWidget(self.mirror_button)
|
||||
@@ -73,9 +63,7 @@ class MirrorEntriesModal(QWidget):
|
||||
|
||||
def refresh_list(self):
|
||||
self.desc_widget.setText(
|
||||
Translations.translate_formatted(
|
||||
"entries.mirror.confirmation", count=self.tracker.groups_count
|
||||
)
|
||||
Translations["entries.mirror.confirmation"].format(count=self.tracker.groups_count)
|
||||
)
|
||||
|
||||
self.model.clear()
|
||||
@@ -84,8 +72,8 @@ class MirrorEntriesModal(QWidget):
|
||||
|
||||
def mirror_entries(self):
|
||||
def displayed_text(x):
|
||||
return Translations.translate_formatted(
|
||||
"entries.mirror.label", idx=x + 1, count=self.tracker.groups_count
|
||||
return Translations["entries.mirror.label"].format(
|
||||
idx=x + 1, count=self.tracker.groups_count
|
||||
)
|
||||
|
||||
pw = ProgressWidget(
|
||||
@@ -93,7 +81,7 @@ class MirrorEntriesModal(QWidget):
|
||||
minimum=0,
|
||||
maximum=self.tracker.groups_count,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "entries.mirror.title")
|
||||
pw.setWindowTitle(Translations["entries.mirror.title"])
|
||||
|
||||
pw.from_iterable_function(
|
||||
self.mirror_entries_runnable,
|
||||
|
||||
@@ -18,8 +18,7 @@ class RelinkUnlinkedEntries(QObject):
|
||||
|
||||
def repair_entries(self):
|
||||
def displayed_text(x):
|
||||
return Translations.translate_formatted(
|
||||
"entries.unlinked.relink.attempting",
|
||||
return Translations["entries.unlinked.relink.attempting"].format(
|
||||
idx=x,
|
||||
missing_count=self.tracker.missing_file_entries_count,
|
||||
fixed_count=self.tracker.files_fixed_count,
|
||||
@@ -31,6 +30,6 @@ class RelinkUnlinkedEntries(QObject):
|
||||
minimum=0,
|
||||
maximum=self.tracker.missing_file_entries_count,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "entries.unlinked.relink.title")
|
||||
pw.setWindowTitle(Translations["entries.unlinked.relink.title"])
|
||||
|
||||
pw.from_iterable_function(self.tracker.fix_unlinked_entries, displayed_text, self.done.emit)
|
||||
|
||||
71
tagstudio/src/qt/modals/settings_panel.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget
|
||||
from src.core.enums import SettingItems
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
|
||||
|
||||
class SettingsPanel(PanelWidget):
|
||||
def __init__(self, driver):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.setMinimumSize(320, 200)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
|
||||
self.form_container = QWidget()
|
||||
self.form_layout = QFormLayout(self.form_container)
|
||||
self.form_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.restart_label = QLabel(Translations["settings.restart_required"])
|
||||
self.restart_label.setHidden(True)
|
||||
self.restart_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
language_label = QLabel(Translations["settings.language"])
|
||||
self.languages = {
|
||||
# "Cantonese (Traditional)": "yue_Hant", # Empty
|
||||
"Chinese (Traditional)": "zh_Hant",
|
||||
# "Czech": "cs", # Minimal
|
||||
# "Danish": "da", # Minimal
|
||||
"Dutch": "nl",
|
||||
"English": "en",
|
||||
"Filipino": "fil",
|
||||
"French": "fr",
|
||||
"German": "de",
|
||||
"Hungarian": "hu",
|
||||
# "Italian": "it", # Minimal
|
||||
"Norwegian Bokmål": "nb_NO",
|
||||
"Polish": "pl",
|
||||
"Portuguese (Brazil)": "pt_BR",
|
||||
# "Portuguese (Portugal)": "pt", # Empty
|
||||
"Russian": "ru",
|
||||
"Spanish": "es",
|
||||
"Swedish": "sv",
|
||||
"Tamil": "ta",
|
||||
"Toki Pona": "tok",
|
||||
"Turkish": "tr",
|
||||
}
|
||||
self.language_combobox = QComboBox()
|
||||
self.language_combobox.addItems(list(self.languages.keys()))
|
||||
current_lang: str = str(
|
||||
driver.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str)
|
||||
)
|
||||
current_lang = "en" if current_lang not in self.languages.values() else current_lang
|
||||
self.language_combobox.setCurrentIndex(list(self.languages.values()).index(current_lang))
|
||||
self.language_combobox.currentIndexChanged.connect(
|
||||
lambda: self.restart_label.setHidden(False)
|
||||
)
|
||||
self.form_layout.addRow(language_label, self.language_combobox)
|
||||
|
||||
self.root_layout.addWidget(self.form_container)
|
||||
self.root_layout.addStretch(1)
|
||||
self.root_layout.addWidget(self.restart_label)
|
||||
|
||||
def get_language(self) -> str:
|
||||
values: list[str] = list(self.languages.values())
|
||||
return values[self.language_combobox.currentIndex()]
|
||||
221
tagstudio/src/qt/modals/tag_color_manager.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from src.core.enums import Theme
|
||||
from src.qt.modals.build_namespace import BuildNamespacePanel
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.color_box import ColorBoxWidget
|
||||
from src.qt.widgets.fields import FieldContainer
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
class TagColorManager(QWidget):
|
||||
create_namespace_modal: PanelModal | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver: "QtDriver",
|
||||
):
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.lib = driver.lib
|
||||
self.setWindowTitle(Translations["color_manager.title"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.is_initialized = False
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
panel_bg_color = (
|
||||
Theme.COLOR_BG_DARK.value
|
||||
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
else Theme.COLOR_BG_LIGHT.value
|
||||
)
|
||||
|
||||
self.title_label = QLabel()
|
||||
self.title_label.setObjectName("titleLabel")
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.title_label.setText(f"<h3>{Translations["color_manager.title"]}</h3>")
|
||||
|
||||
self.scroll_layout = QVBoxLayout()
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_layout.setContentsMargins(3, 3, 3, 3)
|
||||
self.scroll_layout.setSpacing(0)
|
||||
|
||||
scroll_container: QWidget = QWidget()
|
||||
scroll_container.setObjectName("entryScrollContainer")
|
||||
scroll_container.setLayout(self.scroll_layout)
|
||||
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setObjectName("entryScrollArea")
|
||||
self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter)
|
||||
self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
self.scroll_area.setStyleSheet(
|
||||
"QWidget#entryScrollContainer{" f"background:{panel_bg_color};" "border-radius:6px;" "}"
|
||||
)
|
||||
self.scroll_area.setWidget(scroll_container)
|
||||
|
||||
self.setup_color_groups()
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.new_namespace_button = QPushButton(Translations["namespace.new.button"])
|
||||
self.new_namespace_button.clicked.connect(self.create_namespace)
|
||||
self.button_layout.addWidget(self.new_namespace_button)
|
||||
|
||||
# self.import_pack_button = QPushButton()
|
||||
# Translations.translate_qobject(self.import_pack_button, "color.import_pack")
|
||||
# self.button_layout.addWidget(self.import_pack_button)
|
||||
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.done_button = QPushButton(Translations["generic.done_alt"])
|
||||
self.done_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
|
||||
self.root_layout.addWidget(self.title_label)
|
||||
self.root_layout.addWidget(self.scroll_area)
|
||||
self.root_layout.addWidget(self.button_container)
|
||||
logger.info(self.root_layout.dumpObjectTree())
|
||||
|
||||
def setup_color_groups(self):
|
||||
all_default = True
|
||||
if self.driver.lib.engine:
|
||||
for group, colors in self.driver.lib.tag_color_groups.items():
|
||||
if not group.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
all_default = False
|
||||
color_box = ColorBoxWidget(group, colors, self.driver.lib)
|
||||
color_box.updated.connect(
|
||||
lambda: (
|
||||
self.reset(),
|
||||
self.setup_color_groups(),
|
||||
()
|
||||
if len(self.driver.selected) < 1
|
||||
else self.driver.preview_panel.fields.update_from_entry(
|
||||
self.driver.selected[0], update_badges=False
|
||||
),
|
||||
)
|
||||
)
|
||||
field_container = FieldContainer(self.driver.lib.get_namespace_name(group))
|
||||
field_container.set_inner_widget(color_box)
|
||||
if not group.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
field_container.set_remove_callback(
|
||||
lambda checked=False, g=group: self.delete_namespace_dialog(
|
||||
prompt=Translations["color.namespace.delete.prompt"],
|
||||
callback=lambda namespace=g: (
|
||||
self.lib.delete_namespace(namespace),
|
||||
self.reset(),
|
||||
self.setup_color_groups(),
|
||||
()
|
||||
if len(self.driver.selected) < 1
|
||||
else self.driver.preview_panel.fields.update_from_entry(
|
||||
self.driver.selected[0], update_badges=False
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.scroll_layout.addWidget(field_container)
|
||||
|
||||
if all_default:
|
||||
ns_container = QWidget()
|
||||
ns_layout = QHBoxLayout(ns_container)
|
||||
ns_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
ns_layout.setContentsMargins(0, 18, 0, 18)
|
||||
namespace_prompt = QPushButton(Translations["namespace.new.prompt"])
|
||||
namespace_prompt.setFixedSize(namespace_prompt.sizeHint().width() + 8, 24)
|
||||
namespace_prompt.clicked.connect(self.create_namespace)
|
||||
ns_layout.addWidget(namespace_prompt)
|
||||
self.scroll_layout.addWidget(ns_container)
|
||||
|
||||
self.is_initialized = True
|
||||
|
||||
def reset(self):
|
||||
while self.scroll_layout.count():
|
||||
widget = self.scroll_layout.itemAt(0).widget()
|
||||
self.scroll_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
self.is_initialized = False
|
||||
|
||||
def create_namespace(self):
|
||||
build_namespace_panel = BuildNamespacePanel(self.lib)
|
||||
|
||||
self.create_namespace_modal = PanelModal(
|
||||
build_namespace_panel,
|
||||
Translations["namespace.create.title"],
|
||||
Translations["namespace.create.title"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
self.create_namespace_modal.saved.connect(
|
||||
lambda: (
|
||||
self.lib.add_namespace(build_namespace_panel.build_namespace()),
|
||||
self.reset(),
|
||||
self.setup_color_groups(),
|
||||
)
|
||||
)
|
||||
|
||||
self.create_namespace_modal.show()
|
||||
|
||||
def delete_namespace_dialog(self, prompt: str, callback: Callable) -> None:
|
||||
message_box = QMessageBox()
|
||||
message_box.setText(prompt)
|
||||
message_box.setWindowTitle(Translations["color.namespace.delete.title"])
|
||||
message_box.setIcon(QMessageBox.Icon.Warning)
|
||||
cancel_button = message_box.addButton(
|
||||
Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole
|
||||
)
|
||||
message_box.addButton(
|
||||
Translations["generic.delete_alt"], QMessageBox.ButtonRole.DestructiveRole
|
||||
)
|
||||
message_box.setEscapeButton(cancel_button)
|
||||
result = message_box.exec_()
|
||||
if result != QMessageBox.ButtonRole.ActionRole.value:
|
||||
return
|
||||
callback()
|
||||
|
||||
@override
|
||||
def showEvent(self, event: QtGui.QShowEvent) -> None: # noqa N802
|
||||
if not self.is_initialized:
|
||||
self.setup_color_groups()
|
||||
return super().showEvent(event)
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape: # noqa SIM114
|
||||
self.done_button.click()
|
||||
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||
self.done_button.click()
|
||||
return super().keyPressEvent(event)
|
||||
@@ -8,21 +8,25 @@ from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QFrame,
|
||||
QLabel,
|
||||
QRadioButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library import Library
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
from src.qt.widgets.tag_color_preview import (
|
||||
from src.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_primary_color,
|
||||
get_text_color,
|
||||
)
|
||||
|
||||
@@ -37,19 +41,37 @@ class TagColorSelection(PanelWidget):
|
||||
|
||||
self.setMinimumSize(308, 540)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setSpacing(6)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.root_layout.setSpacing(0)
|
||||
|
||||
self.scroll_layout = QVBoxLayout()
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.scroll_layout.setSpacing(3)
|
||||
|
||||
scroll_container: QWidget = QWidget()
|
||||
scroll_container.setObjectName("entryScrollContainer")
|
||||
scroll_container.setLayout(self.scroll_layout)
|
||||
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setObjectName("entryScrollArea")
|
||||
self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scroll_area.setWidget(scroll_container)
|
||||
self.root_layout.addWidget(self.scroll_area)
|
||||
|
||||
# Add Widgets to Layout ================================================
|
||||
tag_color_groups = self.lib.tag_color_groups
|
||||
self.button_group = QButtonGroup(self)
|
||||
|
||||
self.add_no_color_widget()
|
||||
self.root_layout.addSpacerItem(QSpacerItem(1, 12))
|
||||
self.scroll_layout.addSpacerItem(QSpacerItem(1, 6))
|
||||
for group, colors in tag_color_groups.items():
|
||||
display_name: str = self.lib.get_namespace_name(group)
|
||||
self.root_layout.addWidget(
|
||||
self.scroll_layout.addWidget(
|
||||
QLabel(f"<h4>{display_name if display_name else group}</h4>")
|
||||
)
|
||||
color_box_widget = QWidget()
|
||||
@@ -59,10 +81,10 @@ class TagColorSelection(PanelWidget):
|
||||
color_group_layout.setContentsMargins(0, 0, 0, 0)
|
||||
color_box_widget.setLayout(color_group_layout)
|
||||
for color in colors:
|
||||
primary_color = get_primary_color(color)
|
||||
primary_color = self._get_primary_color(color)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (color and color.secondary)
|
||||
if not (color and color.secondary and color.color_border)
|
||||
else (QColor(color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
@@ -78,11 +100,15 @@ class TagColorSelection(PanelWidget):
|
||||
radio_button.setObjectName(f"{color.namespace}.{color.slug}")
|
||||
radio_button.setToolTip(color.name)
|
||||
radio_button.setFixedSize(24, 24)
|
||||
bottom_color: str = (
|
||||
f"border-bottom-color: rgba{text_color.toTuple()};" if color.secondary else ""
|
||||
)
|
||||
radio_button.setStyleSheet(
|
||||
f"QRadioButton{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"{bottom_color}"
|
||||
f"border-radius: 3px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
@@ -99,16 +125,22 @@ class TagColorSelection(PanelWidget):
|
||||
f"QRadioButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QRadioButton::focus{{"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 2px;"
|
||||
f"outline-radius: 3px;"
|
||||
f"outline-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
radio_button.clicked.connect(lambda checked=False, x=color: self.select_color(x))
|
||||
color_group_layout.addWidget(radio_button)
|
||||
self.button_group.addButton(radio_button)
|
||||
self.root_layout.addWidget(color_box_widget)
|
||||
self.root_layout.addSpacerItem(QSpacerItem(1, 12))
|
||||
self.scroll_layout.addWidget(color_box_widget)
|
||||
self.scroll_layout.addSpacerItem(QSpacerItem(1, 6))
|
||||
|
||||
def add_no_color_widget(self):
|
||||
no_color_str: str = Translations.translate_formatted("color.title.no_color")
|
||||
self.root_layout.addWidget(QLabel(f"<h4>{no_color_str}</h4>"))
|
||||
no_color_str: str = Translations["color.title.no_color"]
|
||||
self.scroll_layout.addWidget(QLabel(f"<h4>{no_color_str}</h4>"))
|
||||
color_box_widget = QWidget()
|
||||
color_group_layout = FlowLayout()
|
||||
color_group_layout.setSpacing(4)
|
||||
@@ -116,11 +148,11 @@ class TagColorSelection(PanelWidget):
|
||||
color_group_layout.setContentsMargins(0, 0, 0, 0)
|
||||
color_box_widget.setLayout(color_group_layout)
|
||||
color = None
|
||||
primary_color = get_primary_color(color)
|
||||
primary_color = self._get_primary_color(color)
|
||||
border_color = get_border_color(primary_color)
|
||||
highlight_color = get_highlight_color(primary_color)
|
||||
text_color: QColor
|
||||
if color and color.secondary:
|
||||
if color and color.secondary and color.color_border:
|
||||
text_color = QColor(color.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
@@ -150,13 +182,19 @@ class TagColorSelection(PanelWidget):
|
||||
f"QRadioButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QRadioButton::focus{{"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 2px;"
|
||||
f"outline-radius: 3px;"
|
||||
f"outline-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
radio_button.clicked.connect(lambda checked=False, x=color: self.select_color(x))
|
||||
color_group_layout.addWidget(radio_button)
|
||||
self.button_group.addButton(radio_button)
|
||||
self.root_layout.addWidget(color_box_widget)
|
||||
self.scroll_layout.addWidget(color_box_widget)
|
||||
|
||||
def select_color(self, color: TagColorGroup):
|
||||
def select_color(self, color: TagColorGroup | None):
|
||||
self.selected_color = color
|
||||
|
||||
def select_radio_button(self, color: TagColorGroup | None):
|
||||
@@ -164,4 +202,13 @@ class TagColorSelection(PanelWidget):
|
||||
for button in self.button_group.buttons():
|
||||
if button.objectName() == object_name:
|
||||
button.setChecked(True)
|
||||
self.select_color(color)
|
||||
break
|
||||
|
||||
def _get_primary_color(self, tag_color_group: TagColorGroup | None) -> QColor:
|
||||
primary_color = QColor(
|
||||
get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
|
||||
if not tag_color_group
|
||||
else tag_color_group.primary
|
||||
)
|
||||
return primary_color
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import structlog
|
||||
from PySide6.QtWidgets import (
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
)
|
||||
from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from src.core.library import Library, Tag
|
||||
from src.qt.modals.build_tag import BuildTagPanel
|
||||
@@ -26,8 +23,7 @@ class TagDatabasePanel(TagSearchPanel):
|
||||
super().__init__(library, is_tag_chooser=False)
|
||||
self.driver = driver
|
||||
|
||||
self.create_tag_button = QPushButton()
|
||||
Translations.translate_qobject(self.create_tag_button, "tag.create")
|
||||
self.create_tag_button = QPushButton(Translations["tag.create"])
|
||||
self.create_tag_button.clicked.connect(lambda: self.build_tag(self.search_field.text()))
|
||||
|
||||
self.root_layout.addWidget(self.create_tag_button)
|
||||
@@ -38,8 +34,8 @@ class TagDatabasePanel(TagSearchPanel):
|
||||
panel,
|
||||
has_save=True,
|
||||
)
|
||||
Translations.translate_with_setter(self.modal.setTitle, "tag.new")
|
||||
Translations.translate_with_setter(self.modal.setWindowTitle, "tag.new")
|
||||
self.modal.setTitle(Translations["tag.new"])
|
||||
self.modal.setWindowTitle(Translations["tag.new"])
|
||||
if name.strip():
|
||||
panel.name_field.setText(name)
|
||||
|
||||
@@ -57,17 +53,18 @@ class TagDatabasePanel(TagSearchPanel):
|
||||
)
|
||||
self.modal.show()
|
||||
|
||||
def remove_tag(self, tag: Tag):
|
||||
def delete_tag(self, tag: Tag):
|
||||
if tag.id in range(RESERVED_TAG_START, RESERVED_TAG_END):
|
||||
return
|
||||
|
||||
message_box = QMessageBox()
|
||||
Translations.translate_with_setter(message_box.setWindowTitle, "tag.remove")
|
||||
Translations.translate_qobject(
|
||||
message_box, "tag.confirm_delete", tag_name=self.lib.tag_display_name(tag.id)
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Question, # type: ignore
|
||||
Translations["tag.remove"],
|
||||
Translations["tag.confirm_delete"].format(
|
||||
tag_name=self.lib.tag_display_name(tag.id),
|
||||
),
|
||||
QMessageBox.Ok | QMessageBox.Cancel, # type: ignore
|
||||
)
|
||||
message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore
|
||||
message_box.setIcon(QMessageBox.Question) # type: ignore
|
||||
|
||||
result = message_box.exec()
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ class TagSearchPanel(PanelWidget):
|
||||
self.limit_layout.setSpacing(12)
|
||||
self.limit_layout.addStretch(1)
|
||||
|
||||
self.limit_title = QLabel()
|
||||
Translations.translate_qobject(self.limit_title, "tag.view_limit")
|
||||
self.limit_title = QLabel(Translations["tag.view_limit"])
|
||||
self.limit_layout.addWidget(self.limit_title)
|
||||
|
||||
self.limit_combobox = QComboBox()
|
||||
@@ -95,7 +94,7 @@ class TagSearchPanel(PanelWidget):
|
||||
self.search_field = QLineEdit()
|
||||
self.search_field.setObjectName("searchField")
|
||||
self.search_field.setMinimumSize(QSize(0, 32))
|
||||
Translations.translate_with_setter(self.search_field.setPlaceholderText, "home.search_tags")
|
||||
self.search_field.setPlaceholderText(Translations["home.search_tags"])
|
||||
self.search_field.textEdited.connect(lambda text: self.update_tags(text))
|
||||
self.search_field.returnPressed.connect(lambda: self.on_return(self.search_field.text()))
|
||||
|
||||
@@ -119,9 +118,9 @@ class TagSearchPanel(PanelWidget):
|
||||
"""Set the QtDriver for this search panel. Used for main window operations."""
|
||||
self.driver = driver
|
||||
|
||||
def build_create_button(self, query: str | None):
|
||||
def build_create_button(self, query: str | None, key: str, format_args: dict):
|
||||
"""Constructs a "Create & Add Tag" QPushButton."""
|
||||
create_button = QPushButton(self)
|
||||
create_button = QPushButton(Translations[key].format(**format_args), self)
|
||||
create_button.setFlat(True)
|
||||
|
||||
create_button.setMinimumSize(22, 22)
|
||||
@@ -178,8 +177,8 @@ class TagSearchPanel(PanelWidget):
|
||||
|
||||
self.build_tag_modal: BuildTagPanel = build_tag.BuildTagPanel(self.lib)
|
||||
self.add_tag_modal: PanelModal = PanelModal(self.build_tag_modal, has_save=True)
|
||||
Translations.translate_with_setter(self.add_tag_modal.setTitle, "tag.new")
|
||||
Translations.translate_with_setter(self.add_tag_modal.setWindowTitle, "tag.add")
|
||||
self.add_tag_modal.setTitle(Translations["tag.new"])
|
||||
self.add_tag_modal.setWindowTitle(Translations["tag.add"])
|
||||
|
||||
self.build_tag_modal.name_field.setText(name)
|
||||
self.add_tag_modal.saved.connect(on_tag_modal_saved)
|
||||
@@ -245,11 +244,10 @@ class TagSearchPanel(PanelWidget):
|
||||
|
||||
# Add back the "Create & Add" button
|
||||
if query and query.strip():
|
||||
cb: QPushButton = self.build_create_button(query)
|
||||
cb: QPushButton = self.build_create_button(query, "tag.create_add", {"query": query})
|
||||
with catch_warnings(record=True):
|
||||
cb.clicked.disconnect()
|
||||
cb.clicked.connect(lambda: self.create_and_add_tag(query or ""))
|
||||
Translations.translate_qobject(cb, "tag.create_add", query=query)
|
||||
self.scroll_layout.addWidget(cb)
|
||||
self.create_button_in_layout = True
|
||||
|
||||
@@ -284,7 +282,7 @@ class TagSearchPanel(PanelWidget):
|
||||
|
||||
tag_id = tag.id
|
||||
tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t))
|
||||
tag_widget.on_remove.connect(lambda t=tag: self.remove_tag(t))
|
||||
tag_widget.on_remove.connect(lambda t=tag: self.delete_tag(t))
|
||||
tag_widget.bg_button.clicked.connect(lambda: self.tag_chosen.emit(tag_id))
|
||||
|
||||
if self.driver:
|
||||
@@ -348,7 +346,7 @@ class TagSearchPanel(PanelWidget):
|
||||
self.search_field.setFocus()
|
||||
self.search_field.selectAll()
|
||||
|
||||
def remove_tag(self, tag: Tag):
|
||||
def delete_tag(self, tag: Tag):
|
||||
pass
|
||||
|
||||
def edit_tag(self, tag: Tag):
|
||||
@@ -366,7 +364,7 @@ class TagSearchPanel(PanelWidget):
|
||||
done_callback=(self.update_tags(self.search_field.text())),
|
||||
has_save=True,
|
||||
)
|
||||
Translations.translate_with_setter(self.edit_modal.setWindowTitle, "tag.edit")
|
||||
self.edit_modal.setWindowTitle(Translations["tag.edit"])
|
||||
|
||||
self.edit_modal.saved.connect(lambda: callback(build_tag_panel))
|
||||
self.edit_modal.show()
|
||||
|
||||
@@ -1,47 +1,20 @@
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import structlog
|
||||
import ujson
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtWidgets import QLabel, QMenu, QMessageBox, QPushButton
|
||||
|
||||
from .helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
DEFAULT_TRANSLATION = "en"
|
||||
|
||||
|
||||
class TranslatedString(QObject):
|
||||
changed = Signal(str)
|
||||
|
||||
__default_value: str
|
||||
__value: str | None = None
|
||||
|
||||
def __init__(self, value: str):
|
||||
super().__init__()
|
||||
self.__default_value = value
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
return self.__value or self.__default_value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: str):
|
||||
if self.__value != value:
|
||||
self.__value = value
|
||||
self.changed.emit(self.__value)
|
||||
|
||||
|
||||
class Translator:
|
||||
_strings: dict[str, TranslatedString] = {}
|
||||
_default_strings: dict[str, str]
|
||||
_strings: dict[str, str] = {}
|
||||
_lang: str = DEFAULT_TRANSLATION
|
||||
|
||||
def __init__(self):
|
||||
for k, v in self.__get_translation_dict(DEFAULT_TRANSLATION).items():
|
||||
self._strings[k] = TranslatedString(v)
|
||||
self._default_strings = self.__get_translation_dict(DEFAULT_TRANSLATION)
|
||||
|
||||
def __get_translation_dict(self, lang: str) -> dict[str, str]:
|
||||
with open(
|
||||
@@ -52,44 +25,10 @@ class Translator:
|
||||
|
||||
def change_language(self, lang: str):
|
||||
self._lang = lang
|
||||
translated = self.__get_translation_dict(lang)
|
||||
for k in self._strings:
|
||||
self._strings[k].value = translated.get(k, None)
|
||||
|
||||
def translate_qobject(self, widget: QObject, key: str, **kwargs):
|
||||
"""Translates the text of the QObject using :func:`translate_with_setter`."""
|
||||
if isinstance(widget, (QLabel, QAction, QPushButton, QMessageBox, QPushButtonWrapper)):
|
||||
self.translate_with_setter(widget.setText, key, **kwargs)
|
||||
elif isinstance(widget, (QMenu)):
|
||||
self.translate_with_setter(widget.setTitle, key, **kwargs)
|
||||
else:
|
||||
raise RuntimeError
|
||||
|
||||
def translate_with_setter(self, setter: Callable[[str], None], key: str, **kwargs):
|
||||
"""Calls `setter` everytime the language changes and passes the translated string for `key`.
|
||||
|
||||
Also formats the translation with the given keyword arguments.
|
||||
"""
|
||||
if key in self._strings:
|
||||
self._strings[key].changed.connect(lambda text: setter(self.__format(text, **kwargs)))
|
||||
setter(self.translate_formatted(key, **kwargs))
|
||||
|
||||
def __format(self, text: str, **kwargs) -> str:
|
||||
try:
|
||||
return text.format(**kwargs)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
"Error while formatting translation.", text=text, kwargs=kwargs, language=self._lang
|
||||
)
|
||||
return text
|
||||
|
||||
def translate_formatted(self, key: str, **kwargs) -> str:
|
||||
return self.__format(self[key], **kwargs)
|
||||
self._strings = self.__get_translation_dict(lang)
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
# return "???"
|
||||
return self._strings[key].value if key in self._strings else "Not Translated"
|
||||
return self._strings.get(key) or self._default_strings.get(key) or f"[{key}]"
|
||||
|
||||
|
||||
Translations = Translator()
|
||||
# Translations.change_language("de")
|
||||
|
||||
@@ -87,6 +87,8 @@ from src.qt.modals.file_extension import FileExtensionModal
|
||||
from src.qt.modals.fix_dupes import FixDupeFilesModal
|
||||
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
|
||||
from src.qt.modals.folders_to_tags import FoldersToTagsModal
|
||||
from src.qt.modals.settings_panel import SettingsPanel
|
||||
from src.qt.modals.tag_color_manager import TagColorManager
|
||||
from src.qt.modals.tag_database import TagDatabasePanel
|
||||
from src.qt.modals.tag_search import TagSearchPanel
|
||||
from src.qt.platform_strings import trash_term
|
||||
@@ -140,11 +142,12 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
SIGTERM = Signal()
|
||||
|
||||
preview_panel: PreviewPanel
|
||||
tag_manager_panel: PanelModal
|
||||
preview_panel: PreviewPanel | None = None
|
||||
tag_manager_panel: PanelModal | None = None
|
||||
color_manager_panel: TagColorManager | None = None
|
||||
file_extension_panel: PanelModal | None = None
|
||||
tag_search_panel: TagSearchPanel
|
||||
add_tag_modal: PanelModal
|
||||
tag_search_panel: TagSearchPanel | None = None
|
||||
add_tag_modal: PanelModal | None = None
|
||||
|
||||
lib: Library
|
||||
|
||||
@@ -195,6 +198,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
self.config_path = self.settings.fileName()
|
||||
|
||||
Translations.change_language(
|
||||
str(self.settings.value(SettingItems.LANGUAGE, defaultValue="en", type=str))
|
||||
)
|
||||
|
||||
# NOTE: This should be a per-library setting rather than an application setting.
|
||||
thumb_cache_size_limit: int = int(
|
||||
str(
|
||||
@@ -304,18 +311,19 @@ class QtDriver(DriverMixin, QObject):
|
||||
done_callback=lambda: self.preview_panel.update_widgets(update_preview=False),
|
||||
has_save=False,
|
||||
)
|
||||
Translations.translate_with_setter(self.tag_manager_panel.setTitle, "tag_manager.title")
|
||||
Translations.translate_with_setter(
|
||||
self.tag_manager_panel.setWindowTitle, "tag_manager.title"
|
||||
)
|
||||
self.tag_manager_panel.setTitle(Translations["tag_manager.title"])
|
||||
self.tag_manager_panel.setWindowTitle(Translations["tag_manager.title"])
|
||||
|
||||
# Initialize the Color Group Manager panel
|
||||
self.color_manager_panel = TagColorManager(self)
|
||||
|
||||
# Initialize the Tag Search panel
|
||||
self.tag_search_panel = TagSearchPanel(self.lib, is_tag_chooser=True)
|
||||
self.tag_search_panel.set_driver(self)
|
||||
self.add_tag_modal = PanelModal(
|
||||
widget=self.tag_search_panel,
|
||||
title=Translations.translate_formatted("tag.add.plural"),
|
||||
window_title=Translations.translate_formatted("tag.add.plural"),
|
||||
title=Translations["tag.add.plural"],
|
||||
window_title=Translations["tag.add.plural"],
|
||||
)
|
||||
self.tag_search_panel.tag_chosen.connect(
|
||||
lambda t: (
|
||||
@@ -328,22 +336,15 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.setMenuBar(menu_bar)
|
||||
menu_bar.setNativeMenuBar(True)
|
||||
|
||||
file_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(file_menu, "menu.file")
|
||||
edit_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(edit_menu, "generic.edit_alt")
|
||||
view_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(view_menu, "menu.view")
|
||||
tools_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(tools_menu, "menu.tools")
|
||||
macros_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(macros_menu, "menu.macros")
|
||||
help_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(help_menu, "menu.help")
|
||||
file_menu = QMenu(Translations["menu.file"], menu_bar)
|
||||
edit_menu = QMenu(Translations["generic.edit_alt"], menu_bar)
|
||||
view_menu = QMenu(Translations["menu.view"], menu_bar)
|
||||
tools_menu = QMenu(Translations["menu.tools"], menu_bar)
|
||||
macros_menu = QMenu(Translations["menu.macros"], menu_bar)
|
||||
help_menu = QMenu(Translations["menu.help"], menu_bar)
|
||||
|
||||
# File Menu ============================================================
|
||||
open_library_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(open_library_action, "menu.file.open_create_library")
|
||||
open_library_action = QAction(Translations["menu.file.open_create_library"], menu_bar)
|
||||
open_library_action.triggered.connect(lambda: self.open_library_from_dialog())
|
||||
open_library_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
@@ -354,28 +355,13 @@ class QtDriver(DriverMixin, QObject):
|
||||
open_library_action.setToolTip("Ctrl+O")
|
||||
file_menu.addAction(open_library_action)
|
||||
|
||||
self.open_recent_library_menu = QMenu(menu_bar)
|
||||
Translations.translate_qobject(
|
||||
self.open_recent_library_menu, "menu.file.open_recent_library"
|
||||
self.open_recent_library_menu = QMenu(
|
||||
Translations["menu.file.open_recent_library"], menu_bar
|
||||
)
|
||||
file_menu.addMenu(self.open_recent_library_menu)
|
||||
self.update_recent_lib_menu()
|
||||
|
||||
open_on_start_action = QAction(self)
|
||||
Translations.translate_qobject(open_on_start_action, "settings.open_library_on_start")
|
||||
open_on_start_action.setCheckable(True)
|
||||
open_on_start_action.setChecked(
|
||||
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
|
||||
)
|
||||
open_on_start_action.triggered.connect(
|
||||
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
|
||||
)
|
||||
file_menu.addAction(open_on_start_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
self.save_library_backup_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.save_library_backup_action, "menu.file.save_backup")
|
||||
self.save_library_backup_action = QAction(Translations["menu.file.save_backup"], menu_bar)
|
||||
self.save_library_backup_action.triggered.connect(
|
||||
lambda: self.callback_library_needed_check(self.backup_library)
|
||||
)
|
||||
@@ -393,9 +379,23 @@ class QtDriver(DriverMixin, QObject):
|
||||
file_menu.addAction(self.save_library_backup_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
settings_action = QAction(Translations["menu.settings"], self)
|
||||
settings_action.triggered.connect(self.open_settings_modal)
|
||||
file_menu.addAction(settings_action)
|
||||
|
||||
self.refresh_dir_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.refresh_dir_action, "menu.file.refresh_directories")
|
||||
open_on_start_action = QAction(Translations["settings.open_library_on_start"], self)
|
||||
open_on_start_action.setCheckable(True)
|
||||
open_on_start_action.setChecked(
|
||||
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
|
||||
)
|
||||
open_on_start_action.triggered.connect(
|
||||
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
|
||||
)
|
||||
file_menu.addAction(open_on_start_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
self.refresh_dir_action = QAction(Translations["menu.file.refresh_directories"], menu_bar)
|
||||
self.refresh_dir_action.triggered.connect(
|
||||
lambda: self.callback_library_needed_check(self.add_new_files_callback)
|
||||
)
|
||||
@@ -410,16 +410,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
file_menu.addAction(self.refresh_dir_action)
|
||||
file_menu.addSeparator()
|
||||
|
||||
self.close_library_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.close_library_action, "menu.file.close_library")
|
||||
self.close_library_action = QAction(Translations["menu.file.close_library"], menu_bar)
|
||||
self.close_library_action.triggered.connect(self.close_library)
|
||||
self.close_library_action.setEnabled(False)
|
||||
file_menu.addAction(self.close_library_action)
|
||||
file_menu.addSeparator()
|
||||
|
||||
# Edit Menu ============================================================
|
||||
self.new_tag_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.new_tag_action, "menu.edit.new_tag")
|
||||
self.new_tag_action = QAction(Translations["menu.edit.new_tag"], menu_bar)
|
||||
self.new_tag_action.triggered.connect(lambda: self.add_tag_action_callback())
|
||||
self.new_tag_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
@@ -433,8 +431,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
edit_menu.addSeparator()
|
||||
|
||||
self.select_all_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.select_all_action, "select.all")
|
||||
self.select_all_action = QAction(Translations["select.all"], menu_bar)
|
||||
self.select_all_action.triggered.connect(self.select_all_action_callback)
|
||||
self.select_all_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
@@ -446,8 +443,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.select_all_action.setEnabled(False)
|
||||
edit_menu.addAction(self.select_all_action)
|
||||
|
||||
self.clear_select_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.clear_select_action, "select.clear")
|
||||
self.clear_select_action = QAction(Translations["select.clear"], menu_bar)
|
||||
self.clear_select_action.triggered.connect(self.clear_select_action_callback)
|
||||
self.clear_select_action.setShortcut(QtCore.Qt.Key.Key_Escape)
|
||||
self.clear_select_action.setToolTip("Esc")
|
||||
@@ -456,8 +452,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
self.copy_buffer: dict = {"fields": [], "tags": []}
|
||||
|
||||
self.copy_fields_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.copy_fields_action, "edit.copy_fields")
|
||||
self.copy_fields_action = QAction(Translations["edit.copy_fields"], menu_bar)
|
||||
self.copy_fields_action.triggered.connect(self.copy_fields_action_callback)
|
||||
self.copy_fields_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
@@ -469,8 +464,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.copy_fields_action.setEnabled(False)
|
||||
edit_menu.addAction(self.copy_fields_action)
|
||||
|
||||
self.paste_fields_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.paste_fields_action, "edit.paste_fields")
|
||||
self.paste_fields_action = QAction(Translations["edit.paste_fields"], menu_bar)
|
||||
self.paste_fields_action.triggered.connect(self.paste_fields_action_callback)
|
||||
self.paste_fields_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
@@ -482,9 +476,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.paste_fields_action.setEnabled(False)
|
||||
edit_menu.addAction(self.paste_fields_action)
|
||||
|
||||
self.add_tag_to_selected_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(
|
||||
self.add_tag_to_selected_action, "select.add_tag_to_selected"
|
||||
self.add_tag_to_selected_action = QAction(
|
||||
Translations["select.add_tag_to_selected"], menu_bar
|
||||
)
|
||||
self.add_tag_to_selected_action.triggered.connect(self.add_tag_modal.show)
|
||||
self.add_tag_to_selected_action.setShortcut(
|
||||
@@ -502,9 +495,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
edit_menu.addSeparator()
|
||||
|
||||
self.delete_file_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(
|
||||
self.delete_file_action, "menu.delete_selected_files_ambiguous", trash_term=trash_term()
|
||||
self.delete_file_action = QAction(
|
||||
Translations["menu.delete_selected_files_ambiguous"].format(trash_term=trash_term()),
|
||||
menu_bar,
|
||||
)
|
||||
self.delete_file_action.triggered.connect(lambda f="": self.delete_files_callback(f))
|
||||
self.delete_file_action.setShortcut(QtCore.Qt.Key.Key_Delete)
|
||||
@@ -513,15 +506,13 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
edit_menu.addSeparator()
|
||||
|
||||
self.manage_file_ext_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(
|
||||
self.manage_file_ext_action, "menu.edit.manage_file_extensions"
|
||||
self.manage_file_ext_action = QAction(
|
||||
Translations["menu.edit.manage_file_extensions"], menu_bar
|
||||
)
|
||||
edit_menu.addAction(self.manage_file_ext_action)
|
||||
self.manage_file_ext_action.setEnabled(False)
|
||||
|
||||
self.tag_manager_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.tag_manager_action, "menu.edit.manage_tags")
|
||||
self.tag_manager_action = QAction(Translations["menu.edit.manage_tags"], menu_bar)
|
||||
self.tag_manager_action.triggered.connect(self.tag_manager_panel.show)
|
||||
self.tag_manager_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
@@ -533,16 +524,19 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.tag_manager_action.setToolTip("Ctrl+M")
|
||||
edit_menu.addAction(self.tag_manager_action)
|
||||
|
||||
self.color_manager_action = QAction(Translations["edit.color_manager"], menu_bar)
|
||||
self.color_manager_action.triggered.connect(self.color_manager_panel.show)
|
||||
self.color_manager_action.setEnabled(False)
|
||||
edit_menu.addAction(self.color_manager_action)
|
||||
|
||||
# View Menu ============================================================
|
||||
show_libs_list_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(show_libs_list_action, "settings.show_recent_libraries")
|
||||
show_libs_list_action = QAction(Translations["settings.show_recent_libraries"], menu_bar)
|
||||
show_libs_list_action.setCheckable(True)
|
||||
show_libs_list_action.setChecked(
|
||||
bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool))
|
||||
)
|
||||
|
||||
show_filenames_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(show_filenames_action, "settings.show_filenames_in_grid")
|
||||
show_filenames_action = QAction(Translations["settings.show_filenames_in_grid"], menu_bar)
|
||||
show_filenames_action.setCheckable(True)
|
||||
show_filenames_action.setChecked(
|
||||
bool(self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool))
|
||||
@@ -561,9 +555,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.unlinked_modal = FixUnlinkedEntriesModal(self.lib, self)
|
||||
self.unlinked_modal.show()
|
||||
|
||||
self.fix_unlinked_entries_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(
|
||||
self.fix_unlinked_entries_action, "menu.tools.fix_unlinked_entries"
|
||||
self.fix_unlinked_entries_action = QAction(
|
||||
Translations["menu.tools.fix_unlinked_entries"], menu_bar
|
||||
)
|
||||
self.fix_unlinked_entries_action.triggered.connect(create_fix_unlinked_entries_modal)
|
||||
self.fix_unlinked_entries_action.setEnabled(False)
|
||||
@@ -574,8 +567,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.dupe_modal = FixDupeFilesModal(self.lib, self)
|
||||
self.dupe_modal.show()
|
||||
|
||||
self.fix_dupe_files_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.fix_dupe_files_action, "menu.tools.fix_duplicate_files")
|
||||
self.fix_dupe_files_action = QAction(
|
||||
Translations["menu.tools.fix_duplicate_files"], menu_bar
|
||||
)
|
||||
self.fix_dupe_files_action.triggered.connect(create_dupe_files_modal)
|
||||
self.fix_dupe_files_action.setEnabled(False)
|
||||
tools_menu.addAction(self.fix_dupe_files_action)
|
||||
@@ -583,9 +577,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
tools_menu.addSeparator()
|
||||
|
||||
# TODO: Move this to a settings screen.
|
||||
self.clear_thumb_cache_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(
|
||||
self.clear_thumb_cache_action, "settings.clear_thumb_cache.title"
|
||||
self.clear_thumb_cache_action = QAction(
|
||||
Translations["settings.clear_thumb_cache.title"], menu_bar
|
||||
)
|
||||
self.clear_thumb_cache_action.triggered.connect(
|
||||
lambda: CacheManager.clear_cache(self.lib.library_dir)
|
||||
@@ -598,22 +591,21 @@ class QtDriver(DriverMixin, QObject):
|
||||
# tools_menu.addAction(create_collage_action)
|
||||
|
||||
# Macros Menu ==========================================================
|
||||
self.autofill_action = QAction("Autofill", menu_bar)
|
||||
self.autofill_action.triggered.connect(
|
||||
lambda: (
|
||||
self.run_macros(MacroID.AUTOFILL, self.selected),
|
||||
self.preview_panel.update_widgets(update_preview=False),
|
||||
)
|
||||
)
|
||||
macros_menu.addAction(self.autofill_action)
|
||||
# self.autofill_action = QAction("Autofill", menu_bar)
|
||||
# self.autofill_action.triggered.connect(
|
||||
# lambda: (
|
||||
# self.run_macros(MacroID.AUTOFILL, self.selected),
|
||||
# self.preview_panel.update_widgets(update_preview=False),
|
||||
# )
|
||||
# )
|
||||
# macros_menu.addAction(self.autofill_action)
|
||||
|
||||
def create_folders_tags_modal():
|
||||
if not hasattr(self, "folders_modal"):
|
||||
self.folders_modal = FoldersToTagsModal(self.lib, self)
|
||||
self.folders_modal.show()
|
||||
|
||||
self.folders_to_tags_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.folders_to_tags_action, "menu.macros.folders_to_tags")
|
||||
self.folders_to_tags_action = QAction(Translations["menu.macros.folders_to_tags"], menu_bar)
|
||||
self.folders_to_tags_action.triggered.connect(create_folders_tags_modal)
|
||||
self.folders_to_tags_action.setEnabled(False)
|
||||
macros_menu.addAction(self.folders_to_tags_action)
|
||||
@@ -624,8 +616,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.about_modal = AboutModal(self.config_path)
|
||||
self.about_modal.show()
|
||||
|
||||
self.about_action = QAction(menu_bar)
|
||||
Translations.translate_qobject(self.about_action, "menu.help.about")
|
||||
self.about_action = QAction(Translations["menu.help.about"], menu_bar)
|
||||
self.about_action.triggered.connect(create_about_modal)
|
||||
help_menu.addAction(self.about_action)
|
||||
self.set_macro_menu_viability()
|
||||
@@ -690,14 +681,16 @@ class QtDriver(DriverMixin, QObject):
|
||||
app.exec()
|
||||
self.shutdown()
|
||||
|
||||
def show_error_message(self, message: str):
|
||||
self.main_window.statusbar.showMessage(message, Qt.AlignmentFlag.AlignLeft)
|
||||
self.main_window.landing_widget.set_status_label(message)
|
||||
self.main_window.setWindowTitle(message)
|
||||
def show_error_message(self, error_name: str, error_desc: str | None = None):
|
||||
self.main_window.statusbar.showMessage(error_name, Qt.AlignmentFlag.AlignLeft)
|
||||
self.main_window.landing_widget.set_status_label(error_name)
|
||||
self.main_window.setWindowTitle(f"{self.base_title} - {error_name}")
|
||||
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setIcon(QMessageBox.Icon.Critical)
|
||||
msg_box.setText(message)
|
||||
msg_box.setText(error_name)
|
||||
if error_desc:
|
||||
msg_box.setInformativeText(error_desc)
|
||||
msg_box.setWindowTitle(Translations["window.title.error"])
|
||||
msg_box.addButton(Translations["generic.close"], QMessageBox.ButtonRole.AcceptRole)
|
||||
|
||||
@@ -744,13 +737,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
sort_dir_dropdown: QComboBox = self.main_window.sorting_direction_combobox
|
||||
sort_dir_dropdown.addItem("Ascending", userData=True)
|
||||
sort_dir_dropdown.addItem("Descending", userData=False)
|
||||
Translations.translate_with_setter(
|
||||
lambda text: sort_dir_dropdown.setItemText(0, text), "sorting.direction.ascending"
|
||||
)
|
||||
Translations.translate_with_setter(
|
||||
lambda text: sort_dir_dropdown.setItemText(1, text), "sorting.direction.descending"
|
||||
)
|
||||
sort_dir_dropdown.setCurrentIndex(0) # Default: Ascending
|
||||
sort_dir_dropdown.setItemText(0, Translations["sorting.direction.ascending"])
|
||||
sort_dir_dropdown.setItemText(1, Translations["sorting.direction.descending"])
|
||||
sort_dir_dropdown.setCurrentIndex(1) # Default: Descending
|
||||
sort_dir_dropdown.currentIndexChanged.connect(self.sorting_direction_callback)
|
||||
|
||||
# Thumbnail Size ComboBox
|
||||
@@ -793,10 +782,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
panel,
|
||||
has_save=True,
|
||||
)
|
||||
Translations.translate_with_setter(self.file_extension_panel.setTitle, "ignore_list.title")
|
||||
Translations.translate_with_setter(
|
||||
self.file_extension_panel.setWindowTitle, "ignore_list.title"
|
||||
)
|
||||
self.file_extension_panel.setTitle(Translations["ignore_list.title"])
|
||||
self.file_extension_panel.setWindowTitle(Translations["ignore_list.title"])
|
||||
self.file_extension_panel.saved.connect(lambda: (panel.save(), self.filter_items()))
|
||||
self.manage_file_ext_action.triggered.connect(self.file_extension_panel.show)
|
||||
|
||||
@@ -857,6 +844,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.selected.clear()
|
||||
self.frame_content.clear()
|
||||
[x.set_mode(None) for x in self.item_thumbs]
|
||||
if self.color_manager_panel:
|
||||
self.color_manager_panel.reset()
|
||||
|
||||
self.set_clipboard_menu_viability()
|
||||
self.set_select_actions_visibility()
|
||||
@@ -869,6 +858,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.close_library_action.setEnabled(False)
|
||||
self.refresh_dir_action.setEnabled(False)
|
||||
self.tag_manager_action.setEnabled(False)
|
||||
self.color_manager_action.setEnabled(False)
|
||||
self.manage_file_ext_action.setEnabled(False)
|
||||
self.new_tag_action.setEnabled(False)
|
||||
self.fix_unlinked_entries_action.setEnabled(False)
|
||||
@@ -886,8 +876,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
end_time = time.time()
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted(
|
||||
"status.library_closed", time_span=format_timespan(end_time - start_time)
|
||||
Translations["status.library_closed"].format(
|
||||
time_span=format_timespan(end_time - start_time)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -898,8 +888,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
target_path = self.lib.save_library_backup_to_disk()
|
||||
end_time = time.time()
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted(
|
||||
"status.library_backup_success",
|
||||
Translations["status.library_backup_success"].format(
|
||||
path=target_path,
|
||||
time_span=format_timespan(end_time - start_time),
|
||||
)
|
||||
@@ -911,8 +900,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
panel,
|
||||
has_save=True,
|
||||
)
|
||||
Translations.translate_with_setter(self.modal.setTitle, "tag.new")
|
||||
Translations.translate_with_setter(self.modal.setWindowTitle, "tag.add")
|
||||
self.modal.setTitle(Translations["tag.new"])
|
||||
self.modal.setWindowTitle(Translations["tag.add"])
|
||||
|
||||
self.modal.saved.connect(
|
||||
lambda: (
|
||||
@@ -997,8 +986,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.preview_panel.thumb.stop_file_use()
|
||||
if delete_file(self.lib.library_dir / f):
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted(
|
||||
"status.deleting_file", i=i, count=len(pending), path=f
|
||||
Translations["status.deleting_file"].format(
|
||||
i=i, count=len(pending), path=f
|
||||
)
|
||||
)
|
||||
self.main_window.statusbar.repaint()
|
||||
@@ -1015,19 +1004,17 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.statusbar.showMessage(Translations["status.deleted_none"])
|
||||
elif len(self.selected) <= 1 and deleted_count == 1:
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted("status.deleted_file_plural", count=deleted_count)
|
||||
Translations["status.deleted_file_plural"].format(count=deleted_count)
|
||||
)
|
||||
elif len(self.selected) > 1 and deleted_count == 0:
|
||||
self.main_window.statusbar.showMessage(Translations["status.deleted_none"])
|
||||
elif len(self.selected) > 1 and deleted_count < len(self.selected):
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted(
|
||||
"status.deleted_partial_warning", count=deleted_count
|
||||
)
|
||||
Translations["status.deleted_partial_warning"].format(count=deleted_count)
|
||||
)
|
||||
elif len(self.selected) > 1 and deleted_count == len(self.selected):
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted("status.deleted_file_plural", count=deleted_count)
|
||||
Translations["status.deleted_file_plural"].format(count=deleted_count)
|
||||
)
|
||||
self.main_window.statusbar.repaint()
|
||||
|
||||
@@ -1044,8 +1031,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
# https://github.com/arsenetar/send2trash/issues/28
|
||||
# This warning is applied to all platforms until at least macOS and Linux can be verified
|
||||
# to not exhibit this same behavior.
|
||||
perm_warning_msg = Translations.translate_formatted(
|
||||
"trash.dialog.permanent_delete_warning", trash_term=trash_term()
|
||||
perm_warning_msg = Translations["trash.dialog.permanent_delete_warning"].format(
|
||||
trash_term=trash_term()
|
||||
)
|
||||
perm_warning: str = (
|
||||
f"<h4 style='color: {get_ui_color(ColorType.PRIMARY, UiColor.RED)}'>"
|
||||
@@ -1062,8 +1049,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
msg.setIcon(QMessageBox.Icon.Warning)
|
||||
if count <= 1:
|
||||
msg_text = Translations.translate_formatted(
|
||||
"trash.dialog.move.confirmation.singular", trash_term=trash_term()
|
||||
msg_text = Translations["trash.dialog.move.confirmation.singular"].format(
|
||||
trash_term=trash_term()
|
||||
)
|
||||
msg.setText(
|
||||
f"<h3>{msg_text}</h3>"
|
||||
@@ -1072,8 +1059,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
f"{perm_warning}<br>"
|
||||
)
|
||||
elif count > 1:
|
||||
msg_text = Translations.translate_formatted(
|
||||
"trash.dialog.move.confirmation.plural",
|
||||
msg_text = Translations["trash.dialog.move.confirmation.plural"].format(
|
||||
count=count,
|
||||
trash_term=trash_term(),
|
||||
)
|
||||
@@ -1098,8 +1084,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
minimum=0,
|
||||
maximum=0,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "library.refresh.title")
|
||||
Translations.translate_with_setter(pw.update_label, "library.refresh.scanning_preparing")
|
||||
pw.setWindowTitle(Translations["library.refresh.title"])
|
||||
pw.update_label(Translations["library.refresh.scanning_preparing"])
|
||||
|
||||
pw.show()
|
||||
|
||||
@@ -1108,10 +1094,11 @@ class QtDriver(DriverMixin, QObject):
|
||||
lambda x: (
|
||||
pw.update_progress(x + 1),
|
||||
pw.update_label(
|
||||
Translations.translate_formatted(
|
||||
Translations[
|
||||
"library.refresh.scanning.plural"
|
||||
if x + 1 != 1
|
||||
else "library.refresh.scanning.singular",
|
||||
else "library.refresh.scanning.singular"
|
||||
].format(
|
||||
searched_count=f"{x+1:n}",
|
||||
found_count=f"{tracker.files_count:n}",
|
||||
)
|
||||
@@ -1141,17 +1128,17 @@ class QtDriver(DriverMixin, QObject):
|
||||
minimum=0,
|
||||
maximum=0,
|
||||
)
|
||||
Translations.translate_with_setter(pw.setWindowTitle, "entries.running.dialog.title")
|
||||
Translations.translate_with_setter(
|
||||
pw.update_label, "entries.running.dialog.new_entries", total=f"{files_count:n}"
|
||||
pw.setWindowTitle(Translations["entries.running.dialog.title"])
|
||||
pw.update_label(
|
||||
Translations["entries.running.dialog.new_entries"].format(total=f"{files_count:n}")
|
||||
)
|
||||
pw.show()
|
||||
|
||||
iterator.value.connect(
|
||||
lambda: (
|
||||
pw.update_label(
|
||||
Translations.translate_formatted(
|
||||
"entries.running.dialog.new_entries", total=f"{files_count:n}"
|
||||
Translations["entries.running.dialog.new_entries"].format(
|
||||
total=f"{files_count:n}"
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -1443,7 +1430,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.preview_panel.update_widgets()
|
||||
|
||||
def set_macro_menu_viability(self):
|
||||
self.autofill_action.setDisabled(not self.selected)
|
||||
# self.autofill_action.setDisabled(not self.selected)
|
||||
pass
|
||||
|
||||
def set_clipboard_menu_viability(self):
|
||||
if len(self.selected) == 1:
|
||||
@@ -1710,8 +1698,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
# inform user about completed search
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted(
|
||||
"status.results_found",
|
||||
Translations["status.results_found"].format(
|
||||
count=results.total_count,
|
||||
time_span=format_timespan(end_time - start_time),
|
||||
)
|
||||
@@ -1786,8 +1773,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
action.triggered.connect(lambda checked=False, p=path: self.open_library(p))
|
||||
actions.append(action)
|
||||
|
||||
clear_recent_action = QAction(self.open_recent_library_menu)
|
||||
Translations.translate_qobject(clear_recent_action, "menu.file.clear_recent_libraries")
|
||||
clear_recent_action = QAction(
|
||||
Translations["menu.file.clear_recent_libraries"], self.open_recent_library_menu
|
||||
)
|
||||
clear_recent_action.triggered.connect(self.clear_recent_libs)
|
||||
actions.append(clear_recent_action)
|
||||
|
||||
@@ -1816,26 +1804,42 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.settings.sync()
|
||||
self.update_recent_lib_menu()
|
||||
|
||||
def open_settings_modal(self):
|
||||
# TODO: Implement a proper settings panel, and don't re-create it each time it's opened.
|
||||
settings_panel = SettingsPanel(self)
|
||||
modal = PanelModal(
|
||||
widget=settings_panel,
|
||||
done_callback=lambda: self.update_language_settings(settings_panel.get_language()),
|
||||
has_save=False,
|
||||
)
|
||||
modal.setTitle(Translations["settings.title"])
|
||||
modal.setWindowTitle(Translations["settings.title"])
|
||||
modal.show()
|
||||
|
||||
def update_language_settings(self, language: str):
|
||||
Translations.change_language(language)
|
||||
|
||||
self.settings.setValue(SettingItems.LANGUAGE, language)
|
||||
self.settings.sync()
|
||||
|
||||
def open_library(self, path: Path) -> None:
|
||||
"""Open a TagStudio library."""
|
||||
translation_params = {"key": "splash.opening_library", "library_path": str(path)}
|
||||
Translations.translate_with_setter(
|
||||
self.main_window.landing_widget.set_status_label, **translation_params
|
||||
)
|
||||
self.main_window.statusbar.showMessage(
|
||||
Translations.translate_formatted(**translation_params), 3
|
||||
)
|
||||
message = Translations["splash.opening_library"].format(library_path=str(path))
|
||||
self.main_window.landing_widget.set_status_label(message)
|
||||
self.main_window.statusbar.showMessage(message, 3)
|
||||
self.main_window.repaint()
|
||||
|
||||
if self.lib.library_dir:
|
||||
self.close_library()
|
||||
|
||||
open_status: LibraryStatus = None
|
||||
open_status: LibraryStatus | None = None
|
||||
try:
|
||||
open_status = self.lib.open_library(path)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
open_status = LibraryStatus(success=False, library_path=path, message=type(e).__name__)
|
||||
open_status = LibraryStatus(
|
||||
success=False, library_path=path, message=type(e).__name__, msg_description=str(e)
|
||||
)
|
||||
|
||||
# Migration is required
|
||||
if open_status.json_migration_req:
|
||||
@@ -1851,7 +1855,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
def init_library(self, path: Path, open_status: LibraryStatus):
|
||||
if not open_status.success:
|
||||
self.show_error_message(
|
||||
open_status.message or Translations["window.message.error_opening_library"]
|
||||
error_name=open_status.message
|
||||
or Translations["window.message.error_opening_library"],
|
||||
error_desc=open_status.msg_description,
|
||||
)
|
||||
return open_status
|
||||
|
||||
@@ -1864,11 +1870,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.add_new_files_callback()
|
||||
|
||||
self.update_libs_list(path)
|
||||
Translations.translate_with_setter(
|
||||
self.main_window.setWindowTitle,
|
||||
"app.title",
|
||||
base_title=self.base_title,
|
||||
library_dir=self.lib.library_dir,
|
||||
self.main_window.setWindowTitle(
|
||||
Translations["app.title"].format(
|
||||
base_title=self.base_title, library_dir=self.lib.library_dir
|
||||
)
|
||||
)
|
||||
self.main_window.setAcceptDrops(True)
|
||||
|
||||
@@ -1880,6 +1885,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.close_library_action.setEnabled(True)
|
||||
self.refresh_dir_action.setEnabled(True)
|
||||
self.tag_manager_action.setEnabled(True)
|
||||
self.color_manager_action.setEnabled(True)
|
||||
self.manage_file_ext_action.setEnabled(True)
|
||||
self.new_tag_action.setEnabled(True)
|
||||
self.fix_dupe_files_action.setEnabled(True)
|
||||
|
||||
165
tagstudio/src/qt/widgets/color_box.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import typing
|
||||
from collections.abc import Iterable
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
from src.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.modals.build_color import BuildColorPanel
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.fields import FieldWidget
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
from src.qt.widgets.tag_color_label import TagColorLabel
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.core.library import Library
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class ColorBoxWidget(FieldWidget):
|
||||
updated = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
group: str,
|
||||
colors: list["TagColorGroup"],
|
||||
library: "Library",
|
||||
) -> None:
|
||||
self.namespace = group
|
||||
self.colors: list[TagColorGroup] = colors
|
||||
self.lib: Library = library
|
||||
|
||||
title = "" if not self.lib.engine else self.lib.get_namespace_name(group)
|
||||
super().__init__(title)
|
||||
|
||||
self.add_button_stylesheet = (
|
||||
f"QPushButton{{"
|
||||
f"background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
|
||||
f"color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 4px;"
|
||||
f"padding-bottom: 2px;"
|
||||
f"padding-left: 4px;"
|
||||
f"font-size: 15px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
|
||||
f"}}"
|
||||
f"QPushButton::pressed{{"
|
||||
f"background: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
|
||||
f"color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
|
||||
f"border-color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"border-color: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
|
||||
f"outline:none;"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
self.setObjectName("colorBox")
|
||||
self.base_layout = FlowLayout()
|
||||
self.base_layout.enable_grid_optimizations(value=True)
|
||||
self.base_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.base_layout)
|
||||
|
||||
self.set_colors(self.colors)
|
||||
|
||||
def set_colors(self, colors: Iterable[TagColorGroup]):
|
||||
colors_ = sorted(
|
||||
list(colors), key=lambda color: self.lib.get_namespace_name(color.namespace)
|
||||
)
|
||||
is_mutable = not self.namespace.startswith(RESERVED_NAMESPACE_PREFIX)
|
||||
max_width = 60
|
||||
color_widgets: list[TagColorLabel] = []
|
||||
|
||||
while self.base_layout.itemAt(0):
|
||||
self.base_layout.takeAt(0).widget().deleteLater()
|
||||
|
||||
for color in colors_:
|
||||
color_widget = TagColorLabel(
|
||||
color=color,
|
||||
has_edit=is_mutable,
|
||||
has_remove=is_mutable,
|
||||
library=self.lib,
|
||||
)
|
||||
hint = color_widget.sizeHint().width()
|
||||
if hint > max_width:
|
||||
max_width = hint
|
||||
color_widget.on_click.connect(lambda c=color: self.edit_color(c))
|
||||
color_widget.on_remove.connect(lambda c=color: self.delete_color(c))
|
||||
|
||||
color_widgets.append(color_widget)
|
||||
self.base_layout.addWidget(color_widget)
|
||||
|
||||
for color_widget in color_widgets:
|
||||
color_widget.setFixedWidth(max_width)
|
||||
|
||||
if is_mutable:
|
||||
add_button = QPushButton()
|
||||
add_button.setText("+")
|
||||
add_button.setFlat(True)
|
||||
add_button.setFixedSize(22, 22)
|
||||
add_button.setStyleSheet(self.add_button_stylesheet)
|
||||
add_button.clicked.connect(
|
||||
lambda: self.edit_color(
|
||||
TagColorGroup(
|
||||
slug="slug",
|
||||
namespace=self.namespace,
|
||||
name="Color",
|
||||
primary="#FFFFFF",
|
||||
secondary=None,
|
||||
)
|
||||
)
|
||||
)
|
||||
self.base_layout.addWidget(add_button)
|
||||
|
||||
def edit_color(self, color_group: TagColorGroup):
|
||||
build_color_panel = BuildColorPanel(self.lib, color_group)
|
||||
|
||||
self.edit_modal = PanelModal(
|
||||
build_color_panel,
|
||||
"Edit Color",
|
||||
"Edit Color",
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
self.edit_modal.saved.connect(
|
||||
lambda: (self.lib.update_color(*build_color_panel.build_color()), self.updated.emit())
|
||||
)
|
||||
self.edit_modal.show()
|
||||
|
||||
def delete_color(self, color_group: TagColorGroup):
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Icon.Warning,
|
||||
Translations["color.delete"],
|
||||
Translations["color.confirm_delete"].format(color_name=color_group.name),
|
||||
)
|
||||
cancel_button = message_box.addButton(
|
||||
Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole
|
||||
)
|
||||
message_box.addButton(
|
||||
Translations["generic.delete_alt"], QMessageBox.ButtonRole.DestructiveRole
|
||||
)
|
||||
message_box.setEscapeButton(cancel_button)
|
||||
result = message_box.exec_()
|
||||
logger.info(QMessageBox.ButtonRole.DestructiveRole.value)
|
||||
if result != QMessageBox.ButtonRole.ActionRole.value:
|
||||
return
|
||||
|
||||
logger.info("[ColorBoxWidget] Removing color", color=color_group)
|
||||
self.lib.delete_color(color_group)
|
||||
self.updated.emit()
|
||||
@@ -8,12 +8,15 @@ from pathlib import Path
|
||||
from typing import Callable
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QEnterEvent, QPixmap
|
||||
from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
from src.core.enums import Theme
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FieldContainer(QWidget):
|
||||
# TODO: reference a resources folder rather than path.parents[3]?
|
||||
@@ -78,7 +81,6 @@ class FieldContainer(QWidget):
|
||||
|
||||
self.title_widget = QLabel()
|
||||
self.title_widget.setMinimumHeight(button_size)
|
||||
self.title_widget.setMinimumWidth(200)
|
||||
self.title_widget.setObjectName("fieldTitle")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setText(title)
|
||||
@@ -123,6 +125,7 @@ class FieldContainer(QWidget):
|
||||
self.field.setLayout(self.field_layout)
|
||||
self.inner_layout.addWidget(self.field)
|
||||
|
||||
self.set_title(title)
|
||||
self.setStyleSheet(FieldContainer.container_style)
|
||||
|
||||
def set_copy_callback(self, callback: Callable | None = None):
|
||||
@@ -188,6 +191,10 @@ class FieldContainer(QWidget):
|
||||
self.remove_button.setHidden(True)
|
||||
return super().leaveEvent(event)
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
|
||||
self.title_widget.setFixedWidth(int(event.size().width() // 1.5))
|
||||
return super().resizeEvent(event)
|
||||
|
||||
|
||||
class FieldWidget(QWidget):
|
||||
def __init__(self, title) -> None:
|
||||
|
||||
@@ -216,15 +216,13 @@ class ItemThumb(FlowWidget):
|
||||
self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
|
||||
self.opener = FileOpenerHelper("")
|
||||
open_file_action = QAction(self)
|
||||
Translations.translate_qobject(open_file_action, "file.open_file")
|
||||
open_file_action = QAction(Translations["file.open_file"], self)
|
||||
open_file_action.triggered.connect(self.opener.open_file)
|
||||
open_explorer_action = QAction(open_file_str(), self)
|
||||
open_explorer_action.triggered.connect(self.opener.open_explorer)
|
||||
|
||||
self.delete_action = QAction(self)
|
||||
Translations.translate_qobject(
|
||||
self.delete_action, "trash.context.ambiguous", trash_term=trash_term()
|
||||
self.delete_action = QAction(
|
||||
Translations["trash.context.ambiguous"].format(trash_term=trash_term()), self
|
||||
)
|
||||
|
||||
self.thumb_button.addAction(open_file_action)
|
||||
@@ -313,8 +311,7 @@ class ItemThumb(FlowWidget):
|
||||
self.cb_layout.addWidget(badge)
|
||||
|
||||
# Filename Label =======================================================
|
||||
self.file_label = QLabel()
|
||||
Translations.translate_qobject(self.file_label, "generic.filename")
|
||||
self.file_label = QLabel(Translations["generic.filename"])
|
||||
self.file_label.setStyleSheet(ItemThumb.filename_style)
|
||||
self.file_label.setMaximumHeight(self.label_height)
|
||||
if not show_filename_label:
|
||||
|
||||
@@ -61,11 +61,10 @@ class LandingWidget(QWidget):
|
||||
open_shortcut_text = "(⌘+O)"
|
||||
else:
|
||||
open_shortcut_text = "(Ctrl+O)"
|
||||
self.open_button: QPushButton = QPushButton()
|
||||
self.open_button.setMinimumWidth(200)
|
||||
Translations.translate_qobject(
|
||||
self.open_button, "landing.open_create_library", shortcut=open_shortcut_text
|
||||
self.open_button: QPushButton = QPushButton(
|
||||
Translations["landing.open_create_library"].format(shortcut=open_shortcut_text)
|
||||
)
|
||||
self.open_button.setMinimumWidth(200)
|
||||
self.open_button.clicked.connect(self.driver.open_library_from_dialog)
|
||||
|
||||
# Create status label --------------------------------------------------
|
||||
@@ -161,7 +160,7 @@ class LandingWidget(QWidget):
|
||||
# self.status_pos_anim.setEndValue(self.status_label.pos())
|
||||
# self.status_pos_anim.start()
|
||||
|
||||
def set_status_label(self, text=str):
|
||||
def set_status_label(self, text: str):
|
||||
"""Set the text of the status label.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -57,7 +57,7 @@ class JsonMigrationModal(QObject):
|
||||
self.is_migration_initialized: bool = False
|
||||
self.discrepancies: list[str] = []
|
||||
|
||||
self.title: str = Translations.translate_formatted("json_migration.title", path=self.path)
|
||||
self.title: str = Translations["json_migration.title"].format(path=self.path)
|
||||
self.warning: str = "<b><a style='color: #e22c3c'>(!)</a></b>"
|
||||
|
||||
self.old_entry_count: int = 0
|
||||
@@ -81,17 +81,14 @@ class JsonMigrationModal(QObject):
|
||||
def init_page_info(self) -> None:
|
||||
"""Initialize the migration info page."""
|
||||
body_wrapper: PagedBodyWrapper = PagedBodyWrapper()
|
||||
body_label = QLabel()
|
||||
Translations.translate_qobject(body_label, "json_migration.info.description")
|
||||
body_label = QLabel(Translations["json_migration.info.description"])
|
||||
body_label.setWordWrap(True)
|
||||
body_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
body_wrapper.layout().addWidget(body_label)
|
||||
body_wrapper.layout().setContentsMargins(0, 36, 0, 0)
|
||||
|
||||
cancel_button = QPushButtonWrapper()
|
||||
Translations.translate_qobject(cancel_button, "generic.cancel")
|
||||
next_button = QPushButtonWrapper()
|
||||
Translations.translate_qobject(next_button, "generic.continue")
|
||||
cancel_button = QPushButtonWrapper(Translations["generic.cancel"])
|
||||
next_button = QPushButtonWrapper(Translations["generic.continue"])
|
||||
cancel_button.clicked.connect(self.migration_cancelled.emit)
|
||||
|
||||
self.stack.append(
|
||||
@@ -142,8 +139,7 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
old_lib_container: QWidget = QWidget()
|
||||
old_lib_layout: QVBoxLayout = QVBoxLayout(old_lib_container)
|
||||
old_lib_title = QLabel()
|
||||
Translations.translate_qobject(old_lib_title, "json_migration.title.old_lib")
|
||||
old_lib_title = QLabel(Translations["json_migration.title.old_lib"])
|
||||
old_lib_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
old_lib_layout.addWidget(old_lib_title)
|
||||
|
||||
@@ -210,8 +206,7 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
new_lib_container: QWidget = QWidget()
|
||||
new_lib_layout: QVBoxLayout = QVBoxLayout(new_lib_container)
|
||||
new_lib_title = QLabel()
|
||||
Translations.translate_qobject(new_lib_title, "json_migration.title.new_lib")
|
||||
new_lib_title = QLabel(Translations["json_migration.title.new_lib"])
|
||||
new_lib_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
new_lib_layout.addWidget(new_lib_title)
|
||||
|
||||
@@ -292,15 +287,14 @@ class JsonMigrationModal(QObject):
|
||||
self.body_wrapper_01.layout().addWidget(desc_label)
|
||||
self.body_wrapper_01.layout().setSpacing(12)
|
||||
|
||||
back_button = QPushButtonWrapper()
|
||||
Translations.translate_qobject(back_button, "generic.navigation.back")
|
||||
start_button = QPushButtonWrapper()
|
||||
Translations.translate_qobject(start_button, "json_migration.start_and_preview")
|
||||
back_button = QPushButtonWrapper(Translations["generic.navigation.back"])
|
||||
start_button = QPushButtonWrapper(Translations["json_migration.start_and_preview"])
|
||||
start_button.setMinimumWidth(120)
|
||||
start_button.clicked.connect(self.migrate)
|
||||
start_button.clicked.connect(lambda: start_button.setDisabled(True))
|
||||
finish_button: QPushButtonWrapper = QPushButtonWrapper()
|
||||
Translations.translate_qobject(finish_button, "json_migration.finish_migration")
|
||||
finish_button: QPushButtonWrapper = QPushButtonWrapper(
|
||||
Translations["json_migration.finish_migration"]
|
||||
)
|
||||
finish_button.setMinimumWidth(120)
|
||||
finish_button.setDisabled(True)
|
||||
finish_button.clicked.connect(self.finish_migration)
|
||||
@@ -411,8 +405,8 @@ class JsonMigrationModal(QObject):
|
||||
logger.info('Temporary migration file "temp_path" already exists. Removing...')
|
||||
self.temp_path.unlink()
|
||||
self.sql_lib.open_sqlite_library(self.json_lib.library_dir, is_new=True)
|
||||
yield Translations.translate_formatted(
|
||||
"json_migration.migrating_files_entries", entries=len(self.json_lib.entries)
|
||||
yield Translations["json_migration.migrating_files_entries"].format(
|
||||
entries=len(self.json_lib.entries)
|
||||
)
|
||||
self.sql_lib.migrate_json_to_sqlite(self.json_lib)
|
||||
yield Translations["json_migration.checking_for_parity"]
|
||||
@@ -481,15 +475,12 @@ class JsonMigrationModal(QObject):
|
||||
QApplication.alert(self.paged_panel)
|
||||
if not show_msg_box:
|
||||
return
|
||||
msg_box = QMessageBox()
|
||||
Translations.translate_with_setter(
|
||||
msg_box.setWindowTitle, "json_migration.discrepancies_found"
|
||||
)
|
||||
Translations.translate_qobject(
|
||||
msg_box, "json_migration.discrepancies_found.description"
|
||||
msg_box = QMessageBox(
|
||||
QMessageBox.Icon.Warning,
|
||||
Translations["json_migration.discrepancies_found"],
|
||||
Translations["json_migration.discrepancies_found.description"],
|
||||
)
|
||||
msg_box.setDetailedText("\n".join(self.discrepancies))
|
||||
msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
msg_box.exec()
|
||||
|
||||
def finish_migration(self):
|
||||
|
||||
@@ -15,8 +15,8 @@ class PagedPanelState:
|
||||
title: str,
|
||||
body_wrapper: PagedBodyWrapper,
|
||||
buttons: list[QPushButton | int],
|
||||
connect_to_back=list[QPushButton],
|
||||
connect_to_next=list[QPushButton],
|
||||
connect_to_back: list[QPushButton],
|
||||
connect_to_next: list[QPushButton],
|
||||
):
|
||||
self.title: str = title
|
||||
self.body_wrapper: PagedBodyWrapper = body_wrapper
|
||||
|
||||
@@ -52,8 +52,7 @@ class PanelModal(QWidget):
|
||||
# self.cancel_button.setText('Cancel')
|
||||
|
||||
if not (save_callback or has_save):
|
||||
self.done_button = QPushButton()
|
||||
Translations.translate_qobject(self.done_button, "generic.done")
|
||||
self.done_button = QPushButton(Translations["generic.done"])
|
||||
self.done_button.setAutoDefault(True)
|
||||
self.done_button.clicked.connect(self.hide)
|
||||
if done_callback:
|
||||
@@ -62,16 +61,14 @@ class PanelModal(QWidget):
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
|
||||
if save_callback or has_save:
|
||||
self.cancel_button = QPushButton()
|
||||
Translations.translate_qobject(self.cancel_button, "generic.cancel")
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel"])
|
||||
self.cancel_button.clicked.connect(self.hide)
|
||||
self.cancel_button.clicked.connect(widget.reset)
|
||||
# self.cancel_button.clicked.connect(cancel_callback)
|
||||
self.widget.panel_cancel_button = self.cancel_button
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.save_button = QPushButton()
|
||||
Translations.translate_qobject(self.save_button, "generic.save")
|
||||
self.save_button = QPushButton(Translations["generic.save"])
|
||||
self.save_button.setAutoDefault(True)
|
||||
self.save_button.clicked.connect(self.hide)
|
||||
self.save_button.clicked.connect(self.saved.emit)
|
||||
|
||||
@@ -246,7 +246,7 @@ class FieldContainers(QWidget):
|
||||
return cats
|
||||
|
||||
def remove_field_prompt(self, name: str) -> str:
|
||||
return Translations.translate_formatted("library.field.confirm_remove", name=name)
|
||||
return Translations["library.field.confirm_remove"].format(name=name)
|
||||
|
||||
def add_field_to_selected(self, field_list: list):
|
||||
"""Add list of entry fields to one or more selected items.
|
||||
|
||||
@@ -23,6 +23,7 @@ from src.core.enums import Theme
|
||||
from src.core.library.alchemy.library import Library
|
||||
from src.core.media_types import MediaCategories
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel
|
||||
from src.qt.translations import Translations
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
@@ -108,16 +109,22 @@ class FileAttributes(QWidget):
|
||||
created = dt.fromtimestamp(filepath.stat().st_ctime)
|
||||
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
|
||||
self.date_created_label.setText(
|
||||
f"<b>Date Created:</b> {dt.strftime(created, "%a, %x, %X")}" # TODO: Translate
|
||||
f"<b>{Translations["file.date_created"]}:</b> "
|
||||
f"{dt.strftime(created, "%a, %x, %X")}"
|
||||
)
|
||||
self.date_modified_label.setText(
|
||||
f"<b>Date Modified:</b> {dt.strftime(modified, "%a, %x, %X")}" # TODO: Translate
|
||||
f"<b>{Translations["file.date_modified"]}:</b> "
|
||||
f"{dt.strftime(modified, "%a, %x, %X")}"
|
||||
)
|
||||
self.date_created_label.setHidden(False)
|
||||
self.date_modified_label.setHidden(False)
|
||||
elif filepath:
|
||||
self.date_created_label.setText("<b>Date Created:</b> <i>N/A</i>") # TODO: Translate
|
||||
self.date_modified_label.setText("<b>Date Modified:</b> <i>N/A</i>") # TODO: Translate
|
||||
self.date_created_label.setText(
|
||||
f"<b>{Translations["file.date_created"]}:</b> <i>N/A</i>"
|
||||
)
|
||||
self.date_modified_label.setText(
|
||||
f"<b>{Translations["file.date_modified"]}:</b> <i>N/A</i>"
|
||||
)
|
||||
self.date_created_label.setHidden(False)
|
||||
self.date_modified_label.setHidden(False)
|
||||
else:
|
||||
@@ -132,7 +139,7 @@ class FileAttributes(QWidget):
|
||||
if not filepath:
|
||||
self.layout().setSpacing(0)
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.file_label.setText("<i>No Items Selected</i>") # TODO: Translate
|
||||
self.file_label.setText(f"<i>{Translations["preview.no_selection"]}</i>")
|
||||
self.file_label.set_file_path("")
|
||||
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.dimensions_label.setText("")
|
||||
@@ -204,11 +211,14 @@ class FileAttributes(QWidget):
|
||||
|
||||
if duration_text:
|
||||
stats_label_text = add_newline(stats_label_text)
|
||||
dur_str = str(timedelta(seconds=float(duration_text)))[:-7]
|
||||
if dur_str.startswith("0:"):
|
||||
dur_str = dur_str[2:]
|
||||
if dur_str.startswith("0"):
|
||||
dur_str = dur_str[1:]
|
||||
try:
|
||||
dur_str = str(timedelta(seconds=float(duration_text)))[:-7]
|
||||
if dur_str.startswith("0:"):
|
||||
dur_str = dur_str[2:]
|
||||
if dur_str.startswith("0"):
|
||||
dur_str = dur_str[1:]
|
||||
except OverflowError:
|
||||
dur_str = "-:--"
|
||||
stats_label_text += f"{dur_str}"
|
||||
|
||||
if font_family:
|
||||
@@ -221,7 +231,7 @@ class FileAttributes(QWidget):
|
||||
"""Format attributes for multiple selected items."""
|
||||
self.layout().setSpacing(0)
|
||||
self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.file_label.setText(f"<b>{count}</b> Items Selected") # TODO: Translate
|
||||
self.file_label.setText(Translations["preview.multiple_selection"].format(count=count))
|
||||
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.file_label.set_file_path("")
|
||||
self.dimensions_label.setText("")
|
||||
|
||||
@@ -54,12 +54,10 @@ class PreviewThumb(QWidget):
|
||||
image_layout = QHBoxLayout(self)
|
||||
image_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.open_file_action = QAction(self)
|
||||
Translations.translate_qobject(self.open_file_action, "file.open_file")
|
||||
self.open_file_action = QAction(Translations["file.open_file"], self)
|
||||
self.open_explorer_action = QAction(open_file_str(), self)
|
||||
self.delete_action = QAction(self)
|
||||
Translations.translate_qobject(
|
||||
self.delete_action, "trash.context.ambiguous", trash_term=trash_term()
|
||||
self.delete_action = QAction(
|
||||
Translations["trash.context.ambiguous"].format(trash_term=trash_term()), self
|
||||
)
|
||||
|
||||
self.preview_img = QPushButtonWrapper()
|
||||
@@ -380,7 +378,7 @@ class PreviewThumb(QWidget):
|
||||
self.delete_action.triggered.disconnect()
|
||||
|
||||
self.delete_action.setText(
|
||||
Translations.translate_formatted("trash.context.singular", trash_term=trash_term())
|
||||
Translations["trash.context.singular"].format(trash_term=trash_term())
|
||||
)
|
||||
self.delete_action.triggered.connect(
|
||||
lambda checked=False, f=filepath: self.driver.delete_files_callback(f)
|
||||
|
||||
@@ -74,10 +74,8 @@ class PreviewPanel(QWidget):
|
||||
self.fields = FieldContainers(library, driver)
|
||||
|
||||
self.tag_search_panel = TagSearchPanel(self.driver.lib, is_tag_chooser=True)
|
||||
self.add_tag_modal = PanelModal(
|
||||
self.tag_search_panel, Translations.translate_formatted("tag.add.plural")
|
||||
)
|
||||
Translations.translate_with_setter(self.add_tag_modal.setWindowTitle, "tag.add.plural")
|
||||
self.add_tag_modal = PanelModal(self.tag_search_panel, Translations["tag.add.plural"])
|
||||
self.add_tag_modal.setWindowTitle(Translations["tag.add.plural"])
|
||||
|
||||
self.add_field_modal = AddFieldModal(self.lib)
|
||||
|
||||
@@ -100,19 +98,17 @@ class PreviewPanel(QWidget):
|
||||
add_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
add_buttons_layout.setSpacing(6)
|
||||
|
||||
self.add_tag_button = QPushButton()
|
||||
self.add_tag_button = QPushButton(Translations["tag.add"])
|
||||
self.add_tag_button.setEnabled(False)
|
||||
self.add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.add_tag_button.setMinimumHeight(28)
|
||||
self.add_tag_button.setStyleSheet(PreviewPanel.button_style)
|
||||
self.add_tag_button.setText("Add Tag") # TODO: Translate
|
||||
|
||||
self.add_field_button = QPushButton()
|
||||
self.add_field_button = QPushButton(Translations["library.field.add"])
|
||||
self.add_field_button.setEnabled(False)
|
||||
self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.add_field_button.setMinimumHeight(28)
|
||||
self.add_field_button.setStyleSheet(PreviewPanel.button_style)
|
||||
self.add_field_button.setText("Add Field") # TODO: Translate
|
||||
|
||||
add_buttons_layout.addWidget(self.add_tag_button)
|
||||
add_buttons_layout.addWidget(self.add_field_button)
|
||||
|
||||
@@ -130,7 +130,7 @@ class TagWidget(QWidget):
|
||||
|
||||
if has_edit:
|
||||
edit_action = QAction(self)
|
||||
edit_action.setText(Translations.translate_formatted("generic.edit"))
|
||||
edit_action.setText(Translations["generic.edit"])
|
||||
edit_action.triggered.connect(on_edit_callback)
|
||||
edit_action.triggered.connect(self.on_edit.emit)
|
||||
self.bg_button.addAction(edit_action)
|
||||
@@ -140,7 +140,7 @@ class TagWidget(QWidget):
|
||||
# TODO: This currently doesn't work in "Add Tag" menus. Either fix this or
|
||||
# disable it in that context.
|
||||
self.search_for_tag_action = QAction(self)
|
||||
self.search_for_tag_action.setText(Translations.translate_formatted("tag.search_for_tag"))
|
||||
self.search_for_tag_action.setText(Translations["tag.search_for_tag"])
|
||||
self.bg_button.addAction(self.search_for_tag_action)
|
||||
# add_to_search_action = QAction(self)
|
||||
# add_to_search_action.setText(Translations.translate_formatted("tag.add_to_search"))
|
||||
@@ -185,7 +185,7 @@ class TagWidget(QWidget):
|
||||
primary_color = get_primary_color(tag)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (tag.color and tag.color.secondary)
|
||||
if not (tag.color and tag.color.secondary and tag.color.color_border)
|
||||
else (QColor(tag.color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
@@ -209,7 +209,6 @@ class TagWidget(QWidget):
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 4px;"
|
||||
f"padding-bottom: 1px;"
|
||||
f"padding-left: 4px;"
|
||||
f"font-size: 13px"
|
||||
f"}}"
|
||||
@@ -222,8 +221,12 @@ class TagWidget(QWidget):
|
||||
f"border-color: rgba{primary_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"outline:none;"
|
||||
f"padding-right: 0px;"
|
||||
f"padding-left: 0px;"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 1px;"
|
||||
f"outline-radius: 4px;"
|
||||
f"outline-color: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
@@ -305,6 +308,7 @@ def get_highlight_color(primary_color: QColor) -> QColor:
|
||||
|
||||
|
||||
def get_text_color(primary_color: QColor, highlight_color: QColor) -> QColor:
|
||||
# logger.info("[TagWidget] Evaluating tag text color", lightness=primary_color.lightness())
|
||||
if primary_color.lightness() > 120:
|
||||
text_color = QColor(primary_color)
|
||||
text_color = text_color.toHsl()
|
||||
|
||||
201
tagstudio/src/qt/widgets/tag_color_label.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QAction, QColor, QEnterEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.qt.helpers.escape_text import escape_text
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_text_color,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.core.library.alchemy import Library
|
||||
|
||||
|
||||
class TagColorLabel(QWidget):
|
||||
"""A widget for displaying a tag color's name.
|
||||
|
||||
Not to be confused with a tag color swatch widget.
|
||||
"""
|
||||
|
||||
on_remove = Signal()
|
||||
on_click = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: TagColorGroup | None,
|
||||
has_edit: bool,
|
||||
has_remove: bool,
|
||||
library: "Library | None" = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.color = color
|
||||
self.lib: Library | None = library
|
||||
self.has_edit = has_edit
|
||||
self.has_remove = has_remove
|
||||
|
||||
self.base_layout = QVBoxLayout(self)
|
||||
self.base_layout.setObjectName("baseLayout")
|
||||
self.base_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.bg_button = QPushButton(self)
|
||||
self.bg_button.setFlat(True)
|
||||
|
||||
edit_action = QAction(self)
|
||||
edit_action.setText(Translations["generic.edit"])
|
||||
edit_action.triggered.connect(self.on_click.emit)
|
||||
self.bg_button.addAction(edit_action)
|
||||
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
if has_edit:
|
||||
self.bg_button.clicked.connect(self.on_click.emit)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
else:
|
||||
edit_action.setEnabled(False)
|
||||
|
||||
self.inner_layout = QHBoxLayout()
|
||||
self.inner_layout.setObjectName("innerLayout")
|
||||
self.inner_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.remove_button = QPushButton(self)
|
||||
self.remove_button.setFlat(True)
|
||||
self.remove_button.setText("–")
|
||||
self.remove_button.setHidden(True)
|
||||
self.remove_button.setMinimumSize(22, 22)
|
||||
self.remove_button.setMaximumSize(22, 22)
|
||||
self.inner_layout.addWidget(self.remove_button)
|
||||
self.inner_layout.addStretch(1)
|
||||
if self.has_remove:
|
||||
self.remove_button.clicked.connect(self.on_remove.emit)
|
||||
else:
|
||||
self.remove_button.setHidden(True)
|
||||
|
||||
self.bg_button.setLayout(self.inner_layout)
|
||||
self.bg_button.setMinimumSize(44, 22)
|
||||
self.bg_button.setMaximumHeight(22)
|
||||
|
||||
self.base_layout.addWidget(self.bg_button)
|
||||
|
||||
# NOTE: Do this if you don't want the tag to stretch, like in a search.
|
||||
# self.bg_button.setMaximumWidth(self.bg_button.sizeHint().width())
|
||||
|
||||
self.set_color(color)
|
||||
|
||||
def set_color(self, color: TagColorGroup | None) -> None:
|
||||
self.color = color
|
||||
|
||||
if not color:
|
||||
return
|
||||
|
||||
primary_color = self._get_primary_color(color)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (color and color.secondary and color.color_border)
|
||||
else (QColor(color.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
primary_color if not (color and color.secondary) else QColor(color.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if color and color.secondary:
|
||||
text_color = QColor(color.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
self.bg_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 600;"
|
||||
f"border-color: rgba{border_color.toTuple()};"
|
||||
f"border-radius: 6px;"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 4px;"
|
||||
f"padding-left: 4px;"
|
||||
f"font-size: 13px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::pressed{{"
|
||||
f"background: rgba{highlight_color.toTuple()};"
|
||||
f"color: rgba{primary_color.toTuple()};"
|
||||
f"border-color: rgba{primary_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"padding-right: 0px;"
|
||||
f"padding-left: 0px;"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 1px;"
|
||||
f"outline-radius: 4px;"
|
||||
f"outline-color: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
self.remove_button.setStyleSheet(
|
||||
f"QPushButton{{"
|
||||
f"color: rgba{primary_color.toTuple()};"
|
||||
f"background: rgba{text_color.toTuple()};"
|
||||
f"font-weight: 800;"
|
||||
f"border-radius: 5px;"
|
||||
f"border-width: 4;"
|
||||
f"border-color: rgba(0,0,0,0);"
|
||||
f"padding-bottom: 4px;"
|
||||
f"font-size: 14px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"background: rgba{primary_color.toTuple()};"
|
||||
f"color: rgba{text_color.toTuple()};"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"border-width: 2;"
|
||||
f"border-radius: 6px;"
|
||||
f"}}"
|
||||
f"QPushButton::pressed{{"
|
||||
f"background: rgba{border_color.toTuple()};"
|
||||
f"color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"background: rgba{border_color.toTuple()};"
|
||||
f"outline:none;"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
self.bg_button.setText(escape_text(color.name))
|
||||
|
||||
def _get_primary_color(self, color: TagColorGroup) -> QColor:
|
||||
primary_color = QColor(color.primary)
|
||||
|
||||
return primary_color
|
||||
|
||||
def set_has_remove(self, has_remove: bool):
|
||||
self.has_remove = has_remove
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
|
||||
if self.has_remove:
|
||||
self.remove_button.setHidden(False)
|
||||
self.update()
|
||||
return super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
|
||||
if self.has_remove:
|
||||
self.remove_button.setHidden(True)
|
||||
self.update()
|
||||
return super().leaveEvent(event)
|
||||
@@ -3,6 +3,8 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
@@ -15,6 +17,14 @@ from src.core.library.alchemy.enums import TagColorEnum
|
||||
from src.core.library.alchemy.models import TagColorGroup
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.tag import (
|
||||
get_border_color,
|
||||
get_highlight_color,
|
||||
get_text_color,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.core.library import Library
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -24,9 +34,11 @@ class TagColorPreview(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: "Library",
|
||||
tag_color_group: TagColorGroup | None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.lib: Library = library
|
||||
self.tag_color_group = tag_color_group
|
||||
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
@@ -44,28 +56,36 @@ class TagColorPreview(QWidget):
|
||||
|
||||
self.set_tag_color_group(tag_color_group)
|
||||
|
||||
def set_tag_color_group(self, tag_color_group: TagColorGroup | None):
|
||||
self.tag_color_group = tag_color_group
|
||||
def set_tag_color_group(self, color_group: TagColorGroup | None):
|
||||
logger.info(
|
||||
"[TagColorPreview] Setting tag color",
|
||||
primary=color_group.primary if color_group else None,
|
||||
secondary=color_group.secondary if color_group else None,
|
||||
)
|
||||
self.tag_color_group = color_group
|
||||
|
||||
if tag_color_group:
|
||||
self.button.setText(tag_color_group.name)
|
||||
if color_group:
|
||||
self.button.setText(color_group.name)
|
||||
self.button.setText(
|
||||
f"{color_group.name} ({self.lib.get_namespace_name(color_group.namespace)})"
|
||||
)
|
||||
else:
|
||||
Translations.translate_qobject(self.button, "generic.none")
|
||||
self.button.setText(Translations["color.title.no_color"])
|
||||
|
||||
primary_color = get_primary_color(tag_color_group)
|
||||
primary_color = self._get_primary_color(color_group)
|
||||
border_color = (
|
||||
get_border_color(primary_color)
|
||||
if not (tag_color_group and tag_color_group.secondary)
|
||||
else (QColor(tag_color_group.secondary))
|
||||
if not (color_group and color_group.secondary and color_group.color_border)
|
||||
else (QColor(color_group.secondary))
|
||||
)
|
||||
highlight_color = get_highlight_color(
|
||||
primary_color
|
||||
if not (tag_color_group and tag_color_group.secondary)
|
||||
else QColor(tag_color_group.secondary)
|
||||
if not (color_group and color_group.secondary)
|
||||
else QColor(color_group.secondary)
|
||||
)
|
||||
text_color: QColor
|
||||
if tag_color_group and tag_color_group.secondary:
|
||||
text_color = QColor(tag_color_group.secondary)
|
||||
if color_group and color_group.secondary:
|
||||
text_color = QColor(color_group.secondary)
|
||||
else:
|
||||
text_color = get_text_color(primary_color, highlight_color)
|
||||
|
||||
@@ -79,50 +99,31 @@ class TagColorPreview(QWidget):
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"padding-right: 8px;"
|
||||
f"padding-bottom: 1px;"
|
||||
f"padding-left: 8px;"
|
||||
f"font-size: 14px"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"border-color: rgba{highlight_color.toTuple()};"
|
||||
f"}}"
|
||||
f"QPushButton::focus{{"
|
||||
f"padding-right: 0px;"
|
||||
f"padding-left: 0px;"
|
||||
f"outline-style: solid;"
|
||||
f"outline-width: 1px;"
|
||||
f"outline-radius: 4px;"
|
||||
f"outline-color: rgba{text_color.toTuple()};"
|
||||
f"}}"
|
||||
)
|
||||
# Add back the padding if the hint is generated while the button has focus (no padding)
|
||||
self.button.setMinimumWidth(
|
||||
self.button.sizeHint().width() + (16 if self.button.hasFocus() else 0)
|
||||
)
|
||||
self.button.setMaximumWidth(self.button.sizeHint().width())
|
||||
|
||||
def _get_primary_color(self, tag_color_group: TagColorGroup | None) -> QColor:
|
||||
primary_color = QColor(
|
||||
get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
|
||||
if not tag_color_group
|
||||
else tag_color_group.primary
|
||||
)
|
||||
|
||||
def get_primary_color(tag_color_group: TagColorGroup | None) -> QColor:
|
||||
primary_color = QColor(
|
||||
get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
|
||||
if not tag_color_group
|
||||
else tag_color_group.primary
|
||||
)
|
||||
|
||||
return primary_color
|
||||
|
||||
|
||||
def get_border_color(primary_color: QColor) -> QColor:
|
||||
border_color: QColor = QColor(primary_color)
|
||||
border_color.setRed(min(border_color.red() + 20, 255))
|
||||
border_color.setGreen(min(border_color.green() + 20, 255))
|
||||
border_color.setBlue(min(border_color.blue() + 20, 255))
|
||||
|
||||
return border_color
|
||||
|
||||
|
||||
def get_highlight_color(primary_color: QColor) -> QColor:
|
||||
highlight_color: QColor = QColor(primary_color)
|
||||
highlight_color = highlight_color.toHsl()
|
||||
highlight_color.setHsl(highlight_color.hue(), min(highlight_color.saturation(), 200), 225, 255)
|
||||
highlight_color = highlight_color.toRgb()
|
||||
|
||||
return highlight_color
|
||||
|
||||
|
||||
def get_text_color(primary_color: QColor, highlight_color: QColor) -> QColor:
|
||||
if primary_color.lightness() > 120:
|
||||
text_color = QColor(primary_color)
|
||||
text_color = text_color.toHsl()
|
||||
text_color.setHsl(text_color.hue(), text_color.saturation(), 50, 255)
|
||||
return text_color.toRgb()
|
||||
else:
|
||||
return highlight_color
|
||||
return primary_color
|
||||
|
||||
@@ -116,8 +116,7 @@ class VideoPlayer(QGraphicsView):
|
||||
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.opener = FileOpenerHelper(filepath=self.filepath)
|
||||
autoplay_action = QAction(self)
|
||||
Translations.translate_qobject(autoplay_action, "media_player.autoplay")
|
||||
autoplay_action = QAction(Translations["media_player.autoplay"], self)
|
||||
autoplay_action.setCheckable(True)
|
||||
self.addAction(autoplay_action)
|
||||
autoplay_action.setChecked(
|
||||
@@ -126,8 +125,7 @@ class VideoPlayer(QGraphicsView):
|
||||
autoplay_action.triggered.connect(lambda: self.toggle_autoplay())
|
||||
self.autoplay = autoplay_action
|
||||
|
||||
open_file_action = QAction(self)
|
||||
Translations.translate_qobject(open_file_action, "file.open_file")
|
||||
open_file_action = QAction(Translations["file.open_file"], self)
|
||||
open_file_action.triggered.connect(self.opener.open_file)
|
||||
|
||||
open_explorer_action = QAction(open_file_str(), self)
|
||||
|
||||
BIN
tagstudio/tests/fixtures/empty_libraries/DB_VERSION_6/.TagStudio/ts_library.sqlite
vendored
Normal file
BIN
tagstudio/tests/fixtures/empty_libraries/DB_VERSION_7/.TagStudio/ts_library.sqlite
vendored
Normal file
BIN
tagstudio/tests/fixtures/empty_libraries/DB_VERSION_8/.TagStudio/ts_library.sqlite
vendored
Normal file
47
tagstudio/tests/test_db_migrations.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from src.core.constants import TS_FOLDER_NAME
|
||||
from src.core.library.alchemy.library import Library
|
||||
|
||||
CWD = Path(__file__)
|
||||
FIXTURES = "fixtures"
|
||||
EMPTY_LIBRARIES = "empty_libraries"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_6")),
|
||||
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_7")),
|
||||
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_8")),
|
||||
],
|
||||
)
|
||||
def test_library_migrations(path: str):
|
||||
library = Library()
|
||||
|
||||
# Copy libraries to temp dir so modifications don't show up in version control
|
||||
original_path = Path(path)
|
||||
temp_path = Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_TEMP")
|
||||
temp_path.mkdir(exist_ok=True)
|
||||
temp_path_ts = temp_path / TS_FOLDER_NAME
|
||||
temp_path_ts.mkdir(exist_ok=True)
|
||||
shutil.copy(
|
||||
original_path / TS_FOLDER_NAME / Library.SQL_FILENAME,
|
||||
temp_path / TS_FOLDER_NAME / Library.SQL_FILENAME,
|
||||
)
|
||||
|
||||
try:
|
||||
status = library.open_library(library_dir=temp_path)
|
||||
library.close()
|
||||
shutil.rmtree(temp_path)
|
||||
assert status.success
|
||||
except Exception as e:
|
||||
library.close()
|
||||
shutil.rmtree(temp_path)
|
||||
raise (e)
|
||||