mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-06-23 15:41:45 +00:00
Compare commits
9 Commits
file-metad
...
modify-fie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71a01c7156 | ||
|
|
e499a3e68b | ||
|
|
4321bbfdb3 | ||
|
|
185691bcd2 | ||
|
|
ae787eaf5e | ||
|
|
8b6f649c54 | ||
|
|
204d4be303 | ||
|
|
e5fa85448a | ||
|
|
6f3c66efd9 |
41
.gitignore
vendored
41
.gitignore
vendored
@@ -7,10 +7,10 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
# C Extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
# Distribution / Packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
@@ -41,7 +41,7 @@ MANIFEST
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
# Unit test / Coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
@@ -88,6 +88,8 @@ profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
@@ -95,14 +97,21 @@ ipython_config.py
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
poetry.lock
|
||||
poetry.toml
|
||||
|
||||
# uv
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
uv.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
@@ -111,6 +120,8 @@ ipython_config.py
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
@@ -138,8 +149,24 @@ venv.bak/
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# macoS
|
||||
*.DS_Store
|
||||
# macOS
|
||||
.DocumentRevisions-V100
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
|
||||
# Linux
|
||||
.directory
|
||||
.fuse_hidden*
|
||||
.nfs*
|
||||
.Trash-*
|
||||
|
||||
# Windows
|
||||
[Dd]esktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
BIN
docs/assets/library_information.png
Normal file
BIN
docs/assets/library_information.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/assets/settings_refresh_library_on_open.png
Normal file
BIN
docs/assets/settings_refresh_library_on_open.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -23,7 +23,7 @@ If you wish to discuss TagStudio further, feel free to join the [Discord Server]
|
||||
|
||||
#### Feature Additions
|
||||
|
||||
- [x] I've read the [Feature Roadmap](roadmap.md) and understand what core features are planned, their priorities, and their scheduled timelines
|
||||
- [x] I've read the [Roadmap](roadmap.md) and understand what core features are planned, their priorities, and their scheduled timelines
|
||||
- [x] I've found an existing [feature request](https://github.com/TagStudioDev/TagStudio/issues) or created my own **_before starting work on a feature_** so that the feature can be discussed beforehand
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
||||
@@ -87,7 +87,7 @@ hide:
|
||||
|
||||
[:material-arrow-right: View License](https://github.com/TagStudioDev/TagStudio/blob/main/LICENSE)
|
||||
|
||||
[:material-arrow-right: Roadmap to MIT Core Library License](roadmap.md#core-library-api)
|
||||
[:material-arrow-right: Roadmap to MIT Core Library License](roadmap.md#material-database-cog-core-library--cli)
|
||||
|
||||
- :material-database:{ .lg .middle } **Central Save File**
|
||||
|
||||
|
||||
@@ -8,10 +8,79 @@ icon: material/database
|
||||
|
||||
# :material-database: Libraries
|
||||
|
||||
A TagStudio library represents a folder of content (photos, documents, or [any other files](preview-support.md)) and contains TagStudio data specific to that library ([tags](tags.md), [fields](fields.md), [colors](colors.md), etc.) along with the associations between that data and your files. A library folder can be stored locally on your computer, on an external drive, on a network drive/NAS, or most other locations accessible by your system. TagStudio passively and non-destructively includes contents of this folder (including subfolders) in your library as [file entries](entries.md).
|
||||
|
||||
**Your files are not _moved_, _copied_, or _modified_ in any way!**
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! info
|
||||
This page is a work in progress and needs to be updated with additional information.
|
||||
!!! note "Planned Library Features & Changes"
|
||||
- The *option* to **store library data separately** from library content
|
||||
- This will enable TagStudio libraries to be created for read-only folders
|
||||
- This will enable TagStudio to have different libraries for the same content folder(s)
|
||||
- **Multi-root libraries** that can read from multiple content folders
|
||||
- This will reduce the need for using complex [`.ts_ignore`](ignore.md) rules
|
||||
- This will enable having library content that spans across different drives, most notably on Windows, without the need for OS-dependant workarounds such as symlinks
|
||||
- Sharable tag packs and color packs
|
||||
- Global tags, accessible across different libraries
|
||||
|
||||
The library is how TagStudio represents your chosen directory, with every file inside being represented by a [file entry](entries.md). You can have as many or few libraries as you wish, since each libraries' data is stored within a `.TagStudio` folder at its root. From there the library save file itself is stored as `ts_library.sqlite`, with TagStudio versions 9.4 and below using a the legacy `ts_library.json` format.
|
||||
See the [Roadmap](roadmap.md#library) for more information.
|
||||
|
||||
Note that this means [tags](tags.md) you create only exist _per-library_. Global tags along with other library structure updates are planned for future releases on the [roadmap](roadmap.md#library).
|
||||
## :material-database-plus: Creating/Opening a Library
|
||||
|
||||
To create or open a [library](libraries.md), go to **File -> Open/Create Library** in the menu bar or use <kbd>Ctrl</kbd>+<kbd>O</kbd> (<kbd>⌘ Command </kbd>+<kbd>O</kbd> on macOS) and chose a folder with file contents you'd like to use as a TagStudio library. If a `.TagStudio` folder doesn't already exist inside the directory, TagStudio will create one and automatically scan the folder for files to include. Otherwise, the pre-existing library is opened.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! info "Legacy Library Migration"
|
||||
If you open a library created with TagStudio **v9.4.2 or earlier** in **[v9.5.0](changelog.md#950-march-3rd-2025) or later**, you'll be walked through a migration process that converts the old `ts_library.json` save file to the new `ts_library.sqlite` format. The original JSON file is preserved and can be easily deleted from the **View -> Library Information** panel once you're satisfied with the migration.
|
||||
|
||||
## :material-database-refresh: Refreshing Directories
|
||||
|
||||
TagStudio automatically scans for new or updated files when opening a library by default. This behavior can be toggled in the settings if your library is very large and/or located on a slow drive.
|
||||
|
||||

|
||||
|
||||
To manually refresh your library at any time, use **File -> Refresh Directories** from the menu or by using <kbd>Ctrl</kbd>+<kbd>R</kbd> (<kbd>⌘ Command </kbd>+<kbd>R</kbd> on macOS).
|
||||
|
||||
## :material-database-cog: Library Information Panel
|
||||
|
||||
The "Library Information" panel can be accessed from **Tools -> Library Information** in the menu bar, and includes various statistics about your library along with quick access to managing common library cleanup tasks such as relinking entries, updating ignored files, and managing library data backups.
|
||||
|
||||

|
||||
|
||||
## :material-database-clock: Saving and Creating Backups
|
||||
|
||||
As of v9.5.0, libraries save automatically as you work.
|
||||
|
||||
To create a timestamped backup of your library save file, go to **File -> Save Library Backup** or use <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> (<kbd>⌘ Command </kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> on macOS). Backups are also automatically created whenever the database file is migrated to a newer version as a precautionary measure. Backups currently _only_ include your `ts_library.sqlite` file, as that's the database file that contains your core TagStudio data. Your own files are **not** part of any of these backups.
|
||||
|
||||
Backups are stored inside the library data folder under `.TagStudio/backups/` and can be managed from the **Tools -> Library Information** panel.
|
||||
|
||||
## :material-folder: Library Data Folder
|
||||
|
||||
When you create a library, TagStudio creates a hidden `.TagStudio` folder at the root of the chosen content folder. This "data folder" contains all TagStudio data for that library. Library data includes what files are included in your library, what [tags](tags.md) you've created in that library, which files have what tags, and more. Note that this means tags you create only exist _per-library_. Global tags that are accessible across libraries are planned for a [future update](roadmap.md#library).
|
||||
|
||||
### :material-file-tree: Data Folder Structure
|
||||
|
||||
The library data folder (currently only named `.TagStudio`) is internally structured as follows:
|
||||
|
||||
| File/Folder | Description |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ts_library.sqlite` | The library save file. Stores all entries, tags, fields, and other metadata. _(v9.5.0+)_ |
|
||||
| `.ts_ignore` | An optional ["ignore" file](ignore.md) for excluding files and folders from library scans, similar to a [`.gitignore`](https://git-scm.com/docs/gitignore) file. |
|
||||
| `backups/` | Timestamped backups of the library save file. |
|
||||
| `thumbs/` | Thumbnail images for file previews. |
|
||||
|
||||
```yaml title="Library Folder Example"
|
||||
My Library/ # (Content Folder)
|
||||
├─ file_1.jpg
|
||||
├─ file_2.txt
|
||||
├─ .TagStudio/ # (Data Folder)
|
||||
│ ├─ ts_library.sqlite (References outer folder for files)
|
||||
│ ├─ .ts_ignore
|
||||
│ ├─ backups/
|
||||
│ ├─ thumbs/
|
||||
```
|
||||
|
||||
### :material-bag-suitcase: Library Portability
|
||||
|
||||
Because the `.TagStudio` _data folder_ is located in your library _content folder_, and it stores all file entry paths _relative_ to the content folder, your library folder can be freely moved to another location without files becoming [unlinked](entries.md#unlinked-entries). This also means that if you have a TagStudio library stored on an external drive, it can be freely moved around to different computers running TagStudio with no issues. Likewise, if your library is located on a network drive or NAS, you can access it from different computers that may map the network location differently from each other _(note that TagStudio currently does not support multiple users accessing the same library at once)._
|
||||
|
||||
@@ -50,7 +50,7 @@ An improved SQLite-based library save file format in which legacy JSON libraries
|
||||
See the "[Library](#library)" section for features related to the library database rather than the underlying schema.
|
||||
|
||||
- [x] A SQLite-based library save file format **[[v9.5.0](changelog.md#950-march-3rd-2025)]**
|
||||
- [ ] Cached File Properties Table :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.0]**
|
||||
- [ ] Cached File Properties Table :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.1]**
|
||||
- [x] Date Entry Added to Library
|
||||
- [ ] Date File Created :material-chevron-triple-up:{ .priority-high title="High Priority" }
|
||||
- [ ] Date File Modified :material-chevron-triple-up:{ .priority-high title="High Priority" }
|
||||
@@ -108,8 +108,8 @@ A detailed written specification for the TagStudio tag and/or library format. In
|
||||
- [x] Toggle Mute
|
||||
- [x] Timeline Scrubber
|
||||
- [ ] Fullscreen Mode :material-chevron-double-up:{ .priority-med title="Medium Priority" }
|
||||
- [ ] Fine-Tuned UI/UX :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.x]**
|
||||
- [ ] 3D Model Thumbnails/Previews :material-chevron-triple-up:{ .priority-high title="High Priority" } _(See #1231)_
|
||||
- [ ] Fine-Tuned UI/UX :material-chevron-triple-up:{ .priority-high title="High Priority" }
|
||||
- [ ] 3D Model Thumbnails/Previews :material-chevron-triple-up:{ .priority-high title="High Priority" } _(See Discussion #1231)_
|
||||
- [ ] STL File Support
|
||||
- [ ] OBJ File Support
|
||||
- [ ] Plaintext Thumbnails/Previews
|
||||
@@ -167,13 +167,12 @@ A detailed written specification for the TagStudio tag and/or library format. In
|
||||
File or file-like [entries](entries.md) stored in the library.
|
||||
|
||||
- [x] File Entries **[v1.0.0]**
|
||||
- [ ] Folder Entries :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.x]**
|
||||
- [ ] URL Entries / Bookmarks :material-chevron-up:{ .priority-low title="Low Priority" } **[v9.7.x]**
|
||||
- [ ] URL Entries / Bookmarks :material-chevron-up:{ .priority-low title="Low Priority" } **[v9.6.x]**
|
||||
- [x] Fields
|
||||
- [x] Text Lines
|
||||
- [x] Text Boxes
|
||||
- [x] Datetimes **[[v9.5.4](changelog.md#954-september-1st-2025)]**
|
||||
- [ ] Numeric Fields :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
|
||||
- [ ] Numeric Fields :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.6.x]**
|
||||
- [ ] Optional Units (e.g. inches, cm, height notation, degrees, bytes, etc.) :material-chevron-double-up:{ .priority-med title="Medium Priority" }
|
||||
- [ ] Custom Field Names :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
|
||||
- [x] Removal of Deprecated Fields **[v9.6.0]**
|
||||
@@ -226,14 +225,14 @@ Discrete library objects representing [attributes](<https://en.wikipedia.org/wik
|
||||
- [x] Search by Extension (e.g. ".jpg", ".png") **[[v9.5.0](changelog.md#950-march-3rd-2025)]**
|
||||
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") **[[v9.5.0](changelog.md#950-march-3rd-2025)]**
|
||||
- [x] Search by media type (e.g. "image", "video", "document") **[[v9.5.0](changelog.md#950-march-3rd-2025)]**
|
||||
- [ ] Field Content Search :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
|
||||
- [ ] Field Content Search :material-chevron-triple-up:{ .priority-high title="High Priority" }
|
||||
- [x] [Boolean Operators](search.md) **[[v9.5.0](changelog.md#950-march-3rd-2025)]**
|
||||
- [x] `AND` Operator
|
||||
- [x] `OR` Operator
|
||||
- [x] `NOT` Operator
|
||||
- [x] Parenthesis Grouping
|
||||
- [x] Character Escaping
|
||||
- [ ] `HAS` Operator (for [Component Tags](tags.md#component-tags)) :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
|
||||
- [ ] `HAS` Operator (for [Component Tags](tags.md#component-tags)) :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.8.x]**
|
||||
- [ ] Conditional Search :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.7.x]**
|
||||
- [ ] Compare Dates :material-chevron-double-up:{ .priority-med title="Medium Priority" }
|
||||
- [ ] Compare Durations :material-chevron-double-up:{ .priority-med title="Medium Priority" }
|
||||
|
||||
@@ -144,6 +144,11 @@ h2 > .twemoji {
|
||||
margin-top: 0.08rem;
|
||||
}
|
||||
|
||||
td code {
|
||||
margin-right: 0.3rem;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Matches the palette used by mkdocs-material */
|
||||
.priority-high {
|
||||
color: #f1185a;
|
||||
|
||||
@@ -70,7 +70,7 @@ Lastly, when searching your files with broader categories such as `Character` or
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! warning ""
|
||||
**_Coming in version 9.6.x_**
|
||||
**_Planned for future version_** *(See the [Roadmap](roadmap.md))*
|
||||
|
||||
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 `Ogre`, 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" 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.
|
||||
|
||||
|
||||
@@ -8,13 +8,17 @@ icon: material/mouse
|
||||
|
||||
# :material-mouse: Basic Usage
|
||||
|
||||
## Creating/Opening a Library
|
||||
## :material-database-plus: Creating/Opening a Library
|
||||
|
||||
With TagStudio opened, start by creating a new library or opening an existing one using File -> Open/Create Library from the menu bar. TagStudio will automatically create a new library from the chosen directory if one does not already exist. Upon creating a new library, TagStudio will automatically scan your folders for files and add those to your library (no files are moved during this process!).
|
||||
To create or open a [library](libraries.md), go to **File -> Open/Create Library** in the menu bar or use <kbd>Ctrl</kbd>+<kbd>O</kbd> (<kbd>⌘ Command </kbd>+<kbd>O</kbd> on macOS) and chose a folder with file contents you'd like to use as a TagStudio library. If a `.TagStudio` folder doesn't already exist inside the directory, TagStudio will create one and automatically scan the folder for files to include. Otherwise, the pre-existing library is opened.
|
||||
|
||||
## Refreshing the Library
|
||||
### :material-database-refresh: Refreshing Directories
|
||||
|
||||
Libraries under 10,000 files automatically scan for new or modified files when opened. In order to refresh the library manually, select "Refresh Directories" under the File menu.
|
||||
TagStudio automatically scans for new or updated files when opening a library by default. Manually refresh by going to **File -> Refresh Directories** in the menu or by using <kbd>Ctrl</kbd>+<kbd>R</kbd> (<kbd>⌘ Command </kbd>+<kbd>R</kbd> on macOS).
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! tip "TagStudio Libraries"
|
||||
To learn more about how TagStudio libraries work and how to use them, visit the **[Libraries](libraries.md)** page.
|
||||
|
||||
## Adding Tags to File Entries
|
||||
|
||||
|
||||
@@ -41,18 +41,18 @@ nav:
|
||||
- style.md
|
||||
- Help:
|
||||
- help/ffmpeg.md
|
||||
- Using Libraries:
|
||||
- Using TagStudio:
|
||||
- libraries.md
|
||||
- entries.md
|
||||
- preview-support.md
|
||||
- search.md
|
||||
- ignore.md
|
||||
- macros.md
|
||||
- Fields:
|
||||
- fields.md
|
||||
- Tags:
|
||||
- tags.md
|
||||
- colors.md
|
||||
- Fields:
|
||||
- fields.md
|
||||
- Updates:
|
||||
- changelog.md
|
||||
- roadmap.md
|
||||
|
||||
@@ -23,7 +23,7 @@ dependencies = [
|
||||
"Pillow>=10.2,<12",
|
||||
"pillow-heif~=0.22",
|
||||
"pillow-jxl-plugin~=1.3",
|
||||
"py7zr==1.0.0",
|
||||
"py7zr~=1.1.3",
|
||||
"pydantic~=2.10",
|
||||
"pydub~=0.25",
|
||||
"PySide6==6.8.0.*",
|
||||
@@ -54,7 +54,7 @@ pytest = [
|
||||
"pytest-qt==4.4.0",
|
||||
"syrupy==5.1.0",
|
||||
]
|
||||
ruff = ["ruff==0.11.8"]
|
||||
ruff = ["ruff==0.15.17"]
|
||||
|
||||
[project.gui-scripts]
|
||||
tagstudio = "tagstudio.main:main"
|
||||
@@ -66,6 +66,7 @@ packages = ["src/tagstudio"]
|
||||
[tool.pytest.ini_options]
|
||||
#addopts = "-m 'not qt'"
|
||||
qt_api = "pyside6"
|
||||
pythonpath = ["src"]
|
||||
|
||||
[tool.pyright]
|
||||
ignore = [
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
import enum
|
||||
|
||||
|
||||
class SettingItems(str, enum.Enum):
|
||||
class SettingItems(enum.StrEnum):
|
||||
"""List of setting item names."""
|
||||
|
||||
LAST_LIBRARY = "last_library"
|
||||
LIBS_LIST = "libs_list"
|
||||
|
||||
|
||||
class ShowFilepathOption(int, enum.Enum):
|
||||
class ShowFilepathOption(enum.IntEnum):
|
||||
"""Values representing the options for the "show_filenames" setting."""
|
||||
|
||||
SHOW_FULL_PATHS = 0
|
||||
@@ -21,7 +21,7 @@ class ShowFilepathOption(int, enum.Enum):
|
||||
DEFAULT = SHOW_RELATIVE_PATHS
|
||||
|
||||
|
||||
class TagClickActionOption(int, enum.Enum):
|
||||
class TagClickActionOption(enum.IntEnum):
|
||||
"""Values representing the options for the "tag_click_action" setting."""
|
||||
|
||||
OPEN_EDIT = 0
|
||||
@@ -30,7 +30,7 @@ class TagClickActionOption(int, enum.Enum):
|
||||
DEFAULT = OPEN_EDIT
|
||||
|
||||
|
||||
class Theme(str, enum.Enum):
|
||||
class Theme(enum.StrEnum):
|
||||
COLOR_BG_DARK = "#65000000"
|
||||
COLOR_BG_LIGHT = "#22000000"
|
||||
COLOR_DARK_LABEL = "#DD000000"
|
||||
@@ -49,7 +49,7 @@ class OpenStatus(enum.IntEnum):
|
||||
CORRUPTED = 2
|
||||
|
||||
|
||||
class MacroID(enum.Enum):
|
||||
class MacroID(enum.StrEnum):
|
||||
AUTOFILL = "autofill"
|
||||
SIDECAR = "sidecar"
|
||||
BUILD_URL = "build_url"
|
||||
|
||||
@@ -1251,32 +1251,33 @@ class Library:
|
||||
if limit <= 0:
|
||||
limit = sys.maxsize
|
||||
|
||||
name = name or ""
|
||||
name = name.lower()
|
||||
search_query: str = name.lower() if name else ""
|
||||
|
||||
def sort_key(text: str):
|
||||
priority = text.startswith(name)
|
||||
priority = text.startswith(search_query)
|
||||
p_ordering = len(text) if priority else sys.maxsize
|
||||
return (not priority, p_ordering, text)
|
||||
return not priority, p_ordering, text
|
||||
|
||||
with Session(self.engine) as session:
|
||||
query = select(Tag.id, Tag.name)
|
||||
|
||||
if limit > 0 and not name:
|
||||
if limit > 0 and not search_query:
|
||||
query = query.order_by(Tag.name).limit(limit)
|
||||
|
||||
if name:
|
||||
if search_query:
|
||||
query = query.where(
|
||||
or_(
|
||||
Tag.name.icontains(name),
|
||||
Tag.shorthand.icontains(name),
|
||||
Tag.name.icontains(search_query),
|
||||
Tag.shorthand.icontains(search_query),
|
||||
)
|
||||
)
|
||||
|
||||
tags = list(session.execute(query))
|
||||
|
||||
if name:
|
||||
query = select(TagAlias.tag_id, TagAlias.name).where(TagAlias.name.icontains(name))
|
||||
if search_query:
|
||||
query = select(TagAlias.tag_id, TagAlias.name).where(
|
||||
TagAlias.name.icontains(search_query)
|
||||
)
|
||||
tags.extend(session.execute(query))
|
||||
|
||||
tags.sort(key=lambda t: sort_key(t[1]))
|
||||
@@ -1286,7 +1287,7 @@ class Library:
|
||||
|
||||
logger.info(
|
||||
"searching tags",
|
||||
search=name,
|
||||
search=search_query,
|
||||
limit=limit,
|
||||
statement=str(query),
|
||||
results=len(tag_ids),
|
||||
@@ -1312,6 +1313,49 @@ class Library:
|
||||
|
||||
return direct_tags, descendant_tags
|
||||
|
||||
def search_field_templates(self, name: str | None, limit: int = 100) -> list[BaseFieldTemplate]:
|
||||
"""Return field template rows matching the query, detached from the session."""
|
||||
if limit <= 0:
|
||||
limit = sys.maxsize
|
||||
|
||||
search_query: str = name.lower() if name else ""
|
||||
|
||||
def sort_key(template: BaseFieldTemplate) -> tuple:
|
||||
text = template.name.lower()
|
||||
if not search_query:
|
||||
return (text,)
|
||||
priority = text.startswith(search_query)
|
||||
p_ordering = len(text) if priority else sys.maxsize
|
||||
return (not priority, p_ordering, text)
|
||||
|
||||
with Session(self.engine) as session:
|
||||
text_stmt = select(TextFieldTemplate)
|
||||
datetime_stmt = select(DatetimeFieldTemplate)
|
||||
if search_query:
|
||||
text_stmt = text_stmt.where(TextFieldTemplate.name.icontains(search_query))
|
||||
datetime_stmt = datetime_stmt.where(
|
||||
DatetimeFieldTemplate.name.icontains(search_query)
|
||||
)
|
||||
|
||||
field_templates: list[BaseFieldTemplate] = [
|
||||
*session.scalars(text_stmt),
|
||||
*session.scalars(datetime_stmt),
|
||||
]
|
||||
field_templates.sort(key=sort_key)
|
||||
field_templates = field_templates[:limit]
|
||||
|
||||
for ft in field_templates:
|
||||
session.expunge(ft)
|
||||
make_transient(ft)
|
||||
|
||||
logger.info(
|
||||
"Searching field templates",
|
||||
search=search_query,
|
||||
limit=limit,
|
||||
results=len(field_templates),
|
||||
)
|
||||
return field_templates
|
||||
|
||||
def update_entry_path(self, entry_id: int | Entry, path: Path) -> bool:
|
||||
"""Set the path field of an entry.
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import enum
|
||||
import mimetypes
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
@@ -23,7 +23,7 @@ FILETYPE_EQUIVALENTS = [
|
||||
]
|
||||
|
||||
|
||||
class MediaType(str, Enum):
|
||||
class MediaType(enum.StrEnum):
|
||||
"""Names of media types."""
|
||||
|
||||
ADOBE_PHOTOSHOP = "adobe_photoshop"
|
||||
|
||||
@@ -24,7 +24,7 @@ class ConstraintType(Enum):
|
||||
"filetype": ConstraintType.FileType,
|
||||
"path": ConstraintType.Path,
|
||||
"special": ConstraintType.Special,
|
||||
}.get(text.lower(), None)
|
||||
}.get(text.lower())
|
||||
|
||||
|
||||
class AST:
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def unwrap(optional: T | None, default: T | None = None) -> T:
|
||||
def unwrap[T](optional: T | None, default: T | None = None) -> T:
|
||||
if optional is not None:
|
||||
return optional
|
||||
if default is not None:
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FieldTemplateSearchModal(PanelModal):
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
is_field_template_chooser: bool = True,
|
||||
done_callback=None,
|
||||
save_callback=None,
|
||||
has_save=False,
|
||||
) -> None:
|
||||
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
|
||||
library,
|
||||
is_field_template_chooser,
|
||||
view=FieldTemplateSearchPanelView(is_field_template_chooser),
|
||||
)
|
||||
super().__init__(
|
||||
self.search_panel,
|
||||
Translations["field.add.plural"],
|
||||
done_callback=done_callback,
|
||||
save_callback=save_callback,
|
||||
has_save=has_save,
|
||||
)
|
||||
|
||||
|
||||
class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
field_template_chosen = Signal(object)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
is_field_template_chooser: bool = True,
|
||||
view: FieldTemplateSearchPanelView | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
view=view or FieldTemplateSearchPanelView(is_field_template_chooser),
|
||||
exclude=[],
|
||||
is_chooser=is_field_template_chooser,
|
||||
)
|
||||
self.__lib = library
|
||||
|
||||
self._unlimited_limit_item_label = Translations["field_template.all_field_templates"]
|
||||
self._create_and_add_button_label_key = "field_template.create_add"
|
||||
|
||||
def _get_max_limit(self) -> int:
|
||||
return len(self.__lib.field_templates)
|
||||
|
||||
def on_item_create(self) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
def on_item_edit(self, item: BaseFieldTemplate) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
def _on_item_remove(self, item: BaseFieldTemplate) -> None:
|
||||
if self.is_chooser:
|
||||
return
|
||||
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
def on_item_create_and_add(self) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
def _on_item_chosen(self, item: BaseFieldTemplate) -> None:
|
||||
self.field_template_chosen.emit(item)
|
||||
|
||||
def search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseFieldTemplate]]:
|
||||
return self.__lib.search_field_templates(name=query, limit=self._get_limit()[1]), []
|
||||
|
||||
def set_item_widget(self, item: BaseFieldTemplate | None, index: int) -> None:
|
||||
"""Set the field template of a field template widget at a specific index."""
|
||||
field_template_widget: FieldTemplateWidget = self.get_item_widget(index, self.__lib)
|
||||
field_template_widget.set_field_template(item)
|
||||
field_template_widget.setHidden(item is None)
|
||||
|
||||
if item is None:
|
||||
return
|
||||
|
||||
# field_template_widget.has_remove = not self.is_chooser
|
||||
|
||||
# Disconnect previous callbacks
|
||||
with catch_warnings(record=True):
|
||||
# tag_widget.on_edit.disconnect()
|
||||
# tag_widget.on_remove.disconnect()
|
||||
field_template_widget.on_click.disconnect()
|
||||
|
||||
# Connect callbacks
|
||||
# tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
|
||||
# tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
|
||||
field_template_widget.on_click.connect(
|
||||
lambda checked=False, tag=item: self._on_item_chosen(tag)
|
||||
)
|
||||
|
||||
def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
# TODO: Allow creation of field templates
|
||||
pass
|
||||
@@ -0,0 +1,22 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.field_template_widget_view import FieldTemplateWidgetView
|
||||
|
||||
|
||||
class FieldTemplateWidget(FieldTemplateWidgetView):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.__field_template: BaseFieldTemplate | None = None
|
||||
|
||||
def set_field_template(self, field_template: BaseFieldTemplate | None) -> None:
|
||||
self.__field_template = field_template
|
||||
|
||||
if field_template is None:
|
||||
return
|
||||
|
||||
field_name_key: str = FIELD_TYPE_KEYS.get(field_template.class_name, "field_type.unknown")
|
||||
self._bg_button.setText(f"{field_template.name} ({Translations[field_name_key]})")
|
||||
@@ -5,11 +5,10 @@
|
||||
import typing
|
||||
from warnings import catch_warnings
|
||||
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.mixed.add_field import AddFieldModal
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchModal
|
||||
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
|
||||
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -17,35 +16,37 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
|
||||
class PreviewPanel(PreviewPanelView):
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: "QtDriver") -> None:
|
||||
super().__init__(library, driver)
|
||||
|
||||
self.__add_field_modal = AddFieldModal(self.lib)
|
||||
self.__add_field_modal = FieldTemplateSearchModal(self.lib, is_field_template_chooser=True)
|
||||
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
|
||||
|
||||
@typing.override
|
||||
def _add_field_button_callback(self):
|
||||
def _add_field_button_callback(self) -> None:
|
||||
self.__add_field_modal.show()
|
||||
|
||||
@typing.override
|
||||
def _add_tag_button_callback(self):
|
||||
def _add_tag_button_callback(self) -> None:
|
||||
self.__add_tag_modal.show()
|
||||
|
||||
@typing.override
|
||||
def _set_selection_callback(self):
|
||||
def _set_selection_callback(self) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.__add_field_modal.done.disconnect()
|
||||
self.__add_tag_modal.tsp.tag_chosen.disconnect()
|
||||
self.__add_field_modal.search_panel.field_template_chosen.disconnect()
|
||||
self.__add_tag_modal.tsp.item_chosen.disconnect()
|
||||
|
||||
self.__add_field_modal.done.connect(self._add_field_to_selected)
|
||||
self.__add_tag_modal.tsp.tag_chosen.connect(self._add_tag_to_selected)
|
||||
self.__add_field_modal.search_panel.field_template_chosen.connect(
|
||||
self._add_field_to_selected
|
||||
)
|
||||
self.__add_tag_modal.tsp.item_chosen.connect(self._add_tag_to_selected)
|
||||
|
||||
def _add_field_to_selected(self, field_list: list[QListWidgetItem]):
|
||||
self._fields.add_field_to_selected(field_list)
|
||||
def _add_field_to_selected(self, template: BaseFieldTemplate) -> None:
|
||||
self._fields.add_field_to_selected(template)
|
||||
if len(self._selected) == 1:
|
||||
self._fields.update_from_entry(self._selected[0])
|
||||
|
||||
def _add_tag_to_selected(self, tag_id: int):
|
||||
def _add_tag_to_selected(self, tag_id: int) -> None:
|
||||
self._fields.add_tags_to_selected(tag_id)
|
||||
if len(self._selected) == 1:
|
||||
self._fields.update_from_entry(self._selected[0])
|
||||
|
||||
243
src/tagstudio/qt/controllers/search_panel_controller.py
Normal file
243
src/tagstudio/qt/controllers/search_panel_controller.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtWidgets import QVBoxLayout
|
||||
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
def _item_id(item: object) -> int:
|
||||
item_id: Any = getattr(item, "id") # noqa: B009
|
||||
|
||||
if isinstance(item_id, int):
|
||||
return item_id
|
||||
else:
|
||||
raise AttributeError()
|
||||
|
||||
|
||||
def _item_name(item: object) -> str:
|
||||
item_name: Any = getattr(item, "name") # noqa: B009
|
||||
|
||||
if isinstance(item_name, str):
|
||||
return item_name
|
||||
else:
|
||||
raise AttributeError()
|
||||
|
||||
|
||||
class SearchPanel[T](PanelWidget):
|
||||
item_chosen = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
view: SearchPanelView,
|
||||
exclude: list[int] | None = None,
|
||||
is_chooser: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
self.is_chooser = is_chooser
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.addWidget(self.view)
|
||||
self.view.connect_callbacks(self)
|
||||
self._driver: QtDriver | None = None
|
||||
self.exclude: list[int] = exclude or []
|
||||
|
||||
# Limits
|
||||
self._unlimited_limit_item_label: str = "All Items"
|
||||
self.__limit_items: list[tuple[str, int]] = [
|
||||
("25", 25),
|
||||
("50", 50),
|
||||
("100", 100),
|
||||
("250", 250),
|
||||
("500", 500),
|
||||
(self._unlimited_limit_item_label, -1),
|
||||
]
|
||||
self.__default_limit_index: int = 0 # 25 Limit (Default)
|
||||
self.__previous_limit_index: int = self.__default_limit_index
|
||||
|
||||
self.view.set_limit_items(self.__limit_items)
|
||||
self.view.set_limit_index(self.__default_limit_index)
|
||||
|
||||
# Items
|
||||
self._search_results: list[T] = []
|
||||
|
||||
self._create_and_add_button_label_key: str = ""
|
||||
|
||||
@property
|
||||
def search_field(self):
|
||||
return self.view.search_field
|
||||
|
||||
@property
|
||||
def create_and_add_button(self):
|
||||
return self.view.create_and_add_button
|
||||
|
||||
def get_search_query(self) -> str:
|
||||
return self.view.get_search_query()
|
||||
|
||||
def clear_search_query(self) -> None:
|
||||
self.view.clear_search_query()
|
||||
|
||||
def get_item_widget(self, index: int, library: Any):
|
||||
return self.view.get_item_widget(index, library)
|
||||
|
||||
def set_driver(self, driver: "QtDriver") -> None:
|
||||
self._driver = driver
|
||||
|
||||
def on_limit_changed(self, index: int) -> None:
|
||||
logger.info("[SearchPanel] Updating limit")
|
||||
|
||||
# Method was called outside the limit_combobox callback
|
||||
if index != self.view.get_limit_index():
|
||||
self.view.set_limit_index(index)
|
||||
|
||||
if self.__previous_limit_index == index:
|
||||
return
|
||||
|
||||
self.update_items(self.search_field.text())
|
||||
|
||||
def _get_limit(self) -> tuple[str, int]:
|
||||
return self.__limit_items[self.view.get_limit_index()]
|
||||
|
||||
def _get_previous_limit(self) -> tuple[str, int]:
|
||||
return self.__limit_items[self.__previous_limit_index]
|
||||
|
||||
def _get_max_limit(self) -> int:
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_search_query_changed(self, query: str) -> None:
|
||||
self.create_and_add_button.setText(
|
||||
Translations.format(self._create_and_add_button_label_key, query=query)
|
||||
)
|
||||
self.update_items(query)
|
||||
|
||||
def on_search_query_submitted(self, query: str) -> None:
|
||||
# Focus search field if no query
|
||||
if not query:
|
||||
self.search_field.setFocus()
|
||||
parent = self.parentWidget()
|
||||
if parent is not None:
|
||||
parent.hide()
|
||||
return
|
||||
|
||||
# Create and add item if no search results
|
||||
if len(self._search_results) <= 0:
|
||||
self.on_item_create_and_add()
|
||||
elif self.is_chooser:
|
||||
self._on_item_chosen(self._search_results[0])
|
||||
|
||||
self.clear_search_query()
|
||||
self.update_items()
|
||||
|
||||
def on_item_create(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_item_edit(self, item: T) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_item_remove(self, item: T) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_item_create_and_add(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_item_chosen(self, item: T) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _is_excluded(self, item: T) -> bool:
|
||||
return _item_id(item) in self.exclude
|
||||
|
||||
def update_items(self, query: str | None = None) -> None:
|
||||
"""Update the item list given a search query."""
|
||||
logger.info("[SearchPanel] Updating items", limit=self._get_limit()[1])
|
||||
|
||||
# Remove the "Create & Add" button if one exists
|
||||
self.view.remove_create_and_add_button()
|
||||
|
||||
# Get results for the search query
|
||||
query_lower = "" if not query else query.lower()
|
||||
search_results: tuple[list[T], list[T]] = self.search_items(query_lower)
|
||||
|
||||
# Sort and prioritize the results
|
||||
direct_results = list({item for item in search_results[0] if not self._is_excluded(item)})
|
||||
direct_results.sort(key=lambda item: _item_name(item).lower())
|
||||
|
||||
ancestor_results = list({item for item in search_results[1] if not self._is_excluded(item)})
|
||||
ancestor_results.sort(key=lambda item: _item_name(item).lower())
|
||||
|
||||
raw_results = list(direct_results + ancestor_results)
|
||||
priority_results: set[T] = set()
|
||||
|
||||
if query and query.strip():
|
||||
for raw_item in raw_results:
|
||||
if _item_name(raw_item).lower().startswith(query_lower):
|
||||
priority_results.add(raw_item)
|
||||
|
||||
all_results: list[T] = sorted(list(priority_results), key=lambda i: len(_item_name(i))) + [
|
||||
item for item in raw_results if item not in priority_results
|
||||
]
|
||||
if self._get_limit()[1] > 0:
|
||||
all_results = all_results[: self._get_limit()[1]]
|
||||
|
||||
self._search_results = all_results
|
||||
logger.info("[SearchPanel] Search results", results=self._search_results)
|
||||
|
||||
# Update every item widget with the new search result data
|
||||
previous_limit: int = (
|
||||
self._get_previous_limit()[1] > 0 and self._get_previous_limit()[1]
|
||||
) or self._get_max_limit()
|
||||
current_limit: int = (
|
||||
self._get_limit()[1] > 0 and self._get_limit()[1]
|
||||
) or self._get_max_limit()
|
||||
|
||||
for i in range(0, max(previous_limit, current_limit)):
|
||||
item: T | None = all_results[i] if i < len(all_results) else None
|
||||
self.set_item_widget(item=item, index=i)
|
||||
|
||||
self.__previous_limit_index = self.view.get_limit_index()
|
||||
|
||||
# Add back the "Create & Add" button
|
||||
if query and query.strip():
|
||||
self.view.add_create_and_add_button()
|
||||
|
||||
def search_items(self, query: str) -> tuple[list[T], list[T]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_item_widget(self, item: T | None, index: int) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def showEvent(self, event: QShowEvent) -> None: # noqa N802
|
||||
self.update_items()
|
||||
self.view.scroll_to(0)
|
||||
self.view.clear_search_query()
|
||||
return super().showEvent(event)
|
||||
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
# When Escape is pressed, focus back on the search box.
|
||||
# If focus is already on the search box, close the modal.
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
if self.search_field.hasFocus():
|
||||
super().keyPressEvent(event)
|
||||
else:
|
||||
self.view.focus_search_box(select_all=True)
|
||||
|
||||
def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
raise NotImplementedError()
|
||||
240
src/tagstudio/qt/controllers/tag_search_panel_controller.py
Normal file
240
src/tagstudio/qt/controllers/tag_search_panel_controller.py
Normal file
@@ -0,0 +1,240 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class TagSearchModal(PanelModal):
|
||||
tsp: "TagSearchPanel"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
done_callback=None,
|
||||
save_callback=None,
|
||||
has_save=False,
|
||||
):
|
||||
self.tsp = TagSearchPanel(
|
||||
library,
|
||||
exclude,
|
||||
is_tag_chooser,
|
||||
view=TagSearchPanelView(is_tag_chooser),
|
||||
)
|
||||
super().__init__(
|
||||
self.tsp,
|
||||
Translations["tag.add.plural"],
|
||||
done_callback=done_callback,
|
||||
save_callback=save_callback,
|
||||
has_save=has_save,
|
||||
)
|
||||
|
||||
|
||||
class TagSearchPanel(SearchPanel[Tag]):
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
view: TagSearchPanelView | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
view=view or TagSearchPanelView(is_tag_chooser),
|
||||
exclude=exclude,
|
||||
is_chooser=is_tag_chooser,
|
||||
)
|
||||
self.__lib = library
|
||||
|
||||
self._unlimited_limit_item_label = Translations["tag.all_tags"]
|
||||
self._create_and_add_button_label_key = "tag.create_add"
|
||||
|
||||
def _get_max_limit(self) -> int:
|
||||
return len(self.__lib.tags)
|
||||
|
||||
def on_item_create(self) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
query: str = self.get_search_query()
|
||||
|
||||
build_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
build_tag_modal: PanelModal = PanelModal(
|
||||
build_tag_panel,
|
||||
Translations["tag.new"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
if query.strip():
|
||||
build_tag_panel.name_field.setText(query)
|
||||
|
||||
build_tag_modal.saved.connect(lambda: self.create_item(build_tag_modal))
|
||||
build_tag_modal.show()
|
||||
|
||||
def on_item_edit(self, item: Tag) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
edit_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib, tag=item)
|
||||
edit_tag_modal: PanelModal = PanelModal(
|
||||
edit_tag_panel,
|
||||
self.__lib.tag_display_name(item),
|
||||
Translations["tag.edit"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
edit_tag_modal.saved.connect(lambda: self.edit_item(edit_tag_panel))
|
||||
edit_tag_modal.show()
|
||||
|
||||
def _on_item_remove(self, item: Tag) -> None:
|
||||
if self.is_chooser:
|
||||
return
|
||||
|
||||
if item.id in range(RESERVED_TAG_START, RESERVED_TAG_END):
|
||||
return
|
||||
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Question, # type: ignore
|
||||
Translations["tag.remove"],
|
||||
Translations.format("tag.confirm_delete", tag_name=self.__lib.tag_display_name(item)),
|
||||
QMessageBox.Ok | QMessageBox.Cancel, # type: ignore
|
||||
)
|
||||
|
||||
result = message_box.exec()
|
||||
|
||||
if result != QMessageBox.Ok: # type: ignore
|
||||
return
|
||||
|
||||
self.__lib.remove_tag(item.id)
|
||||
self.update_items(self.get_search_query())
|
||||
|
||||
def on_item_create_and_add(self) -> None:
|
||||
"""Opens "Create Tag" panel to create and add a new tag with given name."""
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
query: str = self.get_search_query()
|
||||
|
||||
logger.info("Create and Add Tag", name=query)
|
||||
|
||||
build_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
build_tag_modal: PanelModal = PanelModal(
|
||||
build_tag_panel,
|
||||
Translations["tag.new"],
|
||||
Translations["tag.add"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
if query.strip():
|
||||
build_tag_panel.name_field.setText(query)
|
||||
|
||||
build_tag_modal.saved.connect(lambda: self.create_item(build_tag_modal, choose_item=True))
|
||||
build_tag_modal.show()
|
||||
|
||||
def _on_item_chosen(self, item: Tag) -> None:
|
||||
self.item_chosen.emit(item.id)
|
||||
|
||||
def search_items(self, query: str) -> tuple[list[Tag], list[Tag]]:
|
||||
return self.__lib.search_tags(name=query, limit=self._get_limit()[1])
|
||||
|
||||
def set_item_widget(self, item: Tag | None, index: int) -> None:
|
||||
"""Set the tag of a tag widget at a specific index."""
|
||||
tag_widget: TagWidget = self.get_item_widget(index, self.__lib)
|
||||
tag_widget.set_tag(item)
|
||||
tag_widget.setHidden(item is None)
|
||||
|
||||
if item is None:
|
||||
return
|
||||
assert item is not None
|
||||
|
||||
tag_widget.has_remove = not self.is_chooser and item.id not in range(
|
||||
RESERVED_TAG_START, RESERVED_TAG_END
|
||||
)
|
||||
|
||||
# Disconnect previous callbacks
|
||||
with catch_warnings(record=True):
|
||||
tag_widget.on_edit.disconnect()
|
||||
tag_widget.on_remove.disconnect()
|
||||
tag_widget.bg_button.clicked.disconnect()
|
||||
tag_widget.search_for_tag_action.triggered.disconnect()
|
||||
|
||||
# Connect callbacks
|
||||
tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
|
||||
tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
|
||||
tag_widget.bg_button.clicked.connect(
|
||||
lambda checked=False, tag=item: self._on_item_chosen(tag)
|
||||
)
|
||||
|
||||
# Connect search action
|
||||
if self._driver is not None:
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda tag_id=item.id: self.search_for_tag(tag_id)
|
||||
)
|
||||
tag_widget.search_for_tag_action.setEnabled(True)
|
||||
else:
|
||||
tag_widget.search_for_tag_action.setEnabled(False)
|
||||
|
||||
def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
if isinstance(build_item_modal.widget, BuildTagPanel):
|
||||
tag: Tag = build_item_modal.widget.build_tag()
|
||||
self.__lib.add_tag(
|
||||
tag,
|
||||
parent_ids=build_item_modal.widget.parent_ids,
|
||||
alias_names=build_item_modal.widget.alias_names,
|
||||
alias_ids=build_item_modal.widget.alias_ids,
|
||||
)
|
||||
|
||||
if choose_item:
|
||||
self._on_item_chosen(tag)
|
||||
self.clear_search_query()
|
||||
|
||||
build_item_modal.hide()
|
||||
self.on_search_query_changed(self.get_search_query())
|
||||
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
if not isinstance(edit_item_panel, BuildTagPanel):
|
||||
return
|
||||
self.__lib.update_tag(
|
||||
tag=edit_item_panel.build_tag(),
|
||||
parent_ids=edit_item_panel.parent_ids,
|
||||
alias_names=edit_item_panel.alias_names,
|
||||
alias_ids=edit_item_panel.alias_ids,
|
||||
)
|
||||
|
||||
self.update_items(self.search_field.text())
|
||||
|
||||
def search_for_tag(self, tag_id: int) -> None:
|
||||
if self._driver is None:
|
||||
return
|
||||
|
||||
self._driver.main_window.search_field.setText(f"tag_id:{tag_id}")
|
||||
self._driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag_id, self._driver.browsing_history.current)
|
||||
)
|
||||
@@ -33,13 +33,13 @@ class AddFieldModal(QWidget):
|
||||
# [Cancel] [Save]
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.setWindowTitle(Translations["library.field.add"])
|
||||
self.setWindowTitle(Translations["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(Translations["library.field.add"])
|
||||
self.title_widget = QLabel(Translations["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;")
|
||||
|
||||
@@ -28,9 +28,9 @@ from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal, TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.mixed.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchModal, TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import (
|
||||
TagWidget,
|
||||
get_border_color,
|
||||
@@ -41,6 +41,7 @@ from tagstudio.qt.mixed.tag_widget import (
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -167,7 +168,7 @@ class BuildTagPanel(PanelWidget):
|
||||
exclude_ids.append(tag.id)
|
||||
|
||||
self.add_tag_modal = TagSearchModal(self.lib, exclude_ids)
|
||||
self.add_tag_modal.tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x))
|
||||
self.add_tag_modal.tsp.item_chosen.connect(lambda x: self.add_parent_tag_callback(x))
|
||||
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
|
||||
|
||||
# Color ----------------------------------------------------------------
|
||||
@@ -431,7 +432,11 @@ class BuildTagPanel(PanelWidget):
|
||||
has_remove=True,
|
||||
)
|
||||
tag_widget.on_remove.connect(lambda t=parent_id: self.remove_parent_tag_callback(t))
|
||||
tag_widget.on_edit.connect(lambda t=tag: TagSearchPanel(library=self.lib).edit_tag(t))
|
||||
tag_widget.on_edit.connect(
|
||||
lambda t=tag: TagSearchPanel(
|
||||
library=self.lib, view=TagSearchPanelView(is_tag_chooser=True)
|
||||
).on_item_edit(t)
|
||||
)
|
||||
row.addWidget(tag_widget)
|
||||
|
||||
# Add Disambiguation Tag Button
|
||||
|
||||
@@ -14,7 +14,6 @@ from PySide6.QtGui import QGuiApplication
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QListWidgetItem,
|
||||
QMessageBox,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
@@ -50,7 +49,7 @@ logger = structlog.get_logger(__name__)
|
||||
class FieldContainers(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: "QtDriver") -> None:
|
||||
super().__init__()
|
||||
|
||||
self.lib = library
|
||||
@@ -103,7 +102,7 @@ class FieldContainers(QWidget):
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.addWidget(self.scroll_area)
|
||||
|
||||
def update_from_entry(self, entry_id: int, update_badges: bool = True):
|
||||
def update_from_entry(self, entry_id: int, update_badges: bool = True) -> None:
|
||||
"""Update tags and fields from a single Entry source."""
|
||||
logger.warning("[FieldContainers] Updating Selection", entry_id=entry_id)
|
||||
|
||||
@@ -113,7 +112,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
def update_granular(
|
||||
self, entry_tags: set[Tag], entry_fields: list[BaseField], update_badges: bool = True
|
||||
):
|
||||
) -> None:
|
||||
"""Individually update elements of the item preview."""
|
||||
container_len: int = len(entry_fields)
|
||||
container_index = 0
|
||||
@@ -139,7 +138,7 @@ class FieldContainers(QWidget):
|
||||
if i > (container_len - 1):
|
||||
c.setHidden(True)
|
||||
|
||||
def update_toggled_tag(self, tag_id: int, toggle_value: bool):
|
||||
def update_toggled_tag(self, tag_id: int, toggle_value: bool) -> None:
|
||||
"""Visually add or remove a tag from the item preview without needing to query the db."""
|
||||
entry = self.cached_entries[0]
|
||||
tag = self.lib.get_tag(tag_id)
|
||||
@@ -152,7 +151,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
self.update_granular(entry_tags=entry.tags, entry_fields=entry.fields, update_badges=False)
|
||||
|
||||
def hide_containers(self):
|
||||
def hide_containers(self) -> None:
|
||||
"""Hide all field and tag containers."""
|
||||
for c in self.containers:
|
||||
c.setHidden(True)
|
||||
@@ -203,29 +202,38 @@ class FieldContainers(QWidget):
|
||||
return dict((c, d) for c, d in categories.items() if len(d) > 0)
|
||||
|
||||
def remove_field_prompt(self, name: str) -> str:
|
||||
return Translations.format("library.field.confirm_remove", name=name)
|
||||
return Translations.format("field.confirm_remove", name=name)
|
||||
|
||||
def add_field_to_selected(self, field_list: list[QListWidgetItem]):
|
||||
"""Add list of entry fields to one or more selected items.
|
||||
def add_field_to_selected(
|
||||
self, field_templates: BaseFieldTemplate | list[BaseFieldTemplate]
|
||||
) -> None:
|
||||
"""Add list of fields to one or more selected items.
|
||||
|
||||
Uses the current driver selection, NOT the field containers cache.
|
||||
"""
|
||||
if isinstance(field_templates, BaseFieldTemplate):
|
||||
field_templates = [field_templates]
|
||||
|
||||
assert isinstance(field_templates, list)
|
||||
|
||||
logger.info(
|
||||
"[FieldContainers][add_field_to_selected]",
|
||||
selected=self.driver.selected,
|
||||
fields=field_list,
|
||||
fields=[
|
||||
(field_template.class_name, field_template.id) for field_template in field_templates
|
||||
],
|
||||
)
|
||||
|
||||
for entry_id in self.driver.selected:
|
||||
for field in field_list:
|
||||
template: BaseFieldTemplate = field.data(Qt.ItemDataRole.UserRole)
|
||||
for field_template in field_templates:
|
||||
logger.info(
|
||||
"[FieldContainers][add_field_to_selected] Adding field",
|
||||
name=template.name,
|
||||
type=template.class_name,
|
||||
name=field_template.name,
|
||||
type=field_template.class_name,
|
||||
)
|
||||
self.lib.add_field_to_entries(entry_id, template.to_field())
|
||||
self.lib.add_field_to_entries(entry_id, field_template.to_field())
|
||||
|
||||
def add_tags_to_selected(self, tags: int | list[int]):
|
||||
def add_tags_to_selected(self, tags: int | list[int]) -> None:
|
||||
"""Add list of tags to one or more selected items.
|
||||
|
||||
Uses the current driver selection, NOT the field containers cache.
|
||||
@@ -239,7 +247,7 @@ class FieldContainers(QWidget):
|
||||
)
|
||||
self.driver.add_tags_to_selected_callback(tags)
|
||||
|
||||
def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
|
||||
def write_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None:
|
||||
"""Update/Create data for a FieldContainer.
|
||||
|
||||
Args:
|
||||
@@ -406,7 +414,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
def write_tag_container(
|
||||
self, index: int, tags: set[Tag], category_tag: Tag | None = None, is_mixed: bool = False
|
||||
):
|
||||
) -> None:
|
||||
"""Update/Create tag data for a FieldContainer.
|
||||
|
||||
Args:
|
||||
@@ -447,7 +455,7 @@ class FieldContainers(QWidget):
|
||||
inner_widget.set_tags(tags)
|
||||
|
||||
inner_widget.on_update.connect(
|
||||
lambda: (self.update_from_entry(self.cached_entries[0].id, update_badges=True))
|
||||
lambda: self.update_from_entry(self.cached_entries[0].id, update_badges=True)
|
||||
)
|
||||
else:
|
||||
text = "<i>Mixed Data</i>"
|
||||
@@ -458,7 +466,7 @@ class FieldContainers(QWidget):
|
||||
container.set_remove_callback()
|
||||
container.setHidden(False)
|
||||
|
||||
def remove_field(self, field: BaseField):
|
||||
def remove_field(self, field: BaseField) -> None:
|
||||
"""Remove a field from all selected Entries."""
|
||||
logger.info(
|
||||
"[FieldContainers] Removing Field",
|
||||
@@ -468,14 +476,14 @@ class FieldContainers(QWidget):
|
||||
entry_ids = [e.id for e in self.cached_entries]
|
||||
self.lib.remove_entry_field(field, entry_ids)
|
||||
|
||||
def update_text_field(self, field: TextField, value: str, is_multiline: bool):
|
||||
def update_text_field(self, field: TextField, value: str, is_multiline: bool) -> None:
|
||||
"""Update a text field across selected entries."""
|
||||
entry_ids = [e.id for e in self.cached_entries]
|
||||
assert entry_ids, "No entries selected"
|
||||
|
||||
self.lib.update_text_field(entry_ids, field, value, is_multiline)
|
||||
|
||||
def update_datetime_field(self, field: DatetimeField, value: str):
|
||||
def update_datetime_field(self, field: DatetimeField, value: str) -> None:
|
||||
"""Update a datetime field across selected entries."""
|
||||
entry_ids = [e.id for e in self.cached_entries]
|
||||
assert entry_ids, "No entries selected"
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import structlog
|
||||
from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# TODO: Once this class is removed, the `is_tag_chooser` option of `TagSearchPanel`
|
||||
# will most likely be enabled in every case
|
||||
# and the possibility of disabling it can therefore be removed
|
||||
|
||||
|
||||
class TagDatabasePanel(TagSearchPanel):
|
||||
def __init__(self, driver, library: Library):
|
||||
super().__init__(library, is_tag_chooser=False)
|
||||
self.driver = driver
|
||||
|
||||
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)
|
||||
|
||||
def build_tag(self, name: str):
|
||||
panel = BuildTagPanel(self.lib)
|
||||
self.modal = PanelModal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
has_save=True,
|
||||
)
|
||||
if name.strip():
|
||||
panel.name_field.setText(name)
|
||||
|
||||
self.modal.saved.connect(
|
||||
lambda: (
|
||||
self.lib.add_tag(
|
||||
tag=panel.build_tag(),
|
||||
parent_ids=panel.parent_ids,
|
||||
alias_names=panel.alias_names,
|
||||
alias_ids=panel.alias_ids,
|
||||
),
|
||||
self.modal.hide(),
|
||||
self.update_tags(self.search_field.text()),
|
||||
)
|
||||
)
|
||||
self.modal.show()
|
||||
|
||||
def delete_tag(self, tag: Tag):
|
||||
if tag.id in range(RESERVED_TAG_START, RESERVED_TAG_END):
|
||||
return
|
||||
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Question, # pyright: ignore[reportAttributeAccessIssue]
|
||||
Translations["tag.remove"],
|
||||
Translations.format("tag.confirm_delete", tag_name=self.lib.tag_display_name(tag)),
|
||||
QMessageBox.Ok | QMessageBox.Cancel, # pyright: ignore[reportAttributeAccessIssue]
|
||||
)
|
||||
|
||||
result = message_box.exec()
|
||||
|
||||
if result != QMessageBox.Ok: # pyright: ignore[reportAttributeAccessIssue]
|
||||
return
|
||||
|
||||
self.lib.remove_tag(tag.id)
|
||||
self.update_tags()
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
class TagSearchModal(PanelModal):
|
||||
tsp: "TagSearchPanel"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
done_callback=None,
|
||||
save_callback=None,
|
||||
has_save=False,
|
||||
):
|
||||
self.tsp = TagSearchPanel(library, exclude, is_tag_chooser)
|
||||
super().__init__(
|
||||
self.tsp,
|
||||
Translations["tag.add.plural"],
|
||||
done_callback=done_callback,
|
||||
save_callback=save_callback,
|
||||
has_save=has_save,
|
||||
)
|
||||
|
||||
|
||||
class TagSearchPanel(PanelWidget):
|
||||
tag_chosen = Signal(int)
|
||||
lib: Library
|
||||
driver: Union["QtDriver", None]
|
||||
is_initialized: bool = False
|
||||
first_tag_id: int | None = None
|
||||
is_tag_chooser: bool
|
||||
exclude: list[int]
|
||||
|
||||
_limit_items: list[int | str] = [25, 50, 100, 250, 500, Translations["tag.all_tags"]]
|
||||
_default_limit_idx: int = 0 # 50 Tag Limit (Default)
|
||||
cur_limit_idx: int = _default_limit_idx
|
||||
tag_limit: int | str = _limit_items[_default_limit_idx]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.driver = None
|
||||
self.exclude = exclude or []
|
||||
|
||||
self.is_tag_chooser = is_tag_chooser
|
||||
self.create_button_in_layout: bool = False
|
||||
|
||||
self.setMinimumSize(300, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
|
||||
self.limit_container = QWidget()
|
||||
self.limit_layout = QHBoxLayout(self.limit_container)
|
||||
self.limit_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.limit_layout.setSpacing(12)
|
||||
self.limit_layout.addStretch(1)
|
||||
|
||||
self.limit_title = QLabel(Translations["tag.view_limit"])
|
||||
self.limit_layout.addWidget(self.limit_title)
|
||||
|
||||
self.limit_combobox = QComboBox()
|
||||
self.limit_combobox.setEditable(False)
|
||||
self.limit_combobox.addItems([str(x) for x in TagSearchPanel._limit_items])
|
||||
self.limit_combobox.setCurrentIndex(TagSearchPanel._default_limit_idx)
|
||||
self.limit_combobox.currentIndexChanged.connect(self.update_limit)
|
||||
self.limit_layout.addWidget(self.limit_combobox)
|
||||
self.limit_layout.addStretch(1)
|
||||
|
||||
self.search_field = QLineEdit()
|
||||
self.search_field.setObjectName("searchField")
|
||||
self.search_field.setMinimumSize(QSize(0, 32))
|
||||
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()))
|
||||
|
||||
self.scroll_contents = QWidget()
|
||||
self.scroll_layout = QVBoxLayout(self.scroll_contents)
|
||||
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scroll_area.setWidget(self.scroll_contents)
|
||||
|
||||
self.root_layout.addWidget(self.limit_container)
|
||||
self.root_layout.addWidget(self.search_field)
|
||||
self.root_layout.addWidget(self.scroll_area)
|
||||
|
||||
def set_driver(self, driver):
|
||||
"""Set the QtDriver for this search panel. Used for main window operations."""
|
||||
self.driver = driver
|
||||
|
||||
def build_create_button(self, query: str | None):
|
||||
"""Constructs a "Create & Add Tag" QPushButton."""
|
||||
create_button = QPushButton(self)
|
||||
create_button.setFlat(True)
|
||||
|
||||
create_button.setMinimumSize(22, 22)
|
||||
|
||||
create_button.setStyleSheet(
|
||||
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:dashed;"
|
||||
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:{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"}}"
|
||||
)
|
||||
|
||||
return create_button
|
||||
|
||||
def create_and_add_tag(self, name: str):
|
||||
"""Opens "Create Tag" panel to create and add a new tag with given name."""
|
||||
logger.info("Create and Add Tag", name=name)
|
||||
|
||||
def on_tag_modal_saved():
|
||||
"""Callback for actions to perform when a new tag is confirmed created."""
|
||||
tag: Tag = self.build_tag_modal.build_tag()
|
||||
self.lib.add_tag(
|
||||
tag,
|
||||
set(self.build_tag_modal.parent_ids),
|
||||
set(self.build_tag_modal.alias_names),
|
||||
set(self.build_tag_modal.alias_ids),
|
||||
)
|
||||
self.add_tag_modal.hide()
|
||||
|
||||
self.tag_chosen.emit(tag.id)
|
||||
self.search_field.setText("")
|
||||
self.search_field.setFocus()
|
||||
self.update_tags()
|
||||
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
self.build_tag_modal: BuildTagPanel = BuildTagPanel(self.lib)
|
||||
self.add_tag_modal: PanelModal = PanelModal(
|
||||
self.build_tag_modal, Translations["tag.new"], Translations["tag.add"], has_save=True
|
||||
)
|
||||
|
||||
self.build_tag_modal.name_field.setText(name)
|
||||
self.add_tag_modal.saved.connect(on_tag_modal_saved)
|
||||
self.add_tag_modal.show()
|
||||
|
||||
def update_tags(self, query: str | None = None):
|
||||
"""Update the tag list given a search query."""
|
||||
logger.info("[TagSearchPanel] Updating Tags")
|
||||
|
||||
# Remove the "Create & Add" button if one exists
|
||||
if self.create_button_in_layout and self.scroll_layout.count():
|
||||
self.scroll_layout.takeAt(self.scroll_layout.count() - 1).widget().deleteLater()
|
||||
self.create_button_in_layout = False
|
||||
|
||||
# Only use the tag limit if it's an actual number (aka not "All Tags")
|
||||
tag_limit = TagSearchPanel.tag_limit if isinstance(TagSearchPanel.tag_limit, int) else -1
|
||||
direct_tags, descendant_tags = self.lib.search_tags(name=query, limit=tag_limit)
|
||||
|
||||
all_results = [t for t in direct_tags if t.id not in self.exclude]
|
||||
all_results.extend(t for t in descendant_tags if t.id not in self.exclude)
|
||||
|
||||
if tag_limit > 0:
|
||||
all_results = all_results[:tag_limit]
|
||||
|
||||
if all_results:
|
||||
self.first_tag_id = None
|
||||
self.first_tag_id = all_results[0].id if len(all_results) > 0 else all_results[0].id
|
||||
|
||||
else:
|
||||
self.first_tag_id = None
|
||||
|
||||
# Update every tag widget with the new search result data
|
||||
for i in range(0, len(all_results)):
|
||||
tag = all_results[i]
|
||||
self.set_tag_widget(tag, i)
|
||||
for i in range(len(all_results), self.scroll_layout.count()):
|
||||
self.set_tag_widget(None, i)
|
||||
|
||||
# Add back the "Create & Add" button
|
||||
if query and query.strip():
|
||||
cb: QPushButton = self.build_create_button(query)
|
||||
cb.setText(Translations.format("tag.create_add", query=query))
|
||||
with catch_warnings(record=True):
|
||||
cb.clicked.disconnect()
|
||||
cb.clicked.connect(lambda: self.create_and_add_tag(query or ""))
|
||||
self.scroll_layout.addWidget(cb)
|
||||
self.create_button_in_layout = True
|
||||
|
||||
def set_tag_widget(self, tag: Tag | None, index: int):
|
||||
"""Set the tag of a tag widget at a specific index."""
|
||||
# Create any new tag widgets needed up to the given index
|
||||
if self.scroll_layout.count() <= index:
|
||||
while self.scroll_layout.count() <= index:
|
||||
new_tw = TagWidget(tag=None, has_edit=True, has_remove=True, library=self.lib)
|
||||
new_tw.setHidden(True)
|
||||
self.scroll_layout.addWidget(new_tw)
|
||||
|
||||
# Assign the tag to the widget at the given index.
|
||||
tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget() # pyright: ignore[reportAssignmentType]
|
||||
assert isinstance(tag_widget, TagWidget)
|
||||
tag_widget.set_tag(tag)
|
||||
|
||||
# Set tag widget viability and potentially return early
|
||||
tag_widget.setHidden(bool(not tag))
|
||||
if not tag:
|
||||
return
|
||||
|
||||
# Configure any other aspects of the tag widget
|
||||
has_remove_button = False
|
||||
if not self.is_tag_chooser:
|
||||
has_remove_button = tag.id not in range(RESERVED_TAG_START, RESERVED_TAG_END)
|
||||
tag_widget.has_remove = has_remove_button
|
||||
|
||||
with catch_warnings(record=True):
|
||||
tag_widget.on_edit.disconnect()
|
||||
tag_widget.on_remove.disconnect()
|
||||
tag_widget.bg_button.clicked.disconnect()
|
||||
tag_widget.search_for_tag_action.triggered.disconnect()
|
||||
|
||||
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.delete_tag(t))
|
||||
tag_widget.bg_button.clicked.connect(lambda: self.tag_chosen.emit(tag_id))
|
||||
|
||||
if self.driver is not None:
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, tag_id=tag.id, driver=self.driver: (
|
||||
driver.main_window.search_field.setText(f"tag_id:{tag_id}"),
|
||||
driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag_id, driver.browsing_history.current)
|
||||
),
|
||||
)
|
||||
)
|
||||
tag_widget.search_for_tag_action.setEnabled(True)
|
||||
else:
|
||||
tag_widget.search_for_tag_action.setEnabled(False)
|
||||
|
||||
def update_limit(self, index: int):
|
||||
logger.info("[TagSearchPanel] Updating tag limit")
|
||||
if TagSearchPanel.cur_limit_idx == index:
|
||||
return
|
||||
|
||||
TagSearchPanel.cur_limit_idx = index
|
||||
|
||||
if index < len(self._limit_items) - 1:
|
||||
TagSearchPanel.tag_limit = int(self._limit_items[index])
|
||||
else:
|
||||
TagSearchPanel.tag_limit = -1
|
||||
|
||||
# Method was called outside the limit_combobox callback
|
||||
if index != self.limit_combobox.currentIndex():
|
||||
self.limit_combobox.setCurrentIndex(index)
|
||||
|
||||
self.update_tags(self.search_field.text())
|
||||
|
||||
def on_return(self, text: str):
|
||||
if text:
|
||||
if self.first_tag_id is not None:
|
||||
if self.is_tag_chooser:
|
||||
self.tag_chosen.emit(self.first_tag_id)
|
||||
self.search_field.setText("")
|
||||
self.update_tags()
|
||||
else:
|
||||
self.create_and_add_tag(text)
|
||||
else:
|
||||
self.search_field.setFocus()
|
||||
self.parentWidget().hide()
|
||||
|
||||
def showEvent(self, event: QShowEvent) -> None: # noqa N802
|
||||
self.update_limit(TagSearchPanel.cur_limit_idx)
|
||||
self.update_tags()
|
||||
self.scroll_area.verticalScrollBar().setValue(0)
|
||||
self.search_field.setText("")
|
||||
self.search_field.setFocus()
|
||||
return super().showEvent(event)
|
||||
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
# When Escape is pressed, focus back on the search box.
|
||||
# If focus is already on the search box, close the modal.
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
if self.search_field.hasFocus():
|
||||
return super().keyPressEvent(event)
|
||||
else:
|
||||
self.search_field.setFocus()
|
||||
self.search_field.selectAll()
|
||||
|
||||
def delete_tag(self, tag: Tag):
|
||||
pass
|
||||
|
||||
def edit_tag(self, tag: Tag):
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
|
||||
def callback(btp: BuildTagPanel):
|
||||
self.lib.update_tag(
|
||||
btp.build_tag(), set(btp.parent_ids), set(btp.alias_names), set(btp.alias_ids)
|
||||
)
|
||||
self.update_tags(self.search_field.text())
|
||||
|
||||
build_tag_panel = BuildTagPanel(self.lib, tag=tag)
|
||||
|
||||
self.edit_modal = PanelModal(
|
||||
build_tag_panel,
|
||||
self.lib.tag_display_name(tag),
|
||||
Translations["tag.edit"],
|
||||
done_callback=(self.update_tags(self.search_field.text())),
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
self.edit_modal.saved.connect(lambda: callback(build_tag_panel))
|
||||
self.edit_modal.show()
|
||||
@@ -17,6 +17,8 @@ logger = structlog.get_logger(__name__)
|
||||
DEFAULT_TRANSLATION = "en"
|
||||
|
||||
LANGUAGES = {
|
||||
# "Amharic": "am", # Minimal
|
||||
"Cebuano": "ceb",
|
||||
"Chinese (Simplified)": "zh_Hans",
|
||||
"Chinese (Traditional)": "zh_Hant",
|
||||
"Czech": "cs",
|
||||
@@ -24,9 +26,12 @@ LANGUAGES = {
|
||||
"Dutch": "nl",
|
||||
"English": "en",
|
||||
"Filipino": "fil",
|
||||
"Finnish": "fi",
|
||||
"French": "fr",
|
||||
"German": "de",
|
||||
"Greek": "el",
|
||||
"Hungarian": "hu",
|
||||
# "Icelandic": "is", # Minimal
|
||||
"Italian": "it",
|
||||
"Japanese": "ja",
|
||||
"Norwegian Bokmål": "nb_NO",
|
||||
@@ -38,6 +43,7 @@ LANGUAGES = {
|
||||
"Spanish": "es",
|
||||
"Swedish": "sv",
|
||||
"Tamil": "ta",
|
||||
# "Thai": "th", # Minimal
|
||||
"Toki Pona": "tok",
|
||||
"Turkish": "tr",
|
||||
"Viossa": "qpv",
|
||||
|
||||
@@ -64,12 +64,16 @@ from tagstudio.core.utils.str_formatting import is_version_outdated
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.cache_manager import CacheManager
|
||||
from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
|
||||
from tagstudio.qt.controllers.field_template_search_panel_controller import (
|
||||
FieldTemplateSearchPanel,
|
||||
)
|
||||
|
||||
# this import has side-effect of import PySide resources
|
||||
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
|
||||
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
|
||||
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
|
||||
from tagstudio.qt.controllers.out_of_date_message_box import OutOfDateMessageBox
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal, TagSearchPanel
|
||||
from tagstudio.qt.global_settings import (
|
||||
DEFAULT_GLOBAL_SETTINGS_PATH,
|
||||
GlobalSettings,
|
||||
@@ -86,8 +90,6 @@ from tagstudio.qt.mixed.migration_modal import JsonMigrationModal
|
||||
from tagstudio.qt.mixed.progress_bar import ProgressWidget
|
||||
from tagstudio.qt.mixed.settings_panel import SettingsPanel
|
||||
from tagstudio.qt.mixed.tag_color_manager import TagColorManager
|
||||
from tagstudio.qt.mixed.tag_database import TagDatabasePanel
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchModal
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.platform_strings import trash_term
|
||||
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
@@ -96,9 +98,11 @@ from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.utils.file_deleter import delete_file
|
||||
from tagstudio.qt.utils.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
|
||||
from tagstudio.qt.views.main_window import MainWindow
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.splash import SplashScreen
|
||||
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
|
||||
|
||||
BADGE_TAGS = {
|
||||
BadgeType.FAVORITE: TAG_FAVORITE,
|
||||
@@ -181,8 +185,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
tag_manager_panel: PanelModal | None = None
|
||||
color_manager_panel: TagColorManager | None = None
|
||||
field_template_manager_panel: PanelModal | None = None
|
||||
ignore_modal: PanelModal | None = None
|
||||
add_tag_modal: PanelModal | None = None
|
||||
add_field_modal: PanelModal | None = None
|
||||
folders_modal: FoldersToTagsModal
|
||||
about_modal: AboutModal
|
||||
unlinked_modal: FixUnlinkedEntriesModal
|
||||
@@ -364,10 +370,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
# Initialize the Tag Manager panel
|
||||
self.tag_manager_panel = PanelModal(
|
||||
widget=TagDatabasePanel(self, self.lib),
|
||||
widget=TagSearchPanel(
|
||||
self.lib,
|
||||
is_tag_chooser=False,
|
||||
view=TagSearchPanelView(is_tag_chooser=False),
|
||||
),
|
||||
title=Translations["tag_manager.title"],
|
||||
done_callback=lambda checked=False: (
|
||||
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
|
||||
done_callback=lambda checked=False: self.main_window.preview_panel.set_selection(
|
||||
self.selected, update_preview=False
|
||||
),
|
||||
has_save=False,
|
||||
)
|
||||
@@ -375,10 +385,24 @@ class QtDriver(DriverMixin, QObject):
|
||||
# Initialize the Color Group Manager panel
|
||||
self.color_manager_panel = TagColorManager(self)
|
||||
|
||||
# Initialize the Field Template Manager panel
|
||||
self.field_template_manager_panel = PanelModal(
|
||||
widget=FieldTemplateSearchPanel(
|
||||
self.lib,
|
||||
is_field_template_chooser=False,
|
||||
view=FieldTemplateSearchPanelView(is_field_template_chooser=False),
|
||||
),
|
||||
title=Translations["field_template_manager.title"],
|
||||
done_callback=lambda checked=False: self.main_window.preview_panel.set_selection(
|
||||
self.selected, update_preview=False
|
||||
),
|
||||
has_save=False,
|
||||
)
|
||||
|
||||
# Initialize the Tag Search panel
|
||||
self.add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
|
||||
self.add_tag_modal.tsp.set_driver(self)
|
||||
self.add_tag_modal.tsp.tag_chosen.connect(
|
||||
self.add_tag_modal.tsp.item_chosen.connect(
|
||||
lambda chosen_tag: (
|
||||
self.add_tags_to_selected_callback([chosen_tag]),
|
||||
self.main_window.preview_panel.set_selection(self.selected),
|
||||
@@ -468,6 +492,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.color_manager_panel.show
|
||||
)
|
||||
|
||||
self.main_window.menu_bar.field_template_manager_action.triggered.connect(
|
||||
self.field_template_manager_panel.show
|
||||
)
|
||||
|
||||
# endregion
|
||||
|
||||
# region View Menu ============================================================
|
||||
@@ -794,6 +822,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.menu_bar.refresh_dir_action.setEnabled(False)
|
||||
self.main_window.menu_bar.tag_manager_action.setEnabled(False)
|
||||
self.main_window.menu_bar.color_manager_action.setEnabled(False)
|
||||
self.main_window.menu_bar.field_template_manager_action.setEnabled(False)
|
||||
self.main_window.menu_bar.ignore_modal_action.setEnabled(False)
|
||||
self.main_window.menu_bar.new_tag_action.setEnabled(False)
|
||||
self.main_window.menu_bar.fix_unlinked_entries_action.setEnabled(False)
|
||||
@@ -1646,6 +1675,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.menu_bar.refresh_dir_action.setEnabled(True)
|
||||
self.main_window.menu_bar.tag_manager_action.setEnabled(True)
|
||||
self.main_window.menu_bar.color_manager_action.setEnabled(True)
|
||||
self.main_window.menu_bar.field_template_manager_action.setEnabled(True)
|
||||
self.main_window.menu_bar.ignore_modal_action.setEnabled(True)
|
||||
self.main_window.menu_bar.new_tag_action.setEnabled(True)
|
||||
self.main_window.menu_bar.fix_unlinked_entries_action.setEnabled(True)
|
||||
|
||||
30
src/tagstudio/qt/views/field_template_search_panel_view.py
Normal file
30
src/tagstudio/qt/views/field_template_search_panel_view.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
|
||||
class FieldTemplateSearchPanelView(SearchPanelView):
|
||||
def __init__(self, is_field_template_chooser: bool) -> None:
|
||||
super().__init__(is_field_template_chooser)
|
||||
|
||||
self.search_field.setPlaceholderText(Translations["home.search_field_templates"])
|
||||
self.create_button.setText(Translations["field_template.create"])
|
||||
|
||||
def get_item_widget(self, index: int, library: Library | None) -> FieldTemplateWidget:
|
||||
"""Gets the item widget at a specific index."""
|
||||
# Create any new item widgets needed up to the given index
|
||||
if self._scroll_layout.count() <= index:
|
||||
while self._scroll_layout.count() <= index:
|
||||
pad_field_template_widget = FieldTemplateWidget()
|
||||
pad_field_template_widget.setHidden(True)
|
||||
self._scroll_layout.addWidget(pad_field_template_widget)
|
||||
|
||||
field_template_widget: QWidget = self._scroll_layout.itemAt(index).widget()
|
||||
assert isinstance(field_template_widget, FieldTemplateWidget)
|
||||
return field_template_widget
|
||||
89
src/tagstudio/qt/views/field_template_widget_view.py
Normal file
89
src/tagstudio/qt/views/field_template_widget_view.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
|
||||
primary_color: QColor = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
|
||||
border_color: QColor = get_border_color(primary_color)
|
||||
highlight_color: QColor = get_highlight_color(primary_color)
|
||||
text_color: QColor = get_text_color(primary_color, highlight_color)
|
||||
|
||||
FIELD_TEMPLATE_BUTTON_STYLESHEET = f"""
|
||||
QPushButton{{
|
||||
background-color: {Theme.COLOR_BG.value};
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
QPushButton::hover{{
|
||||
background-color: {Theme.COLOR_HOVER.value};
|
||||
border-color: {get_ui_color(ColorType.BORDER, UiColor.THEME_DARK)};
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
|
||||
QPushButton::pressed{{
|
||||
background-color: {Theme.COLOR_PRESSED.value};
|
||||
border-color: {get_ui_color(ColorType.LIGHT_ACCENT, UiColor.THEME_DARK)};
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
class FieldTemplateWidgetView(QWidget):
|
||||
on_click = Signal()
|
||||
on_edit = Signal()
|
||||
on_remove = Signal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.__root_layout = QVBoxLayout(self)
|
||||
self.__root_layout.setObjectName("root_layout")
|
||||
|
||||
self.__root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Background button
|
||||
self._bg_button = QPushButton(self)
|
||||
self.__root_layout.addWidget(self._bg_button)
|
||||
|
||||
self._bg_button.setFlat(True)
|
||||
self._bg_button.setMinimumSize(44, 22)
|
||||
self._bg_button.setMinimumHeight(22)
|
||||
self._bg_button.setMaximumHeight(22)
|
||||
self._bg_button.setStyleSheet(FIELD_TEMPLATE_BUTTON_STYLESHEET)
|
||||
|
||||
self.__inner_layout = QHBoxLayout()
|
||||
self.__inner_layout.setObjectName("inner_layout")
|
||||
self._bg_button.setLayout(self.__inner_layout)
|
||||
|
||||
self.__inner_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Remove button
|
||||
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)
|
||||
|
||||
self.__connect_callbacks()
|
||||
|
||||
def __connect_callbacks(self) -> None:
|
||||
self._bg_button.clicked.connect(self.on_click.emit)
|
||||
self.__remove_button.clicked.connect(self.on_remove.emit)
|
||||
@@ -77,6 +77,7 @@ class MainMenuBar(QMenuBar):
|
||||
delete_file_action: QAction
|
||||
ignore_modal_action: QAction
|
||||
tag_manager_action: QAction
|
||||
field_template_manager_action: QAction
|
||||
color_manager_action: QAction
|
||||
|
||||
view_menu: QMenu
|
||||
@@ -299,6 +300,13 @@ class MainMenuBar(QMenuBar):
|
||||
self.color_manager_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.color_manager_action)
|
||||
|
||||
# Manage Field Templates
|
||||
self.field_template_manager_action = QAction(
|
||||
Translations["menu.edit.manage_field_templates"], self
|
||||
)
|
||||
self.field_template_manager_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.field_template_manager_action)
|
||||
|
||||
assign_mnemonics(self.edit_menu)
|
||||
self.addMenu(self.edit_menu)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
BUTTON_STYLE = f"""
|
||||
BUTTON_STYLE: str = f"""
|
||||
QPushButton{{
|
||||
background-color: {Theme.COLOR_BG.value};
|
||||
border-radius: 6px;
|
||||
@@ -61,7 +61,7 @@ class PreviewPanelView(QWidget):
|
||||
|
||||
_selected: list[int]
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
def __init__(self, library: Library, driver: "QtDriver") -> None:
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
|
||||
@@ -96,7 +96,7 @@ class PreviewPanelView(QWidget):
|
||||
self.__add_tag_button.setMinimumHeight(28)
|
||||
self.__add_tag_button.setStyleSheet(BUTTON_STYLE)
|
||||
|
||||
self.__add_field_button = QPushButton(Translations["library.field.add"])
|
||||
self.__add_field_button = QPushButton(Translations["field.add"])
|
||||
self.__add_field_button.setEnabled(False)
|
||||
self.__add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.__add_field_button.setMinimumHeight(28)
|
||||
@@ -120,20 +120,20 @@ class PreviewPanelView(QWidget):
|
||||
|
||||
self.__connect_callbacks()
|
||||
|
||||
def __connect_callbacks(self):
|
||||
def __connect_callbacks(self) -> None:
|
||||
self.__add_field_button.clicked.connect(self._add_field_button_callback)
|
||||
self.__add_tag_button.clicked.connect(self._add_tag_button_callback)
|
||||
|
||||
def _add_field_button_callback(self):
|
||||
def _add_field_button_callback(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _add_tag_button_callback(self):
|
||||
def _add_tag_button_callback(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _set_selection_callback(self):
|
||||
def _set_selection_callback(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_selection(self, selected: list[int], update_preview: bool = True):
|
||||
def set_selection(self, selected: list[int], update_preview: bool = True) -> None:
|
||||
"""Render the panel widgets with the newest data from the Library.
|
||||
|
||||
Args:
|
||||
@@ -193,7 +193,7 @@ class PreviewPanelView(QWidget):
|
||||
return field
|
||||
|
||||
@add_buttons_enabled.setter
|
||||
def add_buttons_enabled(self, enabled: bool):
|
||||
def add_buttons_enabled(self, enabled: bool) -> None:
|
||||
self.__add_field_button.setEnabled(enabled)
|
||||
self.__add_tag_button.setEnabled(enabled)
|
||||
|
||||
|
||||
189
src/tagstudio/qt/views/search_panel_view.py
Normal file
189
src/tagstudio/qt/views/search_panel_view.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
|
||||
CREATE_BUTTON_STYLESHEET: str = f"""
|
||||
QPushButton{{
|
||||
background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};
|
||||
color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};
|
||||
font-weight: 600;
|
||||
border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};
|
||||
border-radius: 6px;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 4px;
|
||||
font-size: 13px
|
||||
}}
|
||||
|
||||
QPushButton::hover{{
|
||||
border-color: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};
|
||||
}}
|
||||
|
||||
QPushButton::pressed{{
|
||||
background: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};
|
||||
color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};
|
||||
border-color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};
|
||||
}}
|
||||
|
||||
QPushButton::focus{{
|
||||
border-color: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};
|
||||
outline: none;
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
class SearchPanelView(PanelWidget):
|
||||
def __init__(self, is_chooser: bool) -> None:
|
||||
self.is_chooser: bool = is_chooser
|
||||
super().__init__()
|
||||
|
||||
self.__root_layout = QVBoxLayout(self)
|
||||
self.__root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.setMinimumSize(300, 400)
|
||||
|
||||
# Limit container
|
||||
self.__limit_container = QWidget()
|
||||
self.__root_layout.addWidget(self.__limit_container)
|
||||
|
||||
self.__limit_layout = QHBoxLayout(self.__limit_container)
|
||||
self.__limit_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.__limit_layout.setSpacing(12)
|
||||
self.__limit_layout.addStretch(1)
|
||||
|
||||
self.__limit_title = QLabel(Translations["home.search.view_limit"])
|
||||
self.__limit_layout.addWidget(self.__limit_title)
|
||||
|
||||
# Limit dropdown
|
||||
self.limit_combobox = QComboBox()
|
||||
self.__limit_layout.addWidget(self.limit_combobox)
|
||||
self.__limit_layout.addStretch(1)
|
||||
|
||||
self.limit_combobox.setEditable(False)
|
||||
|
||||
# Search field
|
||||
self.search_field = QLineEdit()
|
||||
self.search_field.setObjectName("search_field")
|
||||
self.__root_layout.addWidget(self.search_field)
|
||||
|
||||
self.search_field.setMinimumSize(QSize(0, 32))
|
||||
|
||||
# Scroll area
|
||||
self.__scroll_contents = QWidget()
|
||||
|
||||
self._scroll_layout = QVBoxLayout(self.__scroll_contents)
|
||||
self._scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self._scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.__scroll_area = QScrollArea()
|
||||
self.__scroll_area.setWidget(self.__scroll_contents)
|
||||
self.__root_layout.addWidget(self.__scroll_area)
|
||||
|
||||
self.__scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
self.__scroll_area.setWidgetResizable(True)
|
||||
self.__scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.__scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
# Create button
|
||||
self.create_button = QPushButton("")
|
||||
|
||||
if not self.is_chooser:
|
||||
self.__root_layout.addWidget(self.create_button)
|
||||
|
||||
# Create and add button
|
||||
self.create_and_add_button_in_layout: bool = False
|
||||
|
||||
self.create_and_add_button = QPushButton()
|
||||
self.create_and_add_button.setFlat(True)
|
||||
self.create_and_add_button.setMinimumSize(22, 22)
|
||||
self.create_and_add_button.setStyleSheet(CREATE_BUTTON_STYLESHEET)
|
||||
|
||||
@property
|
||||
def scroll_layout(self) -> QVBoxLayout:
|
||||
return self._scroll_layout
|
||||
|
||||
@property
|
||||
def scroll_area(self) -> QScrollArea:
|
||||
return self.__scroll_area
|
||||
|
||||
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None:
|
||||
self.limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
|
||||
|
||||
self.search_field.textChanged.connect(controller.on_search_query_changed)
|
||||
self.search_field.returnPressed.connect(
|
||||
lambda: controller.on_search_query_submitted(self.get_search_query())
|
||||
)
|
||||
|
||||
self.create_button.clicked.connect(controller.on_item_create)
|
||||
self.create_and_add_button.clicked.connect(controller.on_item_create_and_add)
|
||||
|
||||
def set_limit_items(self, limit_items: list[tuple[str, int]]) -> None:
|
||||
# Remove existing limit items
|
||||
for i in reversed(range(self.limit_combobox.count())):
|
||||
self.limit_combobox.removeItem(i)
|
||||
|
||||
# Add new limit items
|
||||
self.limit_combobox.addItems([limit_item[0] for limit_item in limit_items])
|
||||
|
||||
def get_limit_index(self) -> int:
|
||||
return self.limit_combobox.currentIndex()
|
||||
|
||||
def set_limit_index(self, index: int) -> None:
|
||||
self.limit_combobox.setCurrentIndex(index)
|
||||
|
||||
def focus_search_box(self, select_all: bool = False) -> None:
|
||||
self.search_field.setFocus()
|
||||
if select_all:
|
||||
self.search_field.selectAll()
|
||||
|
||||
def get_search_query(self) -> str:
|
||||
return self.search_field.text()
|
||||
|
||||
def clear_search_query(self) -> None:
|
||||
self.search_field.setText("")
|
||||
self.focus_search_box()
|
||||
|
||||
# Item list
|
||||
def scroll_to(self, position: int) -> None:
|
||||
self.__scroll_area.verticalScrollBar().setValue(position)
|
||||
|
||||
def get_item_widget(self, index: int, library: Library | None) -> Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_create_and_add_button(self) -> None:
|
||||
if self.create_and_add_button_in_layout:
|
||||
return
|
||||
self._scroll_layout.addWidget(self.create_and_add_button)
|
||||
self.create_and_add_button.show()
|
||||
self.create_and_add_button_in_layout = True
|
||||
|
||||
def remove_create_and_add_button(self) -> None:
|
||||
if not self.create_and_add_button_in_layout:
|
||||
return
|
||||
self._scroll_layout.removeWidget(self.create_and_add_button)
|
||||
self.create_and_add_button.hide()
|
||||
self.create_and_add_button_in_layout = False
|
||||
32
src/tagstudio/qt/views/tag_search_panel_view.py
Normal file
32
src/tagstudio/qt/views/tag_search_panel_view.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
|
||||
class TagSearchPanelView(SearchPanelView):
|
||||
def __init__(self, is_tag_chooser: bool) -> None:
|
||||
super().__init__(is_tag_chooser)
|
||||
|
||||
self.search_field.setPlaceholderText(Translations["home.search_tags"])
|
||||
self.create_button.setText(Translations["tag.create"])
|
||||
|
||||
def get_item_widget(self, index: int, library: Library | None) -> TagWidget:
|
||||
"""Gets the item widget at a specific index."""
|
||||
# Create any new item widgets needed up to the given index
|
||||
if self._scroll_layout.count() <= index:
|
||||
while self._scroll_layout.count() <= index:
|
||||
pad_tag_widget = TagWidget(
|
||||
tag=None, has_edit=True, has_remove=True, library=library
|
||||
)
|
||||
pad_tag_widget.setHidden(True)
|
||||
self._scroll_layout.addWidget(pad_tag_widget)
|
||||
|
||||
tag_widget: QWidget = self._scroll_layout.itemAt(index).widget()
|
||||
assert isinstance(tag_widget, TagWidget)
|
||||
return tag_widget
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "Unverknüpfte Einträge: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg und/oder FFprobe wurden nicht gefunden. FFmpeg ist für multimediale Wiedergabe und Thumbnails vonnöten.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Feld hinzufügen",
|
||||
"field.confirm_remove": "Wollen Sie dieses \"{name}\" Feld wirklich entfernen?",
|
||||
"field.copy": "Feld kopieren",
|
||||
"field.edit": "Feld bearbeiten",
|
||||
"field.mixed_data": "Gemischte Daten",
|
||||
"field.paste": "Feld einfügen",
|
||||
"field.remove": "Feld entfernen",
|
||||
"field_type.datetime": "Datum - Uhrzeit",
|
||||
"field_type.text": "Text",
|
||||
"field_type.unknown": "Unbekannter Typ",
|
||||
@@ -174,10 +178,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Bibliothek</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Bibliothek</h2>",
|
||||
"landing.open_create_library": "Bibliothek öffnen/erstellen {shortcut}",
|
||||
"library.field.add": "Feld hinzufügen",
|
||||
"library.field.confirm_remove": "Wollen Sie dieses \"{name}\" Feld wirklich entfernen?",
|
||||
"library.field.mixed_data": "Gemischte Daten",
|
||||
"library.field.remove": "Feld entfernen",
|
||||
"library.missing": "Dateiort fehlt",
|
||||
"library.name": "Bibliothek",
|
||||
"library.refresh.scanning.plural": "Durchsuche Verzeichnisse nach neuen Dateien...\n{searched_count} Dateien durchsucht, {found_count} neue Dateien gefunden",
|
||||
@@ -337,7 +337,7 @@
|
||||
"tag.search_for_tag": "Nach Tag suchen",
|
||||
"tag.shorthand": "Kürzel",
|
||||
"tag.tag_name_required": "Tag Name (Pflichtfeld)",
|
||||
"tag.view_limit": "Anzeige-Limit:",
|
||||
"home.search.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",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"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",
|
||||
@@ -19,10 +18,11 @@
|
||||
"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.primary_required": "Primary Color (Required)",
|
||||
"color.secondary": "Secondary Color",
|
||||
"color.title.no_color": "No Color",
|
||||
"color_manager.title": "Manage Tag Colors",
|
||||
"dependency.missing.title": "{dependency} Not Found",
|
||||
"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.",
|
||||
@@ -34,26 +34,27 @@
|
||||
"drop_import.title": "Conflicting File(s)",
|
||||
"edit.color_manager": "Manage Tag Colors",
|
||||
"edit.copy_fields": "Copy Fields",
|
||||
"edit.field_template_manager": "Manage Field Templates",
|
||||
"edit.paste_fields": "Paste Fields",
|
||||
"edit.tag_manager": "Manage Tags",
|
||||
"entries.duplicate.merge.label": "Merging Duplicate Entries...",
|
||||
"entries.duplicate.merge": "Merge Duplicate Entries",
|
||||
"entries.duplicate.merge.label": "Merging Duplicate Entries...",
|
||||
"entries.duplicate.refresh": "Refresh Duplicate Entries",
|
||||
"entries.duplicates.description": "Duplicate entries are defined as multiple entries which point to the same file on disk. Merging these will combine the tags and metadata from all duplicates into a single consolidated entry. These are not to be confused with \"duplicate files\", which are duplicates of your files themselves outside of TagStudio.",
|
||||
"entries.generic.refresh_alt": "&Refresh",
|
||||
"entries.generic.remove.removing_count": "Removing {count} Entries...",
|
||||
"entries.generic.remove.removing": "Removing Entries",
|
||||
"entries.generic.remove.removing_count": "Removing {count} Entries...",
|
||||
"entries.ignored.description": "File entries are considered to be \"ignored\" if they were added to the library before the user's ignore rules (via the '.ts_ignore' file) were updated to exclude it. Ignored files are kept in the library by default in order to prevent accidental data loss when updating ignore rules.",
|
||||
"entries.ignored.ignored_count": "Ignored Entries: {count}",
|
||||
"entries.ignored.remove_alt": "Remo&ve Ignored Entries",
|
||||
"entries.ignored.remove": "Remove Ignored Entries",
|
||||
"entries.ignored.remove_alt": "Remo&ve Ignored Entries",
|
||||
"entries.ignored.scanning": "Scanning Library for Ignored Entries...",
|
||||
"entries.ignored.title": "Fix Ignored Entries",
|
||||
"entries.mirror": "&Mirror",
|
||||
"entries.mirror.confirmation": "Are you sure you want to mirror the following {count} Entries?",
|
||||
"entries.mirror.label": "Mirroring {idx}/{total} Entries...",
|
||||
"entries.mirror.title": "Mirroring Entries",
|
||||
"entries.mirror.window_title": "Mirror Entries",
|
||||
"entries.mirror": "&Mirror",
|
||||
"entries.remove.plural.confirm": "Are you sure you want to remove these <b>{count}</b> entries from your library? No files on disk will be deleted.",
|
||||
"entries.remove.singular.confirm": "Are you sure you want to remove this entry from your library? No files on disk will be deleted.",
|
||||
"entries.running.dialog.new_entries": "Adding {total} New File Entries...",
|
||||
@@ -63,20 +64,29 @@
|
||||
"entries.unlinked.relink.attempting": "Attempting to Relink {index}/{unlinked_count} Entries, {fixed_count} Successfully Relinked",
|
||||
"entries.unlinked.relink.manual": "&Manual Relink",
|
||||
"entries.unlinked.relink.title": "Relinking Entries",
|
||||
"entries.unlinked.remove_alt": "Remo&ve Unlinked Entries",
|
||||
"entries.unlinked.remove": "Remove Unlinked Entries",
|
||||
"entries.unlinked.remove_alt": "Remo&ve Unlinked Entries",
|
||||
"entries.unlinked.scanning": "Scanning Library for Unlinked Entries...",
|
||||
"entries.unlinked.search_and_relink": "&Search && Relink",
|
||||
"entries.unlinked.title": "Fix Unlinked Entries",
|
||||
"entries.unlinked.unlinked_count": "Unlinked Entries: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg and/or FFprobe were not found. FFmpeg is required for multimedia playback and thumbnails.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Add Field",
|
||||
"field.add.plural": "Add Fields",
|
||||
"field.confirm_remove": "Are you sure you want to remove this \"{name}\" field?",
|
||||
"field.copy": "Copy Field",
|
||||
"field.edit": "Edit Field",
|
||||
"field.mixed_data": "Mixed Data",
|
||||
"field.paste": "Paste Field",
|
||||
"field.remove": "Remove Field",
|
||||
"field_template.all_field_templates": "All Field Templates",
|
||||
"field_template.create": "Create Field Template",
|
||||
"field_template.create_add": "Create && Add \"{query}\"",
|
||||
"field_template_manager.title": "Library Field Templates",
|
||||
"field_type.datetime": "Datetime",
|
||||
"field_type.text": "Text",
|
||||
"field_type.unknown": "Unknown Type",
|
||||
"field.copy": "Copy Field",
|
||||
"field.edit": "Edit Field",
|
||||
"field.paste": "Paste Field",
|
||||
"file.date_added": "Date Added",
|
||||
"file.date_created": "Date Created",
|
||||
"file.date_modified": "Date Modified",
|
||||
@@ -88,14 +98,14 @@
|
||||
"file.duplicates.dupeguru.no_file": "No DupeGuru File Selected",
|
||||
"file.duplicates.dupeguru.open_file": "Open DupeGuru Results File",
|
||||
"file.duplicates.fix": "Fix Duplicate Files",
|
||||
"file.duplicates.matches_uninitialized": "Duplicate File Matches: N/A",
|
||||
"file.duplicates.matches": "Duplicate File Matches: {count}",
|
||||
"file.duplicates.mirror_entries": "&Mirror Entries",
|
||||
"file.duplicates.matches_uninitialized": "Duplicate File Matches: 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": "&Mirror Entries",
|
||||
"file.duration": "Length",
|
||||
"file.not_found": "File Not Found",
|
||||
"file.open_file_with": "Open file with",
|
||||
"file.open_file": "Open file",
|
||||
"file.open_file_with": "Open file with",
|
||||
"file.open_location.generic": "Show file in file explorer",
|
||||
"file.open_location.mac": "Reveal in Finder",
|
||||
"file.open_location.windows": "Show in File Explorer",
|
||||
@@ -106,56 +116,58 @@
|
||||
"folders_to_tags.open_all": "Open All",
|
||||
"folders_to_tags.title": "Create Tags From Folders",
|
||||
"generic.add": "Add",
|
||||
"generic.apply_alt": "&Apply",
|
||||
"generic.apply": "Apply",
|
||||
"generic.cancel_alt": "&Cancel",
|
||||
"generic.apply_alt": "&Apply",
|
||||
"generic.cancel": "Cancel",
|
||||
"generic.cancel_alt": "&Cancel",
|
||||
"generic.close": "Close",
|
||||
"generic.continue": "Continue",
|
||||
"generic.copy": "Copy",
|
||||
"generic.cut": "Cut",
|
||||
"generic.delete_alt": "&Delete",
|
||||
"generic.delete": "Delete",
|
||||
"generic.done_alt": "&Done",
|
||||
"generic.delete_alt": "&Delete",
|
||||
"generic.done": "Done",
|
||||
"generic.edit_alt": "&Edit",
|
||||
"generic.done_alt": "&Done",
|
||||
"generic.edit": "Edit",
|
||||
"generic.edit_alt": "&Edit",
|
||||
"generic.filename": "Filename",
|
||||
"generic.missing": "Missing",
|
||||
"generic.navigation.back": "Back",
|
||||
"generic.navigation.next": "Next",
|
||||
"generic.no": "No",
|
||||
"generic.none": "None",
|
||||
"generic.overwrite_alt": "&Overwrite",
|
||||
"generic.overwrite": "Overwrite",
|
||||
"generic.overwrite_alt": "&Overwrite",
|
||||
"generic.paste": "Paste",
|
||||
"generic.recent_libraries": "Recent Libraries",
|
||||
"generic.remove_alt": "&Remove",
|
||||
"generic.remove": "Remove",
|
||||
"generic.rename_alt": "&Rename",
|
||||
"generic.remove_alt": "&Remove",
|
||||
"generic.rename": "Rename",
|
||||
"generic.rename_alt": "&Rename",
|
||||
"generic.reset": "Reset",
|
||||
"generic.save": "Save",
|
||||
"generic.skip_alt": "&Skip",
|
||||
"generic.skip": "Skip",
|
||||
"generic.skip_alt": "&Skip",
|
||||
"generic.yes": "Yes",
|
||||
"home.search": "Search",
|
||||
"home.search.view_limit": "View Limit:",
|
||||
"home.search_entries": "Search Entries",
|
||||
"home.search_field_templates": "Search Field Templates",
|
||||
"home.search_library": "Search Library",
|
||||
"home.search_tags": "Search Tags",
|
||||
"home.search": "Search",
|
||||
"home.show_hidden_entries": "Show Hidden Entries",
|
||||
"home.thumbnail_size": "Thumbnail Size",
|
||||
"home.thumbnail_size.extra_large": "Extra Large Thumbnails",
|
||||
"home.thumbnail_size.large": "Large Thumbnails",
|
||||
"home.thumbnail_size.medium": "Medium Thumbnails",
|
||||
"home.thumbnail_size.mini": "Mini Thumbnails",
|
||||
"home.thumbnail_size.small": "Small Thumbnails",
|
||||
"home.thumbnail_size": "Thumbnail Size",
|
||||
"home.show_hidden_entries": "Show Hidden Entries",
|
||||
"ignore.open_file": "Show \"{ts_ignore}\" File on Disk",
|
||||
"json_migration.checking_for_parity": "Checking for Parity...",
|
||||
"json_migration.creating_database_tables": "Creating SQL Database Tables...",
|
||||
"json_migration.description": "<br>Start and preview the results of the library migration process. The converted library will <i>not</i> be used unless you click \"Finish Migration\". <br><br>Library data should either have matching values or feature a \"Matched\" label. Values that do not match will be displayed in red and feature a \"<b>(!)</b>\" symbol next to them.<br><center><i>This process may take up to several minutes for larger libraries.</i></center>",
|
||||
"json_migration.discrepancies_found.description": "Discrepancies were found between the original and converted library formats. Please review and choose to whether continue with the migration or to cancel.",
|
||||
"json_migration.discrepancies_found": "Library Discrepancies Found",
|
||||
"json_migration.discrepancies_found.description": "Discrepancies were found between the original and converted library formats. Please review and choose to whether continue with the migration or to cancel.",
|
||||
"json_migration.finish_migration": "Finish Migration",
|
||||
"json_migration.heading.aliases": "Aliases:",
|
||||
"json_migration.heading.colors": "Colors:",
|
||||
@@ -168,43 +180,39 @@
|
||||
"json_migration.heading.shorthands": "Shorthands:",
|
||||
"json_migration.info.description": "Library save files created with TagStudio versions <b>9.4 and below</b> will need to be migrated to the new <b>v9.5+</b> format.<br><h2>What you need to know:</h2><ul><li>Your existing library save file will <b><i>NOT</i></b> be deleted</li><li>Your personal files will <b><i>NOT</i></b> be deleted, moved, or modified</li><li>The new v9.5+ save format can not be opened in earlier versions of TagStudio</li></ul><h3>What's changed:</h3><ul><li>\"Tag Fields\" have been replaced by \"Tag Categories\". Instead of adding tags to fields first, tags now get added directly to file entries. They're then automatically organized into categories based on parent tags marked with the new \"Is Category\" property in the tag editing menu. Any tag can be marked as a category, and child tags will sort themselves underneath parent tags marked as categories. The \"Favorite\" and \"Archived\" tags now inherit from a new \"Meta Tags\" tag which is marked as a category by default.</li><li>Tag colors have been tweaked and expanded upon. Some colors have been renamed or consolidated, however all tag colors will still convert to exact or close matches in v9.5.</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "Migrating {entries:,d} File Entries...",
|
||||
"json_migration.migration_complete_with_discrepancies": "Migration Complete, Discrepancies Found",
|
||||
"json_migration.migration_complete": "Migration Complete!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Migration Complete, Discrepancies Found",
|
||||
"json_migration.start_and_preview": "Start and Preview",
|
||||
"json_migration.title": "Save Format Migration: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Library</h2>",
|
||||
"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.missing": "Library Location is Missing",
|
||||
"library.name": "Library",
|
||||
"library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found",
|
||||
"library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found",
|
||||
"library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...",
|
||||
"library.refresh.title": "Refreshing Directories",
|
||||
"library.scan_library.title": "Scanning Library",
|
||||
"library_info.cleanup": "Cleanup",
|
||||
"library_info.cleanup.backups": "Library Backups:",
|
||||
"library_info.cleanup.dupe_files": "Duplicate Files:",
|
||||
"library_info.cleanup.ignored": "Ignored Entries:",
|
||||
"library_info.cleanup.legacy_json": "Leftover Legacy Library:",
|
||||
"library_info.cleanup.unlinked": "Unlinked Entries:",
|
||||
"library_info.cleanup": "Cleanup",
|
||||
"library_info.stats": "Statistics",
|
||||
"library_info.stats.colors": "Tag Colors:",
|
||||
"library_info.stats.entries": "Entries:",
|
||||
"library_info.stats.fields": "Fields:",
|
||||
"library_info.stats.macros": "Macros:",
|
||||
"library_info.stats.namespaces": "Namespaces:",
|
||||
"library_info.stats.tags": "Tags:",
|
||||
"library_info.stats": "Statistics",
|
||||
"library_info.title": "Library '{library_dir}'",
|
||||
"library_info.version": "Library Format Version: {version}",
|
||||
"library_object.name_required": "Name (Required)",
|
||||
"library_object.name": "Name",
|
||||
"library_object.slug_required": "ID Slug (Required)",
|
||||
"library_object.name_required": "Name (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",
|
||||
"library.field.remove": "Remove Field",
|
||||
"library.missing": "Library Location is Missing",
|
||||
"library.name": "Library",
|
||||
"library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...",
|
||||
"library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found",
|
||||
"library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found",
|
||||
"library.refresh.title": "Refreshing Directories",
|
||||
"library.scan_library.title": "Scanning Library",
|
||||
"library_object.slug_required": "ID Slug (Required)",
|
||||
"macros.running.dialog.new_entries": "Running Configured Macros on {count}/{total} New File Entries...",
|
||||
"macros.running.dialog.title": "Running Macros on New Entries",
|
||||
"media_player.autoplay": "Autoplay",
|
||||
@@ -212,10 +220,12 @@
|
||||
"menu.delete_selected_files_ambiguous": "Move File(s) to {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Move Files to {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Move File to {trash_term}",
|
||||
"menu.edit": "Edit",
|
||||
"menu.edit.ignore_files": "Ignore Files and Folders",
|
||||
"menu.edit.manage_field_templates": "Manage Field Templates",
|
||||
"menu.edit.manage_tags": "Manage Tags",
|
||||
"menu.edit.new_tag": "New &Tag",
|
||||
"menu.edit": "Edit",
|
||||
"menu.file": "&File",
|
||||
"menu.file.clear_recent_libraries": "Clear Recent",
|
||||
"menu.file.close_library": "&Close Library",
|
||||
"menu.file.missing_library.message": "The location of the library \"{library}\" cannot be found.",
|
||||
@@ -228,24 +238,23 @@
|
||||
"menu.file.refresh_directories": "&Refresh Directories",
|
||||
"menu.file.save_backup": "&Save Library Backup",
|
||||
"menu.file.save_library": "Save Library",
|
||||
"menu.file": "&File",
|
||||
"menu.help.about": "About",
|
||||
"menu.help": "&Help",
|
||||
"menu.macros.folders_to_tags": "Folders to Tags",
|
||||
"menu.help.about": "About",
|
||||
"menu.macros": "&Macros",
|
||||
"menu.macros.folders_to_tags": "Folders to Tags",
|
||||
"menu.select": "Select",
|
||||
"menu.settings": "Settings...",
|
||||
"menu.tools": "&Tools",
|
||||
"menu.tools.fix_duplicate_files": "Fix &Duplicate Files",
|
||||
"menu.tools.fix_ignored_entries": "Fix &Ignored Entries",
|
||||
"menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries",
|
||||
"menu.tools": "&Tools",
|
||||
"menu.view": "&View",
|
||||
"menu.view.decrease_thumbnail_size": "Decrease Thumbnail Size",
|
||||
"menu.view.increase_thumbnail_size": "Increase Thumbnail Size",
|
||||
"menu.view.library_info": "Library &Information",
|
||||
"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.description_color": "Tag colors use namespaces as color palette groups. All custom colors must be under a namespace group first.",
|
||||
"namespace.create.title": "Create Namespace",
|
||||
"namespace.new.button": "New Namespace",
|
||||
"namespace.new.prompt": "Create a New Namespace to Start Adding Custom Colors!",
|
||||
@@ -313,34 +322,33 @@
|
||||
"status.library_version_expected": "Expected:",
|
||||
"status.library_version_found": "Found:",
|
||||
"status.library_version_mismatch": "Library Version Mismatch!",
|
||||
"status.results_found": "{count} Results Found ({time_span})",
|
||||
"status.results.invalid_syntax": "Invalid Search Syntax:",
|
||||
"status.results": "Results",
|
||||
"tag_manager.title": "Library Tags",
|
||||
"tag.add_to_search": "Add to Search",
|
||||
"tag.add.plural": "Add Tags",
|
||||
"status.results.invalid_syntax": "Invalid Search Syntax:",
|
||||
"status.results_found": "{count} Results Found ({time_span})",
|
||||
"tag.add": "Add Tag",
|
||||
"tag.add.plural": "Add Tags",
|
||||
"tag.add_to_search": "Add to Search",
|
||||
"tag.aliases": "Aliases",
|
||||
"tag.all_tags": "All Tags",
|
||||
"tag.choose_color": "Choose Tag Color",
|
||||
"tag.color": "Color",
|
||||
"tag.confirm_delete": "Are you sure you want to delete the tag \"{tag_name}\"?",
|
||||
"tag.create_add": "Create && Add \"{query}\"",
|
||||
"tag.create": "Create Tag",
|
||||
"tag.create_add": "Create && Add \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Use this tag for disambiguation",
|
||||
"tag.edit": "Edit Tag",
|
||||
"tag.is_category": "Is Category",
|
||||
"tag.is_hidden": "Is Hidden",
|
||||
"tag.name": "Name",
|
||||
"tag.new": "New Tag",
|
||||
"tag.parent_tags": "Parent Tags",
|
||||
"tag.parent_tags.add": "Add Parent Tag(s)",
|
||||
"tag.parent_tags.description": "This tag can be treated as a substitute for any of these Parent Tags in searches.",
|
||||
"tag.parent_tags": "Parent Tags",
|
||||
"tag.remove": "Remove Tag",
|
||||
"tag.search_for_tag": "Search for Tag",
|
||||
"tag.shorthand": "Shorthand",
|
||||
"tag.tag_name_required": "Tag Name (Required)",
|
||||
"tag.view_limit": "View Limit:",
|
||||
"tag_manager.title": "Library Tags",
|
||||
"trash.context.ambiguous": "Move file(s) to {trash_term}",
|
||||
"trash.context.plural": "Move files to {trash_term}",
|
||||
"trash.context.singular": "Move file to {trash_term}",
|
||||
@@ -353,9 +361,9 @@
|
||||
"trash.dialog.title.singular": "Delete File",
|
||||
"trash.name.generic": "Trash",
|
||||
"trash.name.windows": "Recycle Bin",
|
||||
"version_modal.title": "TagStudio Update Available",
|
||||
"version_modal.description": "A new version of TagStudio is available! You can download the latest release from <a href=\"{github_url}\">GitHub</a>.",
|
||||
"version_modal.status": "Installed Version: {installed_version}<br>Latest Release Version: {latest_release_version}",
|
||||
"version_modal.title": "TagStudio Update Available",
|
||||
"view.size.0": "Mini",
|
||||
"view.size.1": "Small",
|
||||
"view.size.2": "Medium",
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"about.module.found": "Encontrado",
|
||||
"about.title": "Acerca de TagStudio",
|
||||
"about.website": "Página web",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-lanzamiento",
|
||||
"app.git": "Commit de Git",
|
||||
"app.pre_release": "Pre-Lanzamiento",
|
||||
"app.title": "{base_title} - Biblioteca '{library_dir}'",
|
||||
"color.color_border": "Usar color secundario para el Borde",
|
||||
"color.confirm_delete": "¿Estás seguro de que quieres eliminar el color \"{color_name}\"?",
|
||||
@@ -71,14 +71,21 @@
|
||||
"entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}",
|
||||
"ffmpeg.missing.description": "No se ha encontrado FFmpeg y/o FFprobe. Se requiere de FFmpeg para la reproducción de contenido multimedia y las miniaturas.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Copiar campo",
|
||||
"field.edit": "Editar campo",
|
||||
"field.paste": "Pegar campo",
|
||||
"file.date_added": "Fecha de adición",
|
||||
"file.date_created": "Fecha de creación",
|
||||
"file.date_modified": "Fecha de modificación",
|
||||
"field.add": "Añadir campo",
|
||||
"field.confirm_remove": "¿Está seguro de que desea eliminar el campo \"{name}\"?",
|
||||
"field.copy": "Copiar Campo",
|
||||
"field.edit": "Editar Campo",
|
||||
"field.mixed_data": "Datos variados",
|
||||
"field.paste": "Pegar Campo",
|
||||
"field.remove": "Eliminar campo",
|
||||
"field_type.datetime": "Fecha y Hora",
|
||||
"field_type.text": "Texto",
|
||||
"field_type.unknown": "Tipo Desconocido",
|
||||
"file.date_added": "Fecha de Adición",
|
||||
"file.date_created": "Fecha de Creación",
|
||||
"file.date_modified": "Fecha de Modificación",
|
||||
"file.dimensions": "Dimensiones",
|
||||
"file.duplicates.description": "TagStudio es compatible con Importación de resultados de DupeGuru para administrar archivos duplicados.",
|
||||
"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 de DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&Cargar archivo DupeGuru",
|
||||
@@ -157,6 +164,7 @@
|
||||
"json_migration.heading.aliases": "Alias:",
|
||||
"json_migration.heading.colors": "Colores:",
|
||||
"json_migration.heading.differ": "Discrepancia",
|
||||
"json_migration.heading.extensions": "Extensiones:",
|
||||
"json_migration.heading.match": "Igualado",
|
||||
"json_migration.heading.names": "Nombres:",
|
||||
"json_migration.heading.parent_tags": "Etiquetas principales:",
|
||||
@@ -171,10 +179,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ biblioteca</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 biblioteca</h2>",
|
||||
"landing.open_create_library": "Abrir/Crear biblioteca {shortcut}",
|
||||
"library.field.add": "Añadir campo",
|
||||
"library.field.confirm_remove": "¿Está seguro de que desea eliminar el campo \"{name}\"?",
|
||||
"library.field.mixed_data": "Datos variados",
|
||||
"library.field.remove": "Eliminar campo",
|
||||
"library.missing": "Falta la ubicación",
|
||||
"library.name": "Biblioteca",
|
||||
"library.refresh.scanning.plural": "Escaneando directorios en busca de nuevos archivos...\n{searched_count} archivos buscados, {found_count} nuevos archivos encontrados",
|
||||
@@ -271,6 +275,7 @@
|
||||
"settings.open_library_on_start": "Abrir biblioteca al iniciar",
|
||||
"settings.page_size": "Tamaño de la página",
|
||||
"settings.restart_required": "Por favor, reinicia TagStudio para que se los cambios surtan efecto.",
|
||||
"settings.scan_files_on_open": "Cargar Nuevos Archivos Automáticamente",
|
||||
"settings.show_filenames_in_grid": "Mostrar el nombre de archivo en la cuadrícula",
|
||||
"settings.show_recent_libraries": "Mostrar bibliotecas recientes",
|
||||
"settings.splash.label": "Pantalla de Bienvenida",
|
||||
@@ -334,7 +339,7 @@
|
||||
"tag.search_for_tag": "Buscar por etiqueta",
|
||||
"tag.shorthand": "Abreviatura",
|
||||
"tag.tag_name_required": "Nombre etiqueta (Obligatorio)",
|
||||
"tag.view_limit": "Límite visualización:",
|
||||
"home.search.view_limit": "Límite visualización:",
|
||||
"tag_manager.title": "Etiquetas de la biblioteca",
|
||||
"trash.context.ambiguous": "Mover archivo(s) a la {trash_term}",
|
||||
"trash.context.plural": "Mover archivos a la {trash_term}",
|
||||
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "Linkittämättömät merkinnät: {count}",
|
||||
"ffmpeg.missing.description": "FFmpegiä ja/tai FFprobea ei löytynyt. FFmpeg vaaditaan multimedian toistoon ja pikkukuvien näyttämiseen.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Lisää kenttä",
|
||||
"field.confirm_remove": "Haluatko varmasti poistaa tämän \"{name}\"-kentän?",
|
||||
"field.copy": "Kopioi kenttä",
|
||||
"field.edit": "Muokkaa kenttää",
|
||||
"field.mixed_data": "Sekalaista dataa",
|
||||
"field.paste": "Liitä kenttä",
|
||||
"field.remove": "Poistettu kenttä",
|
||||
"file.date_added": "Päiväys lisätty",
|
||||
"file.date_created": "Päiväys luotu",
|
||||
"file.date_modified": "Päiväys muokattu",
|
||||
@@ -171,10 +175,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Kirjasto</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Kirjasto</h2>",
|
||||
"landing.open_create_library": "Avaa/Luo kirjasto {shortcut}",
|
||||
"library.field.add": "Lisää kenttä",
|
||||
"library.field.confirm_remove": "Haluatko varmasti poistaa tämän \"{name}\"-kentän?",
|
||||
"library.field.mixed_data": "Sekalaista dataa",
|
||||
"library.field.remove": "Poistettu kenttä",
|
||||
"library.missing": "Kirjaston sijainti puuttuu",
|
||||
"library.name": "Kirjasto",
|
||||
"library.refresh.title": "Virkistetty hakemistot",
|
||||
@@ -301,7 +301,7 @@
|
||||
"tag.search_for_tag": "Etsi tunnistetta",
|
||||
"tag.shorthand": "Lyhenne",
|
||||
"tag.tag_name_required": "Tunnisteen nimi (Vaaditaan)",
|
||||
"tag.view_limit": "Näytä raja:",
|
||||
"home.search.view_limit": "Näytä raja:",
|
||||
"tag_manager.title": "Kirjasto tunnisteet",
|
||||
"trash.dialog.title.plural": "Poista tiedostoja",
|
||||
"trash.dialog.title.singular": "Poista tiedosto",
|
||||
|
||||
@@ -60,9 +60,13 @@
|
||||
"entries.unlinked.unlinked_count": "Mga Naka-unlink na Entry: {count}",
|
||||
"ffmpeg.missing.description": "Hindi nahanap ang FFmpeg at/o FFprobe. Kinakailangan ang FFmpeg para sa playback ng multimedia at mga thumbnail.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Magdagdag ng Field",
|
||||
"field.confirm_remove": "Sigurado ka ba gusto mo tanggalin ang field na \"{name}\"?",
|
||||
"field.copy": "Kopyahin ang Field",
|
||||
"field.edit": "I-edit ang Field",
|
||||
"field.mixed_data": "Halo-halong Data",
|
||||
"field.paste": "I-paste ang Field",
|
||||
"field.remove": "Tanggalin ang Field",
|
||||
"file.date_added": "Petsang Dinagdag",
|
||||
"file.date_created": "Petsa na Ginawa",
|
||||
"file.date_modified": "Binago Noong",
|
||||
@@ -154,10 +158,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ na Library</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 na Library</h2>",
|
||||
"landing.open_create_library": "Buksan/Gumawa ng Library {shortcut}",
|
||||
"library.field.add": "Magdagdag ng Field",
|
||||
"library.field.confirm_remove": "Sigurado ka ba gusto mo tanggalin ang field na \"{name}\"?",
|
||||
"library.field.mixed_data": "Halo-halong Data",
|
||||
"library.field.remove": "Tanggalin ang Field",
|
||||
"library.missing": "Nawawala ang Lokasyon ng Library",
|
||||
"library.name": "Library",
|
||||
"library.refresh.scanning.plural": "Sina-scan ang Direktoryo para sa Mga Bagong File…\n{searched_count} Nahanap na File, {found_count} Nahanap na Bagong FIle",
|
||||
@@ -287,7 +287,7 @@
|
||||
"tag.search_for_tag": "Maghanap para sa Tag",
|
||||
"tag.shorthand": "Shorthand",
|
||||
"tag.tag_name_required": "Pangalan ng Tag (Kinakailangan)",
|
||||
"tag.view_limit": "Limitasyon ng Pagtingin:",
|
||||
"home.search.view_limit": "Limitasyon ng Pagtingin:",
|
||||
"tag_manager.title": "Mga Tag ng Library",
|
||||
"trash.context.ambiguous": "Ilipat ang (mga) file sa {trash_term}",
|
||||
"trash.context.plural": "Ilipat ang mga file sa {trash_term}",
|
||||
|
||||
@@ -71,9 +71,16 @@
|
||||
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg et/ou FFprobe n’ont pas été trouvée. FFmpeg est nécessaire pour la lecture de média et les vignettes.",
|
||||
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
|
||||
"field.add": "Ajouter un Champ",
|
||||
"field.confirm_remove": "Êtes-vous sûr de vouloir supprimer le champ \"{name}\"?",
|
||||
"field.copy": "Copier le Champ",
|
||||
"field.edit": "Modifier le Champ",
|
||||
"field.mixed_data": "Données Mélangées",
|
||||
"field.paste": "Coller le Champ",
|
||||
"field.remove": "Supprimer un Champ",
|
||||
"field_type.datetime": "Date et temps",
|
||||
"field_type.text": "Texte",
|
||||
"field_type.unknown": "Type inconnu",
|
||||
"file.date_added": "Date Ajoutée",
|
||||
"file.date_created": "Date de Création",
|
||||
"file.date_modified": "Date de Modification",
|
||||
@@ -172,10 +179,6 @@
|
||||
"json_migration.title.new_lib": "<h2>Bibliothèque v9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>Bibliothèque v9.4</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 \"{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",
|
||||
@@ -272,6 +275,7 @@
|
||||
"settings.open_library_on_start": "Ouvrir la Bibliothèque au Démarrage",
|
||||
"settings.page_size": "Entités par page",
|
||||
"settings.restart_required": "Veuillez redémarré TagStudio pour que les changements prenne effet.",
|
||||
"settings.scan_files_on_open": "Charger automatiquement les nouveaux fichiers",
|
||||
"settings.show_filenames_in_grid": "Afficher les Noms de Fichiers en Grille",
|
||||
"settings.show_recent_libraries": "Afficher les Bibliothèques Récentes",
|
||||
"settings.splash.label": "Page de guarde",
|
||||
@@ -335,7 +339,7 @@
|
||||
"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 :",
|
||||
"home.search.view_limit": "Limite d'affichage :",
|
||||
"tag_manager.title": "Tags 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}",
|
||||
|
||||
12
src/tagstudio/resources/translations/he.json
Normal file
12
src/tagstudio/resources/translations/he.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"about.license": "רישיון",
|
||||
"about.website": "אתר",
|
||||
"color.delete": "מחק תגית",
|
||||
"color.name": "שם",
|
||||
"color.new": "צבע חדש",
|
||||
"color.placeholder": "צבע",
|
||||
"color.primary": "צבע ראשי",
|
||||
"color.secondary": "צבע משני",
|
||||
"color.title.no_color": "ללא צבע",
|
||||
"color_manager.title": "שנה צבע תגית"
|
||||
}
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "Kapcsolat nélküli elemek: {count}",
|
||||
"ffmpeg.missing.description": "Az FFmpeg és/vagy az FFprobe nem található. Az FFmpeg megléte szükséges a videó- és hangfájlok lejátszásához és a miniatűrök megjelenítéséhez.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Új mező",
|
||||
"field.confirm_remove": "Biztosan el akarja távolítani a(z) „{name}”-mezőt?",
|
||||
"field.copy": "Mező &másolása",
|
||||
"field.edit": "Mező szerkesztése",
|
||||
"field.mixed_data": "Kevert adatok",
|
||||
"field.paste": "Mező &beillesztése",
|
||||
"field.remove": "Mező eltávolítása",
|
||||
"field_type.datetime": "Dátum és idő",
|
||||
"field_type.text": "Szöveg",
|
||||
"field_type.unknown": "Ismeretlen típus",
|
||||
@@ -175,10 +179,6 @@
|
||||
"json_migration.title.new_lib": "<h2>9.5 és afölötti könyvtár</h2>",
|
||||
"json_migration.title.old_lib": "<h2>9.4-es könyvtár</h2>",
|
||||
"landing.open_create_library": "Könyvtár meg&nyitása/létrehozása {shortcut}",
|
||||
"library.field.add": "Új mező",
|
||||
"library.field.confirm_remove": "Biztosan el akarja távolítani a(z) „{name}”-mezőt?",
|
||||
"library.field.mixed_data": "Kevert adatok",
|
||||
"library.field.remove": "Mező eltávolítása",
|
||||
"library.missing": "Hiányzó hely",
|
||||
"library.name": "Könyvtár",
|
||||
"library.refresh.scanning.plural": "Új fájlok keresése a mappákban…\n{searched_count} fájl megvizsgálva; ebből {found_count} új fájl",
|
||||
@@ -339,7 +339,7 @@
|
||||
"tag.search_for_tag": "Címke keresése",
|
||||
"tag.shorthand": "Rövidítés",
|
||||
"tag.tag_name_required": "Címkenév (kötelező)",
|
||||
"tag.view_limit": "Megtekintési korlát:",
|
||||
"home.search.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",
|
||||
|
||||
@@ -71,9 +71,16 @@
|
||||
"entries.unlinked.unlinked_count": "Voci non Collegate: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg e/o FFprobe non sono stati trovati. FFmpeg è necessario per la riproduzione multimediale e per le miniature.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Aggiungi Campo",
|
||||
"field.confirm_remove": "Sei sicuro di voler rimuovere il campo \"{name}\"?",
|
||||
"field.copy": "Copia Campo",
|
||||
"field.edit": "Modifica Campo",
|
||||
"field.mixed_data": "Dati Misti",
|
||||
"field.paste": "Incolla Campo",
|
||||
"field.remove": "Rimuovi Campo",
|
||||
"field_type.datetime": "Data e Ora",
|
||||
"field_type.text": "Testo",
|
||||
"field_type.unknown": "Tipo Sconosciuto",
|
||||
"file.date_added": "Data Aggiunta",
|
||||
"file.date_created": "Data di Creazione",
|
||||
"file.date_modified": "Data di Modifica",
|
||||
@@ -157,6 +164,7 @@
|
||||
"json_migration.heading.aliases": "Alias:",
|
||||
"json_migration.heading.colors": "Colori:",
|
||||
"json_migration.heading.differ": "Discrepanze",
|
||||
"json_migration.heading.extensions": "Estensioni:",
|
||||
"json_migration.heading.match": "Abbinato",
|
||||
"json_migration.heading.names": "Nomi:",
|
||||
"json_migration.heading.parent_tags": "Etichette Genitore:",
|
||||
@@ -171,10 +179,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Biblioteca</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Biblioteca</h2>",
|
||||
"landing.open_create_library": "Apri/Crea Biblioteca {shortcut}",
|
||||
"library.field.add": "Aggiungi Campo",
|
||||
"library.field.confirm_remove": "Sei sicuro di voler rimuovere il campo \"{name}\"?",
|
||||
"library.field.mixed_data": "Dati Misti",
|
||||
"library.field.remove": "Rimuovi Campo",
|
||||
"library.missing": "Manca la Posizione della Biblioteca",
|
||||
"library.name": "Biblioteca",
|
||||
"library.refresh.scanning.plural": "Ricerca di Nuovi File nelle Cartelle...\n{searched_count} Files Cercati, {found_count} Nuovi File Trovati",
|
||||
@@ -271,6 +275,7 @@
|
||||
"settings.open_library_on_start": "Apri Biblioteca all'Avvio",
|
||||
"settings.page_size": "Dimensione Pagina",
|
||||
"settings.restart_required": "Riavvia TagStudio affinchè le modifiche abbiano effetto.",
|
||||
"settings.scan_files_on_open": "Carica Automaticamente i Nuovi File",
|
||||
"settings.show_filenames_in_grid": "Mostra Nomi dei File nella Griglia",
|
||||
"settings.show_recent_libraries": "Mostra Biblioteche Recenti",
|
||||
"settings.splash.label": "Schermata Iniziale",
|
||||
@@ -334,7 +339,7 @@
|
||||
"tag.search_for_tag": "Cerca Etichetta",
|
||||
"tag.shorthand": "Abbreviazione",
|
||||
"tag.tag_name_required": "Nome Etichetta (Obbligatorio)",
|
||||
"tag.view_limit": "Limite di Visualizzazione:",
|
||||
"home.search.view_limit": "Limite di Visualizzazione:",
|
||||
"tag_manager.title": "Etichette della Biblioteca",
|
||||
"trash.context.ambiguous": "Sposta file(s) in {trash_term}",
|
||||
"trash.context.plural": "Sposta files in {trash_term}",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...",
|
||||
"entries.running.dialog.title": "新しいファイルエントリを追加",
|
||||
"entries.tags": "タグ",
|
||||
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。<br><br>リンク切れのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
|
||||
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルが TagStudio 以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。<br><br>リンク切れのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
|
||||
"entries.unlinked.relink.attempting": "{unlinked_count} 件中 {index} 件のエントリを再リンク中、{fixed_count} 件を正常に再リンクしました",
|
||||
"entries.unlinked.relink.manual": "手動で再リンク(&M)",
|
||||
"entries.unlinked.relink.title": "エントリの再リンク",
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "リンク切れのエントリ数: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg または FFprobe が見つかりません。マルチメディアの再生とサムネイルの表示には FFmpeg のインストールが必要です。",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "フィールドの追加",
|
||||
"field.confirm_remove": "「{name}」フィールドを削除してもよろしいですか?",
|
||||
"field.copy": "フィールドをコピー",
|
||||
"field.edit": "フィールドを編集",
|
||||
"field.mixed_data": "混在データ",
|
||||
"field.paste": "フィールドを貼り付け",
|
||||
"field.remove": "フィールドの削除",
|
||||
"file.date_added": "追加日時",
|
||||
"file.date_created": "作成日時",
|
||||
"file.date_modified": "更新日時",
|
||||
@@ -171,10 +175,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ ライブラリ</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 ライブラリ</h2>",
|
||||
"landing.open_create_library": "ライブラリを開く/作成する {shortcut}",
|
||||
"library.field.add": "フィールドの追加",
|
||||
"library.field.confirm_remove": "「{name}」フィールドを削除してもよろしいですか?",
|
||||
"library.field.mixed_data": "混在データ",
|
||||
"library.field.remove": "フィールドの削除",
|
||||
"library.missing": "ライブラリの場所が見つかりません",
|
||||
"library.name": "ライブラリ",
|
||||
"library.refresh.scanning.plural": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
|
||||
@@ -334,7 +334,7 @@
|
||||
"tag.search_for_tag": "このタグで検索",
|
||||
"tag.shorthand": "略称",
|
||||
"tag.tag_name_required": "タグの名前 (必須)",
|
||||
"tag.view_limit": "表示件数:",
|
||||
"home.search.view_limit": "表示件数:",
|
||||
"tag_manager.title": "ライブラリ タグ",
|
||||
"trash.context.ambiguous": "ファイルを {trash_term} に移動",
|
||||
"trash.context.plural": "ファイルを {trash_term} に移動",
|
||||
|
||||
@@ -68,9 +68,13 @@
|
||||
"entries.unlinked.unlinked_count": "Frakoblede Oppføringer: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg og/eller FFprobe ble ikke funnet. FFmpeg er påkrevd for flermediell gjenspilling og miniatyrbilde.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Legg til felt",
|
||||
"field.confirm_remove": "Fjern dette «\"{name}\"»-feltet?",
|
||||
"field.copy": "Kopier Felt",
|
||||
"field.edit": "Rediger Felt",
|
||||
"field.mixed_data": "Blandet data",
|
||||
"field.paste": "Lim Inn Felt",
|
||||
"field.remove": "Fjern felt",
|
||||
"file.date_added": "Dato Lagt til",
|
||||
"file.date_created": "Dato opprettet",
|
||||
"file.date_modified": "Endringsdato",
|
||||
@@ -162,10 +166,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Bibliotek</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Bibliotek</h2>",
|
||||
"landing.open_create_library": "Åpne/Lag nytt Bibliotek {shortcut}",
|
||||
"library.field.add": "Legg til felt",
|
||||
"library.field.confirm_remove": "Fjern dette «\"{name}\"»-feltet?",
|
||||
"library.field.mixed_data": "Blandet data",
|
||||
"library.field.remove": "Fjern felt",
|
||||
"library.missing": "Posisjon mangler",
|
||||
"library.name": "Bibliotek",
|
||||
"library.refresh.scanning.plural": "Skanner Mapper for Nye Filer...\n{searched_count} Filer Sjekket, {found_count} Nye Filer Funnet",
|
||||
@@ -296,7 +296,7 @@
|
||||
"tag.search_for_tag": "Søk etter etikett",
|
||||
"tag.shorthand": "Forkortelse",
|
||||
"tag.tag_name_required": "Etikettnavn (Påkrevd)",
|
||||
"tag.view_limit": "Se Grense:",
|
||||
"home.search.view_limit": "Se Grense:",
|
||||
"tag_manager.title": "Biblioteksetiketter",
|
||||
"trash.context.ambiguous": "Flytt fil(er) til {trash_term}",
|
||||
"trash.context.plural": "Flytt filer til {trash_term}",
|
||||
|
||||
@@ -38,9 +38,12 @@
|
||||
"entries.duplicate.merge.label": "Dubbele vermeldingen samenvoegen...",
|
||||
"entries.duplicate.refresh": "Dubbele Invoer Vernieuwen",
|
||||
"entries.tags": "Labels",
|
||||
"field.add": "Veld Toevoegen",
|
||||
"field.copy": "Veld Kopiëren",
|
||||
"field.edit": "Veld Aanpassen",
|
||||
"field.mixed_data": "Gemixte Data",
|
||||
"field.paste": "Veld Plakken",
|
||||
"field.remove": "Veld Weghalen",
|
||||
"file.date_added": "Datum Toegevoegd",
|
||||
"file.date_created": "Datum Aangemaakt",
|
||||
"file.date_modified": "Datum Aangepast",
|
||||
@@ -94,9 +97,6 @@
|
||||
"json_migration.heading.shorthands": "Afkortingen:",
|
||||
"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 Weghalen",
|
||||
"library.refresh.scanning_preparing": "Mappen scannen voor nieuwe bestanden...\nVoorbereiden...",
|
||||
"library_info.stats.fields": "Velden:",
|
||||
"library_info.stats.tags": "Labels:",
|
||||
|
||||
@@ -60,9 +60,13 @@
|
||||
"entries.unlinked.unlinked_count": "Odłączone wpisy: {count}",
|
||||
"ffmpeg.missing.description": "Nie odnaleziono FFmpeg lub FFprobe. FFmpeg jest wymagany do odtwarzania multimediów i do wyświetlania miniaturek.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Dodaj pole",
|
||||
"field.confirm_remove": "Jesteś pewien że chcesz usunąć pole \"{name}\" ?",
|
||||
"field.copy": "Skopiuj pole",
|
||||
"field.edit": "Edytuj pole",
|
||||
"field.mixed_data": "Mieszane dane",
|
||||
"field.paste": "Wklej pole",
|
||||
"field.remove": "Usuń pole",
|
||||
"file.date_added": "Data dodania",
|
||||
"file.date_created": "Data utworzenia",
|
||||
"file.date_modified": "Data modyfikacji",
|
||||
@@ -152,10 +156,6 @@
|
||||
"json_migration.title.new_lib": "<h2>Biblioteka v9.5+ </h2>",
|
||||
"json_migration.title.old_lib": "<h2>Biblioteka v9.4</h2>",
|
||||
"landing.open_create_library": "Otwórz/Stwórz bibliotekę {shortcut}",
|
||||
"library.field.add": "Dodaj pole",
|
||||
"library.field.confirm_remove": "Jesteś pewien że chcesz usunąć pole \"{name}\" ?",
|
||||
"library.field.mixed_data": "Mieszane dane",
|
||||
"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",
|
||||
@@ -273,7 +273,7 @@
|
||||
"tag.search_for_tag": "Szukaj dla tagu",
|
||||
"tag.shorthand": "Skrót",
|
||||
"tag.tag_name_required": "Nazwa tagu (wymagana)",
|
||||
"tag.view_limit": "Limit wyświetlania:",
|
||||
"home.search.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}",
|
||||
|
||||
@@ -58,9 +58,13 @@
|
||||
"entries.unlinked.title": "Corrigir Registos Não Referenciados",
|
||||
"entries.unlinked.unlinked_count": "Registos Não Referenciados: {count}",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Adicionar Campo",
|
||||
"field.confirm_remove": "Tem certeza que quer remover o campo \"{name}\"?",
|
||||
"field.copy": "Copiar Campo",
|
||||
"field.edit": "Editar Campo",
|
||||
"field.mixed_data": "Dados Mistos",
|
||||
"field.paste": "Colar Campo",
|
||||
"field.remove": "Remover Campo",
|
||||
"file.date_added": "Data de Adição",
|
||||
"file.date_created": "Data de Criação",
|
||||
"file.date_modified": "Data de Modificação",
|
||||
@@ -147,10 +151,6 @@
|
||||
"json_migration.title.new_lib": "<h2>Biblioteca v9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>Biblioteca v9.4</h2>",
|
||||
"landing.open_create_library": "Abrir/Criar Biblioteca {shortcut}",
|
||||
"library.field.add": "Adicionar Campo",
|
||||
"library.field.confirm_remove": "Tem certeza que quer remover o campo \"{name}\"?",
|
||||
"library.field.mixed_data": "Dados Mistos",
|
||||
"library.field.remove": "Remover Campo",
|
||||
"library.missing": "Localização Ausente",
|
||||
"library.name": "Biblioteca",
|
||||
"library.refresh.scanning.plural": "A escanear pastas por Novos Ficheiros ...\n{searched_count} Ficheiros pesquisados, {found_count} Novos Ficheiros",
|
||||
@@ -241,7 +241,7 @@
|
||||
"tag.parent_tags.add": "Adicionar Tag Pai",
|
||||
"tag.search_for_tag": "Procurar por Tag",
|
||||
"tag.shorthand": "Abreviação",
|
||||
"tag.view_limit": "Limite de visualização:",
|
||||
"home.search.view_limit": "Limite de visualização:",
|
||||
"tag_manager.title": "Tags da sua biblioteca",
|
||||
"trash.context.ambiguous": "Mover ficheiro(s) para {trash_term}",
|
||||
"trash.context.plural": "Mover ficheiros para {trash_term}",
|
||||
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "Registros Não Referenciados: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg e/ou FFprobe não foram encontrados. FFmpeg é necessário para reproduzir multimídias e miniaturas.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Adicionar Campo",
|
||||
"field.confirm_remove": "Você tem certeza de que quer remover o campo \"{name}\"?",
|
||||
"field.copy": "Copiar Campo",
|
||||
"field.edit": "Editar Campo",
|
||||
"field.mixed_data": "Dados Mistos",
|
||||
"field.paste": "Colar Campo",
|
||||
"field.remove": "Remover Campo",
|
||||
"file.date_added": "Data de Adição",
|
||||
"file.date_created": "Data de Criação",
|
||||
"file.date_modified": "Data de Modificação",
|
||||
@@ -171,10 +175,6 @@
|
||||
"json_migration.title.new_lib": "<h2>Biblioteca v9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>Biblioteca v9.4</h2>",
|
||||
"landing.open_create_library": "Abrir/Criar Biblioteca {shortcut}",
|
||||
"library.field.add": "Adicionar Campo",
|
||||
"library.field.confirm_remove": "Você tem certeza de que quer remover o campo \"{name}\"?",
|
||||
"library.field.mixed_data": "Dados Mistos",
|
||||
"library.field.remove": "Remover Campo",
|
||||
"library.missing": "Localização Ausente",
|
||||
"library.name": "Biblioteca",
|
||||
"library.refresh.scanning.plural": "Escaneando pastas em busca de novos arquivos ...\n{searched_count} Arquivos encontrados, {found_count} Novos Arquivos",
|
||||
@@ -331,7 +331,7 @@
|
||||
"tag.search_for_tag": "Procurar por Tag",
|
||||
"tag.shorthand": "Abreviação",
|
||||
"tag.tag_name_required": "Nome da Tag (Obrigatório)",
|
||||
"tag.view_limit": "Limite de visualização:",
|
||||
"home.search.view_limit": "Limite de visualização:",
|
||||
"tag_manager.title": "Tags da sua biblioteca",
|
||||
"trash.context.ambiguous": "Mover arquivo(s) para {trash_term}",
|
||||
"trash.context.plural": "Mover arquivos para {trash_term}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about.config_path": "Mlafuplas fu sentakuzma",
|
||||
"about.description": "TagStudio zeting ke parjat mlafu au riso mit festaretol, hadafvui per anta sentakuzma au mange dekizma brukdjin made. Nil kinijena zeting os mlafufal, jamnai mare fu flanka mlafu au perpa tumam fu kompyu.",
|
||||
"about.description": "TagStudio - apli per parjat mlafu au riso mit festaretol, hadafsui per anta sentakuzma au mange dekizma brukdjin made. Nil kinijena apli os mlafufal, nil mare fu flankamlafu, au na nilraz ti kawri hel parjat fu kompyu.",
|
||||
"about.documentation": "Mahaklarazma",
|
||||
"about.license": "Brukruuru",
|
||||
"about.module.found": "Finnajena",
|
||||
@@ -10,11 +10,11 @@
|
||||
"app.pre_release": "De-Gvir",
|
||||
"app.title": "{base_title} - Mlafuhuomi '{library_dir}'",
|
||||
"color.color_border": "Bruk minusvikti varge per flanka",
|
||||
"color.confirm_delete": "Du kestetsa varge \"{color_name}\"?",
|
||||
"color.confirm_delete": "Du kestetsa varge \"{color_name}\" we?",
|
||||
"color.delete": "Keste festaretol",
|
||||
"color.import_pack": "Nasii klaani fu varge",
|
||||
"color.name": "Namae",
|
||||
"color.namespace.delete.prompt": "Du kestetsa afto vargetumam? Afto zol keste alting ine vargetumam au sebja!",
|
||||
"color.namespace.delete.prompt": "Du kestetsa afto vargetumam we? Afto zol keste ALTING ine vargetumam au sebja!",
|
||||
"color.namespace.delete.title": "Keste vargetumam",
|
||||
"color.new": "Neo varge",
|
||||
"color.placeholder": "Varge",
|
||||
@@ -36,47 +36,65 @@
|
||||
"edit.copy_fields": "Mverm shiruzmafal",
|
||||
"edit.paste_fields": "Nasii shiru",
|
||||
"edit.tag_manager": "Jewalt festaretol",
|
||||
"entries.duplicate.merge": "Visk sama shiruzmakaban",
|
||||
"entries.duplicate.merge.label": "Visk sama shiruzmakaban ima...",
|
||||
"entries.duplicate.refresh": "Gotova sama shiruzmakaban gen",
|
||||
"entries.duplicates.description": "Mverm fu shiruzmakaban implajena na plus ka ein shiruzmakaban ke tsunaga na sama mlafu na shiruzmabaksu. Na visk afto, zol festa festaretol au mlafushiruzma al mverm fu mlafu kara ine ein stuur shiruzmakaban. Hej nai sama \"mverm fu mlafu\", ke mverm fu mlafu fu du, ekso TagStudio.",
|
||||
"entries.duplicate.merge": "Visk blisnets shiruzmakaban",
|
||||
"entries.duplicate.merge.label": "Visk blisnets shiruzmakaban ima...",
|
||||
"entries.duplicate.refresh": "Gotova blisnets shiruzmakaban gen",
|
||||
"entries.duplicates.description": "Blisnets shiruzmakaban implajena na plus ka ein shiruzmakaban ke tsunaga na sama mlafu na shiruzmabaksu. Na visk afto, zol festa festaretol au mlafushiruzma al mverm fu mlafu kara ine ein stuur shiruzmakaban. Hej nai sama \"mverm fu mlafu\", ke mverm fu mlafu fu du, ekso TagStudio.",
|
||||
"entries.generic.refresh_alt": "&Gengotova",
|
||||
"entries.generic.remove.removing": "Keste shiruzmakaban ima",
|
||||
"entries.generic.remove.removing_count": "Keste {count} shiruzmakaban...",
|
||||
"entries.ignored.description": "Mlafu shiruzmakaban bli \"hestujena\" li antadan na mlafuhuomi de hesturuuru fu brukdjin (na '.ts_ignore' mlafu) kawaridan per nai eku sore. Hestujena mlafu long mlafuhuomi na snano sit kemuske mlafu na lasazma kesa kawari hesturuuru.",
|
||||
"entries.ignored.ignored_count": "Hestujena Shiruzmakaban: {count}",
|
||||
"entries.ignored.remove": "Keste hestujena shiruzmakaban",
|
||||
"entries.ignored.remove_alt": "Keste hestujena shiruzmakaban (&v)",
|
||||
"entries.ignored.scanning": "Taskame mlafuhuomi grun hestujena shiruzmakaban...",
|
||||
"entries.ignored.title": "Fiks hestujena shiruzmakaban",
|
||||
"entries.mirror": "&Maha melon fu",
|
||||
"entries.mirror.confirmation": "Du mahatsa melon fu afto {count} shiruzmakaban?",
|
||||
"entries.mirror.confirmation": "Du mahatsa melon fu afto {count} shiruzmakaban we?",
|
||||
"entries.mirror.label": "Maha melon fu {idx}/{total} shiruzmakaban ima...",
|
||||
"entries.mirror.title": "Maha melon fu shiruzmakaban ima",
|
||||
"entries.mirror.window_title": "Maha melon fu shiruzmakaban",
|
||||
"entries.remove.plural.confirm": "Du kestetsa afto {count} shiruzmakaban?",
|
||||
"entries.remove.plural.confirm": "Du kestetsa afto <b>{count}</b> shiruzmakaban we? Nil mlafu na shiruzmabaksu bli kestejena.",
|
||||
"entries.remove.singular.confirm": "Du kestetsa afto shiruzmakaban long mlafuhuomi we? Nil mlafu na shiruzmabaksu bli kestejena.",
|
||||
"entries.running.dialog.new_entries": "Nasii {total} neo shiruzmakaban fu mlafu ima...",
|
||||
"entries.running.dialog.title": "Nasii neo shiruzmakaban fu mlafu ima",
|
||||
"entries.tags": "Festaretol",
|
||||
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore tsunaganaijena.<br><br>Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.",
|
||||
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore kntsunagajena.<br><br>Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.",
|
||||
"entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {index}/{unlinked_count} shiruzmakaban, {fixed_count} tsunagajena gen",
|
||||
"entries.unlinked.relink.manual": "&Tsunaga gen mit hant",
|
||||
"entries.unlinked.relink.title": "Tsunaga shiruzmakaban gen",
|
||||
"entries.unlinked.scanning": "Suha mlafuhuomi ima per tsunaganaijena shiruzmakaban...",
|
||||
"entries.unlinked.remove": "Keste kntsunagajena shiruzmakaban",
|
||||
"entries.unlinked.remove_alt": "Keste kntsunagajena shiruzmakaban (&v)",
|
||||
"entries.unlinked.scanning": "Taskame mlafuhuomi ima grun kntsunagajena shiruzmakaban...",
|
||||
"entries.unlinked.search_and_relink": "&Suha &&Tsunaga gen",
|
||||
"entries.unlinked.title": "Fiks tsunaganaijena shiruzmakaban",
|
||||
"entries.unlinked.unlinked_count": "Tsunaganaijena shiruzmakaban: {count}",
|
||||
"entries.unlinked.title": "Fiks kntsunagajena shiruzmakaban",
|
||||
"entries.unlinked.unlinked_count": "Kntsunagajena shiruzmakaban: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg au/os FFprobe nai finnajena. TagStudio treng FFmpeg per mahase riso.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Nasii shiruzmafal",
|
||||
"field.confirm_remove": "Du kestetsa afto \"{name}\" shiruzmafal we?",
|
||||
"field.copy": "Mverm shiruzmafal",
|
||||
"field.edit": "Kawari shiruzmafal",
|
||||
"field.mixed_data": "Viskena shiruzma",
|
||||
"field.paste": "Nasii shiruzmafal",
|
||||
"file.date_added": "Dag nasiijenadan",
|
||||
"file.date_created": "Dag mahajenadan",
|
||||
"file.date_modified": "Dag kawarijenadan",
|
||||
"field.remove": "Keste shiruzmafal",
|
||||
"field_type.datetime": "Dag-tid",
|
||||
"field_type.text": "Tekst",
|
||||
"field_type.unknown": "Fal nai shirujena",
|
||||
"file.date_added": "Dag antajena",
|
||||
"file.date_created": "Dag mahajena",
|
||||
"file.date_modified": "Dag kawarijena",
|
||||
"file.dimensions": "Stuuratailasku",
|
||||
"file.duplicates.description": "TagStudio kjoka nasii shiruzma DupeGuru kara per jewalt mverm.",
|
||||
"file.duplicates.dupeguru.advice": "Za melon mahajena, du deki bruk DupeGuru per keste nai vilena mlafu. Sit, bruk tel fu TagStudio, haisa \"Fiks tsunaganaijena shiruzmakaban\" na tropos sentakutumam per keste tsunaganaijena shiruzmakaban.",
|
||||
"file.duplicates.description": "TagStudio kjoka nasii shiruzma DupeGuru kara per jewalt blisnets mlafu.",
|
||||
"file.duplicates.dupeguru.advice": "Za melon mahajena, du deki bruk DupeGuru per keste nai vilena mlafu. Sit, bruk tel fu TagStudio, haisa \"Fiks kntsunagajena shiruzmakaban\" na tropos sentakutumam per keste kntsunagajena shiruzmakaban.",
|
||||
"file.duplicates.dupeguru.file_extension": "DupeGuru mlafu (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&Gotova mlafu fu DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "Jamnai sentakujena mlafu fu DupeGuru",
|
||||
"file.duplicates.dupeguru.open_file": "Auki shiruzma mlafu fu Dupeguru",
|
||||
"file.duplicates.fix": "Fiks mverm",
|
||||
"file.duplicates.matches": "Mverm-atai: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Mverm-atai: N/A",
|
||||
"file.duplicates.mirror.description": "Maha melon fu shiruzma shiruzmakaban kara na al mverm fu samakaban, afto zol visk al shiruzma, men zolnai keste os mverm shiruzmafal. Afto zolnai keste joku mlafu os shiruzma.",
|
||||
"file.duplicates.dupeguru.open_file": "Auki sitan mlafu fu Dupeguru",
|
||||
"file.duplicates.fix": "Fiks blisnets mlafu",
|
||||
"file.duplicates.matches": "Blisnets-atai: {count}",
|
||||
"file.duplicates.matches_uninitialized": "blisntes-atai: N/A",
|
||||
"file.duplicates.mirror.description": "Maha melon fu shiruzma shiruzmakaban kara na al blisnets fu samakaban, afto zol visk al shiruzma, men zolnai keste os mverm shiruzmafal. Afto zolnai keste joku mlafu os shiruzma.",
|
||||
"file.duplicates.mirror_entries": "&Maha melon fu shiruzmakaban",
|
||||
"file.duration": "Pitkatai",
|
||||
"file.not_found": "Mlafu nai finnajena",
|
||||
@@ -110,33 +128,43 @@
|
||||
"generic.missing": "Harnai",
|
||||
"generic.navigation.back": "Suruk",
|
||||
"generic.navigation.next": "Mirai",
|
||||
"generic.no": "Nai",
|
||||
"generic.none": "Nil",
|
||||
"generic.overwrite": "Vasu",
|
||||
"generic.overwrite_alt": "&Vasu",
|
||||
"generic.paste": "Nasii",
|
||||
"generic.recent_libraries": "Moloda mlafuhuomi",
|
||||
"generic.remove": "Keste",
|
||||
"generic.remove_alt": "&Keste",
|
||||
"generic.rename": "Haisagen",
|
||||
"generic.rename_alt": "&Haisagen",
|
||||
"generic.reset": "Gensintua",
|
||||
"generic.save": "Ufne",
|
||||
"generic.skip": "Pal",
|
||||
"generic.skip_alt": "&Pal",
|
||||
"generic.yes": "Akk",
|
||||
"home.search": "Suha",
|
||||
"home.search_entries": "Suha shiruzmakaban",
|
||||
"home.search_library": "Suha mlafuhuomi",
|
||||
"home.search_tags": "Suha festaretol",
|
||||
"home.show_hidden_entries": "Mahase furijena shiruzmakaban",
|
||||
"home.thumbnail_size": "Stuuratai fu risonen",
|
||||
"home.thumbnail_size.extra_large": "Plusstuur risonen",
|
||||
"home.thumbnail_size.large": "Stuur risonen",
|
||||
"home.thumbnail_size.medium": "Mellan risonen",
|
||||
"home.thumbnail_size.mini": "Chisai risonen",
|
||||
"home.thumbnail_size.small": "Chisaidai risonen",
|
||||
"ignore.open_file": "Mahase \"{ts_ignore}\" mlafu na shiruzmabaksu",
|
||||
"json_migration.checking_for_parity": "Taskama per chigauzma ima...",
|
||||
"json_migration.creating_database_tables": "Maha tumam na SQL Database ima...",
|
||||
"json_migration.description": "<br>Hadji au de se shiruzma fu",
|
||||
"json_migration.discrepancies_found": "Chiguazma finnajena na mlafuhuomi",
|
||||
"json_migration.discrepancies_found.description": "Chiguazma finnajena mellan ranja au kjannos. Bitte gense au sentaku per beng na dlabdel os yamete.",
|
||||
"json_migration.finish_migration": "Owari dlabdel",
|
||||
"json_migration.heading.aliases": "Andrnamae:",
|
||||
"json_migration.heading.colors": "Varge:",
|
||||
"json_migration.heading.differ": "Tchigauzma",
|
||||
"json_migration.heading.extensions": "Andr ilo:",
|
||||
"json_migration.heading.match": "Finnajena sama",
|
||||
"json_migration.heading.names": "Namae:",
|
||||
"json_migration.heading.parent_tags": "Atama festaretol:",
|
||||
@@ -150,19 +178,24 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Mlafuhuomi</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Mlafuhuomi</h2>",
|
||||
"landing.open_create_library": "Auki/Maha mlafuhuomi {shortcut}",
|
||||
"library.field.add": "Nasii shiruzmafal",
|
||||
"library.field.confirm_remove": "Du kestetsa afto \"{name}\" shiruzmafal we?",
|
||||
"library.field.mixed_data": "Viskena shiruzma",
|
||||
"library.field.remove": "Keste shiruzmafal",
|
||||
"library.missing": "Mlafuplas fu mlafuhuomi nai finnajenadan",
|
||||
"library.name": "Mlafuhuomi",
|
||||
"library.refresh.scanning.plural": "Taskama mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
|
||||
"library.refresh.scanning.singular": "Taskama mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
|
||||
"library.refresh.scanning_preparing": "Taskama mlafukaban fu neo mlafu ima...\nGotova ima...",
|
||||
"library.refresh.scanning.plural": "Taskame mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
|
||||
"library.refresh.scanning.singular": "Taskame mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
|
||||
"library.refresh.scanning_preparing": "Taskame mlafukaban fu neo mlafu ima...\nGotova ima...",
|
||||
"library.refresh.title": "Gengotova al mlafukaban",
|
||||
"library.scan_library.title": "Taskama mlafuhuomi ima",
|
||||
"library.scan_library.title": "Taskame mlafuhuomi ima",
|
||||
"library_info.cleanup": "Parjat",
|
||||
"library_info.cleanup.backups": "Mverm long mlafuhuomi:",
|
||||
"library_info.cleanup.dupe_files": "Blisnets mlafu:",
|
||||
"library_info.cleanup.ignored": "Hestujena shiruzmakaban:",
|
||||
"library_info.cleanup.legacy_json": "Gammel verso fu mlafuhuomi:",
|
||||
"library_info.cleanup.unlinked": "Kntsunagajena shiruzmakaban:",
|
||||
"library_info.stats.colors": "Varge fu festaretol:",
|
||||
"library_info.stats.entries": "Shiruzmakaban:",
|
||||
"library_info.stats.fields": "Shiruzmafal:",
|
||||
"library_info.stats.macros": "Aplinen:",
|
||||
"library_info.stats.namespaces": "Tumam:",
|
||||
"library_info.stats.tags": "Festaretol:",
|
||||
"library_object.name": "Namae",
|
||||
"library_object.name_required": "Namae (Trengjena)",
|
||||
@@ -190,15 +223,16 @@
|
||||
"menu.file.save_library": "Ufne mlafuhuomi",
|
||||
"menu.help": "&Aputsa",
|
||||
"menu.help.about": "Shiruplus",
|
||||
"menu.macros": "&Zetingnen",
|
||||
"menu.macros": "&Aplinen",
|
||||
"menu.macros.folders_to_tags": "Mlafukaban festaretol made",
|
||||
"menu.select": "Sentaku",
|
||||
"menu.settings": "Sentakuzma...",
|
||||
"menu.tools": "&Tropos",
|
||||
"menu.tools.fix_duplicate_files": "Fiks sama &mlafu",
|
||||
"menu.tools.fix_duplicate_files": "Fiks &blisnets mlafu",
|
||||
"menu.tools.fix_unlinked_entries": "Fiks &tsunagajenanai shiruzmakaban",
|
||||
"menu.view": "&Anse",
|
||||
"menu.window": "Ekrannen",
|
||||
"namespace.create.description_color": "Varge fu festaretol bruk tumam na vargekaban. Al neo varge mus long tumamklaani na eins.",
|
||||
"namespace.create.title": "Maha vargetumam",
|
||||
"namespace.new.button": "Neo vargetumam",
|
||||
"namespace.new.prompt": "Maha neo vargetumam per nasii varge fu sebja!",
|
||||
@@ -222,17 +256,17 @@
|
||||
"settings.show_filenames_in_grid": "Mahase namae fu mlafu ine karta",
|
||||
"settings.show_recent_libraries": "Mahase moloda mlafuhuomi",
|
||||
"settings.theme.dark": "Kirai",
|
||||
"settings.theme.label": "Klea fu zeting:",
|
||||
"settings.theme.label": "Ekran klea:",
|
||||
"settings.theme.light": "Kirkas",
|
||||
"settings.theme.system": "Kompyu",
|
||||
"settings.title": "Sentakuzma",
|
||||
"sorting.direction.ascending": "Leste owaris",
|
||||
"sorting.direction.descending": "Leste eins",
|
||||
"sorting.direction.ascending": "Plus made",
|
||||
"sorting.direction.descending": "Minus made",
|
||||
"splash.opening_library": "Auki mlafuhuomi \"{library_path}\" ima...",
|
||||
"status.deleted_file_plural": "Kestejenadan {count} mlafu!",
|
||||
"status.deleted_file_singular": "Kestejenadan 1 mlafu!",
|
||||
"status.deleted_none": "Nil mlafu kestejenadan.",
|
||||
"status.deleted_partial_warning": "Mono kestejenadan {count} mlafu! Bitte taskamatsa li du dekinai finna mlafu os ke ima brukjena na andr zeting.",
|
||||
"status.deleted_partial_warning": "Mono kestejenadan {count} mlafu! Bitte taskamatsa li du dekinai finna mlafu os ke ima brukjena na andr apli.",
|
||||
"status.deleting_file": "Keste mlafu [{i}/{count}]: \"{path}\" ima...",
|
||||
"status.library_closing": "Kini mlafuhuomi ima...",
|
||||
"status.library_save_success": "Mlafuhuomi ufnejenadan au kinijenadan!",
|
||||
@@ -250,7 +284,7 @@
|
||||
"tag.all_tags": "Al festaretol",
|
||||
"tag.choose_color": "Sentaku varge fu festaretol",
|
||||
"tag.color": "Varge",
|
||||
"tag.confirm_delete": "Du kestetsa festaretol \"{tag_name}\"?",
|
||||
"tag.confirm_delete": "Du kestetsa festaretol \"{tag_name}\" we?",
|
||||
"tag.create": "Maha festaretol",
|
||||
"tag.create_add": "Maha && Nasii \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Bruk afto festaretol grun plusklar",
|
||||
@@ -265,15 +299,15 @@
|
||||
"tag.search_for_tag": "Suha fu festaretol",
|
||||
"tag.shorthand": "Namaenen",
|
||||
"tag.tag_name_required": "Namae fu festaretol (Trengjena)",
|
||||
"tag.view_limit": "Lesteatai per anse:",
|
||||
"home.search.view_limit": "Lesteatai per anse:",
|
||||
"tag_manager.title": "Festaretol fu mlafuhuomi",
|
||||
"trash.context.ambiguous": "Ugoki mlafu {trash_term} made",
|
||||
"trash.context.plural": "Ugoki mlafu {trash_term} made",
|
||||
"trash.context.singular": "Ugoki mlafu {trash_term} made",
|
||||
"trash.dialog.disambiguation_warning.plural": "Afto zol keste tuo TagStudio kara <i>AU</i> kompyu fu du kara!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Afto zol keste sore TagStudio kara <i>AU</i> kompyu fu du kara!",
|
||||
"trash.dialog.move.confirmation.plural": "Du ugokitsa afto {count} mlafu {trash_term} made?",
|
||||
"trash.dialog.move.confirmation.singular": "Du ugokitsa afto mlafu {trash_term} made?",
|
||||
"trash.dialog.move.confirmation.plural": "Du ugokitsa afto {count} mlafu {trash_term} made we?",
|
||||
"trash.dialog.move.confirmation.singular": "Du ugokitsa afto mlafu {trash_term} made we?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>VIKTI!</b> Li afto mlafu nai dekijena ugoki {trash_term} made, sore zol <b>kestejena!!</b>",
|
||||
"trash.dialog.title.plural": "Keste mlafu",
|
||||
"trash.dialog.title.singular": "Keste mlafu",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"entries.generic.refresh_alt": "&Обновить",
|
||||
"entries.generic.remove.removing": "Удаление записей",
|
||||
"entries.generic.remove.removing_count": "Удаление {count} записей...",
|
||||
"entries.ignored.description": "Записи файлов будут считаться \"проигнорированными\", если они были добавлены в библиотеку до обновления пользовательских правил игнора (через файл \".ts_ignore\") для исключения записей. Проигнорированные файлы остаются в библиотеке по умолчанию с целью предотвращения случайной потери данных при обновления правил.",
|
||||
"entries.ignored.ignored_count": "Проигнорированных записей: {count}",
|
||||
"entries.ignored.remove": "Удалить игнорируемые записи",
|
||||
"entries.ignored.scanning": "Поиск игнорируемых записей...",
|
||||
@@ -68,9 +69,16 @@
|
||||
"entries.unlinked.unlinked_count": "Откреплённых записей: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg и/или FFprobe не были найдены. FFmpeg необходим для воспроизведения мультимедиа и превью.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Добавить поле",
|
||||
"field.confirm_remove": "Вы уверены, что хотите удалить поле \"{name}\"?",
|
||||
"field.copy": "Копировать поле",
|
||||
"field.edit": "Редактировать поле",
|
||||
"field.mixed_data": "Смешанные данные",
|
||||
"field.paste": "Вставить поле",
|
||||
"field.remove": "Удалить поле",
|
||||
"field_type.datetime": "Дата",
|
||||
"field_type.text": "Текст",
|
||||
"field_type.unknown": "Неизвестный тип",
|
||||
"file.date_added": "Дата добавления",
|
||||
"file.date_created": "Дата создания",
|
||||
"file.date_modified": "Дата изменения",
|
||||
@@ -118,27 +126,33 @@
|
||||
"generic.missing": "Отсутствует",
|
||||
"generic.navigation.back": "Назад",
|
||||
"generic.navigation.next": "Далее",
|
||||
"generic.no": "Нет",
|
||||
"generic.none": "Ничего",
|
||||
"generic.overwrite": "Перезаписать",
|
||||
"generic.overwrite_alt": "&Перезаписать",
|
||||
"generic.paste": "Вставить",
|
||||
"generic.recent_libraries": "Недавние библиотеки",
|
||||
"generic.remove": "Убрать",
|
||||
"generic.remove_alt": "&Убрать",
|
||||
"generic.rename": "Переименовать",
|
||||
"generic.rename_alt": "&Переименовать",
|
||||
"generic.reset": "Cбросить",
|
||||
"generic.save": "Сохранить",
|
||||
"generic.skip": "Пропустить",
|
||||
"generic.skip_alt": "&Пропустить",
|
||||
"generic.yes": "Да",
|
||||
"home.search": "Поиск",
|
||||
"home.search_entries": "Поиск записей",
|
||||
"home.search_library": "Поиск по библиотеке",
|
||||
"home.search_tags": "Поиск тегов",
|
||||
"home.show_hidden_entries": "Показать скрытые записи",
|
||||
"home.thumbnail_size": "Размер иконок",
|
||||
"home.thumbnail_size.extra_large": "Очень большие иконки",
|
||||
"home.thumbnail_size.large": "Большие иконки",
|
||||
"home.thumbnail_size.medium": "Средние иконки",
|
||||
"home.thumbnail_size.mini": "Крохотные иконки",
|
||||
"home.thumbnail_size.small": "Маленькие иконки",
|
||||
"ignore.open_file": "Показать файл \"{ts_ignore}\" на диске",
|
||||
"json_migration.checking_for_parity": "Проверка целостности...",
|
||||
"json_migration.creating_database_tables": "Создание таблиц базы данных SQL...",
|
||||
"json_migration.description": "<br>Начните и просмотрите результаты процесса миграции библиотеки. Конвертированная библиотека <i>не</i> будет использоваться, если вы не нажмете \"Завершить миграцию\". Данные библиотеки должны либо иметь совпадающие значения, либо содержать метку \"Совпало\". Значения, которые не совпадают, будут отображаться красным цветом и сопровождаться символом \"<b>(!)</b>\" рядом с ними. <br><center><i>Этот процесс может занять несколько минут для больших библиотек.</i></center>",
|
||||
@@ -148,6 +162,7 @@
|
||||
"json_migration.heading.aliases": "Псевдонимы:",
|
||||
"json_migration.heading.colors": "Цвета:",
|
||||
"json_migration.heading.differ": "Несоответствие",
|
||||
"json_migration.heading.extensions": "Расширения:",
|
||||
"json_migration.heading.match": "Совпало",
|
||||
"json_migration.heading.names": "Имена:",
|
||||
"json_migration.heading.parent_tags": "Родительские теги:",
|
||||
@@ -162,10 +177,6 @@
|
||||
"json_migration.title.new_lib": "<h2>Библиотека версии 9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>Библиотека версии 9.4</h2>",
|
||||
"landing.open_create_library": "Открыть/создать библиотеку {shortcut}",
|
||||
"library.field.add": "Добавить поле",
|
||||
"library.field.confirm_remove": "Вы уверены, что хотите удалить поле \"{name}\"?",
|
||||
"library.field.mixed_data": "Смешанные данные",
|
||||
"library.field.remove": "Удалить поле",
|
||||
"library.missing": "Отсутствует путь к библиотеке",
|
||||
"library.name": "Библиотека",
|
||||
"library.refresh.scanning.plural": "Сканирование папок на наличие новых файлов...\nПросканировано {searched_count} файлов, найдено {found_count} новых",
|
||||
@@ -173,9 +184,20 @@
|
||||
"library.refresh.scanning_preparing": "Сканирование папок на наличие новых файлов...\nПодготовка...",
|
||||
"library.refresh.title": "Обновление папок",
|
||||
"library.scan_library.title": "Сканирование библиотеки",
|
||||
"library_info.cleanup.backups": "Резервные копии библиотек:",
|
||||
"library_info.cleanup.dupe_files": "Файлы-дубликаты:",
|
||||
"library_info.cleanup.ignored": "Проигнорированные записи:",
|
||||
"library_info.cleanup.legacy_json": "Оставшиеся устаревшие библиотеки:",
|
||||
"library_info.cleanup.unlinked": "Несвязанные записи:",
|
||||
"library_info.stats": "Статистика",
|
||||
"library_info.stats.colors": "Цвета тегов:",
|
||||
"library_info.stats.entries": "Записи:",
|
||||
"library_info.stats.fields": "Поля:",
|
||||
"library_info.stats.macros": "Макросы:",
|
||||
"library_info.stats.namespaces": "Пространства:",
|
||||
"library_info.stats.tags": "Теги:",
|
||||
"library_info.title": "Библиотека \"{library_dir}\"",
|
||||
"library_info.version": "Версия библиотеки: {version}",
|
||||
"library_object.name": "Название",
|
||||
"library_object.name_required": "Название (Обязательно)",
|
||||
"library_object.slug": "Уникальный ID",
|
||||
@@ -197,6 +219,7 @@
|
||||
"menu.file.missing_library.message": "Не найдено расположение библиотеки \"{library}\".",
|
||||
"menu.file.missing_library.title": "Отсутствующая библиотека",
|
||||
"menu.file.new_library": "Новая библиотека",
|
||||
"menu.file.open_backups_folder": "Открыть папку резервных копий",
|
||||
"menu.file.open_create_library": "&Открыть/создать библиотеку",
|
||||
"menu.file.open_library": "Открыть библиотеку",
|
||||
"menu.file.open_recent_library": "Открыть последнюю",
|
||||
@@ -211,18 +234,22 @@
|
||||
"menu.settings": "Настройки...",
|
||||
"menu.tools": "&Инструменты",
|
||||
"menu.tools.fix_duplicate_files": "Исправить дубликаты &файлов",
|
||||
"menu.tools.fix_ignored_entries": "Исправленные &Проигнорированные записи",
|
||||
"menu.tools.fix_unlinked_entries": "Исправить &откреплённые записи",
|
||||
"menu.view": "&Вид",
|
||||
"menu.view.decrease_thumbnail_size": "Уменьшить размер иконок",
|
||||
"menu.view.increase_thumbnail_size": "Увеличить размер иконок",
|
||||
"menu.view.library_info": "&Информация о библиотеке",
|
||||
"menu.window": "Окно",
|
||||
"namespace.create.description": "TagStudio использует пространства имён, чтобы категоризировать цвета и теги для удобства их экспорта и передачи. Пространства имён, начинающиеся с \"tagstudio\", зарезервированы TagStudio для внутреннего использования.",
|
||||
"namespace.create.description_color": "Именные пространства используются в качестве палитр для цветов тегов. Все пользовательские цвета хранятся в именных пространствах.",
|
||||
"namespace.create.title": "Создать пространство имён",
|
||||
"namespace.new.button": "Новое пространство имён",
|
||||
"namespace.new.prompt": "Создайте пространство имён, чтобы добавить пользовательские цвета!",
|
||||
"preview.ignored": "Проигнорированные",
|
||||
"preview.multiple_selection": "Выбрано <b>{count}</b> элементов",
|
||||
"preview.no_selection": "Ничего не выбрано",
|
||||
"preview.unlinked": "Несвязанные",
|
||||
"select.add_tag_to_selected": "Добавить тег к выбранному",
|
||||
"select.all": "Выбрать всё",
|
||||
"select.clear": "Отменить выбор",
|
||||
@@ -236,15 +263,24 @@
|
||||
"settings.filepath.option.full": "Показывать путь полностью",
|
||||
"settings.filepath.option.name": "Показывать только имена файлов",
|
||||
"settings.filepath.option.relative": "Показывать относительные пути",
|
||||
"settings.generate_thumbs": "Создание обложек",
|
||||
"settings.global": "Глобальные настройки",
|
||||
"settings.hourformat.label": "24-часовое время",
|
||||
"settings.infinite_scroll": "Бесконечное пролистывание",
|
||||
"settings.language": "Язык",
|
||||
"settings.library": "Настройки библиотеки",
|
||||
"settings.open_library_on_start": "Открывать библиотеку при запуске",
|
||||
"settings.page_size": "Размер страницы",
|
||||
"settings.restart_required": "Пожалуйста, перезапустите TagStudio для применения изменений.",
|
||||
"settings.scan_files_on_open": "Автоматически загружать новые файлы",
|
||||
"settings.show_filenames_in_grid": "Показывать имена файлов",
|
||||
"settings.show_recent_libraries": "Показывать недавние библиотеки",
|
||||
"settings.splash.label": "Экран приветствия",
|
||||
"settings.splash.option.classic": "Классика (9.0)",
|
||||
"settings.splash.option.default": "По умолчанию",
|
||||
"settings.splash.option.goo_gears": "Открытый код (9.4)",
|
||||
"settings.splash.option.ninety_five": "'95 (9.5)",
|
||||
"settings.splash.option.random": "Случайный",
|
||||
"settings.tag_click_action.add_to_search": "Добавить тег в поиск",
|
||||
"settings.tag_click_action.label": "Действие по нажатию на тег",
|
||||
"settings.tag_click_action.open_edit": "Редактировать тег",
|
||||
@@ -253,10 +289,12 @@
|
||||
"settings.theme.label": "Тема:",
|
||||
"settings.theme.light": "Светлая",
|
||||
"settings.theme.system": "Системная",
|
||||
"settings.thumb_cache_size.label": "Размер кэша обложек",
|
||||
"settings.title": "Настройки",
|
||||
"settings.zeropadding.label": "Ведущие нули",
|
||||
"sorting.direction.ascending": "По возрастанию",
|
||||
"sorting.direction.descending": "По убыванию",
|
||||
"sorting.mode.random": "Случайный",
|
||||
"splash.opening_library": "Открытие библиотеки \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "{count} файлов удалено!",
|
||||
"status.deleted_file_singular": "Файл удалён!",
|
||||
@@ -288,6 +326,7 @@
|
||||
"tag.disambiguation.tooltip": "Использовать этот тег для пояснения",
|
||||
"tag.edit": "Редактировать тег",
|
||||
"tag.is_category": "Является категорией",
|
||||
"tag.is_hidden": "Спрятан",
|
||||
"tag.name": "Название",
|
||||
"tag.new": "Новый тег",
|
||||
"tag.parent_tags": "Родительский тег",
|
||||
@@ -297,7 +336,7 @@
|
||||
"tag.search_for_tag": "Поиск тега",
|
||||
"tag.shorthand": "Сокращённое название",
|
||||
"tag.tag_name_required": "Название тега (Обязательно)",
|
||||
"tag.view_limit": "Лимит просмотра:",
|
||||
"home.search.view_limit": "Лимит просмотра:",
|
||||
"tag_manager.title": "Теги этой библиотеки",
|
||||
"trash.context.ambiguous": "Перемеcтить файл(ы) в {trash_term}",
|
||||
"trash.context.plural": "Перемеcтить файлы в {trash_term}",
|
||||
@@ -311,6 +350,9 @@
|
||||
"trash.dialog.title.singular": "Удалить файл",
|
||||
"trash.name.generic": "Корзина",
|
||||
"trash.name.windows": "Корзина",
|
||||
"version_modal.description": "Новая версия TagStudio доступна! Вы можете скачать последний релиз из <a href=\"{github_url}\">GitHub</a>'а.",
|
||||
"version_modal.status": "Установленная версия: {installed_version}<br>Последняя доступная версия: {latest_release_version}",
|
||||
"version_modal.title": "Доступно обновление TagStudio",
|
||||
"view.size.0": "Крохотный",
|
||||
"view.size.1": "Маленький",
|
||||
"view.size.2": "Средний",
|
||||
|
||||
@@ -71,9 +71,11 @@
|
||||
"entries.unlinked.unlinked_count": "Olänkade Poster: {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg och/eller FFprobe hittades inte. FFmpeg krävs för uppspelning av multimedia och tumnaglar.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "Lägg till fält",
|
||||
"field.copy": "Kopiera Fält",
|
||||
"field.edit": "Redigera Fält",
|
||||
"field.paste": "Klistra In Fält",
|
||||
"field.remove": "Ta bort fält",
|
||||
"file.date_added": "Datum Tillagd",
|
||||
"file.date_created": "Skapad den",
|
||||
"file.date_modified": "Senast ändrad",
|
||||
@@ -98,8 +100,6 @@
|
||||
"home.search_entries": "Sök poster",
|
||||
"home.search_tags": "Sök etikett",
|
||||
"home.thumbnail_size": "Miniatyrbildsstorlek",
|
||||
"library.field.add": "Lägg till fält",
|
||||
"library.field.remove": "Ta bort fält",
|
||||
"library.missing": "Platsen saknas",
|
||||
"library.name": "Bibliotek",
|
||||
"library.refresh.scanning_preparing": "Skannar kataloger efter nya filer...\nFörbereder...",
|
||||
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "இணைக்கப்படாத உள்ளீடுகள்: {count}",
|
||||
"ffmpeg.missing.description": "FFMPEG மற்றும்/அல்லது FFPROBE கண்டுபிடிக்கப்படவில்லை. மல்டிமீடியா பிளேபேக் மற்றும் சிறுபடங்களுக்கு FFMPEG தேவைப்படுகிறது.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status} <br> {ffprobe}: {ffprobe_status}",
|
||||
"field.add": "புலத்தைச் சேர்க்க",
|
||||
"field.confirm_remove": "இந்த \"{name}\" புலத்தை நிச்சயமாக அகற்ற விரும்புகிறீர்களா?",
|
||||
"field.copy": "நகல் புலம்",
|
||||
"field.edit": "புலம் திருத்து",
|
||||
"field.mixed_data": "கலப்பு தரவு",
|
||||
"field.paste": "புலம் ஒட்டவும்",
|
||||
"field.remove": "புலத்தை அகற்று",
|
||||
"file.date_added": "தேதி சேர்க்கப்பட்டது",
|
||||
"file.date_created": "உருவாக்கப்பட்ட தேதி",
|
||||
"file.date_modified": "மாற்றப்பட்ட தேதி",
|
||||
@@ -171,10 +175,6 @@
|
||||
"json_migration.title.new_lib": "<h2> V9.5+ நூலகம் </h2>",
|
||||
"json_migration.title.old_lib": "<h2> V9.4 நூலகம் </h2>",
|
||||
"landing.open_create_library": "நூலகத்தைத் திறக்கவும்/உருவாக்கவும் {shortcut}",
|
||||
"library.field.add": "புலத்தைச் சேர்க்க",
|
||||
"library.field.confirm_remove": "இந்த \"{name}\" புலத்தை நிச்சயமாக அகற்ற விரும்புகிறீர்களா?",
|
||||
"library.field.mixed_data": "கலப்பு தரவு",
|
||||
"library.field.remove": "புலத்தை அகற்று",
|
||||
"library.missing": "இடம் காணவில்லை",
|
||||
"library.name": "நூலகம்",
|
||||
"library.refresh.scanning.plural": "புதிய கோப்புகளுக்கான கோப்பகங்களை ச்கேன் செய்தல் ...\n {searched_count} கோப்புகள் தேடப்பட்டன, {found_count} புதிய கோப்புகள் காணப்படுகின்றன",
|
||||
@@ -334,7 +334,7 @@
|
||||
"tag.search_for_tag": "குறிச்சொல்லைத் தேடு",
|
||||
"tag.shorthand": "சுருக்கெழுத்து",
|
||||
"tag.tag_name_required": "குறிச்சொல் பெயர் (தேவை)",
|
||||
"tag.view_limit": "வரம்பைக் காண்க:",
|
||||
"home.search.view_limit": "வரம்பைக் காண்க:",
|
||||
"tag_manager.title": "நூலக குறிச்சொற்கள்",
|
||||
"trash.context.ambiguous": "கோப்புகளை நகர்த்தவும்) {trash_term}",
|
||||
"trash.context.plural": "கோப்புகளை {trash_term} பெறுநர் க்கு நகர்த்தவும்",
|
||||
|
||||
@@ -70,9 +70,16 @@
|
||||
"entries.unlinked.unlinked_count": "ijo pi ijo lon ala: {count}",
|
||||
"ffmpeg.missing.description": "mi lukin ala e ilo FFmpeg e/anu ilo FFprobe. sina wile e kepeken sin pi musi mute e sitelen lili pi musi mute la sina wile e ilo FFmpeg.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "o pana e ma",
|
||||
"field.confirm_remove": "sina wile ala wile weka e ma \"{name}\" ni?",
|
||||
"field.copy": "o kama jo e ma sama",
|
||||
"field.edit": "o ante e ma",
|
||||
"field.mixed_data": "sona nasa",
|
||||
"field.paste": "o pana e ma sama",
|
||||
"field.remove": "o weka e ma",
|
||||
"field_type.datetime": "tenpo",
|
||||
"field_type.text": "toki",
|
||||
"field_type.unknown": "nasa",
|
||||
"file.date_added": "tenpo pi kama namako",
|
||||
"file.date_created": "tenpo pi kama sin",
|
||||
"file.date_modified": "tenpo pi kama ante",
|
||||
@@ -168,10 +175,6 @@
|
||||
"json_migration.title.new_lib": "<h2>tomo pi ilo nanpa 9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>tomo pi ilo nanpa 9.4</h2>",
|
||||
"landing.open_create_library": "o open anu pali sin e tomo {shortcut}",
|
||||
"library.field.add": "o pana e ma",
|
||||
"library.field.confirm_remove": "sina wile ala wile weka e ma \"{name}\" ni?",
|
||||
"library.field.mixed_data": "sona nasa",
|
||||
"library.field.remove": "o weka e ma",
|
||||
"library.missing": "tomo li lon ala",
|
||||
"library.name": "tomo",
|
||||
"library.refresh.scanning.plural": "mi alasa e lipu sin lon tomo...\nmi alasa e lipu {searched_count}, mi lukin e lipu sin {found_count}",
|
||||
@@ -321,7 +324,7 @@
|
||||
"tag.search_for_tag": "o alasa e poki",
|
||||
"tag.shorthand": "nimi lili",
|
||||
"tag.tag_name_required": "nimi poki (wile mute)",
|
||||
"tag.view_limit": "sina ken lukin e:",
|
||||
"home.search.view_limit": "sina ken lukin e:",
|
||||
"tag_manager.title": "poki tomo",
|
||||
"trash.context.ambiguous": "o tawa e lipu tawa {trash_term}",
|
||||
"trash.context.plural": "o tawa e lipu tawa {trash_term}",
|
||||
|
||||
@@ -57,9 +57,13 @@
|
||||
"entries.unlinked.search_and_relink": "&Ara && Yeniden Eşleştir",
|
||||
"entries.unlinked.title": "Kopmuş Kayıtları Düzelt",
|
||||
"entries.unlinked.unlinked_count": "Kopmuş Kayıtlar: {count}",
|
||||
"field.add": "Ek Bilgi Ekle",
|
||||
"field.confirm_remove": "Bu \"{name}\" ek bilgisini silmek istediğinden emin misin?",
|
||||
"field.copy": "Ek Bilgiyi Kopyala",
|
||||
"field.edit": "Ek Bilgiyi Düzenle",
|
||||
"field.mixed_data": "Karışık Veri",
|
||||
"field.paste": "Ek Bilgiyi Yapıştır",
|
||||
"field.remove": "Ek Bilgiyi Kaldır",
|
||||
"file.date_added": "Eklenme Tarihi",
|
||||
"file.date_created": "Oluşturulma Tarihi",
|
||||
"file.date_modified": "Değiştirilme Tarihi",
|
||||
@@ -150,10 +154,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Kütüphane</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Kütüphane</h2>",
|
||||
"landing.open_create_library": "Kütüphane Aç/Oluştur {shortcut}",
|
||||
"library.field.add": "Ek Bilgi Ekle",
|
||||
"library.field.confirm_remove": "Bu \"{name}\" ek bilgisini silmek istediğinden emin misin?",
|
||||
"library.field.mixed_data": "Karışık Veri",
|
||||
"library.field.remove": "Ek Bilgiyi Kaldır",
|
||||
"library.missing": "Lokasyon bulunamadı",
|
||||
"library.name": "Kütüphane",
|
||||
"library.refresh.scanning.plural": "Yeni Dosyalar İçin Dizinler Taranıyor...\n{searched_count} Dosya Tarandı, {found_count} Yeni Dosya Bulundu",
|
||||
@@ -258,7 +258,7 @@
|
||||
"tag.search_for_tag": "Etiket Ara",
|
||||
"tag.shorthand": "Kısaltma",
|
||||
"tag.tag_name_required": "Etiket İsmi (Gerekli)",
|
||||
"tag.view_limit": "Görünüm Limiti:",
|
||||
"home.search.view_limit": "Görünüm Limiti:",
|
||||
"tag_manager.title": "Kütüphane Etiketleri",
|
||||
"trash.context.ambiguous": "Dosya(ları) {trash_term} klasörüne taşı",
|
||||
"trash.context.plural": "Dosyaları {trash_term} klasörüne taşı",
|
||||
|
||||
@@ -68,9 +68,13 @@
|
||||
"entries.unlinked.unlinked_count": "未链接的项目: {count}",
|
||||
"ffmpeg.missing.description": "找不到 FFmpeg 或 FFprobe。多媒体播放和缩略图生成需要 FFmpeg 支持。",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "新增字段",
|
||||
"field.confirm_remove": "您确定要移除此 \"{name}\" 字段?",
|
||||
"field.copy": "复制字段",
|
||||
"field.edit": "编辑字段",
|
||||
"field.mixed_data": "混合数据",
|
||||
"field.paste": "粘贴字段",
|
||||
"field.remove": "移除字段",
|
||||
"file.date_added": "加入日期",
|
||||
"file.date_created": "建立日期",
|
||||
"file.date_modified": "更改日期",
|
||||
@@ -166,10 +170,6 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ 仓库</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 仓库</h2>",
|
||||
"landing.open_create_library": "打开/创建仓库 {shortcut}",
|
||||
"library.field.add": "新增字段",
|
||||
"library.field.confirm_remove": "您确定要移除此 \"{name}\" 字段?",
|
||||
"library.field.mixed_data": "混合数据",
|
||||
"library.field.remove": "移除字段",
|
||||
"library.missing": "仓库路径缺失",
|
||||
"library.name": "仓库",
|
||||
"library.refresh.scanning.plural": "正在扫描文件夹中的新文件...\n已找到 {searched_count} 个文件,找到 {found_count} 个新文件",
|
||||
@@ -312,7 +312,7 @@
|
||||
"tag.search_for_tag": "搜索标签",
|
||||
"tag.shorthand": "缩写",
|
||||
"tag.tag_name_required": "标签名称(必填)",
|
||||
"tag.view_limit": "查看限制:",
|
||||
"home.search.view_limit": "查看限制:",
|
||||
"tag_manager.title": "仓库标签",
|
||||
"trash.context.ambiguous": "移动文件到 {trash_term}",
|
||||
"trash.context.plural": "移动文件到 {trash_term}",
|
||||
|
||||
@@ -71,9 +71,13 @@
|
||||
"entries.unlinked.unlinked_count": "未連接項目:{count}",
|
||||
"ffmpeg.missing.description": "未找到「FFmpeg」和/或「FFprobe」。必須安裝「FFmpeg」才能進行多媒體播放和縮圖產生。",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "新增欄位",
|
||||
"field.confirm_remove": "您確定要刪除「{name}」欄位嗎?",
|
||||
"field.copy": "複製欄位",
|
||||
"field.edit": "編輯欄位",
|
||||
"field.mixed_data": "混合資料",
|
||||
"field.paste": "貼上欄位",
|
||||
"field.remove": "刪除欄位",
|
||||
"file.date_added": "新增日期",
|
||||
"file.date_created": "建立日期",
|
||||
"file.date_modified": "修改日期",
|
||||
@@ -170,10 +174,6 @@
|
||||
"json_migration.title.new_lib": "<h2>9.5 版本以上文件庫</h2>",
|
||||
"json_migration.title.old_lib": "<h2>9.4 版本文件庫</h2>",
|
||||
"landing.open_create_library": "開啟/建立文件庫 {shortcut}",
|
||||
"library.field.add": "新增欄位",
|
||||
"library.field.confirm_remove": "您確定要刪除「{name}」欄位嗎?",
|
||||
"library.field.mixed_data": "混合資料",
|
||||
"library.field.remove": "刪除欄位",
|
||||
"library.missing": "文件庫路徑遺失",
|
||||
"library.name": "文件庫",
|
||||
"library.refresh.scanning.plural": "正在掃描目錄尋找新檔案...\n已搜尋 {searched_count} 個檔案,找到 {found_count} 個新檔案",
|
||||
@@ -332,7 +332,7 @@
|
||||
"tag.search_for_tag": "搜尋標籤",
|
||||
"tag.shorthand": "簡寫",
|
||||
"tag.tag_name_required": "標籤名稱 (必填)",
|
||||
"tag.view_limit": "檢視限制:",
|
||||
"home.search.view_limit": "檢視限制:",
|
||||
"tag_manager.title": "文件庫標籤",
|
||||
"trash.context.ambiguous": "移動檔案至「{trash_term}」",
|
||||
"trash.context.plural": "移動多個檔案移至「{trash_term}」",
|
||||
|
||||
@@ -6,30 +6,31 @@ from PySide6.QtCore import SIGNAL
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.mixed.tag_search import TagSearchPanel
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
|
||||
|
||||
|
||||
def test_update_tags(qtbot: QtBot, library: Library):
|
||||
# Given
|
||||
panel = TagSearchPanel(library)
|
||||
panel = TagSearchPanel(library, view=TagSearchPanelView(is_tag_chooser=True))
|
||||
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
# When
|
||||
panel.update_tags()
|
||||
panel.update_items()
|
||||
|
||||
|
||||
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver, library: Library):
|
||||
panel = TagSearchPanel(library)
|
||||
panel = TagSearchPanel(library, view=TagSearchPanelView(is_tag_chooser=True))
|
||||
qtbot.addWidget(panel)
|
||||
panel.driver = qt_driver
|
||||
panel.set_driver(qt_driver)
|
||||
|
||||
# Set the widget
|
||||
tags = library.tags
|
||||
panel.set_tag_widget(tags[0], 0)
|
||||
tag_widget: TagWidget = panel.scroll_layout.itemAt(0).widget() # pyright: ignore[reportAssignmentType]
|
||||
panel.set_item_widget(tags[0], 0)
|
||||
tag_widget: TagWidget = panel.get_item_widget(0, library)
|
||||
|
||||
should_replace_actions = {
|
||||
tag_widget: ["on_edit()", "on_remove()"],
|
||||
@@ -41,7 +42,7 @@ def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver
|
||||
ensure_one_receiver_per_action(should_replace_actions)
|
||||
|
||||
# Set the widget again
|
||||
panel.set_tag_widget(tags[0], 0)
|
||||
panel.set_item_widget(tags[0], 0)
|
||||
|
||||
# Ensure each action has been replaced (amount of receivers is still 1)
|
||||
ensure_one_receiver_per_action(should_replace_actions)
|
||||
|
||||
Reference in New Issue
Block a user