Compare commits

..

29 Commits

Author SHA1 Message Date
Hosted Weblate 42f4292647 Translated using Weblate (French)
Currently translated at 100.0% (387 of 387 strings)

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings
2026-07-06 17:23:06 +00:00
Hosted Weblate 2931046518 Translated using Weblate (Spanish)
Currently translated at 99.7% (387 of 388 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Application Strings [GPLv3]
2026-07-06 17:23:06 +00:00
Hosted Weblate c174a61314 Translated using Weblate (Hungarian)
Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (418 of 418 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Application Strings
Translation: TagStudio/Application Strings [GPLv3]
2026-07-06 17:23:06 +00:00
Hosted Weblate 9174310500 Translated using Weblate (Japanese)
Currently translated at 86.1% (360 of 418 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/
Translation: TagStudio/Application Strings
2026-07-06 17:23:05 +00:00
Hosted Weblate 76d72eb769 Translated using Weblate (Russian)
Currently translated at 93.7% (392 of 418 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: romandobra <romanreachless@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Application Strings
2026-07-06 17:23:05 +00:00
Hosted Weblate bd222908de Translated using Weblate (Estonian)
Currently translated at 0.7% (3 of 418 strings)

Added translation using Weblate (Estonian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/et/
Translation: TagStudio/Application Strings
2026-07-06 17:23:05 +00:00
Hosted Weblate c8b628eb0c Translated using Weblate (German)
Currently translated at 85.7% (362 of 422 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Application Strings
2026-07-06 17:23:04 +00:00
TheBobBobs 2965d2056b fix: Thumbnail Grid not resizing to fill width (#1433)
* fix: update thumb_grid when row width changes

* fix: prevent index error in thumb_grid
2026-07-05 18:26:41 -07:00
purpletennisball 45b4b57b3a fix: mouse down selects thumbnail (#1420)
* fix: mouse down selects thumbnail

* fix: selecting a single item out of a selecting

* fix: multiple file dragging
2026-07-05 16:59:31 -07:00
Travis Abendshien 0ec562929c chore: bump version to v9.6.1 2026-07-05 16:56:48 -07:00
Travis Abendshien 23ef9964d3 fix/refactor: use generic ModuleStatus class for getting status of optional modules (#1427)
* fix/refactor: use generic ModuleStatus class for getting status of optional modules

* fix: add and order macOS bin locations by PATH precedence

* ui: add title for optional modules section in the about window

* fix: revert errant license change

* fix: return which(cmd) instead of the original cmd

* refactor: implement review feedback

* feat(ui): add tooltips for module locations on about screen
2026-07-05 15:25:56 -07:00
purpletennisball 85a51f8e2b feat(ui): organize settings into panels (#1425)
* feat(ui): organize settings into new panels

* fix: typo

* fix(ui): add localization for setting panels

* fix(ui): move language to localization panel
2026-07-04 17:39:13 -07:00
Travis Abendshien 34814e8723 fix: fix "search for tag" function in tag manager (#1411)
* fix: set driver for tag manager

* refactor(ui): require titles for TagSearch widgets
2026-07-04 13:35:51 -07:00
Travis Abendshien 95e2fe7b44 fix: remove invalid child_id relationships from tag_parents (#1423)
* fix: remove invalid child_id relationships from tag_parents

* fix: use single statements in DB 102 & 202 migrations

* fix: remove unnecessary session.scalars() from migrations
2026-07-04 13:35:40 -07:00
purpletennisball 83473379db feat: on click edit tag while in tag manager (#1416) 2026-07-04 13:13:53 -07:00
Travis Abendshien 5af3cc4faf docs: update library-changes 2026-07-03 10:15:35 -07:00
Andreas Heglingegård 4a543396ae fix: use optimized SQL when selecting non-hidden entries (#1240) 2026-07-02 16:38:29 -07:00
Jann Stute 3ffa012857 fix: make tagstudio python package executable (#1414)
* fix: make tagstudio python package executable by adding stub __main__.py

* fix: add license note + set exec bit

* Update src/tagstudio/__main__.py

Co-authored-by: Xarvex <60973030+xarvex@users.noreply.github.com>

---------

Co-authored-by: Xarvex <60973030+xarvex@users.noreply.github.com>
2026-07-02 21:44:03 +02:00
Jann Stute 00b6f918f3 fix(nix): add pyright to devshell (#1415)
---------

Co-authored-by: Xarvex <dev.ellz6@xarvex.simplelogin.com>
2026-07-02 14:15:32 -05:00
Travis Abendshien 7c1c1dd6ae fix: fix issues with tag alias updating (#1412) 2026-07-02 20:16:35 +02:00
Travis Abendshien d7918fd82c tests: filter irrelevant SQLAlchemy or_() warning 2026-07-01 23:20:45 -07:00
Travis Abendshien c062f6b6bd build: relax Send2Trash version 2026-07-01 23:20:03 -07:00
Travis Abendshien 26e04e829c docs: update roadmap 2026-07-01 21:37:50 -07:00
Travis Abendshien 4d49e9c270 translations: translate language names 2026-07-01 21:37:16 -07:00
Travis Abendshien daaae17113 tests: filter irrelevant SQLAlchemy DISTINCT ON warning spam 2026-06-30 14:17:05 -07:00
Xarvex c38fbb034d chore(nix): relax more dependencies
fixes: #1403
2026-06-30 12:51:39 -05:00
Travis Abendshien 0433908ae8 refactor: remove QPushButtonWrapper 2026-06-30 02:22:48 -07:00
Travis Abendshien 9e04922e7b fix(docs): fix header levels in changelog 2026-06-29 14:36:10 -07:00
Travis Abendshien c6f2e3c7d1 docs: update changelog 2026-06-29 14:33:56 -07:00
44 changed files with 964 additions and 477 deletions
+82 -7
View File
@@ -9,6 +9,85 @@ toc_depth: 2
# :material-script-text: Changelog
## 9.6.0 <small>June 29th, 2026</small>
<p align="center">
<img width="480" alt="aurora" src="https://github.com/user-attachments/assets/6103bb88-bc66-4222-9844-ed64d27dd2e7" />
</p>
### Added
#### Customizable Fields
[Fields](fields.md) are now fully customizable!
- The built-in list of fields (now referred to as "field templates") has been reduced to a list of handy essential templates that can be fully modified or deleted
- "Text Line" and "Text Box" field types have been combined into a single "Text" type with a "Multiline" option that can be configured on templates or toggled dynamically on existing text fields
- You can now create your own field templates in with custom names, types, and other options
- Fields applied to file entries can now have their names edited
<img width="314" height="507" alt="field_template_manager" src="https://github.com/user-attachments/assets/523fac64-e0d6-4697-ba25-6ff89273d0cb" />
<img width="494" height="342" alt="text_field_editor" src="https://github.com/user-attachments/assets/918956ee-647b-43ca-9843-e8acb67ee58f" />
- refactor!: uncouple fields from hard-coded values by @CyanVoxel in #1354
- fix: drop type_key column from text_fields and datetime_fields tables by @CyanVoxel in #1370
- feat: field template manager by @TrigamDev in #1374
- feat: add field template editor, editable field names by @CyanVoxel in #1396
#### Other Additions
- feat: add ability to configure automatic loading of new files over 10000 by @socalledtheraven in #1349
- feat: dismiss update notification, change missing ffmpeg popup to banner by @CyanVoxel in #1400
- feat: allow all raster images to be used as archive thumbnails. by @Sola-ris in #1373
### Fixed
- fix: Fix update notification crash by @Sola-ris in #1278
- fix: correct ESCAPABLE_CHARS in query_lang tokenizer by @hieuit095 in #1334
- fix: empty ORLists now return false by @TheBobBobs in #1297
- fix: update thumb grid when adding tags from preview panel by @TheBobBobs in #1245
- fix: match against the correct path in the ignore registry by @TrigamDev in #1382
- fix: fix tag aliases by @CyanVoxel in #1399
- fix: fix faulty progress bars by @ludvig-sandh in #1293
- perf: Optimize searching tags with DB indexes by @TheBobBobs in #1129
### Changed
- ui: add v9.6 assets, update misc resources by @CyanVoxel in #1398
#### Internal Changes
- minor refactor: cleanup parameters of open_library and open_sqlite_library by @Computerdores in #1294
- refactor: sql migrations by @Computerdores in #1295
- fix: remove preferences table by @Computerdores in #1298
- doc: add REUSE license information by @Computerdores in #1361
- docs/chore: add .editorconfig and .prettierrc.toml configs by @CyanVoxel in #1362
- refactor(docs): uniform formatting pass by @CyanVoxel in #1363
- fix: pyright errors in ts_qt.py by @Computerdores in #1237
- fix(ci): address remaining pyright errors by @CyanVoxel in #1368
- ci: add pyright workflow by @Computerdores in #1232
- refactor(ci): remove mypy by @CyanVoxel in #1371
- chore(deps-dev): bump pytest from 8.3.5 to 9.0.3 by @dependabot[bot] in #1372
- doc(install): add common error message help text by @Computerdores in #1369
- chore(ci): bump ruff to 0.15.17 by @CyanVoxel in #1393
- fix(pyproject): allow Python 3.13 to function by @xarvex in #1047
#### Translations
- **Amharic** added by @Birhant _(11%, can manually enable in settings file)_
- **Hebrew** added by @JonathanGlixman _(2%, can manually enable in settings file)_
- **French** updated by @SodiumBismuth, @kitsumed
- **Russian** updated by @WerDei, @NikitaNik-of, @Dott-rus
- **Hungarian** updated by @smileyhead
- **Japanese** updated by wany-oh
- **Italian** updated by @EdelFlosWeiss
- **Spanish** updated by @JulArr22
- **Toki Pona** updated by Cyborus
- **Viossa** updated by @Nginearing
- **Portuguese** updated by ssantos
---
## 9.5.7 <small>May 5th, 2026</small>
This update adds several bugfixes and additions that have been sitting on the main branch for quite some time.
@@ -68,12 +147,6 @@ This update adds several bugfixes and additions that have been sitting on the ma
- **Tamil** updated by @TamilNeram
- **Toki Pona** updated by @Math-Bee, Star Athendwyl
### New Contributors
- @Ambossmann made their first contribution in #1189
- @CallMeHein made their first contribution in #1173
- @terahidro2003 made their first contribution in #1328
---
## 9.5.6 <small>October 20th, 2025</small>
@@ -422,7 +495,9 @@ A new "Library Information" window has been added and is accessible under the "V
## 9.5.0 <small>March 3rd, 2025</small>
<img width="500" src="https://github.com/user-attachments/assets/858f1494-216f-4521-aefe-d0aa4f754b9e" alt="TagStudio 9.5 Banner" />
<p align="center">
<img width="480" alt="TagStudio 9.5 Banner" src="https://github.com/user-attachments/assets/858f1494-216f-4521-aefe-d0aa4f754b9e" />
</p>
### Added
+61 -43
View File
@@ -18,9 +18,9 @@ Legacy (JSON) library save format versions were tied to the release version of t
### Versions 1.0.0 - 9.4.2
| Used From | Format | Location |
| --------- | ------ | --------------------------------------------- |
| v1.0.0 | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
| Used in Releases | Format | Location |
| ---------------- | ------ | --------------------------------------------- |
| v1.0.0 - v9.4.2 | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
The legacy database format for public TagStudio releases [v9.1](https://github.com/TagStudioDev/TagStudio/tree/Alpha-v9.1) through [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2). Variations of this format had been used privately since v1.0.0.
@@ -30,7 +30,11 @@ Replaced by the new SQLite format introduced in TagStudio [v9.5.0 Pre-Release 1]
## SQLite <small>v9.5.0+</small>
Starting with TagStudio [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1), the library save format has been moved to a [SQLite](https://sqlite.org) format. Legacy JSON libraries are migrated (with the user's consent) to the new format when opening in current versions of the program. The save format versioning is now separate from the program's versioning number.
Starting with TagStudio [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1), the library save format has been moved to the [SQLite](https://sqlite.org) format. Legacy JSON libraries are migrated (with the user's consent) to the new format when opening in current versions of the program. The save format versioning is now separate from the program's versioning number.
The database storage location of all SQLite versions to date is at:
`<Library Folder>`/.TagStudio/ts_library.sqlite
### Versioning
@@ -38,12 +42,12 @@ Versions **1-100** stored the database version in a table called `preferences` i
Versions **>101** store the database version in a table called `versions` in a row with the `key` column of `'CURRENT'` inside the corresponding `value` column. The `versions` table also stores the initial database version in which the file was created with under the `'INITIAL'` key. Databases created before this key was introduced will always have `'INITIAL'` value of `100`.
#### "versions" Table
#### `versions` Table
| key (`VARCHAR`) | value (`INTEGER`) |
| --------------- | --------------------------------------------- |
| `'INITIAL'` | <Version DB was created with, minimum `100`\> |
| `'CURRENT'` | <Current version of DB\> |
| key (`VARCHAR`) | value (`INTEGER`) |
| --------------- | ------------------------------------------ |
| `'INITIAL'` | Version DB was created with, minimum `100` |
| `'CURRENT'` | Current version of DB |
#### Major and Minor Versioning
@@ -51,19 +55,23 @@ Version **100** came along with a major/minor versioning system built into to th
For example, a database with version 204 would still be readable in an older version of TagStudio that understands version 200. A database with version 300, on the other hand, would no longer be readable in that same older version and an error message would display.
<!-- prettier-ignore -->
!!! note ""Version 0" Message"
If you see an message when opening a library along the lines of **"Found Version 0"**, this means that you're opening a library created with a newer version of TagStudio in an older version of TagStudio that does not recognize the future versioning system. To open your library, you should use a TagStudio version greater than or equal to the one the library was created or last used with.
---
### Versions 1 - 5
These versions were used while developing the new SQLite file format, outside any official or recommended release. These versions **were never supported** in any official capacity and were actively warned against using for real libraries.
These versions were used while developing the new SQLite file format, outside of any official or recommended release. These versions **were never supported** in any official capacity and were actively warned against using for real libraries.
---
### Version 6
| Used From | Format | Location |
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ------------------------------------------------------------------------------- | ------ |
| d1b006a8978a7fa1b7f1a82243e490aca8a8355e | [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite |
The first public version of the SQLite save file format.
@@ -73,9 +81,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
### Version 7
| Used From | Format | Location |
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ------------------------------------------------------------------------------- | ------ |
| 480328b83bc1c69ab52a3eb11e2935337d3460ab | [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | SQLite |
- ~~Repairs "Description" fields to use a TEXT_LINE key instead of a TEXT_BOX key.~~ _See [Version 200](#version-200)_
- Repairs tags that may have a disambiguation_id pointing towards a deleted tag.
@@ -84,9 +92,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
### Version 8
| Used From | Format | Location |
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ------------------------------------------------------------------------------- | ------ |
| 28de21ade757aa5b80e87f77a459f4a3af21ffe0 | [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | SQLite |
- Adds the `color_border` column to the `tag_colors` table. Used for instructing the [secondary color](colors.md#secondary-color) to apply to a tag's border as a new optional behavior.
- Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)".
@@ -96,9 +104,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
### Version 9
| Used From | Format | Location |
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 31833245a4d7a655dc4e66e6005a9dd78c36c04d | [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | SQLite |
- Adds the `filename` column to the `entries` table. Used for sorting entries by filename in search results.
@@ -108,9 +116,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 100
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| 74383e3c3c12f72be1481ab0b86c7360b95c2d85 | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | --------------------- | ------ |
| 74383e3c3c12f72be1481ab0b86c7360b95c2d85 | _None_ | SQLite |
- Introduces built-in minor versioning
- The version number divided by 100 (and floored) constitutes the **major** version. Major version indicate breaking changes that prevent libraries from being opened in TagStudio versions older than the ones they were created in.
@@ -119,9 +127,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 101
| Used From | Format | Location |
| ---------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| 12e074b71d8860282b44e49e0e1a41b7a2e4bae8/[v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 12e074b71d8860282b44e49e0e1a41b7a2e4bae8 | [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite |
- Deprecates the `preferences` table, set to be removed in a [future](#version-104) TagStudio version.
- Introduces the `versions` table
@@ -133,24 +141,26 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 102
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| 71d04254cf87f4200bb7ffc81656e50dfb122e4d | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 71d04254cf87f4200bb7ffc81656e50dfb122e4d | [v9.5.5](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.5) | SQLite |
- Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted.
| Used From | Format | Location |
| ---------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| 88d0b47a86821ccfadba653f30a515abce5b24b0/[v9.5.7](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.7) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
#### Version 103
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 88d0b47a86821ccfadba653f30a515abce5b24b0 | [v9.5.7](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.7) | SQLite |
- Adds the `is_hidden` column to the `tags` table (default `0`). Used for excluding entries tagged with hidden tags from library searches.
- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default.
#### Version 104
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| ad2cbbca483018d245b44348e2c4f5a0e0bb28f1 | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | --------------------- | ------ |
| ad2cbbca483018d245b44348e2c4f5a0e0bb28f1 | _None_ | SQLite |
- Removes the `preferences` table, after migrating the contained extension list to the .ts_ignore file, if necessary.
@@ -160,9 +170,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 200
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| c15e2b56eedd0a3c13391fa43571b8f8f7c7a91f | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | --------------------- | ------ |
| c15e2b56eedd0a3c13391fa43571b8f8f7c7a91f | _None_ | SQLite |
- Adds `text_field_templates` and `date_field_templates` tables.
- Drops `boolean_fields` and `value_type` tables.
@@ -177,9 +187,17 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 201
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| 38da7bb3a920a01d4d70fa065fd19c83ff6eecb1 | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 38da7bb3a920a01d4d70fa065fd19c83ff6eecb1 | [v9.6.0](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.0) | SQLite |
- Drops `type_key` columns from `text_fields` and `datetime_fields` tables.
- Enforces column positions for `text_fields` and `datetime_fields` tables.
#### Version 202
| Added in Commit | Introduced in Release | Format |
| --------------- | ----------------------------------------------------------------------- | ------ |
| | [v9.6.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.1) | SQLite |
- Applies repairs to the `tag_parents` table, removing rows that reference child tags that have been deleted.
+5 -6
View File
@@ -122,7 +122,6 @@ A detailed specification written for the TagStudio tag and/or library format. In
- [ ] Tag Search
- [ ] Pinned Tags
- [ ] New Tabbed Tag Building UI to Support New Tag Features :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.8.x]**
- [ ] Custom Thumbnail Overrides :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.7.x]**
- [ ] Media Duration Labels :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
- [ ] Word/Line Count Labels :material-chevron-up:{ .priority-low title="Low Priority" }
- [ ] Custom Tag Badges :material-chevron-up:{ .priority-low title="Low Priority" }
@@ -174,14 +173,14 @@ File or file-like [entries](entries.md) stored in the library.
- [x] Datetimes **[[v9.5.4](changelog.md#954-september-1st-2025)]**
- [ ] 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]**
- [x] Custom Field Names **[[v9.6.0](changelog.md#960-june-29th-2026)]**
- [x] Removal of Deprecated Fields **[[v9.6.0](changelog.md#960-june-29th-2026)]**
- [ ] Custom Thumbnail Overrides :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.7.x]**
- [ ] Entry Groups :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.x]**
- [ ] Non-exclusive; Entries can be in multiple groups :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to number entries within group :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to number entries within group (i.e. page numbers) :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to set sorting method for group :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to set custom thumbnail for group :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Group is treated as entry with tags and metadata :material-chevron-double-up:{ .priority-med title="Medium Priority" }
- [ ] Ability to set custom thumbnail for group :material-chevron-double-up:{ .priority-med title="Medium Priority" }
- [ ] Nested groups :material-chevron-double-up:{ .priority-med title="Medium Priority" }
### :material-tag-text: Tags
Generated
+6 -6
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1763759067,
"narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=",
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1763835633,
"narHash": "sha256-HzxeGVID5MChuCPESuC0dlQL1/scDKu+MmzoVBJxulM=",
"lastModified": 1782723713,
"narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "050e09e091117c3d7328c7b2b7b577492c43c134",
"rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
"type": "github"
},
"original": {
+3 -1
View File
@@ -1,7 +1,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
{
ffmpeg-headless,
lib,
@@ -84,8 +83,10 @@ python3Packages.buildPythonApplication {
"py7zr"
"pyside6"
"rarfile"
"rawpy"
"requests"
"semver"
"Send2Trash"
"structlog"
"typing-extensions"
];
@@ -130,6 +131,7 @@ python3Packages.buildPythonApplication {
"test_close_library" # TODO: Look into segfault.
"test_flow_layout_happy_path"
"test_get" # TODO: Look further into, might be possible to run.
"test_github_api_unavailable"
"test_json_migration"
"test_library_migrations"
"test_update_tags"
+1 -1
View File
@@ -1,7 +1,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
{
lib,
pkgs,
@@ -78,6 +77,7 @@ pkgs.mkShellNoCC {
coreutils
uv
pyright
ruff
];
buildInputs = [
+8 -4
View File
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
[project]
name = "TagStudio"
description = "A User-Focused Photo & File Management System."
version = "9.6.0"
version = "9.6.1"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.14"
@@ -30,7 +30,7 @@ dependencies = [
"PySide6==6.8.0.*",
"rarfile==4.2",
"rawpy~=0.27",
"Send2Trash~=1.8",
"Send2Trash>=1.8,<3",
"SQLAlchemy~=2.0",
"srctools~=2.6",
"structlog~=25.3",
@@ -65,9 +65,12 @@ packages = ["src/tagstudio"]
[tool.pytest.ini_options]
#addopts = "-m 'not qt'"
qt_api = "pyside6"
pythonpath = ["src"]
filterwarnings = [
"ignore:DISTINCT ON is currently supported only by the PostgreSQL dialect:sqlalchemy.exc.SADeprecationWarning",
'ignore:Invoking or_\(\) without arguments is deprecated:sqlalchemy.exc.SADeprecationWarning',
]
[tool.pyright]
ignore = [
@@ -76,7 +79,8 @@ ignore = [
"src/tagstudio/qt/previews/vendored/pydub/",
]
include = ["src/tagstudio", "tests"]
# reference for the settings here: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
# Reference for the settings here: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
# Some rules exclusive to basedpyright: https://docs.basedpyright.com/latest/benefits-over-pyright/new-diagnostic-rules/
reportAny = false
reportIgnoreCommentWithoutRule = false
reportImplicitStringConcatenation = false
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from tagstudio.main import main
if __name__ == "__main__":
main()
+2 -1
View File
@@ -1,8 +1,9 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from importlib.metadata import version
VERSION: str = "9.6.0" # Major.Minor.Patch
VERSION: str = version("tagstudio") # Major.Minor.Patch
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
COPYRIGHT_YEARS: str = "2021-2026"
COPYRIGHT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien & TagStudio Contributors"
@@ -9,7 +9,7 @@ JSON_FILENAME: str = "ts_library.json"
DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 201
DB_VERSION: int = 202
TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
+60 -46
View File
@@ -571,12 +571,16 @@ class Library:
self.__apply_db103_migration(session)
if loaded_db_version < 104:
# changes: deletes preferences
self.__apply_db104_migrations(session, library_dir)
self.__apply_db104_migration(session, library_dir)
if loaded_db_version < 200:
self.__apply_db200_migrations(session)
# changes: field tables
self.__apply_db200_migration(session)
if initial_db_version < 200 and loaded_db_version < 201:
self.__apply_db201_migrations(session)
# changes: field tables
self.__apply_db201_migration(session)
if loaded_db_version < 202:
# changes: tag_parents
self.__apply_db202_migration(session)
session.execute(
text("CREATE INDEX IF NOT EXISTS idx_tags_name_shorthand ON tags (name, shorthand)")
@@ -719,8 +723,7 @@ class Library:
def __apply_db102_migration(self, session: Session):
"""Migrate DB to DB_VERSION 102."""
with session:
all_tag_ids = session.scalars(text("SELECT DISTINCT id FROM tags")).all()
stmt = delete(TagParent).where(TagParent.parent_id.not_in(all_tag_ids))
stmt = delete(TagParent).where(TagParent.parent_id.not_in(select(Tag.id).distinct()))
session.execute(stmt)
session.commit()
logger.info("[Library][Migration] Verified TagParent table data")
@@ -754,7 +757,7 @@ class Library:
)
session.rollback()
def __apply_db104_migrations(self, session: Session, library_dir: Path):
def __apply_db104_migration(self, session: Session, library_dir: Path):
"""Migrate DB from DB_VERSION 103 to 104."""
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
self.__migrate_sql_to_ts_ignore(library_dir)
@@ -779,7 +782,7 @@ class Library:
with open(ts_ignore, "w") as f:
f.write(migrate_ext_list(extensions, is_exclude_list))
def __apply_db200_migrations(self, session: Session):
def __apply_db200_migration(self, session: Session):
"""Migrate DB to DB_VERSION 200."""
with session:
# Drop unused 'boolean_fields' and 'value_type' tables
@@ -866,7 +869,7 @@ class Library:
session.commit()
def __apply_db201_migrations(self, session: Session):
def __apply_db201_migration(self, session: Session):
"""Migrate DB to DB_VERSION 201."""
with session:
create_text_fields_table = text("""
@@ -917,6 +920,14 @@ class Library:
session.commit()
def __apply_db202_migration(self, session: Session):
"""Migrate DB to DB_VERSION 202."""
with session:
stmt = delete(TagParent).where(TagParent.child_id.not_in(select(Tag.id).distinct()))
session.execute(stmt)
session.commit()
logger.info("[Library][Migration] Verified TagParent table data")
@property
def field_templates(self) -> Sequence[BaseFieldTemplate]:
with Session(self.engine) as session:
@@ -1193,7 +1204,11 @@ class Library:
ast = search.ast
if not search.show_hidden_entries:
statement = statement.where(~Entry.tags.any(Tag.is_hidden))
hidden_tag_ids = select(Tag.id).where(Tag.is_hidden)
hidden_entry_ids = select(TagEntry.entry_id).where(
TagEntry.tag_id.in_(hidden_tag_ids)
)
statement = statement.where(Entry.id.not_in(hidden_entry_ids))
if ast:
start_time = time.time()
@@ -1719,9 +1734,11 @@ class Library:
if parent_ids is not None:
self.update_parent_tags(tag, parent_ids, session)
session.flush()
if aliases is not None:
self.update_aliases(tag, aliases)
self.update_aliases(tag, aliases, session)
session.flush()
session.commit()
session.expunge(tag)
@@ -2053,48 +2070,45 @@ class Library:
else:
self.add_color(new_color_group)
def update_aliases(self, tag: Tag, aliases: Iterable[TagAlias]) -> bool:
def update_aliases(self, tag: Tag, aliases: Iterable[TagAlias], session: Session) -> bool:
"""Update TagAliases for a given Tag."""
with Session(self.engine) as session:
# Remove aliases that are no longer on the Tag
unique_alias_names: set[str] = set()
# Remove aliases that are no longer on the Tag
try:
old_aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)).all()
unique_alias_names = set([x.name for x in old_aliases])
old_alias_ids: list[int] = [a.id for a in old_aliases]
for old_alias in old_aliases:
if old_alias.id not in [a.id for a in aliases] or not old_alias.name:
logger.warning(
"[Library] Deleting removed alias", id=old_alias.id, name=old_alias.name
)
session.delete(old_alias)
session.commit()
except IntegrityError as e:
session.rollback()
logger.error("[Library] Could not update aliases", error=e)
return False
# Update or Add aliases
for alias in aliases:
# Sanitize alias names
alias.name = alias.name.strip()
if not alias.name or alias.name in unique_alias_names:
continue
try:
old_aliases = session.scalars(
select(TagAlias).where(TagAlias.tag_id == tag.id)
).all()
old_alias_ids: list[int] = [a.id for a in old_aliases]
for old_alias in old_aliases:
if old_alias.id not in [a.id for a in aliases] or not old_alias.name:
logger.warning(
"[Library] Deleting removed alias", id=old_alias.id, name=old_alias.name
)
session.delete(old_alias)
session.commit()
if alias.id in old_alias_ids:
stmt = update(TagAlias).where(TagAlias.id == alias.id).values(name=alias.name)
session.execute(stmt)
else:
session.add(alias)
except IntegrityError as e:
session.rollback()
logger.error("[Library] Could not update aliases", error=e)
logger.error("[Library] Could not update or add alias", error=e)
return False
# Update or Add aliases
for alias in aliases:
# Sanitize alias names
alias.name = alias.name.strip()
if not alias.name:
continue
try:
if alias.id in old_alias_ids:
stmt = (
update(TagAlias).where(TagAlias.id == alias.id).values(name=alias.name)
)
session.execute(stmt)
else:
session.add(alias)
except IntegrityError as e:
session.rollback()
logger.error("[Library] Could not update or add alias", error=e)
return False
session.commit()
session.commit()
return True
+59
View File
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import contextlib
from typing import override
import structlog
from tagstudio.core.utils.module_status import ModuleStatus
from tagstudio.core.utils.silent_subprocess import (
silent_run, # pyright: ignore[reportUnknownVariableType]
)
logger = structlog.get_logger(__name__)
class _FfModuleStatus(ModuleStatus):
"""A base class that implements common logic for reading FFmpeg/FFprobe version output."""
_FFMPEG = "ffmpeg"
_FFPROBE = "ffprobe"
@classmethod
def ff_version(cls, command: str):
ff_cmd = cls._which(command)
if ff_cmd:
out = silent_run([ff_cmd, "-version"], shell=False, capture_output=True, text=True)
if out.returncode == 0:
with contextlib.suppress(Exception):
return str(out.stdout).split(" ")[2]
class FfmpegStatus(_FfModuleStatus):
"""Class for getting the location and version of FFmpeg, if it exists."""
@override
@classmethod
def which(cls):
return cls._which(cls._FFMPEG)
@override
@classmethod
def _version(cls):
return cls.ff_version(cls._FFMPEG)
class FfprobeStatus(_FfModuleStatus):
"""Class for getting the location and version of FFprobe, if it exists."""
@override
@classmethod
def which(cls):
return cls._which(cls._FFPROBE)
@override
@classmethod
def _version(cls):
return cls.ff_version(cls._FFPROBE)
+75
View File
@@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import os
import platform
from shutil import which
import structlog
logger = structlog.get_logger(__name__)
user = os.environ.get("USER", None)
# NOTE: macOS does not make its PATH variable available to processes started outside the terminal.
# The following is a list of common directories to search for binaries in.
# TODO: Use a library for XDG compliance
_MACOS_BIN_LOCATIONS: list[str] = [
"", # Equates to PATH itself
# User level
"~/.local/share/bin/", # XDG-compliant user-created bin
"~/.local/bin/", # Fallback user-created bin
"~/.local/state/nix/profile/bin/", # XDG-compliant home Nix bin
"~/.nix-profile/bin/", # Fallback home Nix bin
# System level
f"/etc/profiles/per-user/{user}/bin/", # Per-user Nix bin
"/nix/var/nix/profiles/default/bin/", # Inherited Nix bin
"/opt/homebrew/bin/", # Homebrew bin
"/usr/local/bin/", # Administrator-configured bin
"/usr/bin/", # System bin
"/bin/", # Core system bin
]
class ModuleStatus:
"""An abstract base class for module status logic including the binary location and version."""
__cached_location: str | None = None
__cached_version: str | None = None
@classmethod
def which(cls) -> str | None:
raise NotImplementedError()
@classmethod
def _cache_location(cls) -> str | None:
raise NotImplementedError()
@classmethod
def version(cls) -> str | None:
if cls.__cached_version is None:
cls.__cached_version = cls._version()
return cls.__cached_version
@classmethod
def _version(cls) -> str | None:
raise NotImplementedError()
@classmethod
def _which(cls, cmd: str) -> str | None:
"""Internal method for determining the correct location for which().
Args:
cmd (str): The process command to search for.
"""
if cls.__cached_location:
return cls.__cached_location
if platform.system() == "Darwin":
for loc in _MACOS_BIN_LOCATIONS:
full_command = which(loc + cmd)
if full_command:
cls.__cached_location = full_command
return full_command
cls.__cached_location = which(cmd)
return cls.__cached_location
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import contextlib
from typing import override
import structlog
from tagstudio.core.utils.module_status import ModuleStatus
from tagstudio.core.utils.silent_subprocess import (
silent_run, # pyright: ignore[reportUnknownVariableType]
)
logger = structlog.get_logger(__name__)
class RipgrepStatus(ModuleStatus):
"""Class for getting the location and version of ripgrep, if it exists."""
@override
@classmethod
def which(cls):
return cls._which("rg")
@override
@classmethod
def _version(cls):
ripgrep_cmd = cls.which()
if ripgrep_cmd:
out = silent_run([ripgrep_cmd, "-V"], shell=False, capture_output=True, text=True)
if out.returncode == 0:
with contextlib.suppress(Exception):
return str(out.stdout).split(" ")[1].rstrip("\n")
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
@@ -3,14 +3,14 @@
import typing
from shutil import which
from warnings import catch_warnings
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
if typing.TYPE_CHECKING:
@@ -22,7 +22,10 @@ class PreviewPanel(PreviewPanelView):
super().__init__(library, driver)
self.__add_field_modal = FieldTemplateSearchModal(self.lib, is_field_template_chooser=True)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
self.__add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], is_tag_chooser=True
)
self.__add_tag_modal.tsp.set_driver(driver)
self._thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning)
@typing.override
@@ -55,7 +58,7 @@ class PreviewPanel(PreviewPanelView):
self._containers.update_from_entry(self._selected[0])
def _toggle_ffmpeg_warning(self, enable_warning: bool = True) -> None:
if enable_warning and (not which(FFMPEG_CMD) or not which(FFPROBE_CMD)):
if enable_warning and (not FfmpegStatus.which() or not FfprobeStatus.which()):
self._ffmpeg_warning_widget.show()
return
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only
from typing import TYPE_CHECKING, override
from typing import override
from warnings import catch_warnings
import structlog
@@ -20,10 +20,6 @@ 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"
@@ -31,6 +27,7 @@ class TagSearchModal(PanelModal):
def __init__(
self,
library: Library,
title: str,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
has_save: bool = False,
@@ -42,8 +39,8 @@ class TagSearchModal(PanelModal):
view=TagSearchPanelView(is_tag_chooser),
)
super().__init__(
self.tsp,
Translations["tag.add.plural"],
widget=self.tsp,
title=title,
is_savable=has_save,
)
@@ -169,17 +166,25 @@ class TagSearchPanel(SearchPanel[Tag]):
# 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)
)
if self.is_chooser:
tag_widget.bg_button.clicked.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
else:
tag_widget.bg_button.clicked.connect(
lambda checked=False, edit_tag=item: self.on_item_edit(edit_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)
lambda checked=False, tag_id=item.id: self.search_for_tag(tag_id)
)
tag_widget.search_for_tag_action.setEnabled(True)
else:
logger.warning(
"[TagSearchPanel] No driver was set for this TagSearchPanel. Was this on purpose?"
)
tag_widget.search_for_tag_action.setEnabled(False)
@override
+3 -3
View File
@@ -6,9 +6,7 @@ from pathlib import Path
import ffmpeg
from tagstudio.qt.previews.vendored.ffmpeg import (
probe, # pyright: ignore[reportUnknownVariableType]
)
from tagstudio.qt.previews.vendored.probe import probe
def is_readable_video(filepath: Path | str):
@@ -21,6 +19,8 @@ def is_readable_video(filepath: Path | str):
"""
try:
result = probe(Path(filepath))
if not result:
return False
for stream in result["streams"]:
# DRM check
if stream.get("codec_tag_string") in [
+56 -33
View File
@@ -4,7 +4,6 @@
import math
from pathlib import Path
from shutil import which
from PIL import ImageQt
from PySide6.QtCore import QSize, Qt
@@ -28,15 +27,16 @@ from tagstudio.core.constants import (
VERSION_BRANCH,
)
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.core.utils.ripgrep_status import RipgrepStatus
from tagstudio.core.utils.str_formatting import is_version_outdated
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.controllers.clickable_label import ClickableLabel
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored import ffmpeg
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.file_opener import open_file
from tagstudio.qt.views.stylesheets.stylesheets import form_content_style
from tagstudio.qt.views.stylesheets.stylesheets import form_content_style, header
class AboutModal(QWidget):
@@ -66,6 +66,10 @@ class AboutModal(QWidget):
self.content_layout.setContentsMargins(12, 12, 12, 12)
self.content_layout.setSpacing(12)
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
amber = get_ui_color(ColorType.PRIMARY, UiColor.AMBER)
# TagStudio Logo -------------------------------------------------------
self.logo_widget = QLabel()
self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.ts_logo_text_color))
@@ -78,7 +82,7 @@ class AboutModal(QWidget):
self.logo_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Version --------------------------------------------------------------
self.version_label = QLabel(f"<h3>{AboutModal.VERSION_STR}</h3>")
self.version_label = QLabel(header(AboutModal.VERSION_STR, 3))
self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Copyright ------------------------------------------------------------
@@ -94,31 +98,10 @@ class AboutModal(QWidget):
self.desc_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# System Info ----------------------------------------------------------
ff_version = ffmpeg.version()
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
amber = get_ui_color(ColorType.PRIMARY, UiColor.AMBER)
missing = Translations["generic.missing"]
found = Translations["about.module.found"]
ffmpeg_status = f'<span style="color:{red}">{missing}</span>'
if ff_version["ffmpeg"] is not None:
ffmpeg_status = (
f'<span style="color:{green}">{found}</span> (' + ff_version["ffmpeg"] + ")"
)
ffprobe_status = f'<span style="color:{red}">{missing}</span>'
if ff_version["ffprobe"] is not None:
ffprobe_status = (
f'<span style="color:{green}">{found}</span> (' + ff_version["ffprobe"] + ")"
)
ripgrep_status = f'<span style="color:{amber}">{missing}</span>'
if which("rg") is not None:
ripgrep_status = f'<span style="color:{green}">{found}</span>'
self.system_info_widget = QWidget()
self.system_info_layout = QFormLayout(self.system_info_widget)
self.system_info_layout.setSpacing(4)
self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
# Version
@@ -149,46 +132,85 @@ class AboutModal(QWidget):
# TODO: Add row for "App Cache Path" (currently that TagStudio.ini file)
# Optional Modules -----------------------------------------------------
self.parent_optional_modules_widget = QWidget()
self.parent_optional_modules_layout = QVBoxLayout(self.parent_optional_modules_widget)
self.parent_optional_modules_layout.setContentsMargins(0, 0, 0, 0)
self.parent_optional_modules_layout.setSpacing(0)
# Subtitle
self.optional_modules_label = QLabel(header(Translations["about.modules.title"], 4))
self.optional_modules_widget = QWidget()
self.optional_modules_layout = QFormLayout(self.optional_modules_widget)
self.optional_modules_layout.setSpacing(4)
self.optional_modules_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
ffmpeg_ver = FfmpegStatus.version()
ffprobe_ver = FfprobeStatus.version()
ripgrep_ver = RipgrepStatus.version()
missing = Translations["generic.missing"]
found = Translations["about.module.found"]
ffmpeg_status = f'<span style="color:{red}">{missing}</span>'
if ffmpeg_ver is not None:
ffmpeg_status = f'<span style="color:{green}">{found}</span> (' + ffmpeg_ver + ")"
ffprobe_status = f'<span style="color:{red}">{missing}</span>'
if ffprobe_ver is not None:
ffprobe_status = f'<span style="color:{green}">{found}</span> (' + ffprobe_ver + ")"
ripgrep_status = f'<span style="color:{amber}">{missing}</span>'
if ripgrep_ver is not None:
ripgrep_status = f'<span style="color:{green}">{found}</span> (' + ripgrep_ver + ")"
# FFmpeg Status
ffmpeg_path_title = QLabel("FFmpeg")
ffmpeg_path_content = ClickableLabel(f"{ffmpeg_status}")
ffmpeg_location = which(ffmpeg._get_ffmpeg_location()) # pyright: ignore[reportPrivateUsage]
ffmpeg_location = FfmpegStatus.which()
if ffmpeg_location:
ffmpeg_path_content.clicked.connect(
lambda: open_file(ffmpeg_location, file_manager=True)
)
ffmpeg_path_content.setCursor(Qt.CursorShape.PointingHandCursor)
ffmpeg_path_content.setToolTip(ffmpeg_location)
ffmpeg_path_content.setStyleSheet(form_content_style())
self.system_info_layout.addRow(ffmpeg_path_title, ffmpeg_path_content)
self.optional_modules_layout.addRow(ffmpeg_path_title, ffmpeg_path_content)
ffmpeg_path_content.setMaximumWidth(ffmpeg_path_content.sizeHint().width())
# FFprobe Status
ffprobe_path_title = QLabel("FFprobe")
ffprobe_path_content = ClickableLabel(f"{ffprobe_status}")
ffprobe_location = which(ffmpeg._get_ffprobe_location()) # pyright: ignore[reportPrivateUsage]
ffprobe_location = FfprobeStatus.which()
if ffprobe_location:
ffprobe_path_content.clicked.connect(
lambda: open_file(ffprobe_location, file_manager=True)
)
ffprobe_path_content.setCursor(Qt.CursorShape.PointingHandCursor)
ffprobe_path_content.setToolTip(ffprobe_location)
ffprobe_path_content.setStyleSheet(form_content_style())
self.system_info_layout.addRow(ffprobe_path_title, ffprobe_path_content)
self.optional_modules_layout.addRow(ffprobe_path_title, ffprobe_path_content)
ffprobe_path_content.setMaximumWidth(ffprobe_path_content.sizeHint().width())
# ripgrep Status
# TODO: Add a central class to find ripgrep info, similar to ffmpeg
ripgrep_path_title = QLabel("ripgrep") # NOTE: Don't localize
ripgrep_path_content = ClickableLabel()
ripgrep_path_content.setText(f"{ripgrep_status}") # TODO: Pass in constructor after #1386
ripgrep_location = which("rg")
ripgrep_location = RipgrepStatus.which()
if ripgrep_location:
ripgrep_path_content.clicked.connect(
lambda: open_file(ripgrep_location, file_manager=True)
)
ripgrep_path_content.setCursor(Qt.CursorShape.PointingHandCursor)
ripgrep_path_content.setToolTip(ripgrep_location)
ripgrep_path_content.setStyleSheet(form_content_style())
ripgrep_path_content.setMaximumWidth(ripgrep_path_content.sizeHint().width())
self.system_info_layout.addRow(ripgrep_path_title, ripgrep_path_content)
self.optional_modules_layout.addRow(ripgrep_path_title, ripgrep_path_content)
self.parent_optional_modules_layout.addWidget(self.optional_modules_label)
self.parent_optional_modules_layout.addWidget(self.optional_modules_widget)
# Links ----------------------------------------------------------------
@@ -217,6 +239,7 @@ class AboutModal(QWidget):
self.content_layout.addWidget(self.version_label)
self.content_layout.addWidget(self.desc_label)
self.content_layout.addWidget(self.system_info_widget)
self.content_layout.addWidget(self.parent_optional_modules_widget)
self.content_layout.addStretch(1)
self.content_layout.addWidget(self.links_label)
self.content_layout.addWidget(self.copyright_label)
+3 -1
View File
@@ -176,7 +176,9 @@ class BuildTagPanel(PanelWidget):
if tag is not None:
exclude_ids.append(tag.id)
self.add_tag_modal = TagSearchModal(self.lib, exclude_ids)
self.add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], exclude=exclude_ids
)
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)
+25 -4
View File
@@ -300,11 +300,25 @@ class ItemThumb(FlowWidget):
# NOTE: self.item_id seems to act as a reference here and does not need to be updated inside
# QtDriver.update_thumbs() while item_thumb.delete_action does.
# If this behavior ever changes, move this method back to QtDriver.update_thumbs().
self.thumb_button.pressed.connect(
lambda: (
self.toggle_item_selection()
if (
QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier
or not self.thumb_button.selected
)
else None
)
)
self.thumb_button.clicked.connect(
lambda: self.driver.toggle_item_selection(
self.item_id,
append=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier),
bridge=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier),
lambda: (
self.toggle_item_selection()
if (
QGuiApplication.keyboardModifiers() != Qt.KeyboardModifier.ControlModifier
and self.thumb_button.selected
)
else None
)
)
self.set_mode(mode)
@@ -317,6 +331,13 @@ class ItemThumb(FlowWidget):
def is_archived(self) -> bool:
return self.badge_active[BadgeType.ARCHIVED]
def toggle_item_selection(self):
self.driver.toggle_item_selection(
self.item_id,
append=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier),
bridge=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier),
)
def set_mode(self, mode: ItemType | None) -> None:
if mode is None:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=True)
+7 -9
View File
@@ -16,6 +16,7 @@ from PySide6.QtWidgets import (
QLabel,
QMessageBox,
QProgressDialog,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
@@ -47,7 +48,6 @@ from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.custom_runnable import CustomRunnable
from tagstudio.qt.utils.function_iterator import FunctionIterator
from tagstudio.qt.views.paged_body_wrapper import PagedBodyWrapper
from tagstudio.qt.views.qbutton_wrapper import QPushButtonWrapper
from tagstudio.qt.views.stylesheets.stylesheets import header
logger = structlog.get_logger(__name__)
@@ -99,8 +99,8 @@ class JsonMigrationModal(QObject):
body_wrapper.layout().addWidget(body_label)
body_wrapper.layout().setContentsMargins(0, 36, 0, 0)
cancel_button = QPushButtonWrapper(Translations["generic.cancel"])
next_button = QPushButtonWrapper(Translations["generic.continue"])
cancel_button = QPushButton(Translations["generic.cancel"])
next_button = QPushButton(Translations["generic.continue"])
cancel_button.clicked.connect(self.migration_cancelled.emit)
self.stack.append(
@@ -289,14 +289,12 @@ class JsonMigrationModal(QObject):
self.body_wrapper_01.layout().addWidget(desc_label)
self.body_wrapper_01.layout().setSpacing(12)
back_button = QPushButtonWrapper(Translations["generic.navigation.back"])
start_button = QPushButtonWrapper(Translations["json_migration.start_and_preview"])
back_button = QPushButton(Translations["generic.navigation.back"])
start_button = QPushButton(Translations["json_migration.start_and_preview"])
start_button.setMinimumWidth(120)
start_button.clicked.connect(self.migrate)
start_button.clicked.connect(lambda: start_button.setDisabled(True))
finish_button: QPushButtonWrapper = QPushButtonWrapper(
Translations["json_migration.finish_migration"]
)
finish_button = QPushButton(Translations["json_migration.finish_migration"])
finish_button.setMinimumWidth(120)
finish_button.setDisabled(True)
finish_button.clicked.connect(self.finish_migration)
@@ -381,7 +379,7 @@ class JsonMigrationModal(QObject):
pb.setMinimum(1),
pb.setValue(1),
# Enable the finish button
cast(QPushButtonWrapper, self.stack[1].buttons[4]).setDisabled(False),
cast(QPushButton, self.stack[1].buttons[4]).setDisabled(False),
)
)
QThreadPool.globalInstance().start(r)
+14 -15
View File
@@ -5,15 +5,15 @@
"""A pagination widget created for TagStudio."""
from typing import cast, override
from warnings import catch_warnings
from PIL import Image, ImageQt
from PySide6.QtCore import QSize, Signal
from PySide6.QtGui import QIntValidator, QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSizePolicy, QWidget
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QWidget
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.views.qbutton_wrapper import QPushButtonWrapper
class Pagination(QWidget):
@@ -21,8 +21,8 @@ class Pagination(QWidget):
index = Signal(int)
def __init__(self, parent=None) -> None:
super().__init__(parent)
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent=parent)
self.rm = ResourceManager()
self.page_count: int = 0
self.current_page_index: int = 0
@@ -43,7 +43,7 @@ class Pagination(QWidget):
self.root_layout.setSpacing(3)
# [<] ----------------------------------
self.prev_button = QPushButtonWrapper()
self.prev_button = QPushButton()
prev_icon: Image.Image = self.rm.bxs_left_arrow
prev_icon = auto_theme_overlay(prev_icon, use_alpha=False)
self.prev_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(prev_icon)))
@@ -52,7 +52,7 @@ class Pagination(QWidget):
self.prev_button.setMaximumSize(self.button_size)
# --- [1] ------------------------------
self.start_button = QPushButtonWrapper()
self.start_button = QPushButton()
self.start_button.setMinimumSize(self.button_size)
self.start_button.setMaximumSize(self.button_size)
@@ -91,12 +91,12 @@ class Pagination(QWidget):
self.end_ellipses.setText(". . .")
# ----------------------------- [42] ---
self.end_button = QPushButtonWrapper()
self.end_button = QPushButton()
self.end_button.setMinimumSize(self.button_size)
self.end_button.setMaximumSize(self.button_size)
# ---------------------------------- [>]
self.next_button = QPushButtonWrapper()
self.next_button = QPushButton()
next_icon: Image.Image = self.rm.bxs_right_arrow
next_icon = auto_theme_overlay(next_icon, use_alpha=False)
self.next_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(next_icon)))
@@ -223,7 +223,7 @@ class Pagination(QWidget):
)
self._assign_click(
cast(
QPushButtonWrapper,
QPushButton,
self.start_buffer_layout.itemAt(i - start_offset).widget(),
),
i,
@@ -240,7 +240,7 @@ class Pagination(QWidget):
self.end_buffer_layout.itemAt(i - end_offset).widget().setText(str(i + 1)) # pyright: ignore[reportAttributeAccessIssue]
self._assign_click(
cast(
QPushButtonWrapper,
QPushButton,
self.end_buffer_layout.itemAt(i - end_offset).widget(),
),
i,
@@ -268,21 +268,20 @@ class Pagination(QWidget):
def _goto_page(self, index: int):
self.update_buttons(self.page_count, index)
def _assign_click(self, button: QPushButtonWrapper, index):
if button.is_connected:
def _assign_click(self, button: QPushButton, index: int):
with catch_warnings(record=True):
button.clicked.disconnect()
button.clicked.connect(lambda checked=False, i=index: self._goto_page(i))
button.is_connected = True
def _populate_buffer_buttons(self):
for _ in range(max(self.buffer_page_count * 2, 5)):
button = QPushButtonWrapper()
button = QPushButton()
button.setMinimumSize(self.button_size)
button.setMaximumSize(self.button_size)
button.setHidden(True)
self.start_buffer_layout.addWidget(button)
end_button = QPushButtonWrapper()
end_button = QPushButton()
end_button.setMinimumSize(self.button_size)
end_button.setMaximumSize(self.button_size)
end_button.setHidden(True)
+114 -86
View File
@@ -98,6 +98,9 @@ class SettingsPanel(PanelWidget):
self.root_layout.setContentsMargins(0, 6, 0, 0)
self.library_settings_container = QWidget()
self.appearance_settings_container = QWidget()
self.localization_settings_container = QWidget()
self.media_settings_container = QWidget()
# Tabs
self.tab_widget = QTabWidget()
@@ -105,6 +108,19 @@ class SettingsPanel(PanelWidget):
self.__build_global_settings()
self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"])
self.__build_appearance_settings()
self.tab_widget.addTab(
self.appearance_settings_container, Translations["settings.appearance"]
)
self.__build_localization_settings()
self.tab_widget.addTab(
self.localization_settings_container, Translations["settings.localization"]
)
self.__build_media_settings()
self.tab_widget.addTab(self.media_settings_container, Translations["settings.media"])
# self.__build_library_settings()
# self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"])
@@ -132,17 +148,6 @@ class SettingsPanel(PanelWidget):
form_layout = QFormLayout(self.global_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Language
self.language_combobox = QComboBox()
for k in LANGUAGES:
self.language_combobox.addItem(k, LANGUAGES[k])
current_lang: str = self.driver.settings.language
if current_lang not in LANGUAGES.values():
current_lang = DEFAULT_TRANSLATION
self.language_combobox.setCurrentIndex(list(LANGUAGES.values()).index(current_lang))
self.language_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.language"], self.language_combobox)
# Open Last Library on Start
self.open_last_lib_checkbox = QCheckBox()
self.open_last_lib_checkbox.setChecked(self.driver.settings.open_last_loaded_on_startup)
@@ -150,54 +155,6 @@ class SettingsPanel(PanelWidget):
Translations["settings.open_library_on_start"], self.open_last_lib_checkbox
)
# Generate Thumbnails
self.generate_thumbs = QCheckBox()
self.generate_thumbs.setChecked(self.driver.settings.generate_thumbs)
form_layout.addRow(Translations["settings.generate_thumbs"], self.generate_thumbs)
# Thumbnail Cache Size
self.thumb_cache_size_container = QWidget()
self.thumb_cache_size_layout = QHBoxLayout(self.thumb_cache_size_container)
self.thumb_cache_size_layout.setContentsMargins(0, 0, 0, 0)
self.thumb_cache_size_layout.setSpacing(6)
self.thumb_cache_size = QLineEdit()
self.thumb_cache_size.setAlignment(Qt.AlignmentFlag.AlignRight)
self.thumb_cache_size_validator = QDoubleValidator(
MIN_THUMB_CACHE_SIZE, 1_000_000_000, 2
) # High limit
self.thumb_cache_size.setValidator(self.thumb_cache_size_validator)
self.thumb_cache_size.setText(
str(max(self.driver.settings.thumb_cache_size, MIN_THUMB_CACHE_SIZE)).removesuffix(".0")
)
self.thumb_cache_size_layout.addWidget(self.thumb_cache_size)
self.thumb_cache_size_layout.setStretch(1, 2)
self.thumb_cache_size_layout.addWidget(QLabel("MiB"))
form_layout.addRow(
Translations["settings.thumb_cache_size.label"], self.thumb_cache_size_container
)
# Cached Thumbnail Resolution
self.cached_thumb_res_container = QWidget()
self.cached_thumb_res_layout = QHBoxLayout(self.cached_thumb_res_container)
self.cached_thumb_res_layout.setContentsMargins(0, 0, 0, 0)
self.cached_thumb_res_layout.setSpacing(6)
self.cached_thumb_res = QLineEdit()
self.cached_thumb_res.setAlignment(Qt.AlignmentFlag.AlignRight)
self.cached_thumb_res_validator = QIntValidator(MIN_CACHED_THUMB_RES, MAX_CACHED_THUMB_RES)
self.cached_thumb_res.setValidator(self.cached_thumb_res_validator)
self.cached_thumb_res.setText(str(self.driver.settings.cached_thumb_resolution))
self.cached_thumb_res_layout.addWidget(self.cached_thumb_res)
self.cached_thumb_res_layout.setStretch(1, 2)
self.cached_thumb_res_layout.addWidget(QLabel("px"))
form_layout.addRow(
Translations["settings.cached_thumb_resolution.label"], self.cached_thumb_res_container
)
# Autoplay
self.autoplay_checkbox = QCheckBox()
self.autoplay_checkbox.setChecked(self.driver.settings.autoplay)
form_layout.addRow(Translations["media_player.autoplay"], self.autoplay_checkbox)
# Scan for new files when a library is opened
self.scan_files_on_open_checkbox = QCheckBox()
self.scan_files_on_open_checkbox.setChecked(self.driver.settings.scan_files_on_open)
@@ -259,26 +216,83 @@ class SettingsPanel(PanelWidget):
Translations["settings.tag_click_action.label"], self.tag_click_action_combobox
)
# Dark Mode
self.theme_combobox = QComboBox()
for k in SettingsPanel.theme_map:
self.theme_combobox.addItem(SettingsPanel.theme_map[k], k)
theme = self.driver.settings.theme
if theme not in SettingsPanel.theme_map:
theme = Theme.DEFAULT
self.theme_combobox.setCurrentIndex(list(SettingsPanel.theme_map.keys()).index(theme))
self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox)
# TODO: Implement Library Settings
def __build_library_settings(self): # pyright: ignore[reportUnusedFunction]
form_layout = QFormLayout(self.library_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Splash Screen
self.splash_combobox = QComboBox()
for k in SettingsPanel.splash_map:
self.splash_combobox.addItem(SettingsPanel.splash_map[k], k)
splash = self.driver.settings.splash
if splash not in SettingsPanel.splash_map:
splash = Splash.DEFAULT
self.splash_combobox.setCurrentIndex(list(SettingsPanel.splash_map.keys()).index(splash))
form_layout.addRow(Translations["settings.splash.label"], self.splash_combobox)
todo_label = QLabel("TODO")
form_layout.addRow(todo_label)
def __build_media_settings(self):
form_layout = QFormLayout(self.media_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Autoplay
self.autoplay_checkbox = QCheckBox()
self.autoplay_checkbox.setChecked(self.driver.settings.autoplay)
form_layout.addRow(Translations["media_player.autoplay"], self.autoplay_checkbox)
# Generate Thumbnails
self.generate_thumbs = QCheckBox()
self.generate_thumbs.setChecked(self.driver.settings.generate_thumbs)
form_layout.addRow(Translations["settings.generate_thumbs"], self.generate_thumbs)
# Thumbnail Cache Size
self.thumb_cache_size_container = QWidget()
self.thumb_cache_size_layout = QHBoxLayout(self.thumb_cache_size_container)
self.thumb_cache_size_layout.setContentsMargins(0, 0, 0, 0)
self.thumb_cache_size_layout.setSpacing(6)
self.thumb_cache_size = QLineEdit()
self.thumb_cache_size.setAlignment(Qt.AlignmentFlag.AlignRight)
self.thumb_cache_size_validator = QDoubleValidator(
MIN_THUMB_CACHE_SIZE, 1_000_000_000, 2
) # High limit
self.thumb_cache_size.setValidator(self.thumb_cache_size_validator)
self.thumb_cache_size.setText(
str(max(self.driver.settings.thumb_cache_size, MIN_THUMB_CACHE_SIZE)).removesuffix(".0")
)
self.thumb_cache_size_layout.addWidget(self.thumb_cache_size)
self.thumb_cache_size_layout.setStretch(1, 2)
self.thumb_cache_size_layout.addWidget(QLabel("MiB"))
form_layout.addRow(
Translations["settings.thumb_cache_size.label"], self.thumb_cache_size_container
)
# Cached Thumbnail Resolution
self.cached_thumb_res_container = QWidget()
self.cached_thumb_res_layout = QHBoxLayout(self.cached_thumb_res_container)
self.cached_thumb_res_layout.setContentsMargins(0, 0, 0, 0)
self.cached_thumb_res_layout.setSpacing(6)
self.cached_thumb_res = QLineEdit()
self.cached_thumb_res.setAlignment(Qt.AlignmentFlag.AlignRight)
self.cached_thumb_res_validator = QIntValidator(MIN_CACHED_THUMB_RES, MAX_CACHED_THUMB_RES)
self.cached_thumb_res.setValidator(self.cached_thumb_res_validator)
self.cached_thumb_res.setText(str(self.driver.settings.cached_thumb_resolution))
self.cached_thumb_res_layout.addWidget(self.cached_thumb_res)
self.cached_thumb_res_layout.setStretch(1, 2)
self.cached_thumb_res_layout.addWidget(QLabel("px"))
form_layout.addRow(
Translations["settings.cached_thumb_resolution.label"], self.cached_thumb_res_container
)
def __build_localization_settings(self):
form_layout = QFormLayout(self.localization_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Language
self.language_combobox = QComboBox()
translated_langs = [(Translations[f"language.{lang}"], lang) for lang in LANGUAGES]
translated_langs.sort(key=lambda x: x[0])
for lang in translated_langs:
self.language_combobox.addItem(lang[0], lang[1])
current_lang: str = self.driver.settings.language
if current_lang not in LANGUAGES:
current_lang = DEFAULT_TRANSLATION
self.language_combobox.setCurrentIndex([x[1] for x in translated_langs].index(current_lang))
self.language_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.language"], self.language_combobox)
# Date Format
self.dateformat_combobox = QComboBox()
@@ -303,20 +317,34 @@ class SettingsPanel(PanelWidget):
self.zeropadding_checkbox.setChecked(self.driver.settings.zero_padding)
form_layout.addRow(Translations["settings.zeropadding.label"], self.zeropadding_checkbox)
# TODO: Implement Library Settings
def __build_library_settings(self): # pyright: ignore[reportUnusedFunction]
form_layout = QFormLayout(self.library_settings_container)
def __build_appearance_settings(self):
form_layout = QFormLayout(self.appearance_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
todo_label = QLabel("TODO")
form_layout.addRow(todo_label)
# Dark Mode
self.theme_combobox = QComboBox()
for k in SettingsPanel.theme_map:
self.theme_combobox.addItem(SettingsPanel.theme_map[k], k)
theme = self.driver.settings.theme
if theme not in SettingsPanel.theme_map:
theme = Theme.DEFAULT
self.theme_combobox.setCurrentIndex(list(SettingsPanel.theme_map.keys()).index(theme))
self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox)
def __get_language(self) -> str:
return list(LANGUAGES.values())[self.language_combobox.currentIndex()]
# Splash Screen
self.splash_combobox = QComboBox()
for k in SettingsPanel.splash_map:
self.splash_combobox.addItem(SettingsPanel.splash_map[k], k)
splash = self.driver.settings.splash
if splash not in SettingsPanel.splash_map:
splash = Splash.DEFAULT
self.splash_combobox.setCurrentIndex(list(SettingsPanel.splash_map.keys()).index(splash))
form_layout.addRow(Translations["settings.splash.label"], self.splash_combobox)
def get_settings(self) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
return {
"language": self.__get_language(),
"language": self.language_combobox.currentData(),
"open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(),
"generate_thumbs": self.generate_thumbs.isChecked(),
"thumb_cache_size": max(
@@ -1,102 +0,0 @@
# SPDX-FileCopyrightText: (c) 2022 Karl Kroening (kkroening)
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
import contextlib
import json
import os
import platform
import subprocess
from shutil import which
import ffmpeg
import structlog
from tagstudio.core.utils.silent_subprocess import silent_popen, silent_run
logger = structlog.get_logger(__name__)
user = os.environ.get("USER", None)
FFMPEG_MACOS_LOCATIONS: list[str] = [
"",
"/opt/homebrew/bin/",
"/usr/local/bin/",
f"/etc/profiles/per-user/{user}/bin",
]
# TODO: Make this more intuitive to use in other classes
def _get_ffprobe_location() -> str:
cmd: str = "ffprobe"
if platform.system() == "Darwin":
for loc in FFMPEG_MACOS_LOCATIONS:
if which(loc + cmd):
cmd = loc + cmd
break
logger.info(
f"[FFmpeg] Using FFprobe location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
)
return cmd
# TODO: Make this more intuitive to use in other classes
def _get_ffmpeg_location() -> str:
cmd: str = "ffmpeg"
if platform.system() == "Darwin":
for loc in FFMPEG_MACOS_LOCATIONS:
if which(loc + cmd):
cmd = loc + cmd
break
logger.info(
f"[FFmpeg] Using FFmpeg location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
)
return cmd
FFPROBE_CMD = _get_ffprobe_location()
FFMPEG_CMD = _get_ffmpeg_location()
def probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
"""Run ffprobe on the specified file and return a JSON representation of the output.
Raises:
:class:`ffmpeg.Error`: if ffprobe returns a non-zero exit code,
an :class:`Error` is returned with a generic error message.
The stderr output can be retrieved by accessing the
``stderr`` property of the exception.
"""
args = [cmd, "-show_format", "-show_streams", "-of", "json"]
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs) # pyright: ignore[reportAttributeAccessIssue]
args += [filename]
# PATCHED
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
communicate_kwargs = {}
if timeout is not None:
communicate_kwargs["timeout"] = timeout
out, err = p.communicate(**communicate_kwargs)
if p.returncode != 0:
raise ffmpeg.Error("ffprobe", out, err)
return json.loads(out.decode("utf-8"))
def version():
"""Checks the version of FFmpeg and FFprobe and returns None if they dont exist."""
version: dict[str, str | None] = {"ffmpeg": None, "ffprobe": None}
if which(FFMPEG_CMD):
ret = silent_run([FFMPEG_CMD, "-version"], shell=False, capture_output=True, text=True)
if ret.returncode == 0:
with contextlib.suppress(Exception):
version["ffmpeg"] = str(ret.stdout).split(" ")[2]
if which(FFPROBE_CMD):
ret = silent_run([FFPROBE_CMD, "-version"], shell=False, capture_output=True, text=True)
if ret.returncode == 0:
with contextlib.suppress(Exception):
version["ffprobe"] = str(ret.stdout).split(" ")[2]
return version
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: (c) 2022 Karl Kroening (kkroening)
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
import json
import subprocess
from pathlib import Path
import ffmpeg
import structlog
from tagstudio.core.utils.ffmpeg_status import FfprobeStatus
from tagstudio.core.utils.silent_subprocess import (
silent_popen, # pyright: ignore[reportUnknownVariableType]
)
logger = structlog.get_logger(__name__)
def probe(filename: Path | str, timeout: int | None = None, **kwargs: ...):
"""Run ffprobe on the specified file and return a JSON representation of the output.
Raises:
Error: If ffprobe returns a non-zero exit code, an Error is raised
with a generic error message. The stderr output can be retrieved
by accessing the `stderr` property of the exception.
"""
ffprobe_cmd: str | None = FfprobeStatus.which()
if not ffprobe_cmd:
return
args: list[str] = [ffprobe_cmd, "-show_format", "-show_streams", "-of", "json"]
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs) # pyright: ignore
args += [filename] # pyright: ignore
# PATCHED
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
communicate_kwargs = {}
if timeout is not None:
communicate_kwargs["timeout"] = timeout
out, err = p.communicate(**communicate_kwargs)
if p.returncode != 0:
raise ffmpeg.Error("ffprobe", out, err)
return json.loads(out.decode("utf-8"))
@@ -18,7 +18,7 @@ from tempfile import NamedTemporaryFile
from pydub.logging_utils import log_conversion, log_subprocess_output
from pydub.utils import fsdecode
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus
try:
from itertools import izip
@@ -161,7 +161,7 @@ class _AudioSegment:
slice = a[5000:10000] # get a slice from 5 to 10 seconds of an mp3
"""
converter = FFMPEG_CMD
converter = FfmpegStatus.which()
# TODO: remove in 1.0 release
# maintain backwards compatibility for ffmpeg attr (now called converter)
@@ -727,7 +727,7 @@ class _AudioSegment:
stdin_parameter = None
stdin_data = None
else:
if cls.converter == FFMPEG_CMD:
if cls.converter == FfmpegStatus.which():
conversion_command += [
"-read_ahead_limit",
str(read_ahead_limit),
@@ -14,13 +14,13 @@ from pydub.utils import (
get_extra_info,
)
from tagstudio.core.utils.ffmpeg_status import FfprobeStatus
from tagstudio.core.utils.silent_subprocess import silent_popen
from tagstudio.qt.previews.vendored.ffmpeg import FFPROBE_CMD
def _mediainfo_json(filepath, read_ahead_limit=-1):
"""Return json dictionary with media info(codec, duration, size, bitrate...) from filepath."""
prober = FFPROBE_CMD
prober = FfprobeStatus.which()
command_args = [
"-v",
"info",
+6 -5
View File
@@ -48,8 +48,8 @@ class ThumbGridLayout(QLayout):
self._renderer.updated.connect(self._on_rendered)
self._render_cutoff: float = 0.0
# _entry_ids[StartIndex:EndIndex]
self._last_page_update: tuple[int, int] | None = None
# _entry_ids[StartIndex:EndIndex], per_row
self._last_page_update: tuple[int, int, int] | None = None
self._scroll_to: int | None = None
@@ -215,7 +215,8 @@ class ThumbGridLayout(QLayout):
start = offset * per_row
end = start + (visible_rows * per_row)
self.visible_changed.emit(self._entry_ids[start])
first_visible = self._entry_ids[start] if 0 <= start < len(self._entry_ids) else None
self.visible_changed.emit(first_visible)
# Load closest off screen rows
start -= per_row * 3
@@ -223,9 +224,9 @@ class ThumbGridLayout(QLayout):
start = max(0, start)
end = min(len(self._entry_ids), end)
if (start, end) == self._last_page_update:
if (start, end, per_row) == self._last_page_update:
return
self._last_page_update = (start, end)
self._last_page_update = (start, end, per_row)
# Clear render queue if len > 2 pages
if len(self.driver.thumb_job_queue.queue) > (per_row * visible_rows * 2):
+32 -32
View File
@@ -16,38 +16,38 @@ logger = structlog.get_logger(__name__)
DEFAULT_TRANSLATION = "en"
LANGUAGES = {
# "Amharic": "am", # Minimal
"Cebuano": "ceb",
"Chinese (Simplified)": "zh_Hans",
"Chinese (Traditional)": "zh_Hant",
"Czech": "cs",
# "Danish": "da", # Minimal
"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",
"Polish": "pl",
"Portuguese (Brazil)": "pt_BR",
"Portuguese (Portugal)": "pt",
"Romanian": "ro",
"Russian": "ru",
"Spanish": "es",
"Swedish": "sv",
"Tamil": "ta",
# "Thai": "th", # Minimal
"Toki Pona": "tok",
"Turkish": "tr",
"Viossa": "qpv",
}
LANGUAGES = [
# "am", # Minimal
"ceb",
"cs",
# "da", # Minimal
"de",
"el",
"en",
"es",
"fi",
"fil",
"fr",
"hu",
# "is", # Minimal
"it",
"ja",
"nb_NO",
"nl",
"pl",
"pt_BR",
"pt",
"qpv",
"ro",
"ru",
"sv",
"ta",
# "th", # Minimal
"tok",
"tr",
"zh_Hans",
"zh_Hant",
]
# A map of field class names to their respective translation keys.
FIELD_TYPE_KEYS = {
+33 -13
View File
@@ -39,7 +39,6 @@ from PySide6.QtGui import (
)
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QPushButton, QScrollArea
# This import has side-effect of importing PySide resources
import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport]
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH
from tagstudio.core.driver import DriverMixin
@@ -52,6 +51,11 @@ from tagstudio.core.library.refresh import RefreshTracker
from tagstudio.core.media_types import MediaCategories
from tagstudio.core.query_lang.util import ParsingError
from tagstudio.core.ts_core import TagStudioCore
# This import has side-effect of importing PySide resources
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.core.utils.module_status import ModuleStatus
from tagstudio.core.utils.ripgrep_status import RipgrepStatus
from tagstudio.core.utils.str_formatting import is_version_outdated
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
@@ -59,7 +63,7 @@ from tagstudio.qt.controllers.field_template_search_panel_controller import Fiel
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.tag_search_panel_controller import TagSearchModal, TagSearchPanel
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.controllers.update_available_message_box import UpdateAvailableMessageBox
from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
from tagstudio.qt.mixed.about_modal import AboutModal
@@ -85,7 +89,6 @@ 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.stylesheets.stylesheets import header
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
BADGE_TAGS = {
BadgeType.FAVORITE: TAG_FAVORITE,
@@ -321,6 +324,9 @@ class QtDriver(DriverMixin, QObject):
timer.start(500)
timer.timeout.connect(lambda: None)
# Detect optional modules and versions for logging
self.log_optional_modules()
# self.main_window = loader.load(home_path)
self.main_window = MainWindow(self)
self.main_window.setWindowTitle(self.base_title)
@@ -352,15 +358,13 @@ class QtDriver(DriverMixin, QObject):
self.app.setDesktopFileName("tagstudio")
# Initialize the Tag Manager panel
self.tag_manager_panel = PanelModal(
widget=TagSearchPanel(
self.lib,
is_tag_chooser=False,
view=TagSearchPanelView(is_tag_chooser=False),
),
self.tag_manager_panel = TagSearchModal(
self.lib,
title=Translations["tag_manager.title"],
is_savable=False,
is_tag_chooser=False,
)
self.tag_manager_panel.tsp.set_driver(self)
self.tag_manager_panel.done.connect(
lambda checked=False: self.main_window.preview_panel.set_selection(
self.selected, update_preview=False
@@ -386,8 +390,10 @@ class QtDriver(DriverMixin, QObject):
)
)
# Initialize the Tag Search panel
self.add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
# Initialize the "Add Tag" panel
self.add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], is_tag_chooser=True
)
self.add_tag_modal.tsp.set_driver(self)
self.add_tag_modal.tsp.item_chosen.connect(
lambda chosen_tag: (
@@ -859,7 +865,7 @@ class QtDriver(DriverMixin, QObject):
self.modal = PanelModal(
panel,
Translations["tag.new"],
Translations["tag.add"],
Translations["tag.create"],
is_savable=True,
)
@@ -1756,3 +1762,17 @@ class QtDriver(DriverMixin, QObject):
def clear_selected(self):
self._selected.clear()
self.main_window.thumb_layout.update_selected()
def log_optional_modules(self) -> None:
"""Logs the status of optional modules."""
status_classes: list[tuple[str, type[ModuleStatus]]] = [
("FFmpeg", FfmpegStatus),
("FFprobe", FfprobeStatus),
("ripgrep", RipgrepStatus),
]
for name, sc in status_classes:
if sc.which():
logger.info(f"[QtDriver] {name} found", which=sc.which(), version=sc.version())
else:
logger.warning(f"[QtDriver] {sc} not found")
-21
View File
@@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from PySide6.QtWidgets import QPushButton
from typing_extensions import deprecated
@deprecated("Use QPushButton with 'catch warnings' to silence disconnect warnings instead.")
class QPushButtonWrapper(QPushButton):
"""Custom QPushButton wrapper.
This is a customized implementation of the PySide6 QPushButton that allows to suppress
the warning that is triggered by disconnecting a signal that is not currently connected.
"""
is_connected: bool
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_connected = False
@@ -205,14 +205,16 @@ def container_style() -> str:
def form_content_style() -> str:
return f"""
background-color: {
QLabel{{
background-color: {
Theme.COLOR_BG.value
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
else Theme.COLOR_BG_LIGHT.value
};
border-radius: 3px;
font-weight: 500;
padding: 1px;
border-radius: 3px;
font-weight: 500;
padding: 1px;
}}
"""
+2 -4
View File
@@ -16,12 +16,10 @@ from PySide6.QtGui import (
QPalette,
QPen,
)
from PySide6.QtWidgets import QWidget
from tagstudio.qt.views.qbutton_wrapper import QPushButtonWrapper
from PySide6.QtWidgets import QPushButton, QWidget
class ThumbButton(QPushButtonWrapper):
class ThumbButton(QPushButton):
def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None:
super().__init__(parent)
self.thumb_size: tuple[int, int] = thumb_size
@@ -3,6 +3,7 @@
"about.description": "TagStudio ist eine Anwendung zum organisieren von Fotos und Dateien mit einem zugrunde liegendem Tag-basierten System, welches sich darauf konzentriert, dem Nutzer Freiraum und Flexibilität zu bieten. Keine proprietären Programme oder Formate, kein Meer an Hilfsdateien und keine komplette Umwälzung deiner Dateisystemstruktur.",
"about.documentation": "Dokumentation",
"about.module.found": "Gefunden",
"about.modules.title": "Optionale Module",
"about.title": "Über TagStudio",
"about.website": "Webseite",
"app.git": "Git Commit",
@@ -4,6 +4,7 @@
"about.description": "TagStudio is a photo and file organization application with an underlying tag-based system that focuses on giving freedom and flexibility to the user. No proprietary programs or formats, no sea of sidecar files, and no complete upheaval of your filesystem structure.",
"about.documentation": "Documentation",
"about.module.found": "Found",
"about.modules.title": "Optional Modules",
"about.title": "About TagStudio",
"about.version": "Version",
"about.version.latest": "{built_version} (Latest Release: {latest_version})",
@@ -196,6 +197,36 @@
"json_migration.title.new_lib": "<h2>v9.5+ Library</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Library</h2>",
"landing.open_create_library": "Open/Create Library {shortcut}",
"language.am": "Amharic",
"language.ceb": "Cebuano",
"language.cs": "Czech",
"language.da": "Danish",
"language.de": "German",
"language.el": "Greek",
"language.en": "English",
"language.es": "Spanish",
"language.fi": "Finnish",
"language.fil": "Filipino",
"language.fr": "French",
"language.hu": "Hungarian",
"language.is": "Icelandic",
"language.it": "Italian",
"language.ja": "Japanese",
"language.nb_NO": "Norwegian Bokmål",
"language.nl": "Dutch",
"language.pl": "Polish",
"language.pt": "Portuguese",
"language.pt_BR": "Portuguese (Brazil)",
"language.qpv": "Viossa",
"language.ro": "Romanian",
"language.ru": "Russian",
"language.sv": "Swedish",
"language.ta": "Tamil",
"language.th": "Thai",
"language.tok": "Toki Pona",
"language.tr": "Turkish",
"language.zh_Hans": "Chinese (Simplified)",
"language.zh_Hant": "Chinese (Traditional)",
"library_info.cleanup": "Cleanup",
"library_info.cleanup.backups": "Library Backups:",
"library_info.cleanup.dupe_files": "Duplicate Files:",
@@ -277,6 +308,7 @@
"select.all": "Select All",
"select.clear": "Clear Selection",
"select.inverse": "Invert Selection",
"settings.appearance": "Appearance",
"settings.cached_thumb_resolution.label": "Cached Thumbnail Resolution",
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
"settings.dateformat.english": "English",
@@ -293,6 +325,8 @@
"settings.infinite_scroll": "Infinite Scrolling",
"settings.language": "Language",
"settings.library": "Library Settings",
"settings.localization": "Localization",
"settings.media": "Media",
"settings.open_library_on_start": "Open Library on Start",
"settings.page_size": "Page Size",
"settings.restart_required": "Please restart TagStudio for changes to take effect.",
@@ -1,9 +1,12 @@
{
"about.app_cache_path": "Ruta caché aplicación",
"about.config_path": "Ruta de Configuración",
"about.description": "TagStudio es una aplicación para organizar fotografías y archivos que utiliza un sistema de etiquetas subyacentes centrado en dar libertad y flexibilidad al usuario. Sin programas ni formatos propios, ni un mar de archivos y sin trastornar completamente la estructura de tu sistema de archivos.",
"about.documentation": "Documentación",
"about.module.found": "Encontrado",
"about.title": "Acerca de TagStudio",
"about.version": "Versión",
"about.version.latest": "{built_version} (Última versión: {latest_version})",
"about.website": "Página web",
"app.git": "Commit de Git",
"app.pre_release": "Pre-Lanzamiento",
@@ -70,12 +73,25 @@
"entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
"field.add": "Añadir campo",
"field.add.plural": "Añadir Campos",
"field.confirm_remove": "¿Está seguro de que desea eliminar el campo \"{name}\"?",
"field.copy": "Copiar Campo",
"field.edit": "Editar Campo",
"field.field_name_required": "Nombre del Campo (Obligatorio)",
"field.mixed_data": "Datos variados",
"field.name": "Nombre",
"field.paste": "Pegar Campo",
"field.remove": "Eliminar campo",
"field.text.is_multiline": "Multilínea",
"field.type": "Tipo",
"field_template.all_field_templates": "Todas las plantillas de campos",
"field_template.confirm_delete": "¿Seguro que quieres eliminar la plantilla de Campos \"{field_template_name}\"?",
"field_template.create": "Crear Plantilla de Campos",
"field_template.create_add": "Crear && Añadir \"{query}\"",
"field_template.delete": "Eliminar Plantilla de Campos",
"field_template.edit": "Editar plantilla de campos",
"field_template.new": "Nueva plantilla de campos",
"field_template_manager.title": "Plantillas de campos de biblioteca",
"field_type.datetime": "Fecha y Hora",
"field_type.text": "Texto",
"field_type.unknown": "Tipo Desconocido",
@@ -120,6 +136,7 @@
"generic.delete_alt": "&Eliminar",
"generic.done": "Terminado",
"generic.done_alt": "&Hecho",
"generic.dont_remind": "No me lo vuelvas a recordar",
"generic.edit": "Editar",
"generic.edit_alt": "&Editar",
"generic.filename": "Nombre de archivo",
@@ -144,6 +161,7 @@
"home.search": "Buscar",
"home.search.view_limit": "Límite visualización:",
"home.search_entries": "Buscar entradas",
"home.search_field_templates": "Buscar plantillas de Campos",
"home.search_library": "Buscar el biblioteca",
"home.search_tags": "Buscar etiquetas",
"home.show_hidden_entries": "Mostrar entradas ocultas",
@@ -213,6 +231,7 @@
"menu.delete_selected_files_singular": "Mover archivo a la {trash_term}",
"menu.edit": "Editar",
"menu.edit.ignore_files": "Ignorar archivos y carpetas",
"menu.edit.manage_field_templates": "Gestionar plantillas de Campos",
"menu.edit.manage_tags": "Gestionar etiquetas",
"menu.edit.new_tag": "Nueva &Etiqueta",
"menu.file": "&Archivo",
@@ -249,6 +268,8 @@
"namespace.new.button": "Nuevo espacio de nombre",
"namespace.new.prompt": "¡Crea un nuevo espacio de nombre para empezar a añadir colores personalizados!",
"preview.ignored": "Ignorado",
"preview.missing_module.jxl": "{module} es necesario para la previsualización de JPEG XL",
"preview.missing_module.multimedia": "{module} es necesario para la reproducción multimedia",
"preview.multiple_selection": "<b>{count}</b> Elementos seleccionados",
"preview.no_selection": "No hay elementos seleccionados",
"preview.unlinked": "Desvinculado",
@@ -256,6 +277,7 @@
"select.all": "Seleccionar todo",
"select.clear": "Borrar selección",
"select.inverse": "Invertir selección",
"settings.cached_thumb_resolution.label": "Resolución Miniaturas de vídeo en caché",
"settings.clear_thumb_cache.title": "Borrar cache de las miniaturas",
"settings.dateformat.english": "Inglés",
"settings.dateformat.international": "Internacional",
@@ -278,6 +300,7 @@
"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",
"settings.splash.option.aurora": "Aurora (9.6)",
"settings.splash.option.classic": "Clásico (9.0)",
"settings.splash.option.default": "Por Defecto",
"settings.splash.option.goo_gears": "Código abierto (9.4)",
@@ -334,6 +357,7 @@
"tag.parent_tags": "Etiquetas principales",
"tag.parent_tags.add": "Añadir etiquetas principales",
"tag.parent_tags.description": "Esta etiqueta se puede tratar como sustituto de cualquiera de las etiquetas padre en las búsquedas.",
"tag.properties": "Propiedades",
"tag.remove": "Eliminar etiqueta",
"tag.search_for_tag": "Buscar por etiqueta",
"tag.shorthand": "Abreviatura",
@@ -351,6 +375,7 @@
"trash.dialog.title.singular": "Eliminar archivo",
"trash.name.generic": "Basura",
"trash.name.windows": "Papelera de reciclaje",
"update.view_update": "Ver Actualización",
"version_modal.description": "¡Ya está disponible una nueva versión de TagStudio! Puedes descargar la última versión desde <a href=\"{github_url}\">Github</a>.",
"version_modal.status": "Versión Instalada: {installed_version}<br>Última Versión Publicada: {latest_release_version}",
"version_modal.title": "Actualización de TagStudio disponible",
@@ -0,0 +1,5 @@
{
"about.version": "Versioon",
"about.version.latest": "{built_version} (Viimane versioon: {latest_version})",
"about.website": "Veebisait"
}
@@ -1,9 +1,12 @@
{
"about.app_cache_path": "Chemin d'accès du cache de l'application",
"about.config_path": "Chemin de Configuration",
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui met en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
"about.documentation": "Documentation",
"about.module.found": "Trouvé",
"about.title": "À propos de TagStudio",
"about.version": "Version",
"about.version.latest": "{built_version} (Dernière version : {latest_version})",
"about.website": "Site Internet",
"app.git": "Git Commit",
"app.pre_release": "Version Préliminaire",
@@ -70,12 +73,25 @@
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
"field.add": "Ajouter un Champ",
"field.add.plural": "Ajouter des champs",
"field.confirm_remove": "Êtes-vous sûr de vouloir supprimer le champ \"{name}\"?",
"field.copy": "Copier le Champ",
"field.edit": "Modifier le Champ",
"field.field_name_required": "Nom du champ (obligatoire)",
"field.mixed_data": "Données Mélangées",
"field.name": "Nom",
"field.paste": "Coller le Champ",
"field.remove": "Supprimer un Champ",
"field.text.is_multiline": "Multiligne",
"field.type": "Type",
"field_template.all_field_templates": "Tous les formats de champs",
"field_template.confirm_delete": "Êtes-vous sûr de vouloir supprimer le format de champ « {field_template_name} » ?",
"field_template.create": "Créer un format de champ",
"field_template.create_add": "Créer && Ajouter « {query} »",
"field_template.delete": "Supprimer un format de champ",
"field_template.edit": "Modifier le format de champ",
"field_template.new": "Nouveau format de champ",
"field_template_manager.title": "Format de champs de bibliothèque",
"field_type.datetime": "Date et temps",
"field_type.text": "Texte",
"field_type.unknown": "Type inconnu",
@@ -120,6 +136,7 @@
"generic.delete_alt": "&Supprimer",
"generic.done": "Terminé",
"generic.done_alt": "&Terminé",
"generic.dont_remind": "Ne pas me le rappelez",
"generic.edit": "Modifier",
"generic.edit_alt": "&Modifier",
"generic.filename": "Nom de fichier",
@@ -144,6 +161,7 @@
"home.search": "Rechercher",
"home.search.view_limit": "Limite d'affichage :",
"home.search_entries": "Recherche",
"home.search_field_templates": "Rechercher un format de champs",
"home.search_library": "Rechercher dans la Bibliothèque",
"home.search_tags": "Recherche de Tags",
"home.show_hidden_entries": "Afficher les entrées cachées",
@@ -213,6 +231,7 @@
"menu.delete_selected_files_singular": "Déplacer le Fichier vers {trash_term}",
"menu.edit": "Édition",
"menu.edit.ignore_files": "Ignorer les Fichiers et Dossiers",
"menu.edit.manage_field_templates": "Gérer les format de champs",
"menu.edit.manage_tags": "Gérer les Tags",
"menu.edit.new_tag": "Nouveaux &Tag",
"menu.file": "&Fichier",
@@ -249,6 +268,8 @@
"namespace.new.button": "Nouvelle Namespace",
"namespace.new.prompt": "Commencer par créer une nouvelle namespace pour pouvoir créer des couleurs personnalisées!",
"preview.ignored": "Ignoré",
"preview.missing_module.jxl": "Le module {module} est nécessaire pour les aperçus JPEG XL",
"preview.missing_module.multimedia": "{module} est nécessaire pour la lecture multimédia",
"preview.multiple_selection": "<b>{count}</b> Éléments Sélectionner",
"preview.no_selection": "Pas d'Objet Selectionné",
"preview.unlinked": "Non-lié",
@@ -278,6 +299,7 @@
"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",
"settings.splash.option.aurora": "Aurora (9.6)",
"settings.splash.option.classic": "Classique (9.0)",
"settings.splash.option.default": "Défaut",
"settings.splash.option.goo_gears": "Open Source (9.4)",
@@ -334,6 +356,7 @@
"tag.parent_tags": "Tags Parent",
"tag.parent_tags.add": "Ajouter des Tags Parents",
"tag.parent_tags.description": "Ce Tag peut être utilisé en replacement de tous ces Tags Parents dans les recherches.",
"tag.properties": "Proprietes",
"tag.remove": "Supprimer un Tag",
"tag.search_for_tag": "Recherche de Label",
"tag.shorthand": "Abrégé",
@@ -351,6 +374,7 @@
"trash.dialog.title.singular": "Supprimer le Fichier",
"trash.name.generic": "Poubelle",
"trash.name.windows": "Corbeille",
"update.view_update": "Afficher la mise à jour",
"version_modal.description": "Une nouvelle version de TagStudio est disponible! Vous pouvez télécharger la version la plus récente sur <a href=\"{github_url}\">Github</a>.",
"version_modal.status": "Version installer : {installed_version}<br>Dernière version disponible : {latest_release_version}",
"version_modal.title": "Mise à jour de TagStudio disponible",
+59 -7
View File
@@ -1,9 +1,13 @@
{
"about.app_cache_path": "Gyorsítótár elérési útvonala",
"about.config_path": "Konfigurációs fájl",
"about.description": "A TagStudio egy fénykép- és fájlkezelő program, mely címkék segítségével nyújt felhasználói szabadságot és rugalmasságot. A TagStudio nem használ jogvédett formátumokat, társfájlokat és nem fordítja a feje tetejére a már létező fájlrendszert.",
"about.documentation": "Dokumentáció",
"about.module.found": "Telepítve",
"about.modules.title": "Nemkötelező modulok",
"about.title": "A TagStudio névjegye",
"about.version": "Kiadás",
"about.version.latest": "{built_version} (legújabb kiadás: {latest_version})",
"about.website": "Honlap",
"app.git": "Git-véglegesítés",
"app.pre_release": "Kísérleti verzió",
@@ -74,12 +78,20 @@
"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.field_name_required": "Mező neve (kötelező)",
"field.mixed_data": "Kevert adatok",
"field.name": "Név",
"field.paste": "Mező &beillesztése",
"field.remove": "Mező eltávolítása",
"field.text.is_multiline": "Többsoros",
"field.type": "Típus",
"field_template.all_field_templates": "Mezőminták hozzáadása",
"field_template.confirm_delete": "Biztosan törölni akarja a(z) „{field_template_name}” mezőmintát?",
"field_template.create": "Mezőminta létrehozása",
"field_template.create_add": "A(z) „{query}” mező létrehozása és hozzáadása",
"field_template.delete": "Mezőminta törlése",
"field_template.edit": "Mezőminta szerkesztése",
"field_template.new": "Új mezőminta",
"field_template_manager.title": "Mezőminták a jelenlegi könyvtárban",
"field_type.datetime": "Dátum és idő",
"field_type.text": "Szöveg",
@@ -125,6 +137,7 @@
"generic.delete_alt": "&Törlés",
"generic.done": "Kész",
"generic.done_alt": "Kész",
"generic.dont_remind": "Ne emlékeztessen többször",
"generic.edit": "Szerkesztés",
"generic.edit_alt": "S&zerkesztés",
"generic.filename": "Fájlnév",
@@ -184,6 +197,36 @@
"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}",
"language.am": "amhara",
"language.ceb": "szebuano",
"language.cs": "cseh",
"language.da": "dán",
"language.de": "német",
"language.el": "görög",
"language.en": "angol",
"language.es": "spanyol",
"language.fi": "finn",
"language.fil": "filippínó",
"language.fr": "francia",
"language.hu": "magyar",
"language.is": "izlandi",
"language.it": "olasz",
"language.ja": "japán",
"language.nb_NO": "bokmål norvég",
"language.nl": "holland",
"language.pl": "lengyel",
"language.pt": "portugál",
"language.pt_BR": "brazíliai portugál",
"language.qpv": "viossa",
"language.ro": "román",
"language.ru": "orosz",
"language.sv": "svéd",
"language.ta": "tamil",
"language.th": "thai",
"language.tok": "toki pona",
"language.tr": "török",
"language.zh_Hans": "kínai (egyszerűsített)",
"language.zh_Hant": "kínai (hagyományos)",
"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",
@@ -246,8 +289,8 @@
"menu.tools.fix_ignored_entries": "Figyelmen &kívül hagyott elemek javítása",
"menu.tools.fix_unlinked_entries": "Kapcsolat &nélküli elemek javítása",
"menu.view": "&Nézet",
"menu.view.decrease_thumbnail_size": "Indexkép méretének csökkentése",
"menu.view.increase_thumbnail_size": "Indexkép méretének növelése",
"menu.view.decrease_thumbnail_size": "Bélyegkép méretének csökkentése",
"menu.view.increase_thumbnail_size": "Bélyegkép méretének növelése",
"menu.view.library_info": "&Könyvtárinformáció",
"menu.window": "&Ablak",
"namespace.create.description": "A TagStudio névterekkel különíti el az adatcsoportokat, mint a címkék és a színek, így azok könnyen exportálhatóak és megoszthatóak. A „tagstudio”-val kezdődő névterek belső használatra vannak lefoglalva.",
@@ -256,6 +299,8 @@
"namespace.new.button": "Új névtér",
"namespace.new.prompt": "Az egyéni színek használatához először hozzon létre egy névteret!",
"preview.ignored": "Figyelmen kívül hagyva",
"preview.missing_module.jxl": "A JPEG XL-előnézetekhez a(z) {module} szükséges",
"preview.missing_module.multimedia": "A hangok és videók lejátszásához a(z) {module} szükséges",
"preview.multiple_selection": "<b>{count}</b> kijelölt elem",
"preview.no_selection": "Nincs kijelölt elem",
"preview.unlinked": "Kapcsolat nélküli elem",
@@ -263,21 +308,25 @@
"select.all": "&Az összes kijelölése",
"select.clear": "&Kijelölés megszüntetése",
"select.inverse": "Kijelölés &megfordítása",
"settings.appearance": "Megjelenés",
"settings.cached_thumb_resolution.label": "Gyorsítótárazott bélyegképek felbontása",
"settings.clear_thumb_cache.title": "&Miniatűr-gyorsítótár ürítése",
"settings.dateformat.english": "Angol",
"settings.dateformat.international": "Nemzetközi",
"settings.dateformat.label": "Dátumformátum",
"settings.dateformat.system": "Rendszer",
"settings.filepath.label": "&Elérési utak láthatósága",
"settings.filepath.option.full": "Teljes elérési út mutatása",
"settings.filepath.option.name": "Csak a fájlnév mutatása",
"settings.filepath.option.relative": "Relatív elérési út mutatása",
"settings.generate_thumbs": "Indexkép-előállítás",
"settings.filepath.option.full": "Teljes elérési út megjelenítése",
"settings.filepath.option.name": "Csak a fájlnév megjelenítése",
"settings.filepath.option.relative": "Relatív elérési út megjelenítése",
"settings.generate_thumbs": "Bélyegkép-előállítás",
"settings.global": "Globális beállítások",
"settings.hourformat.label": "24-órás idő",
"settings.infinite_scroll": "Végtelen görgetés",
"settings.language": "&Nyelv",
"settings.library": "Könyvtárbeállítások",
"settings.localization": "Nyelv és formátumok",
"settings.media": "Média",
"settings.open_library_on_start": "&Könyvtár megnyitása a program indulásakor",
"settings.page_size": "&Oldalméret",
"settings.restart_required": "A módosítások érvénybeléptetéséhez<br>újra kell indítani a TagStudiót.",
@@ -285,6 +334,7 @@
"settings.show_filenames_in_grid": "&Fájlnevek megjelenítése rácsnézetben",
"settings.show_recent_libraries": "&Legutóbbi könyvtárak megjelenítése",
"settings.splash.label": "Indítókép",
"settings.splash.option.aurora": "Aurora (9.6)",
"settings.splash.option.classic": "Klasszikus (9.0)",
"settings.splash.option.default": "Alapértelmezett",
"settings.splash.option.goo_gears": "Nyílt forráskódú (9.4)",
@@ -298,7 +348,7 @@
"settings.theme.label": "&Téma:",
"settings.theme.light": "Világos",
"settings.theme.system": "Automatikus",
"settings.thumb_cache_size.label": "Indexkép-gyorsítótár mérete",
"settings.thumb_cache_size.label": "Bélyegkép-gyorsítótár mérete",
"settings.title": "Beállítások",
"settings.zeropadding.label": "Egyszámjegyű napok kezdése nullával",
"sorting.direction.ascending": "Növekvő sorrend",
@@ -341,6 +391,7 @@
"tag.parent_tags": "Szülőcímkék",
"tag.parent_tags.add": "Új szülőcímke",
"tag.parent_tags.description": "Ez a címke képes helyettesíteni bármely alábbi szülőcímkét kereséskor.",
"tag.properties": "Tulajdonságok",
"tag.remove": "Címke eltávolítása",
"tag.search_for_tag": "Címke keresése",
"tag.shorthand": "Rövidítés",
@@ -358,6 +409,7 @@
"trash.dialog.title.singular": "Fájl törlése",
"trash.name.generic": "kukába",
"trash.name.windows": "lomtárba",
"update.view_update": "Frissítés megtekintése",
"version_modal.description": "Elérhetővé vált egy TagStudio-frissítés. A legújabb verziót a <a href=\"{github_url}\">Githubról</a> töltheti le.",
"version_modal.status": "Telepített verzió: {installed_version}<br>Legújabb stabil verzió: {latest_release_version}",
"version_modal.title": "TagStudio-frissítés",
@@ -174,6 +174,8 @@
"json_migration.title.new_lib": "<h2>v9.5+ ライブラリ</h2>",
"json_migration.title.old_lib": "<h2>v9.4 ライブラリ</h2>",
"landing.open_create_library": "ライブラリを開く/作成する {shortcut}",
"language.en": "英語",
"language.es": "スペイン語",
"library.missing": "ライブラリの場所が見つかりません",
"library.name": "ライブラリ",
"library.refresh.scanning.plural": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
@@ -1,9 +1,12 @@
{
"about.app_cache_path": "Путь к кешу",
"about.config_path": "Путь к конфигурации",
"about.description": "TagStudio — это приложение для организации фотографий и прочих файлов, основанное на системе \"тегов\", и ориентированное на предоставление пользователю свободы и гибкости. Никаких проприетарных форматов и программ, никакой кучи сопроводительных файлов, и никакого переворота вашей файловой системы.",
"about.documentation": "Документация",
"about.module.found": "Найдено",
"about.title": "О программе TagStudio",
"about.version": "Версия",
"about.version.latest": "{built_version} (Актуальный релиз: {latest_version})",
"about.website": "Веб-сайт",
"app.git": "Коммит Git",
"app.pre_release": "Пре-релиз",
@@ -72,9 +75,19 @@
"field.confirm_remove": "Вы уверены, что хотите удалить поле \"{name}\"?",
"field.copy": "Копировать поле",
"field.edit": "Редактировать поле",
"field.field_name_required": "Название поля (обязательно)",
"field.mixed_data": "Смешанные данные",
"field.name": "Название",
"field.paste": "Вставить поле",
"field.remove": "Удалить поле",
"field.type": "Тип",
"field_template.all_field_templates": "Все шаблоны полей",
"field_template.confirm_delete": "Вы уверены, что хотите удалить шаблон поля \"{field_template_name}\"?",
"field_template.create": "Создать шаблон поля",
"field_template.create_add": "Создать и добавить \"{query}\"",
"field_template.delete": "Удалить шаблон поля",
"field_template.edit": "Редактировать шаблон поля",
"field_template.new": "Новый шаблон поля",
"field_type.datetime": "Дата",
"field_type.text": "Текст",
"field_type.unknown": "Неизвестный тип",
@@ -177,6 +190,25 @@
"json_migration.title.new_lib": "<h2>Библиотека версии 9.5+</h2>",
"json_migration.title.old_lib": "<h2>Библиотека версии 9.4</h2>",
"landing.open_create_library": "Открыть/создать библиотеку {shortcut}",
"language.en": "Английский",
"language.es": "Испанский",
"language.fi": "Финский",
"language.fil": "Филипинский",
"language.fr": "Французский",
"language.hu": "Венгерский",
"language.is": "Исландский",
"language.it": "Итальянский",
"language.ja": "Японский",
"language.pl": "Польский",
"language.pt": "Португальский",
"language.pt_BR": "Португальский (Бразилия)",
"language.ro": "Румынский",
"language.ru": "Русский",
"language.sv": "Шведский",
"language.th": "Тайский",
"language.tr": "Турецкий",
"language.zh_Hans": "Китайский (упрощенный)",
"language.zh_Hant": "Китайский (традиционный)",
"library.missing": "Отсутствует путь к библиотеке",
"library.name": "Библиотека",
"library.refresh.scanning.plural": "Сканирование папок на наличие новых файлов...\nПросканировано {searched_count} файлов, найдено {found_count} новых",
Binary file not shown.
+1 -1
View File
@@ -11,7 +11,7 @@ def test_github_api_unavailable(qtbot: QtBot, mocker) -> None:
mocker.patch(
"requests.get",
side_effect=ConnectionError(
"Failed to resolve 'api.github.com' ([Errno -3] Temporary failure in name resolution)"
"Emulating a failure with 'api.github.com' ([Errno 0] This should be handled)"
),
)
modal = AboutModal("/tmp")