Compare commits

...

177 Commits

Author SHA1 Message Date
Travis Abendshien
62f1b7ca55 ci: bump windows runner image to 2022 2025-08-07 19:46:29 -07:00
Travis Abendshien
8aec8ca11a chore: bump version to v9.5.3 2025-08-07 18:45:02 -07:00
Travis Abendshien
c235d4f727 ui: tweak media player style and behavior (#1025)
* ui: tweak media player style and behavior

* ui: change player overlay to gradient

* ui: add slight outline to player bar

* feat: realtime playback seeking

* fix(ui): click to jump to unbuffered media

* chore: organize imports

* refactor: remove unused code
2025-08-07 14:23:20 -07:00
Travis Abendshien
78e29a9a69 fix(ui): show correct thumb labels (#1010)
* fix(ui): show correct thumb labels

* fix(ui): use timestamp cutoff for thumb filename and ext
2025-08-07 12:33:38 -07:00
Trigam
c71032ff51 feat(ui): clickable links in text fields (#924)
* feat: links in text fields are clickable

* style: fix linting errors

* style: remove unnecessary comment

* feat: better url pattern

* chore: disable Ruff error

* style: add trailing line

* style: maybe format it? that'd be smart

* ui: chance text_label from RichText to Markdown

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-08-07 12:29:09 -07:00
Travis Abendshien
77cfa697fc fix: use srctools instead of vtf2img for vtf files (#1014) 2025-08-06 18:34:12 -07:00
Travis Abendshien
89fc8a7280 fix(docs): load non-local fonts 2025-08-06 13:40:20 -07:00
Travis Abendshien
809885e8b5 docs: add git clone step to develop page 2025-08-06 12:42:34 -07:00
Travis Abendshien
424ee3bc2c ci: relax pillow version, upgrade mkdocs-material 2025-08-06 12:29:50 -07:00
Travis Abendshien
278a6de8a6 feat(docs): vauxhall theme for docs site 2025-08-06 11:48:33 -07:00
Travis Abendshien
142e464f54 fix(ci): restore imaging extra for mkdocs-material 2025-08-05 11:40:36 -07:00
Jann Stute
fa0b82c004 fix: don't fail when posix env var is not present (#1018)
* fix: don't set MacOS specific values when not on MacOS

* refactor: use dict.get instead
2025-08-05 11:24:41 -07:00
Travis Abendshien
0d1311557a refactor: replace remaining instances of logging with structlog (#1012) 2025-08-05 10:38:51 -07:00
Travis Abendshien
4105e178bd fix: use np.frombuffer instead of np.fromstring for numpy 2.3 (#1013) 2025-08-05 10:37:15 -07:00
Travis Abendshien
4d4a4874d7 ui: add arrow icons for navigation buttons (#1016)
* ui: add arrow icons for nav buttons

* ui: use arrow icons for pagination buttons
2025-08-05 10:32:47 -07:00
Travis Abendshien
40f555edc7 fix(ui): hide empty ProgressWidget cancel button (#1011) 2025-08-05 10:32:28 -07:00
Travis Abendshien
7bef302f90 fix: restore item_thumb delete action 2025-08-04 16:06:52 -07:00
TheBobBobs
e115443811 perf: optimize page loading by referencing entries by ID (#954)
* refactor: search_library now returns ids instead of entries

* perf: optimize update_thumbs
2025-08-04 16:04:07 -07:00
Jann Stute
c2261d5b83 refactor(tag_box): mvc split (#1003)
* refactor: split into view and controller

* refactor: controller simplifications

* Update src/tagstudio/qt/controller/components/tag_box_controller.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* refactor: split method

* refactor: add override specifiers

* fix: shutup mypy

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-08-04 15:06:22 -07:00
Thibault Maekelbergh
1459f79b23 build: add Nix path to ffmpeg locations on macOS (#990)
* Add Nix path to ffmpeg locations on macOS

* Add 'os' import
2025-08-04 15:02:14 -07:00
Jann Stute
1ee1ccbe8d fix: preview thumbnails don't scale as large as they could (#1005)
* fix: preview thumbnails don't scale as large as they could

* fix: only scale as large as necessary

* fix: explicitly declare vars
2025-08-04 14:39:17 -07:00
Travis Abendshien
4653eb2a04 fix: ensure initial browsing state uses UI values (#1008) 2025-08-04 12:43:37 -07:00
Jann Stute
940617cd63 fix(library): get_tag_by_name (#1006) 2025-08-03 13:03:36 -07:00
ugurozturk
9b287cb5af docs: fix incorrect release dates (#966)
* Wrong Date on Changelog fixed

* docs: fix remaining release dates

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-08-02 21:48:18 -07:00
VasigaranAndAngel
6b96ee79e1 refactor: type improvements for main_window.py (#957) 2025-08-02 21:37:11 -07:00
Travis Abendshien
7d9480e3cf ui: reorder thumbnail zoom actions 2025-08-02 21:28:27 -07:00
purpletennisball
45943ba00d feat: zoom keyboard shortcut (#956)
* feat: add increase/decrease zoom menu items

* fix: increase thumbnail size not working on windows

* fix: duplicated menu items
2025-08-02 21:22:44 -07:00
ngivanyh
d86fc5cdd0 translations: update Traditional Chinese (#974)
* translation - almost done

* almost done

* small stuft left

* finish, the only untranslated text  "The \"Favorite\" and \"Archived\" tags now inherit from a new 「Meta Tags」 tag which is marked as a category by default." of json_migration.info.description

* small changes - done
2025-08-01 17:00:51 -07:00
Weblate (bot)
ed142203aa translations: update from Hosted Weblate (#947)
* Added translation using Weblate (Thai)

Co-authored-by: Anucha Hlownonkor <tony.chompoo@gmail.com>

* Translated using Weblate (Filipino)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (319 of 319 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: searinminecraft <kitakita@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/
Translation: TagStudio/Strings

* Translated using Weblate (Tamil)

Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/
Translation: TagStudio/Strings

* Translated using Weblate (German)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (German)

Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Stereo157E <pascal-reinartz157e@gmx.de>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Strings

* Translated using Weblate (Russian)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Maxim Truhonin <maxim.truhonin@gmail.com>
Co-authored-by: Utof <vip.ilyabondar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings

* Translated using Weblate (Japanese)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (319 of 319 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (319 of 319 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/Strings

* Translated using Weblate (Dutch)

Currently translated at 38.0% (123 of 323 strings)

Translated using Weblate (Dutch)

Currently translated at 38.0% (123 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Pheubel <luca.guagliardo@live.nl>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nl/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 99.6% (322 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: danpg94 <danpalma94@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (French)

Currently translated at 100.0% (319 of 319 strings)

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/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Luoyu <tkiuvvv2333@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hans/
Translation: TagStudio/Strings

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (319 of 319 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Neemek <sindodeg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nb_NO/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 95.3% (308 of 323 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Anucha Hlownonkor <tony.chompoo@gmail.com>
Co-authored-by: searinminecraft <kitakita@disroot.org>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: Stereo157E <pascal-reinartz157e@gmx.de>
Co-authored-by: Maxim Truhonin <maxim.truhonin@gmail.com>
Co-authored-by: Utof <vip.ilyabondar@gmail.com>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Pheubel <luca.guagliardo@live.nl>
Co-authored-by: danpg94 <danpalma94@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Luoyu <tkiuvvv2333@gmail.com>
Co-authored-by: Neemek <sindodeg@gmail.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2025-08-01 16:34:44 -07:00
mashed5894
fd7d1f1e95 feat(ui): krita/open raster thumbs (#985)
* added kra/krz media categories and thumb rendering logic

* added .ora support (follows open doc standard)

---------

Co-authored-by: fj92hg <>
2025-07-31 23:53:35 -07:00
Jann Stute
a7d98e765b refactor(preview_thumb): mvc split (#978)
* refactor: basic split

* fix: renaming and usage test didn't work for the tests

* fix: tests

* refactor: restructuring

* refactor: further separation and lots of related changes

* refactor: remove last reference to a widget from controller

* refactor: address todo

* fix: failing tests and mypy compaint

* refactor: move control logic to controller

* refactor: more readable button style

* refactor: move existing code to view

* refactor: move existing code to controller

* fix: imports

* refactor: make methods private by default

* refactor: privatise fields

* refactor: reduce code duplication

* refactor: consolidate and sort display methods

* refactor: remove needless setting of delete action text

* refactor: extract control logic from _display_file

* refactor: use MediaType for __switch_preview

* fix: import in preview_panel_view.py

* refactor: remove unnecessary wrapper on view side

* refactor: move image data retrieval to control side

* refactor: move audio / video specific code to the respective method

* refactor: remove superfluos methods

* refactor: this and that

* refactor: use proper type instead of dict for file stats

* refactor: extract gif parsing to controller

* refactor: extract video size extraction to controller

* doc: add rule of thumb to Qt MVC Style Guide

* doc: change rule of thumb from note to tip

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-07-31 23:46:46 -07:00
Jann Stute
192af25f6f refactor(preview_panel): mvc split (#952)
* refactor: basic split

* fix: renaming and usage test didn't work for the tests

* fix: tests

* refactor: restructuring

* refactor: further separation and lots of related changes

* refactor: remove last reference to a widget from controller

* refactor: address todo

* fix: failing tests and mypy compaint

* refactor: move control logic to controller

* refactor: more readable button style

* fix: set_selection was called with invalid argument
2025-07-31 22:53:32 -07:00
Jann Stute
7176908274 docs: Add QT MVC structure to style guide (#950)
* doc: first draft

* doc: highlight key aspects of example

* doc: address feedback

* doc: add comment on making methods private by default

* doc: fix typo

* doc: fix typos
2025-07-05 11:26:14 -07:00
VasigaranAndAngel
29154babf8 refactor: type improvements to landing.py and panel.py (#960)
* landing.py

* panel.py and a method's name change

* remove # noqa

* remove set_title method and pass title to the PanelModal constructor

Co-Authored-By: Jann Stute <46534683+Computerdores@users.noreply.github.com>

* send keyPressEvent only if it's not used

* pass window_title to constructor

use `title` if `window_title` is `None`

Co-Authored-By: Jann Stute <46534683+Computerdores@users.noreply.github.com>

---------

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>
2025-07-03 11:10:20 -07:00
VasigaranAndAngel
001ff14799 recator: fixes to preview_panel.py, progress.py, tag.py and tag_box.py (#961)
* preview_panel.py

* progress.py

* tag.py

* tag_box.py

* Revert "preview_panel.py"

This reverts commit bcb4515834.
2025-07-03 09:03:22 -07:00
VasigaranAndAngel
57ab919d1b refactor: type fixes to folders_to_tags.py, collage_icon.py and item_thumb.py (#959)
* folders_to_tags.py type fixes

* collage_icon.py

* item_thumb.py

* fix test and mypy errors

* naming fix

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>

---------

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>
2025-07-03 08:39:33 -07:00
VasigaranAndAngel
4e722278d8 refactor: resource_manager.py (#958)
* refactor

* describe return types in get method

* make ResourceManager singleton

* match case statement
2025-06-27 14:11:29 -07:00
Travis Abendshien
b8313c3f15 ui: enable Chinese (Simplified) and Viossa language options 2025-06-07 14:49:00 -07:00
Jann Stute
c100babd9f feat(ui): option to change tag primary click behavior (#945)
* feat: add settings field

* feat: change click behaviour based on settings value

* fix: make ignore comment pyright specific to shut up mypy

* fix: add german and english translations for new strings

* fix: settings dropdowns were always english not matter the selected language
2025-06-06 12:45:30 -07:00
Jann Stute
3999d5d39b feat(ui): improved datetime field modal using QDateTimeEdit (#946)
* feat: custom modal making use of QDateTimeEdit

* fix: add back license notice

* refactor: remove unnecessary line

* feat: use date and hour format from settings for date time picker
2025-06-06 12:41:52 -07:00
Jann Stute
14d1c2b618 refactor: split QtDriver into View and Controller to follow MVC model (#935)
* fix: incorrect qt enum accesses

* refactor: clean up main_window.py and reenable ruff checks for it

* refactor: completely reorganise main_window.py

* refactor: factor out regions into separate methods

* refactor: move preview_panel to MainWindow

* refactor: move file menu construction to MainMenuBar

* refactor: move edit menu construction to MainMenuBar

* refactor: move view menu construction to MainMenuBar

* refactor: move tools menu construction to MainMenuBar

* refactor: move macros menu construction to MainMenuBar

* refactor: move help menu construction to MainMenuBar

* refactor: move ui construction code out of QtDriver and into MainWindow

* fix: remove unnecessary imports

* fix: tests were failing
2025-06-05 16:02:03 -07:00
Weblate (bot)
da18bd0dd5 translations: update from Hosted Weblate (#907)
* Translated using Weblate (Filipino)

Currently translated at 100.0% (313 of 313 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (313 of 313 strings)

Translated using Weblate (Filipino)

Currently translated at 74.7% (234 of 313 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: searinminecraft <kitakita@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/
Translation: TagStudio/Strings

* Translated using Weblate (Tamil)

Currently translated at 100.0% (313 of 313 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/
Translation: TagStudio/Strings

* Translated using Weblate (German)

Currently translated at 100.0% (313 of 313 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Liveside <livesi5e@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Strings

* Translated using Weblate (Russian)

Currently translated at 91.3% (286 of 313 strings)

Translated using Weblate (Russian)

Currently translated at 91.3% (286 of 313 strings)

Translated using Weblate (Russian)

Currently translated at 89.1% (279 of 313 strings)

Translated using Weblate (Russian)

Currently translated at 88.8% (278 of 313 strings)

Co-authored-by: Dott-rus <antonamelin8@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Utof <vip.ilyabondar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings

* Translated using Weblate (Japanese)

Currently translated at 100.0% (313 of 313 strings)

Translated using Weblate (Japanese)

Currently translated at 63.2% (198 of 313 strings)

Translated using Weblate (Japanese)

Currently translated at 52.3% (164 of 313 strings)

Update translation files

Updated by "Remove blank strings" add-on in Weblate.

Translated using Weblate (Japanese)

Currently translated at 52.0% (163 of 313 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (313 of 313 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/Strings

* Translated using Weblate (Polish)

Currently translated at 92.6% (290 of 313 strings)

Co-authored-by: Feather <featherprinceyt@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pl/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 99.3% (311 of 313 strings)

Translated using Weblate (Spanish)

Currently translated at 96.4% (302 of 313 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Sunny <0t2sruto@pwall.anonaddy.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (313 of 313 strings)

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/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (313 of 313 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (312 of 312 strings)

Added translation using Weblate (Chinese (Simplified Han script))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Luoyu <rainluoyu@outlook.com>
Co-authored-by: Luoyu <tkiuvvv2333@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hans/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 95.8% (300 of 313 strings)

Translated using Weblate (Toki Pona)

Currently translated at 94.8% (297 of 313 strings)

Translated using Weblate (Toki Pona)

Currently translated at 90.4% (283 of 313 strings)

Translated using Weblate (Toki Pona)

Currently translated at 90.0% (282 of 313 strings)

Translated using Weblate (Toki Pona)

Currently translated at 83.0% (260 of 313 strings)

Translated using Weblate (Toki Pona)

Currently translated at 80.7% (252 of 312 strings)

Translated using Weblate (Toki Pona)

Currently translated at 80.7% (252 of 312 strings)

Translated using Weblate (Toki Pona)

Currently translated at 80.7% (252 of 312 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

* Translated using Weblate (Viossa)

Currently translated at 96.1% (301 of 313 strings)

Translated using Weblate (Viossa)

Currently translated at 96.1% (301 of 313 strings)

Translated using Weblate (Viossa)

Currently translated at 96.1% (301 of 313 strings)

Translated using Weblate (Viossa)

Currently translated at 46.3% (145 of 313 strings)

Translated using Weblate (Viossa)

Currently translated at 45.0% (141 of 313 strings)

Translated using Weblate (Viossa)

Currently translated at 45.1% (141 of 312 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/qpv/
Translation: TagStudio/Strings

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: searinminecraft <kitakita@disroot.org>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: Liveside <livesi5e@gmail.com>
Co-authored-by: Dott-rus <antonamelin8@gmail.com>
Co-authored-by: Utof <vip.ilyabondar@gmail.com>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Feather <featherprinceyt@gmail.com>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Sunny <0t2sruto@pwall.anonaddy.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Luoyu <rainluoyu@outlook.com>
Co-authored-by: Luoyu <tkiuvvv2333@gmail.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
2025-06-04 02:14:32 -07:00
Travis Abendshien
9b0eb90d06 Merge branch 'Alpha-v9.5.3' 2025-06-04 01:35:25 -07:00
rsazra
e4bb07e852 fix: add parent tags to folders_to_tags macro and start tagging at root folder (#940)
* start tagging from root and add parent tags

* update snapshot to pass test
2025-06-04 01:22:17 -07:00
purpletennisball
e1117bf1a1 fix: creating new tag now refreshes the menu using the current search text (#939) 2025-06-04 00:43:09 -07:00
rsazra
c21e2d657d fix(ui): add selected field on return (#941) 2025-06-04 00:36:27 -07:00
Jann Stute
25fb6883c1 fix: proper error on unterminated quoted string (#936)
* fix: minor warnings

* fix: return ParsingError on unterminated quoted string
2025-06-04 00:33:33 -07:00
Jann Stute
cf6c56c9d2 fix: restore page navigation state (#933)
* refactor: store browsing history for navigation purposes

* refactor: remove page_size from FilterState

* refactor: move on from the term "filter" in favor of "BrowsingState"

* fix: refactors didn't propagate to the tests

* fix: ruff complaints

* fix: remaing refactoring errors

* fix: navigation works again

* fix: also store and restore query
2025-06-04 00:29:07 -07:00
Bee
b0a9d045fd docs: update CHANGELOG.md (#914)
* Update CHANGELOG.md

* Update CHANGELOG.md again

add the more recent changelog parts
2025-06-03 23:32:03 -07:00
Xarvex
1e783a5e3c fix(nix/package): override PySide6 if later version is being used (#917) 2025-05-06 16:42:21 -05:00
Travis Abendshien
6517ef560f Merge branch 'main' into Alpha-v9.5.3 2025-05-05 14:25:38 -07:00
Jann Stute
d061e2e866 fix: tests were overwriting the settings.toml (#928)
* fix: tests were overwriting the settings.toml

* fix(GlobalSettings): add default value for _loaded_from

* fix: a case in read_settings didn't set loaded_from correctly

* fix(GlobalSettings): proper serialisation
2025-05-05 14:04:48 -07:00
Travis Abendshien
ee56f3e2bc feat(ui): show stems for extension-less files (#899)
* feat(ui): show stems for extension-less files

* chore: remove unnecessary call and comment

* fix: only access filepath if not none
2025-05-05 13:09:26 -07:00
Xarvex
702ecd4118 chore(pyproject): version bumping/relaxing (#886)
* chore(pyproject): version bumping/relaxing

* fix(pyproject): remove imaging extra for mkdocs-material

* fix: ruff formatting

* fix: mypy

* fix(pyproject): PySide violates SemVer

* chore(pyproject): set Python version, bump pydantic

* fix(ci): up Ruff to consistent version

* fix(pyproject): fixup requires-python

* fix: ruff checks

* chore(pyproject): bump dependencies

* fix(ci): up Ruff to consistent version

* fix(tests): strip out non-reproducible tests
2025-05-05 12:47:57 -07:00
Travis Abendshien
c14734d25e chore: remove unused "dev/null" check 2025-05-04 17:29:07 -07:00
Tony
36f3b45e66 fix(tests): fix tests failing on Windows (#903)
* create and delete tmp directory for tests

* Update tests/conftest.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Update tests/conftest.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* remove unused import

* cleanup and added type hints

* fix mypy assignment error

* swap gettempdir() with TemporaryDirectory()

* reuse temp library for more tests + cleanup

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-05-04 17:23:36 -07:00
VasigaranAndAngel
efb062034d refactor: remove placeholder video, fix type hints in preview_thumb.py (#906)
* type fixes

* remove `stop_file_use()` method and release the file properly.

* remove "placeholder_mp4" from resources.json
2025-05-04 15:13:34 -07:00
Jann Stute
6598630984 feat: datetime fields settings integration (#926)
* refactor: move datetime formatting to global settings

* feat: datetime fields settings integration
2025-05-04 11:49:52 -07:00
Jann Stute
98c4b1c359 feat(ui): temporary modal to edit datetime fields and small refactors (#921)
* refactor: simplify datetime widget creation

* refactor: fix warnings

* feat: text modal to enter date

* chore: change ignore comment to only apply to pyright not mypy
2025-05-04 11:13:58 -07:00
Travis Abendshien
525b382803 chore: remove unused import 2025-05-04 10:50:57 -07:00
JCC1998
f770614c4e feat: add date_format and hour_format settings (#904)
* feat: add date_format and hour_format settings

* fix: fix ruff validation errors

* fix: use ruff format command

* fix: Refactor code and improve some logic

* fix: remove unused import

* Added zero padding setting and implement some comments

* Add test assert property

* fix: Unclutter selector and clarify zero-padding literal

* fix: Use static strings

Co-authored-by: Tony <1414927+zfbx@users.noreply.github.com>

---------

Co-authored-by: Tony <1414927+zfbx@users.noreply.github.com>
2025-05-04 10:46:26 -07:00
Tony
97ee43c875 feat: add ability to invert selection (#909)
* implement selection inversing

* Rename Select Inverse to Invert Selection
2025-04-05 19:59:44 -05:00
Xarvex
a72c2c4ba8 docs(develop): use double quotes for pip
Some shells, notably Zsh, try parsing the brackets.

Fixes: #910
2025-04-05 19:11:46 -05:00
Xarvex
29d3d64a32 nix(nix/package): ignore incompatible tests 2025-03-31 23:53:46 -05:00
Travis Abendshien
b0bfc10a67 docs: update roadmap 2025-03-31 20:36:08 -07:00
Travis Abendshien
7c5b8e51e6 chore: bump version to v9.5.2 2025-03-31 15:39:46 -07:00
Weblate (bot)
efb4c4a6ed translations: update Hungarian, French, Toki Pona (#891)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (312 of 312 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (309 of 309 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/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (312 of 312 strings)

Translated using Weblate (French)

Currently translated at 100.0% (309 of 309 strings)

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/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 80.9% (250 of 309 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2025-03-31 15:33:17 -07:00
Travis Abendshien
f88200f38e fix(ui): seamlessly loop videos (#902) 2025-03-31 15:28:08 -07:00
VasigaranAndAngel
7f7d861800 refactor: fix type hints and overrides in flowlayout.py (#880)
* refactor and fixes

* type annotations and parameter name changes

* Update src/tagstudio/qt/flowlayout.py

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>

* Update src/tagstudio/qt/flowlayout.py

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>

* ruff format

---------

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>
2025-03-31 11:51:36 -07:00
Tony
cccd858078 fix(ui): remove media player border (#900) 2025-03-31 11:45:26 -07:00
VasigaranAndAngel
46996d50e5 refactor: fix various missing and broken type hints (#901)
* fix type hints

* text_wrapper.py type fix and minor improvements

* fields.py fixes
2025-03-31 11:14:15 -07:00
Travis Abendshien
1939f118f1 ci: add reportUnknownLambdaType = false to pyproject.toml 2025-03-31 09:49:32 -07:00
Travis Abendshien
3b5a9605d1 fix: use proper not check against MatLike type
Fixes video thumbnails not rendering.
2025-03-31 02:13:06 -07:00
Travis Abendshien
7dd1905b6e translations: add Japanese to settings, catch FileNotFound error 2025-03-30 23:37:22 -07:00
Travis Abendshien
9c24272caf settings: update default settings values 2025-03-30 21:28:09 -07:00
Travis Abendshien
d7d7e21d13 feat(ui): add more default icons and file type equivalencies (#882)
* feat(ui): expand file and thumbnail support

* feat: add iwork and powerpoint thumb support

Note: a lot of the zip-based code is becoming duplicated - this should be consolidated in the future.

* fix: remove decompression bomb check and catch others

* feat: add .aiff file equivalencies

* ui: update database icon

* feat: add .effect and .shader to shader set

* fix: correct malformed or missing media types

* feat: add misc code/plaintext types to media types

* fix: catch BadZipFile error for iWork thumbs

* chore: add type hints to thumb_renderer dicts

* refactor: change most internal render methods to static
2025-03-30 19:24:10 -07:00
Travis Abendshien
33e6bc180d ui: recent libraries list improvements (#881)
* ui: improve missing library message

* ui: update recent library max to 10
2025-03-30 19:23:52 -07:00
csponge
13afb0f664 feat(ui): merge media controls (#805)
* feat: merge media controls.

Initial commit to merge audio/video files. There are
still a few bugs around widget sizing that need fixing.

* fix: center widgets in preview area

Add widgets to a sublayout to allow for centering
in a QStackedLayout.

Remove references to the legacy video player in
the thumb preview.

* fix: resolve commit suggestions.

Subclass QSlider to handle click events
and allow for easier seeking.

Implement context menu along with autoplay
setting for the media widget.

Pause video when media player is clicked
instead of opening file.

* fix: start media muted

Start video/audio muted on initial load of
the media player.

Remove code causing mypy issue.

Add new method for getting slider click state.

* refactor: use layouts instead of manual positioning.

Add various layouts for positioning widgets instead
of manually moving widgets.

Change the volume slider orientation at smaller
media sizes.

* fix: color position label white

Fix position label color to white so it stays visible
regardless of theme.

* fix: allow dragging slider after click

* Apply suggestions from code review

fix: apply suggestions from code review.

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* fix: remove references to legacy video player.

Combine the stats logic for video/audio into one method.

Fix several issues after incorrectly implementing suggestions.

* fix: add loop setting and other actions.

* refactor: simplify widget state management.

Make a single method to control widget state.

Works with the main QStackLayout and cleans up
widget state if it is needed (i.e., stopping the media
player when switching to a different preview).

* fix: add pages to QStackLayout to fix widget position.

Fixes a regression in commit 4c6934. We need the pages
to properly center the widgets in the QStackLayout.

* fix: ensure media_player doesn't exceed maximum size if thumbnail.

Fix and issue where the media_player would expand past the
thumbnail on resize.

* refactor: move settings to new system
2025-03-30 19:23:24 -07:00
Leonard2
27fb54ed65 build: update spec file to use proper pathex and datas paths (#895)
* build: add "src" to pathex.

Instead of having to pre-install tagstudio, this will have PyInstaller directly build it from source.

* build: be more selective about the data files.

Since PyInstaller builds from source now there is no need to manually add it.

---------

Co-authored-by: Léonard <Leeooonaard@gmail.com>
2025-03-30 17:34:01 -07:00
Jann Stute
e3b2eaf96a perf: improve responsiveness of GIF entries (#894)
* feat: create QMovie with file path instead of copying image around

* fix: make ignore pyright specific so mypy doesn't complain

* fix: restore ability to delete file while viewing it
2025-03-30 17:21:46 -07:00
Jann Stute
33dd330c73 fix: close pdf file object in thumb renderer (#893) 2025-03-30 16:36:04 -07:00
Emmanuel Ferdman
1c02e75dd9 docs: update ThumbRenderer source (#896)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-03-30 09:18:37 -07:00
Xarvex
6400236823 fix(nix/package): add missing dependencies from #859
Adds toml and pydantic as Python packages, and ignore tests that require
mutating files.

Fixes: #890
2025-03-26 17:26:17 -05:00
Travis Abendshien
a4e61f9387 fix: remove unescaped ampersand from "about.description" (#885) 2025-03-25 15:04:45 -07:00
Jann Stute
adb996e1d2 feat: new settings menu + settings backend (#859)
* feat: add tab widget

* refactor: move languages dict to translations.py

* refactor: move build of Settings Modal to SettingsPanel class

* feat: hide title label

* feat: global settings class

* fix: initialise settings

* fix: properly store grid files changes

* fix: placeholder text for library settings

* feat: add ui elements for remaining global settings

* feat: add page size setting

* fix: version mismatch between pydantic and typing_extensions

* fix: update test_driver.py

* fix(test_file_path_options): replace patch with change of settings

* feat: setting for dark mode

* fix: only show restart_label when necessary

* fix: change modal from "done" type to "Save/Cancel" type

* feat: add test for GlobalSettings

* docs: mark roadmap item as completed

* fix(test_filepath_setting): Mock the app field of QtDriver

* Update src/tagstudio/main.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* fix: address review suggestions

* fix: page size setting

* feat: change dark mode option to theme dropdown

* fix: test was expecting wrong behaviour

* fix: test was testing for correct behaviour, fix behaviour instead

* fix: test fr fr

* fix: tests fr fr fr

* fix: tests fr fr fr fr

* fix: update test

* fix: tests fr fr fr fr fr

* fix: select all was selecting hidden entries

* fix: create more thumbitems as necessary
2025-03-25 15:02:53 -07:00
Weblate (bot)
e112788466 translations: update Hungarian, Spanish, French, Toki Pona (#884)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (302 of 302 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/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (302 of 302 strings)

Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (302 of 302 strings)

Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 75.8% (229 of 302 strings)

Translated using Weblate (Toki Pona)

Currently translated at 75.9% (227 of 299 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2025-03-25 15:01:01 -07:00
Travis Abendshien
64dc88afa9 fix(ui): display 0 frame webp files in preview panel 2025-03-22 12:09:31 -07:00
VasigaranAndAngel
477173661a refactor: type hints and improvements in file_opener.py (#876)
* type fixes and minor improvements

* remove `# noqa: N802`
2025-03-21 08:30:25 -07:00
Travis Abendshien
9d60c78adc ci: update pyright rules
- Ignore "reportIgnoreCommentWithoutRule"
- Ignore "reportMissingTypeArgument"
2025-03-20 01:15:45 -07:00
Travis Abendshien
5b5e878a69 fix: use UNION instead of UNION ALL (#877) 2025-03-20 01:09:46 -07:00
Travis Abendshien
d1eb7d646e fix: hide mnemonics on macOS (#856) 2025-03-20 01:09:23 -07:00
Travis Abendshien
880ca07a6f fix: stop ffmpeg cmd windows, refactor ffmpeg_checker (#855)
* fix: remove log statement as it is redundant (#840)

* refactor: rework ffmpeg_checker.py

Move backend logic from ffmpeg_checker.py to vendored/ffmpeg.py, add translation strings for ffmpeg_checker, update vendored/ffmpeg.py

* fix: stop ffmpeg cmd windows, fix version outputs

* chore: ensure stdout is cast to str

---------

Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>
2025-03-20 01:09:02 -07:00
Weblate (bot)
0701a45e75 translations: add Japanese, Viossa; update others (#861)
* Translated using Weblate (Russian)

Currently translated at 89.9% (269 of 299 strings)

Co-authored-by: Dott-rus <antonamelin8@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings

* Translated using Weblate (Japanese)

Currently translated at 52.8% (158 of 299 strings)

Added translation using Weblate (Japanese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: needledetector <kenshinzumi.mail@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/
Translation: TagStudio/Strings

* Translated using Weblate (Czech)

Currently translated at 15.7% (47 of 299 strings)

Translated using Weblate (Czech)

Currently translated at 14.3% (43 of 299 strings)

Co-authored-by: Filip Jaruška <filip.jaruska@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/cs/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (299 of 299 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/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (299 of 299 strings)

Translated using Weblate (Spanish)

Currently translated at 96.9% (290 of 299 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (299 of 299 strings)

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/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 67.8% (203 of 299 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

* Translated using Weblate (Viossa)

Currently translated at 21.0% (63 of 299 strings)

Translated using Weblate (Viossa)

Currently translated at 21.0% (63 of 299 strings)

Added translation using Weblate (Viossa)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/qpv/
Translation: TagStudio/Strings

---------

Co-authored-by: Dott-rus <antonamelin8@gmail.com>
Co-authored-by: needledetector <kenshinzumi.mail@gmail.com>
Co-authored-by: Filip Jaruška <filip.jaruska@gmail.com>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2025-03-20 01:07:58 -07:00
Gawi
35f409089a docs: fix typos and grammar (#879) 2025-03-20 00:43:34 -07:00
Xarvex
010a4524d6 fix(flake): remove pinned input, only consume in Nix shell (#872) 2025-03-17 18:36:06 -07:00
Xarvex
861df898e2 feat(resources): provide desktop file (#870)
* feat(resources): provide desktop file

Co-authored-by: Florian Zier <9168602+zierf@users.noreply.github.com>

* fix(ts_qt): remove duplicate logic

* fix(ts_qt): add fallback values

---------

Co-authored-by: Florian Zier <9168602+zierf@users.noreply.github.com>
2025-03-17 18:35:45 -07:00
Xarvex
0ac06a125a fix(ui): do not set palette for Linux-like systems that offer theming (#869) 2025-03-17 18:34:04 -07:00
Xarvex
b8ee63ef73 fix(nix/package): account for GTK platform (#868) 2025-03-17 18:33:33 -07:00
Xarvex
a5e535ba78 feat(ci): development tooling refresh and split documentation (#867)
* feat(nix/shell): use uv for faster evaluation

* feat(contrib): define developer configurations

* feat(ci): configure pre-commit to use project dependencies, add mypy

* fix(docs): typo

* docs: split develop and install, document integrations

* nit(contrib): add shellcheck directive to envrc's

* docs: move third-party dependencies to install page

* nit(flake): use pythonPackages variable

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-03-17 18:32:08 -07:00
Travis Abendshien
ed6ac246f4 docs: update schema_changes.md for DB_VERSION 9 2025-03-12 17:40:07 -07:00
Jann Stute
31833245a4 feat: add filename and path sorting (#842)
* feat: add filename sorting to dropdown

* feat: add file path sorting to dropdown

* feat: implement path sorting

* feat: add filename column and bump db version

* feat: implement filename sorting

* doc: tick off roadmap item for filename sorting

* fix: use existing filename translation instead

* fix: populate Entry.filename in constructor

* fix: add missing assertion in search_library fixture

* fix: update search test library

* feat: add db migration test

* fix: add missing library for test
2025-03-12 17:28:42 -07:00
Weblate (bot)
93dcfdd51c translations: update Turkish, Portuguese(BR), Hungarian, French (#857)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (294 of 294 strings)

Co-authored-by: Nyghl <hknimre@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tr/
Translation: TagStudio/Strings

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 89.1% (262 of 294 strings)

Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (294 of 294 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/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (294 of 294 strings)

Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

---------

Co-authored-by: Nyghl <hknimre@gmail.com>
Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
2025-03-12 17:27:53 -07:00
HermanKassler
f680ecb648 feat: add setting to not display full filepath (#841)
* feat: add show file path option to settings menu (#4)

* feat: implement file path option for file attributes (#10)

* feat: implement file path option for file attributes

---------

Co-authored-by: Zarko Sesto <sesto@kth.se>

* feat: update ui after changing file path setting (#9)

* feat: implement file path options for main window (#11)

Co-authored-by: Zarko Sesto <sesto@kth.se>

* feat: add realtime feedback for setting changes (#13)

Co-authored-by: Zarko Sesto <sesto@kth.se>

style: formatted code changes with ruff  (#16)

* tests: Added tests to test file path display and settings menu

enhancement: formated using RUFF

test: addeded test to check title bar update

* fix: move check for file option to correct spot in open_library (#17)

* fix: change translate_with_setter to new method (#18)

* fix: update call to translator to be inline with upstream (#19)

* refactor: update some imports to reflect refactor on upstream (#20)

* refactor: update import paths to reflect recent reformat

* format: run ruff format

* translation: add translations for filepath setting

* refactor: store filepath options in integer enum

---------

Co-authored-by: RubenSocha <40490633+RubenSocha@users.noreply.github.com>
Co-authored-by: ErzaDuNord <102242407+ErzaDuNord@users.noreply.github.com>
Co-authored-by: Zarko Sesto <sesto@kth.se>
Co-authored-by: BitGatito <usmanbinmujeeb@gmail.com>
2025-03-12 15:51:55 -07:00
Travis Abendshien
234ddec78b refactor: remove unused ui files
Remove unused UI files (Port #605)

Co-Authored-By: VasigaranAndAngel <72515046+VasigaranAndAngel@users.noreply.github.com>
2025-03-12 03:35:35 -07:00
Travis Abendshien
9858ad1078 translations: sort en.json file 2025-03-10 19:06:41 -07:00
Travis Abendshien
b0047b2065 refactor: split translation keys for about screen (#845)
* fix(translations): remove errant `<b>` tag

* refactor: split translation keys for about screen

* ui: change form field style

* fix: split new translation keys

* fix: remove unused key

* translations: re-split toki pona "about.content"
2025-03-10 19:02:34 -07:00
Weblate (bot)
039cebddae translations: update Turkish, Danish, Spanish, French, Toki Pona (#852)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Nyghl <hknimre@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tr/
Translation: TagStudio/Strings

* Translated using Weblate (Danish)

Currently translated at 4.4% (13 of 289 strings)

Co-authored-by: Emma J. M <em@juulsgaard.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/da/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 63.3% (183 of 289 strings)

Translated using Weblate (Toki Pona)

Currently translated at 57.4% (166 of 289 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Nyghl <hknimre@gmail.com>
Co-authored-by: Emma J. M <em@juulsgaard.org>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2025-03-10 18:47:31 -07:00
Jann Stute
e308e8e80d fix: log all problems in translations test (#839) 2025-03-10 18:41:40 -07:00
Travis Abendshien
8c90f21c13 fix: catch NotImplementedError for Float16 JPEG-XL files (#849) 2025-03-10 00:58:09 -07:00
Travis Abendshien
e0fb73117f ci: exclude "build" and "dist" folders from mypy 2025-03-09 23:28:56 -07:00
Travis Abendshien
55d4f1882b docs: add horizontal rules to install instructions 2025-03-09 23:18:04 -07:00
Travis Abendshien
301157df00 fix(docs): remove incorrect format flags 2025-03-09 22:38:39 -07:00
Weblate (bot)
7c48cdefab translations: update translations (#846)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nyghl <hknimre@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tr/
Translation: TagStudio/Strings

* Translated using Weblate (Filipino)

Currently translated at 59.5% (172 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: searinminecraft <kitakita@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/
Translation: TagStudio/Strings

* Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/
Translation: TagStudio/Strings

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.5% (256 of 289 strings)

Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/
Translation: TagStudio/Strings

* Translated using Weblate (German)

Currently translated at 99.3% (287 of 289 strings)

Translated using Weblate (German)

Currently translated at 98.6% (285 of 289 strings)

Co-authored-by: DontBlameMe99 <github@addymem.anonaddy.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jann Stute <jann.stute@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Strings

* Translated using Weblate (Russian)

Currently translated at 90.3% (261 of 289 strings)

Translated using Weblate (Russian)

Currently translated at 82.3% (238 of 289 strings)

Translated using Weblate (Russian)

Currently translated at 79.2% (229 of 289 strings)

Translated using Weblate (Russian)

Currently translated at 78.8% (228 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: werdei <6pe3ep1lu@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 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/Strings

* Translated using Weblate (Spanish)

Currently translated at 95.8% (277 of 289 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/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

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/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 49.4% (143 of 289 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Nyghl <hknimre@gmail.com>
Co-authored-by: searinminecraft <kitakita@disroot.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: DontBlameMe99 <github@addymem.anonaddy.com>
Co-authored-by: Jann Stute <jann.stute@protonmail.com>
Co-authored-by: werdei <6pe3ep1lu@mozmail.com>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2025-03-09 22:22:06 -07:00
Xarvex
42577b792c Merge pull request #844 from TagStudioDev/layout-refactor-1
refactor!: restructure project to use proper src-layout
2025-03-09 23:25:13 -05:00
Xarvex
55bc7aac88 refactor!: change layout; import and build change
Fixes: #200
Fixes: #365
Fixes: #512
Fixes: #800

fix(pyproject): resolve mix-up of mypy and pytest

chore(ci): remove legacy scripts

chore: format with new mypy rules; fix translation test

wip(ci/mypy): remove config flag

fix(pyinstaller): use correct dict access

fix(resources): usage in ts_qt.py

feat(nix/package): validate tests with pytest hook

fix(nix/package): remove old dependency patch

feat(nix): support Darwin

fix(nix/package): move check deps to checkInputs

fix(nix/shell): typo

fix(nix/shell): correctly wrap Python with Qt args

fix(pyproject): specify mypy-extensions

feat(nix/package): provide pillow-jxl-plugin

nix(nix/package): split into multiple files, allow overriding of JXL and vtf2img

fix(nix/shell): provide FFmpeg on runtime

feat(flake): output pillow-jxl-plugin and vtf2img

fix(nix/package): load pipewire

feat(nix/package): run tests on pillow-jxl-plugin

fix: remove extra noqa comment

docs: update installation docs

docs: shrink table size on docs site

nit(nix/package): pipewire not needed in buildInputs

docs: update commands, environment, setup

fix: use consistent possessives

chore: format with prettier, add ignore flags

fix(pyinstaller): consume from pyproject

Revert "fix(pyinstaller): consume from pyproject"

This reverts commit 398cd4e5630a3e83d22d15286d7ac59b4c07c5d6.

refactor: use icon from resource manager

Also fixes incorrect path currently used in ts_qt.py.

nix(pyinstaller): replace use of sys.platform with platform.system

docs: add build section

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-03-09 18:55:32 -05:00
Xarvex
226d18e743 refactor!: change layout; renaming, import and build change incoming 2025-03-09 18:54:25 -05:00
Travis Abendshien
981cc60135 chore: bump version to v9.5.1 2025-03-06 16:32:19 -08:00
Travis Abendshien
2073a284fe fix: separate about screen title from translations (#836)
* fix: separate about screen title from translations

* chore: format with ruff's evil twin
2025-03-06 16:22:54 -08:00
Jann Stute
6d1ff90355 test: add tests for translations (#833)
* refactor: simplify pathlib imports

* feat: add tests for invalid format keys and missing / unnecessary translations

* feat: parametrise format key test

* feat: parametrise completeness test

* fix: include more useful information in the error message

* refactor: remove unused method

* refactor: extract translation loading to a separate function

* fix: include more useful information in the error message

* fix: don't fail test when language hasn't been completely translated
2025-03-06 15:28:48 -08:00
Weblate (bot)
4fe76f13fd fix(translations): fix invalid placeholders (#835)
* Translated using Weblate (Turkish)

Currently translated at 24.9% (72 of 289 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/tr/
Translation: TagStudio/Strings

* Translated using Weblate (Filipino)

Currently translated at 44.6% (129 of 289 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/fil/
Translation: TagStudio/Strings

* Translated using Weblate (Tamil)

Currently translated at 24.9% (72 of 289 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/ta/
Translation: TagStudio/Strings

* Translated using Weblate (Danish)

Currently translated at 3.8% (11 of 289 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/da/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 96.1% (278 of 289 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/es/
Translation: TagStudio/Strings

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 17.9% (52 of 289 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/nb_NO/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 36.3% (105 of 289 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/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2025-03-06 15:22:57 -08:00
Weblate (bot)
6690f5bc09 translations: update Portuguese(BR), German, Russian, Spanish, French; fix formatting keys (#826)
* Translated using Weblate (Turkish)

Currently translated at 22.4% (65 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tr/
Translation: TagStudio/Strings

* Translated using Weblate (Filipino)

Currently translated at 43.9% (127 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/
Translation: TagStudio/Strings

* Translated using Weblate (Tamil)

Currently translated at 22.4% (65 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/
Translation: TagStudio/Strings

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.9% (257 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.9% (257 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.7% (187 of 289 strings)

Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
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/pt_BR/
Translation: TagStudio/Strings

* Translated using Weblate (German)

Currently translated at 98.9% (286 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Theasacraft <spam@accounts.samuelbellmann.de>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Strings

* Translated using Weblate (Danish)

Currently translated at 3.8% (11 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/da/
Translation: TagStudio/Strings

* Translated using Weblate (Russian)

Currently translated at 73.3% (212 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: werdei <6pe3ep1lu@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings

* Translated using Weblate (Polish)

Currently translated at 97.5% (282 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pl/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 95.8% (277 of 289 strings)

Translated using Weblate (Spanish)

Currently translated at 95.8% (277 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Aless <dev@aless.ch>
Co-authored-by: Bamowen <mathieu.monsauret@gmail.com>
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/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Swedish)

Currently translated at 25.2% (73 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/sv/
Translation: TagStudio/Strings

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 68.8% (199 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hant/
Translation: TagStudio/Strings

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 16.9% (49 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nb_NO/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 33.2% (96 of 289 strings)

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: Theasacraft <spam@accounts.samuelbellmann.de>
Co-authored-by: werdei <6pe3ep1lu@mozmail.com>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Co-authored-by: Aless <dev@aless.ch>
Co-authored-by: Bamowen <mathieu.monsauret@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
2025-03-06 14:02:21 -08:00
Travis Abendshien
7e75087b24 fix: restore translate_formatted() method as format() (#830)
* fix: restore `translate_formatted()` method

* fix: set "Create & Add" button text

* refactor: rename "translate_formatted" to "format"

* translations: replace invalid format key names with "{unknown_key}"
2025-03-06 13:05:56 -08:00
Paula Rumeu
07b7d40926 docs: fix category typo (#834) 2025-03-06 03:54:44 -08:00
Travis Abendshien
80aab1ec96 chore: tweak pull request template 2025-03-04 14:27:40 -08:00
Travis Abendshien
bcf3b2f96b fix: prevent future library versions from being opened 2025-03-03 15:02:35 -08:00
Travis Abendshien
d94befed5d Merge branch 'main' of https://github.com/TagStudioDev/TagStudio 2025-03-03 14:18:25 -08:00
Travis Abendshien
d91ee5dbf7 chore: bump version to v9.5.0 2025-03-03 14:16:44 -08:00
Travis Abendshien
ea8d074c51 chore: remove leftover log statement 2025-03-03 14:16:34 -08:00
Travis Abendshien
186c6e2139 ui: change default tag color to white 2025-03-03 14:15:10 -08:00
Xarvex
c695f1765f chore(ci): add PR template (#824) 2025-03-03 14:14:46 -06:00
Travis Abendshien
077f91af88 ui: don't show the deprecated "Autofill" macro 2025-03-03 11:39:32 -08:00
Travis Abendshien
accf50ccf9 ui: change default sorting direction to descending 2025-03-03 11:32:41 -08:00
Travis Abendshien
9f9a5ccf44 fix: catch OverflowError when displaying video duration 2025-03-03 11:26:07 -08:00
Weblate (bot)
ee23ef3451 translations: update Filipino, Portuguese(BR), German, Hungarian, Polish, Spanish, French (#809)
* Translated using Weblate (Filipino)

Currently translated at 43.9% (127 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: searinminecraft <114207889+searinminecraft@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/
Translation: TagStudio/Strings

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.7% (187 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 63.3% (183 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.4% (143 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.4% (143 of 289 strings)

Co-authored-by: Alexander Lennart Formiga Johnsson <alexander_formiga@hotmail.com>
Co-authored-by: DaviMarquezeli <Davi.Marquezeli@gmail.com>
Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/
Translation: TagStudio/Strings

* Translated using Weblate (German)

Currently translated at 98.9% (286 of 289 strings)

Translated using Weblate (German)

Currently translated at 98.9% (286 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: JoeJoeTV <hannes1033@gmail.com>
Co-authored-by: Kurt S <mail@kurtys.de>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 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/Strings

* Translated using Weblate (Polish)

Currently translated at 97.5% (282 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: qronikarz <qronikarz@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pl/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 57.7% (167 of 289 strings)

Translated using Weblate (Spanish)

Currently translated at 55.0% (159 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Obscaeris <translation@obscaeris.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

---------

Co-authored-by: searinminecraft <114207889+searinminecraft@users.noreply.github.com>
Co-authored-by: Alexander Lennart Formiga Johnsson <alexander_formiga@hotmail.com>
Co-authored-by: DaviMarquezeli <Davi.Marquezeli@gmail.com>
Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: JoeJoeTV <hannes1033@gmail.com>
Co-authored-by: Kurt S <mail@kurtys.de>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: qronikarz <qronikarz@users.noreply.hosted.weblate.org>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Obscaeris <translation@obscaeris.com>
2025-03-03 11:17:33 -08:00
Jann Stute
2a787592b6 fix: resouce leak in translate_with_setter by deleting the offending code (#817)
* feat: implement first class for translated widget

* feat: add further translated widget types

* refactor: replace usages of translate_qobject with appropriate classes

* refactor: remove wrapper classes

* refactor: remove unnecessary used of Translations.formatted

* refactor: remove further unnecessary format calls

* refactor: replace calls for Translations.formatted with calls to str.format and remove some unused code

* refactor: remove TranslatedString class

* refactor: replace translate_with_setter with direct calls to the setter
2025-03-03 11:13:49 -08:00
Travis Abendshien
152c2b20ba docs: add "sort by filename" to roadmap 2025-03-02 22:13:00 -08:00
Travis Abendshien
1b5b40792c Create FUNDING.yml 2025-03-02 21:58:42 -08:00
SkeleyM
4b381e78ba docs: fix slight spelling error (#820) 2025-02-27 14:23:05 -08:00
Travis Abendshien
b1126d5313 fix: open libraries from v9.5.0-pr1 in newer versions (#815)
* ui: show more informative library error messages

* tests: add sqlite db migration tests

* tests: fix and refactor migration tests

* fix: apply db8 schema changes before repairing db6

* docs: add save file format change log

* chore: remove db version explanations from docstrings
2025-02-24 15:14:54 -08:00
Travis Abendshien
f9ca743b64 docs: add custom color info 2025-02-20 15:14:51 -08:00
Travis Abendshien
04bd1bc027 fix(docs): restore broken links, ignore callout formatting 2025-02-20 14:24:12 -08:00
Haider Ali
ecc0bb57fb docs: change "Tag Manager" to "Manage Tags" (#806) 2025-02-17 20:42:48 -08:00
Travis Abendshien
d11b514bab fix: use remove() instead of difference() for self collision checks 2025-02-17 15:39:43 -08:00
Travis Abendshien
fe94d84b94 ci: ignore specific pyright flags 2025-02-17 14:52:59 -08:00
Travis Abendshien
db408f09c4 chore: bump version to v9.5.0-pr4 2025-02-17 14:47:22 -08:00
Travis Abendshien
61b9fcf764 feat(ui): add language setting (#803)
* feat(ui): add language setting

* translations: implement remaining todos

* ui: show language names in settings instead of codes

* translations: add Dutch setting, anticipating #798
2025-02-17 14:45:30 -08:00
Travis Abendshien
28de21ade7 feat(ui)!: user-created tag colors (#801)
* feat: custom tag colors

* ui: minor ui polish

* ui: add confirmation for deleting colors

* ui: match tag_color_preview focused style

* ui: reduce spacing between color swatch groups

* ui!: change default behavior of secondary color

The secondary color now acts as only the text color by default, with the new `color_border` bool serving to optionally restore the previous text + colored border behavior.

* ui: adjust focused tag/color button styles

* fix: avoid namespace collision

* fix: make reserved namespace check case-insensitive

* ui: add namespace description + prompt

* fix: don't reset tag color if none are chosen

* refactor(ui): use form layout for build_color

* fix(ui): dynamically scale field title widget

* feat(ui): add additional tag shade colors

Add "burgundy", "dark-teal", and "dark_lavender" tag colors.

* fix: don't check for self in collision checks

* fix: update tag references on color update

* fix(ui): stop fields widgets expanding indefinitely
2025-02-17 14:36:34 -08:00
Weblate (bot)
2173d1d4f4 translations: add Dutch; update Hungarian, Spanish, French (#798)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (259 of 259 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/Strings

* Translated using Weblate (Dutch)

Currently translated at 40.1% (104 of 259 strings)

Added translation using Weblate (Dutch)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Pheubel <luca.guagliardo@live.nl>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nl/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 51.7% (134 of 259 strings)

Translated using Weblate (Spanish)

Currently translated at 51.3% (133 of 259 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Co-authored-by: noceno <pasitobajet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (French)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (French)

Currently translated at 100.0% (259 of 259 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Obscaeris <translation@obscaeris.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

---------

Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Pheubel <luca.guagliardo@live.nl>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Co-authored-by: noceno <pasitobajet@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: Obscaeris <translation@obscaeris.com>
2025-02-17 14:33:21 -08:00
SkeleyM
c1ec8a6650 feat(ui): clickable links in about section (#799) 2025-02-14 09:32:00 -08:00
Travis Abendshien
69d3a6ed09 docs: update library search 2025-02-14 09:25:57 -08:00
Travis Abendshien
e481ab64c9 docs: consolidate feature roadmap 2025-02-14 08:42:44 -08:00
Travis Abendshien
986ccabc81 chore: bump version to v9.5.0-pr3 2025-02-10 11:22:00 -08:00
Travis Abendshien
297fdf22e8 feat: add smartcase and globless path searches (#743)
* fix: return path_strings in session

* feat: add smartcase and globless path search

Known issues: failing tests, sluggish autocomplete

* fix: all operational searches

* fix: limit path autocomplete to 100 items

* tests: add test cases
2025-02-10 10:26:02 -08:00
Weblate (bot)
319ef9a5fe translations: update Danish, Hungarian (#785)
* Translated using Weblate (Danish)

Currently translated at 4.6% (11 of 237 strings)

Co-authored-by: Jumle <juliusv135@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/da/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (237 of 237 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (236 of 236 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/Strings

---------

Co-authored-by: Jumle <juliusv135@gmail.com>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
2025-02-10 10:23:58 -08:00
Travis Abendshien
6b646f8955 perf: optimize query methods and reduce preview panel updates (#794)
* fix(ui): don't update preview for non-selected badge toggles

* ui: optimize badge updating

NOTE: In theory this approach opens the doors for a visual state mismatch between the tags shown and the tags on the actual entry. The performance increase comes from the fact that `get_entry_full()` isn't called, which is the real bottleneck here.

* perf: optimize query methods
2025-02-09 18:33:47 -08:00
Travis Abendshien
a2b9237be4 fix(ui): expand usage of esc for closing modals (#793)
* fix(ui): expand usage of esc for closing modals

* chore: remove log statements

* refactor: use Qt enum in place of magic number

* ui: use enter key to save panel widgets
2025-02-09 18:33:29 -08:00
ChloeVZ
abc7cc3915 docs: improve python and venv setup instructions in CONTRIBUTING.md (#791)
* docs: refer to pyenv in CONTRIBUTING.md

This commit was made in order to account for users who may already have
a Python installation that isn't 3.12.

A good real world scenario where this may be a problem is users running
on rolling release Linux distributions, such as Arch Linux which
provides bleeding edge updates in a weekly basis.

Continuing with Arch Linux as an example, it provides Python 3.13, which
the current version of TagStudio doesn't seem to work with, an issue I
encountered myself this morning.

In some systems like Windows, this may not matter, but it is an issue
nonetheless, and users using a package manager centric system can't be
expected to downgrade/upgrade their Python version for TagStudio, as
this may cause issues in their system.

The easiest way to solve this is with pyenv, which enables users to
install other Python versions independent from each other, aside from
the system install, and allows specifying a version to use for a
directory (And its subdirectories), the current terminal session, or the
entire system if desired.

Here is a short, technical and objective summary of the changes:
- Updated `.gitignore` to to ignore `.python-version` which pyenv uses.
- Updated CONTRIBUTING.md to refer to pyenv for versioning issues.
    - pyenv is listed as a prerequisite, specifying in parenthesis that it's only for when your Python install isn't 3.12.
    - The "Creating a Python Virtual Environment" section now has an additional step at the beginning, to check that the current Python installation is 3.12.
        - As part of this added step, there are steps to use pyenv to install Python 3.12 separately from the existing Python installation, and activate it for the TagStudio root folder, in case it's not the appropriate version.
    - The numbering of the other steps was offset accordingly.
    - The "Manually Launching (Outside of an IDE)" section now refers to the above first step, should the user encounter some kind of error.

* docs: refer to alternate shells in CONTRIBUTING.md

Some users may not be using the default shell for their system, and
Linux users are the most likely to do this, though this can be seen in
macOS as well. People will often use shells like fish which is not POSIX
compliant but works very well and has great auto-completion, it's very
user friendly.

The reason why I care about this is that the instructions specify to use
.venv/bin/activate which is a bash script and will not run on fish or
csh, and if you take a look at the script, indeed it says it will only
run with bash.

Python will provide alternate scripts for other shells, and at least on
my system (CachyOS), those are scripts for Powershell, bash, fish and
csh. People using these alternate shells, who don't know this may be
confused when running the script referenced in CONTRIBUTING.md and
seeing it fail completely.

The only real change in this commit was adding under the "Linux/macOS"
command in step 3 (step 2 prior to my last commit) of the "Creating a
Python Virtual Environment" section, a short instruction to switch to
the default shell, or use the appropriate script for the user's
preferred shell if any (Since, at least on my system, there aren't
scripts for every shell, for example there is no activate.zsh file).

* fix: modal windows now have the Qt.Dialog flag

Set the Qt.Dialog flag for modal windows, such as "Add Field" so that
they will be seen as dialogs in the backend. This change is unlikely to
be at all noticeable in systems like Windows, but for users running
tiling window managers like bspwm or Sway, this will prevent these modal
windows from being tiled alongside the main window, instead they will be
floating on top, which is the expected behaviour seen on floating window
managers, like the ones used by Windows and macOS.

Added self.setWindowFlag(Qt.Dialog, on=True) # type: ignore to the
following files:
- tagstudio/src/qt/modals/add_field.py
- tagstudio/src/qt/modals/delete_unlinked.py
- tagstudio/src/qt/modals/drop_import.py
- tagstudio/src/qt/modals/file_extension.py
- tagstudio/src/qt/modals/fix_dupes.py
- tagstudio/src/qt/modals/fix_unlinked.py
- tagstudio/src/qt/modals/folders_to_tags.py
- tagstudio/src/qt/modals/mirror_entities.py
- tagstudio/src/qt/widgets/paged_panel/paged_panel.py
- tagstudio/src/qt/widgets/panel.py
- tagstudio/src/qt/widgets/progress.py

Note that without adding # type: ignore, MyPy *will* give out an error,
presumably because PySide6 is missing type hints for several things, and
this may *or* may not be a justifiable use of # type: ignore, which
throws the MyPy type checking for that line out the window.

Ref: #392, #464

* fix: I forgot to run ruff format

* revert: changes to docs and gitignore

This revert is for moving the referenced changes to their own pull request.

Refs: 15f4f38, a25ef8c

* docs: other shells and pyenv in CONTRIBUTING.md

This commit changes CONTRIBUTING.md to refer to pyenv in the case of
issues with Python versions, as well as downloading the correct version
off of the Python website alternatively. It also now elaborates on the
process of running the Python virtual environment on Linux and macOS, by
referencing activation scripts for alternative shells such as fish and
CSH.

The table added to CONTRIBUTING.md is taken from the official Python
documentation: https://docs.python.org/3.12/library/venv.html

* revert: accidental inclution of #464

Refs: a51550f, 5854ccc
2025-02-06 21:41:01 -08:00
Travis Abendshien
a3df70bb8d feat: port file trashing (#409) to v9.5 (#792)
* feat: port file trashing (#409) to sql

* translations: translate file deletion actions

* fix: rename method from refactor conflict

* refactor: implement feedback
2025-02-05 19:25:10 -08:00
Travis Abendshien
466af1e6a6 refactor(ui): recycle tag list in TagSearchPanel (#788)
* feat(ui): recycle tag list in `TagSearchPanel`

* chore: address mypy warnings

* fix: order results from sql before limiting

* fix(ui): check for self.exclude before remaking sets

* fix(ui): only init tag manager and file ext manager once

* fix(ui:): remove redundant tag search panel updates

* update code comments and docstrings

* feat(ui): add tag view limit dropdown

* ensure disconnection of file_extension_panel.saved
2025-02-05 19:15:28 -08:00
Travis Abendshien
26d3b1908b fix(ui): hide library actions when no library is open (#787) 2025-02-04 13:53:03 -08:00
Travis Abendshien
f38a79b06e chore: bump version to v9.5.0-pr2 2025-02-03 16:22:02 -08:00
Travis Abendshien
dbf7353bdf fix(ui): improve tagging ux (#784)
* fix(ui): always reset tag search panel when opened

* feat: return parent tags in tag search

Known issue: this bypasses the tag_limit

* refactor: use consistant `datetime` imports

* refactor: sort by base tag name to improve performance

* fix: escape `&` when displaying tag names

* ui: show "create and add" tag with other results

* fix: optimize and fix tag result sorting

* feat(ui): allow tags in list to be selected and added by keyboard

* ui: use `esc` to reset search focus and/or close modal

* fix(ui): add pressed+focus styling to "create tag" button

* ui: use `esc` key to close `PanelWidget`

* ui: move disambiguation button to right side

* ui: expand clickable area of "-" tag button, improve styling

* ui: add "Ctrl+M" shortcut to open tag manager

* fix(ui): show "add tags" window title when accessing from home
2025-02-03 16:15:40 -08:00
Travis Abendshien
480328b83b fix: patch incorrect description type & invalid disambiguation_id refs (#782)
* fix: update DESCRIPTION to the TEXT_BOX type

* fix: remove `disambiguation_id` refs with a tag is deleted
2025-02-03 15:32:11 -08:00
Travis Abendshien
6a54323307 refactor: wrap migration_iterator lambda in a try/except block (#773) 2025-02-03 15:31:43 -08:00
Travis Abendshien
f48b363383 fix: catch ParsingError (#779)
* fix: move `path_strings` var inside `with` block

* refactor: move lambda to local function

* fix: catch `ParsingError` and return no results

* refactor: move `ParsingError` handling out to `QtDriver`

Reverts changes made to `test_search.py` and `enums.py`.
2025-02-03 15:31:12 -08:00
Travis Abendshien
634e1c7fe9 chore: update pyproject.toml 2025-02-02 23:29:53 -08:00
Travis Abendshien
90a826d128 fix(ui): reduce field title width to make room for edit and delete buttons 2025-02-02 17:13:08 -08:00
SkeleyM
93fc28e092 fix: allow tag names with colons in search (#765)
* Refactor allowing colons

* fix formatting
2025-02-02 15:56:50 -08:00
SkeleyM
80c7e81e69 fix: save all tag attributes from "Create & Add" modal (#762) 2025-02-02 13:49:17 -08:00
SkeleyM
f212e2393a fix(docs): fix screenshot sometimes not rendering (#775)
* Fix image not showing up

* formatting
2025-02-02 13:44:34 -08:00
Travis Abendshien
d7958892b7 docs: add more links to index.md 2025-02-01 14:09:17 -08:00
Travis Abendshien
5be7dfc314 docs: add library_search page 2025-02-01 14:06:41 -08:00
Travis Abendshien
6e402ac34d docs: add note about glob searching in the readme 2025-01-31 23:45:31 -08:00
Travis Abendshien
9bdbafa40c docs: add information about "tag manager" 2025-01-31 23:42:37 -08:00
Travis Abendshien
2215403201 fix: don't wrap field names too early 2025-01-31 23:40:47 -08:00
pinhead
16ebd89196 docs: fix typo for "category" in usage.md (#760) 2025-01-31 21:46:30 -08:00
Travis Abendshien
f5ff4d78c1 docs: update field and library pages 2025-01-31 17:23:33 -08:00
361 changed files with 17738 additions and 8889 deletions

View File

@@ -1,16 +0,0 @@
# vi: ft=bash
#
# If you wish to use this file, copy or symlink it to `.envrc` for direnv to read it.
# This will use the flake development shell.
if ! has nix_direnv_version || ! nix_direnv_version 3.0.5; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.5/direnvrc" "sha256-RuwIS+QKFj/T9M2TFXScjBsLR6V3A17YVoEW/Q6AZ1w="
fi
devenv_root_file="$(mktemp -t devenv-root-XXXXXXXX)"
printf %s "${PWD}" >"${devenv_root_file}"
if ! use flake . --override-input devenv-root "file+file://${devenv_root_file}"; then
printf '%s\n' "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
fi
rm "${devenv_root_file}"
unset devenv_root_file

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
---
patreon: cyanvoxel

View File

@@ -1,3 +1,4 @@
---
name: Bug Report
description: File a bug or issue report.
title: '[Bug]: '
@@ -51,7 +52,7 @@ body:
- type: textarea
attributes:
label: Steps to Reproduce
description: Minimal steps neded for the problem to occur.
description: Minimal steps needed for the problem to occur.
placeholder: |
1.
2.

View File

@@ -1,3 +1,4 @@
---
name: Feature Request
description: Suggest a new feature.
title: '[Feature Request]: '

30
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,30 @@
### Summary
<!--
^^^ Summarize the changes done and why they were done above.
By submitting this pull request, you certify that you have read the
[CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md).
IMPORTANT FOR FEATURES: Please verify that a feature request or some other form
of communication with maintainers was already conducted in terms of approving.
Thank you for your eagerness to contribute!
-->
### Tasks Completed
<!-- No requirements, just context for reviewers. -->
- Platforms Tested:
- [ ] Windows x86
- [ ] Windows ARM
- [ ] macOS x86
- [ ] macOS ARM
- [ ] Linux x86
- [ ] Linux ARM
<!-- If an unspecified platform was tested, please add it here -->
- Tested For:
- [ ] Basic functionality
- [ ] PyInstaller executable <!-- Not necessarily required, but appreciated! -->
<!-- If other important criteria was tested for, please add it here -->

View File

@@ -1,38 +1,34 @@
---
name: MyPy
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
mypy:
name: Run MyPy
runs-on: ubuntu-latest
steps:
- name: Checkout code
- name: Checkout repo
uses: actions/checkout@v4
- uses: reviewdog/action-setup@v1
- name: Setup reviewdog
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- uses: actions/setup-python@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache: pip
- name: Install dependencies
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system -r requirements.txt
uv pip install --system mypy==1.11.2
mkdir tagstudio/.mypy_cache
uv pip install --system .[mypy]
- uses: tsuyoshicho/action-mypy@v4
- name: Execute MyPy
uses: tsuyoshicho/action-mypy@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-check
fail_on_error: true
workdir: tagstudio
level: error
mypy_flags: --config-file ../pyproject.toml

View File

@@ -1,19 +1,20 @@
name: Publish Docs
---
name: Publish Docs
on:
push:
branches:
- main
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'CHANGELOG.md'
- '.github/workflows/publish_docs.yaml'
- .github/workflows/publish_docs.yaml
- docs/**
- mkdocs.yml
- CHANGELOG.md
permissions:
contents: write
concurrency:
concurrency:
group: publish-docs
cancel-in-progress: true
@@ -21,18 +22,28 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
python-version: '3.12'
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[mkdocs]
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mkdocs-material mkdocs-material[imaging]
- run: mkdocs gh-deploy --force
- name: Execute mkdocs
run: mkdocs gh-deploy --force

View File

@@ -1,10 +1,11 @@
---
name: pytest
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
pytest:
name: Run tests
name: Run pytest
runs-on: ubuntu-24.04
steps:
@@ -15,56 +16,54 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pytest]
- name: Install system dependencies
run: |
# dont run update, it is slow
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libxkbcommon-x11-0 \
x11-utils \
libyaml-dev \
libgl1 \
libegl1 \
libgl1 \
libopengl0 \
libpulse0 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-xinerama0 \
libopengl0 \
libxcb-cursor0 \
libpulse0
libxkbcommon-x11-0 \
libyaml-dev \
x11-utils
- name: Install dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system -r requirements.txt
uv pip install --system -r requirements-dev.txt
- name: Run pytest
- name: Execute pytest
run: |
xvfb-run pytest --cov-report xml --cov=tagstudio
- name: Store coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: 'coverage'
path: 'coverage.xml'
name: coverage
path: coverage.xml
coverage:
name: Check Code Coverage
name: Check coverage
runs-on: ubuntu-latest
needs: pytest
steps:
- name: Load coverage
- name: Fetch coverage
uses: actions/download-artifact@v4
with:
name: 'coverage'
name: coverage
- name: Check Code Coverage
- name: Check coverage
uses: yedpodtrzitko/coverage@main
with:
thresholdAll: 0.4

View File

@@ -1,3 +1,4 @@
---
name: Release
on:
@@ -19,15 +20,27 @@ jobs:
suffix: _portable
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -Ur requirements.txt pyinstaller
- run: pyinstaller tagstudio.spec -- ${{ matrix.build-flag }}
- run: tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio
- uses: actions/upload-artifact@v4
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pyinstaller]
- name: Execute PyInstaller
run: |
pyinstaller tagstudio.spec -- ${{ matrix.build-flag }}
tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tagstudio_linux_x86_64${{ matrix.suffix }}
path: dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz
@@ -41,20 +54,35 @@ jobs:
arch: x86_64
- os-version: '14'
arch: aarch64
runs-on: macos-${{ matrix.os-version }}
env:
# even though we run on 12, target towards compatibility
# INFO: Even though we run on 13, target towards compatibility
MACOSX_DEPLOYMENT_TARGET: '11.0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -Ur requirements.txt pyinstaller
- run: pyinstaller tagstudio.spec
- run: tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app
- uses: actions/upload-artifact@v4
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pyinstaller]
- name: Execute PyInstaller
run: |
pyinstaller tagstudio.spec
tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tagstudio_macos_${{ matrix.arch }}
path: dist/tagstudio_macos_${{ matrix.arch }}.tar.gz
@@ -72,30 +100,51 @@ jobs:
build-flag: --portable
suffix: _portable
file-end: .exe
runs-on: windows-2019
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -Ur requirements.txt pyinstaller
- run: PyInstaller tagstudio.spec -- ${{ matrix.build-flag }}
- run: Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
- uses: actions/upload-artifact@v4
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pyinstaller]
- name: Execute PyInstaller
run: |
PyInstaller tagstudio.spec -- ${{ matrix.build-flag }}
Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tagstudio_windows_x86_64${{ matrix.suffix }}
path: dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip
publish:
needs: [linux, macos, windows]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: softprops/action-gh-release@v2
- name: Checkout repo
uses: actions/checkout@v4
- name: Fetch artifacts
uses: actions/download-artifact@v4
- name: Publish release
uses: softprops/action-gh-release@v2
with:
files: |
tagstudio_linux_x86_64/*

View File

@@ -1,20 +1,31 @@
---
name: Ruff
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
ruff-format:
name: Run Ruff format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
- name: Checkout repo
uses: actions/checkout@v4
- name: Execute Ruff format
uses: astral-sh/ruff-action@v3
with:
version: 0.6.4
args: 'format --check'
version: 0.11.0
args: format --check
ruff-check:
name: Run Ruff check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
- name: Checkout repo
uses: actions/checkout@v4
- name: Execute Ruff check
uses: astral-sh/ruff-action@v3
with:
version: 0.6.4
args: 'check'
version: 0.11.8
args: check

7
.gitignore vendored
View File

@@ -88,9 +88,7 @@ profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@@ -267,3 +265,6 @@ TagStudio.ini
.envrc
.direnv
.devenv
result
result-*

View File

@@ -1,7 +1,26 @@
---
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.6.4
- repo: local
hooks:
- id: ruff-format
- id: mypy
name: mypy
entry: mypy
language: system
types_or: [python, pyi]
require_serial: true
- id: ruff
name: ruff
entry: ruff check
language: system
types_or: [python, pyi, jupyter]
args: [--force-exclude]
require_serial: true
- id: ruff-format
name: ruff-format
entry: ruff format
language: system
types_or: [python, pyi, jupyter]
args: [--force-exclude, --check]
require_serial: true

View File

@@ -1,69 +0,0 @@
#! /usr/bin/env bash
# GETTING BASE DIR
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# SETTING UP CONSTANTS
TAGSTUDIO_NAME="TagStudio"
TAGSTUDIO_DIR="$SCRIPT_DIR/tagstudio"
TAGSTUDIO_DIR_RESOURCES="$TAGSTUDIO_DIR/resources"
TAGSTUDIO_ICON="$TAGSTUDIO_DIR/resources/icon.ico"
TAGSTUDIO_SRC="$TAGSTUDIO_DIR/src"
TAGSTUDIO_MAIN="$TAGSTUDIO_DIR/tag_studio.py"
DIST_PATH="$SCRIPT_DIR/dist"
BUILD_PATH="$SCRIPT_DIR/build"
LOGS_PATH="$BUILD_PATH/logs"
printf -- "🏁 Starting Script \n"
# CREATE VENV AND INSTALL REQUIREMENTS
printf -- "🐍 Creating Python virtual env\n"
python3 -m venv .venv
source .venv/bin/activate
if [ ! -d $LOGS_PATH ]; then
printf -- "📁 Creating Logs folder\n"
mkdir -p $LOGS_PATH;
fi
printf -- "💻 Installing Requirements \n"
pip install -r requirements.txt > "$LOGS_PATH/pip.log" 2>&1
pip install PyInstaller > "$LOGS_PATH/pip.log" 2>&1
if [[ "$OSTYPE" == "darwin"* ]]; then
printf -- "🍏 MacOS Detected \n"
SYS_CMD="--windowed"
OS=0
fi
SECONDS=0
# CREATE COMMAND
printf -- "⏳ Building App \n"
COMMAND=$( python -m PyInstaller \
--name "$TAGSTUDIO_NAME" \
--icon "$TAGSTUDIO_ICON" \
--add-data "$TAGSTUDIO_DIR_RESOURCES:./resources" \
--add-data "$TAGSTUDIO_SRC:./src" \
--distpath "$DIST_PATH" \
-p "$TAGSTUDIO_DIR" \
--noconsole \
--workpath "$BUILD_PATH" \
-y "$SYS_CMD" "$TAGSTUDIO_MAIN" \
> "$LOGS_PATH/pyinstaller.log" 2>&1 )
duration=$SECONDS
if $COMMAND; then
printf -- "✅ Build Successfull \n"
printf -- "$((duration)) seconds of build\n"
if [[ "$OS" == 0 ]]; then
printf -- "📁 Opening App folder \n"
open $DIST_PATH
fi
else
printf -- "❌ Error Building the app\nPlease read the logs\navailable at build/logs\n"
fi
printf -- "🏁 END OF TRANSMISSION"

View File

@@ -1,31 +0,0 @@
@echo off
set TAGSTUDIO_NAME=TagStudio
set TAGSTUDIO_DIR=tagstudio
set TAGSTUDIO_DIR_RESOURCES=%TAGSTUDIO_DIR%/resources
set TAGSTUDIO_ICON=%TAGSTUDIO_DIR%/resources/icon.ico
set TAGSTUDIO_SRC=%TAGSTUDIO_DIR%/src
set TAGSTUDIO_MAIN=%TAGSTUDIO_DIR%/tag_studio.py
set BUILD_MODE=--onedir
if "%1" == "--help" (
echo run "%~nx0" for normal Build
echo run "%~nx0 --portable" for Build packaged into one file
goto end
)
if "%1" == "--portable" (
echo Building portable executable...
set BUILD_MODE=--onefile
goto run
)
if not "%1" == "" (
echo Invalid argument run "%~nx0 --help" for help
goto end
)
:run
echo Building executable...
set COMMAND=PyInstaller --name "%TAGSTUDIO_NAME%" --icon "%TAGSTUDIO_ICON%" --add-data "%TAGSTUDIO_DIR_RESOURCES%:./resources" --add-data "%TAGSTUDIO_SRC%:./src" -p "%TAGSTUDIO_DIR%" --console %BUILD_MODE% "%TAGSTUDIO_MAIN%" -y
call .venv\Scripts\activate.bat
%COMMAND%
deactivate
:end

View File

@@ -5,6 +5,805 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [9.5.2] - 2025-03-31
### Added
#### Search
- feat(ui): add setting to not display full filepaths by [@HermanKassler](https://github.com/HermanKassler) in [#841](https://github.com/TagStudioDev/TagStudio/pull/841)
- feat: add filename and path sorting by [@Computerdores](https://github.com/Computerdores) in [#842](https://github.com/TagStudioDev/TagStudio/pull/842)
#### Settings
- feat: new settings menu + settings backend by [@Computerdores](https://github.com/Computerdores) in [#859](https://github.com/TagStudioDev/TagStudio/pull/859)
#### UI
- feat(ui): merge media controls by [@csponge](https://github.com/csponge) in [#805](https://github.com/TagStudioDev/TagStudio/pull/805)
- fix: Remove border from video preview top and left by [@zfbx](https://github.com/zfbx) in [#900](https://github.com/TagStudioDev/TagStudio/pull/900)
- feat(ui): add more default icons and file type equivalencies by [@CyanVoxel](https://github.com/CyanVoxel) in [#882](https://github.com/TagStudioDev/TagStudio/pull/882)
- ui: recent libraries list improvements by [@CyanVoxel](https://github.com/CyanVoxel) in [#881](https://github.com/TagStudioDev/TagStudio/pull/881)
#### Misc
- feat: provide a .desktop file by [@xarvex](https://github.com/xarvex) in [#870](https://github.com/TagStudioDev/TagStudio/pull/870)
### Fixed
- fix: catch NotImplementedError for Float16 JPEG-XL files by [@CyanVoxel](https://github.com/CyanVoxel) in [#849](https://github.com/TagStudioDev/TagStudio/pull/849)
- fix(nix/package): account for GTK platform by [@xarvex](https://github.com/xarvex) in [#868](https://github.com/TagStudioDev/TagStudio/pull/868)
- fix: do not set palette for Linux-like systems that offer theming by [@xarvex](https://github.com/xarvex) in [#869](https://github.com/TagStudioDev/TagStudio/pull/869)
- fix(flake): remove pinned input, only consume in Nix shell by [@xarvex](https://github.com/xarvex) in [#872](https://github.com/TagStudioDev/TagStudio/pull/872)
- fix: stop ffmpeg cmd windows, refactor ffmpeg_checker by [@CyanVoxel](https://github.com/CyanVoxel) in [#855](https://github.com/TagStudioDev/TagStudio/pull/855)
- fix: hide mnemonics on macOS by [@CyanVoxel](https://github.com/CyanVoxel) in [#856](https://github.com/TagStudioDev/TagStudio/pull/856)
- fix: use UNION instead of UNION ALL by [@CyanVoxel](https://github.com/CyanVoxel) in [#877](https://github.com/TagStudioDev/TagStudio/pull/877)
- fix: remove unescaped ampersand from "about.description" by [@CyanVoxel](https://github.com/CyanVoxel) in [#885](https://github.com/TagStudioDev/TagStudio/pull/885)
- fix(ui): display 0 frame webp files in preview panel by [@CyanVoxel](https://github.com/CyanVoxel) in [64dc88a](https://github.com/TagStudioDev/TagStudio/commit/64dc88afa90bb11f3c9b74a2522f947370ce21db)
- fix: close pdf file object in thumb renderer by [@Computerdores](https://github.com/Computerdores) in [#893](https://github.com/TagStudioDev/TagStudio/pull/893)
- perf: improve responsiveness of GIF entries by [@Computerdores](https://github.com/Computerdores) in [#894](https://github.com/TagStudioDev/TagStudio/pull/894)
- fix(ui): seamlessly loop videos by [@CyanVoxel](https://github.com/CyanVoxel) in [#902](https://github.com/TagStudioDev/TagStudio/pull/902)
### Internal Changes
- refactor!: change layout; import and build change by [@xarvex](https://github.com/xarvex) and [@CyanVoxel](https://github.com/CyanVoxel) in [#844](https://github.com/TagStudioDev/TagStudio/pull/844)
- fix: log all problems in translation test by [@Computerdores](https://github.com/Computerdores) in [#839](https://github.com/TagStudioDev/TagStudio/pull/839)
- refactor: split translation keys for about screen by [@CyanVoxel](https://github.com/CyanVoxel) in [#845](https://github.com/TagStudioDev/TagStudio/pull/845)
- feat(ci): development tooling refresh and split documentation by [@xarvex](https://github.com/xarvex) in [#867](https://github.com/TagStudioDev/TagStudio/pull/867)
- refactor: type hints and improvements in file_opener.py by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#876](https://github.com/TagStudioDev/TagStudio/pull/876)
- build: update spec file to use proper pathex and datas paths by [@Leonard2](https://github.com/Leonard2) in [#895](https://github.com/TagStudioDev/TagStudio/pull/895)
- refactor: fix various missing and broken type hints@VasigaranAndAngel in [#901](https://github.com/TagStudioDev/TagStudio/pull/901)
- refactor: fix type hints and overrides in flowlayout.py by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#880](https://github.com/TagStudioDev/TagStudio/pull/880)
### Documentation
- docs: fix typos and grammar by [@Gawidev](https://github.com/Gawidev) in [#879](https://github.com/TagStudioDev/TagStudio/pull/879)
- docs: update `ThumbRenderer` source by [@emmanuel-ferdman](https://github.com/emmanuel-ferdman) in [#896](https://github.com/TagStudioDev/TagStudio/pull/896)
### Translations
- Added Japanese (50%)
- [@needledetector](https://github.com/needledetector)
- Updated Turkish (93%)
- [@Nyghl](https://github.com/Nyghl)
- Updated Filipino (57%)
- [@searinminecraft](https://github.com/searinminecraft)
- Updated Tamil (92%)
- [@TamilNeram](https://github.com/TamilNeram)
- Updated Portuguese (Brazil) (83%)
- [@viniciushelder](https://github.com/viniciushelder)
- Updated German (95%)
- [@DontBlameMe99](https://github.com/DontBlameMe99), [@Computerdores](https://github.com/Computerdores)
- Updated Russian (85%)
- werdi, [@Dott-rus](https://github.com/Dott-rus)
- Updated Hungarian (100%)
- Szíjártó Levente Pál
- Updated Spanish (96%)
- Joan, [@Nginearing](https://github.com/Nginearing)
- Updated French (100%)
- [@kitsumed](https://github.com/kitsumed)
- Updated Toki Pona (80%)
- [@Math-Bee](https://github.com/Math-Bee)
## [9.5.1] - 2025-03-06
### Fixed
- Fixed translations crashing the program and preventing it from being reopened ([#827](https://github.com/TagStudioDev/TagStudio/issues/827))
- fix: restore `translate_formatted()` method as `format()` by [@CyanVoxel](https://github.com/CyanVoxel) in [#830](https://github.com/TagStudioDev/TagStudio/pull/830)
- tests: add tests for translations by [@Computerdores](https://github.com/Computerdores) in [#833](https://github.com/TagStudioDev/TagStudio/pull/833)
- fix(translations): fix invalid placeholders by [@CyanVoxel](https://github.com/CyanVoxel) in [#835](https://github.com/TagStudioDev/TagStudio/pull/835)
- Removed empty parentheses from the "About" screen title
- fix: separate about screen title from translations by [@CyanVoxel](https://github.com/CyanVoxel) in [#836](https://github.com/TagStudioDev/TagStudio/pull/836)
### Translations
- Updated French (99%)
- [@alessdangelo](https://github.com/alessdangelo), [@Bamowen](https://github.com/Bamowen), [@kitsumed](https://github.com/kitsumed)
- Updated German (98%)
- [@Thesacraft](https://github.com/Thesacraft)
- Updated Portuguese (Brazil) (88%)
- [@viniciushelder](https://github.com/viniciushelder)
- Updated Russian (73%)
- werdei
- Updated Spanish (95%)
- [@JCC1998](https://github.com/JCC1998)
### Documentation
- docs: fix category typo by [@salem404](https://github.com/salem404) in [#834](https://github.com/TagStudioDev/TagStudio/pull/834)
## [9.5.0] - 2025-03-03
### Added
#### Overhauled Search Engine
##### Boolean Operators
- feat: implement query language by [@Computerdores](https://github.com/Computerdores) in [#606](https://github.com/TagStudioDev/TagStudio/pull/606)
- feat: optimize AND queries by [@Computerdores](https://github.com/Computerdores) in [#679](https://github.com/TagStudioDev/TagStudio/pull/679)
##### Filetype, Mediatype, and Glob Path + Smartcase Searches
- fix: remove wildcard requirement for tags by [@Tyrannicodin](https://github.com/Tyrannicodin) in [#481](https://github.com/TagStudioDev/TagStudio/pull/481)
- feat: add filetype and mediatype searches by [@python357-1](https://github.com/python357-1) in [#575](https://github.com/TagStudioDev/TagStudio/pull/575)
- feat: make path search use globs by [@python357-1](https://github.com/python357-1) in [#582](https://github.com/TagStudioDev/TagStudio/pull/582)
- feat: implement search equivalence of "jpg" and "jpeg" filetypes by [@Computerdores](https://github.com/Computerdores) in [#649](https://github.com/TagStudioDev/TagStudio/pull/649)
- feat: add smartcase and globless path searches by [@CyanVoxel](https://github.com/CyanVoxel) in [#743](https://github.com/TagStudioDev/TagStudio/pull/743)
##### Sortable Results
- feat: sort by "date added" in library by [@Computerdores](https://github.com/Computerdores) in [#674](https://github.com/TagStudioDev/TagStudio/pull/674)
##### Autocomplete
- feat: add autocomplete for search engine by [@python357-1](https://github.com/python357-1) in [#586](https://github.com/TagStudioDev/TagStudio/pull/586)
#### Replaced "Tag Fields" with Tag Categories
Instead of tags needing to be added to a tag field type such as "Meta Tags", "Content Tags", or just the "Tags" field, tags are now added directly to file entries with no intermediary step. While tag field types offered a way to further organize tags, it was cumbersome, inflexible, and simply not fully fleshed out. Tag Categories offer all of the previous (intentional) functionality while greatly increasing the ease of use and customization.
- feat!: tag categories by [@CyanVoxel](https://github.com/CyanVoxel) in [#655](https://github.com/TagStudioDev/TagStudio/pull/655)
[![Screenshot 2025-01-04 at 04 23 43](https://private-user-images.githubusercontent.com/46939827/400138597-0b92eca5-db8f-4e3e-954b-1b4f3795f073.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MDAxMzg1OTctMGI5MmVjYTUtZGI4Zi00ZTNlLTk1NGItMWI0ZjM3OTVmMDczLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWY4ZWEzOWRkZjkwOGZmYjZmZDUzMjU1MjJhNDNkNzYzZmM4YjZkMTUyNWIzMjNhMGY1NWNhYmU4ODNiNzlhMzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.cE_WO9AHsigusAbtaQV0QtN4FjYJz0lyHLLwFFDBO-0)](https://private-user-images.githubusercontent.com/46939827/400138597-0b92eca5-db8f-4e3e-954b-1b4f3795f073.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MDAxMzg1OTctMGI5MmVjYTUtZGI4Zi00ZTNlLTk1NGItMWI0ZjM3OTVmMDczLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWY4ZWEzOWRkZjkwOGZmYjZmZDUzMjU1MjJhNDNkNzYzZmM4YjZkMTUyNWIzMjNhMGY1NWNhYmU4ODNiNzlhMzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.cE_WO9AHsigusAbtaQV0QtN4FjYJz0lyHLLwFFDBO-0)
#### Thumbnails and File Previews
##### New Thumbnail Support
- feat: add svg thumbnail support (port [#442](https://github.com/TagStudioDev/TagStudio/pull/442)) by [@Tyrannicodin](https://github.com/Tyrannicodin) and [@CyanVoxel](https://github.com/CyanVoxel) in [#540](https://github.com/TagStudioDev/TagStudio/pull/540)
- feat: add pdf thumbnail support (port [#378](https://github.com/TagStudioDev/TagStudio/pull/378)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#543](https://github.com/TagStudioDev/TagStudio/pull/543)
- feat: add ePub thumbnail support (port [#387](https://github.com/TagStudioDev/TagStudio/pull/387)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#539](https://github.com/TagStudioDev/TagStudio/pull/539)
- feat: add OpenDocument thumbnail support (port [#366](https://github.com/TagStudioDev/TagStudio/pull/366)) by [@Joshua-Beatty](https://github.com/Joshua-Beatty) and [@CyanVoxel](https://github.com/CyanVoxel) in [#545](https://github.com/TagStudioDev/TagStudio/pull/545)
- feat: add JXL thumbnail and animated APNG + WEBP support (port [#344](https://github.com/TagStudioDev/TagStudio/pull/344) and partially port [#357](https://github.com/TagStudioDev/TagStudio/pull/357)) by [@BPplays](https://github.com/BPplays) and [@CyanVoxel](https://github.com/CyanVoxel) in [#549](https://github.com/TagStudioDev/TagStudio/pull/549)
- fix: catch ImportError for pillow_jxl module by [@CyanVoxel](https://github.com/CyanVoxel) in [a2f9685](https://github.com/TagStudioDev/TagStudio/commit/a2f9685bc0d744ea6f5334c6d2926aad3f6d375a)
##### Audio Playback
- feat: audio playback by [@csponge](https://github.com/csponge) in [#576](https://github.com/TagStudioDev/TagStudio/pull/576)
- feat(ui): add audio volume slider by [@SkeleyM](https://github.com/SkeleyM) in [#691](https://github.com/TagStudioDev/TagStudio/pull/691)
##### Thumbnail Caching
- feat(ui): add thumbnail caching by [@CyanVoxel](https://github.com/CyanVoxel) in [#694](https://github.com/TagStudioDev/TagStudio/pull/694)
#### Tags
##### Delete Tags _(Finally!)_
- feat: remove and create tags from tag database panel by [@DandyDev01](https://github.com/DandyDev01) in [#569](https://github.com/TagStudioDev/TagStudio/pull/569)
##### Custom User-Created Tag Colors
Create your own custom tag colors via the new Tag Color Manager! Tag colors are assigned a namespace (group) and include a name, primary color, and optional secondary color. By default the secondary color is used for the tag text color, but this can also be toggled to apply to the border color as well!
- feat(ui)!: user-created tag colors@CyanVoxel in [#801](https://github.com/TagStudioDev/TagStudio/pull/801)
[![](https://private-user-images.githubusercontent.com/46939827/413668576-b591f1fe-1c44-4d82-b6e5-d166590aeab1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg1NzYtYjU5MWYxZmUtMWM0NC00ZDgyLWI2ZTUtZDE2NjU5MGFlYWIxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjM4NzczYTFhYmFhNjgwMWJlYjgwNTkzYjA3ZWFlNTkwNzBiYTlhNTAzM2Y0MWM1MWQ0MzY1YmEyNmE4NjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.EOEVWLDMx5CT-Gg5UhBmdMIYT49IZPKrrA9VL7N-pBQ)](https://private-user-images.githubusercontent.com/46939827/413668576-b591f1fe-1c44-4d82-b6e5-d166590aeab1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg1NzYtYjU5MWYxZmUtMWM0NC00ZDgyLWI2ZTUtZDE2NjU5MGFlYWIxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjM4NzczYTFhYmFhNjgwMWJlYjgwNTkzYjA3ZWFlNTkwNzBiYTlhNTAzM2Y0MWM1MWQ0MzY1YmEyNmE4NjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.EOEVWLDMx5CT-Gg5UhBmdMIYT49IZPKrrA9VL7N-pBQ) [![](https://private-user-images.githubusercontent.com/46939827/413668612-96e81b08-6993-4a5e-96d0-3b05b50fbe44.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg2MTItOTZlODFiMDgtNjk5My00YTVlLTk2ZDAtM2IwNWI1MGZiZTQ0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJkNTUxODJiMDZhN2I2MDAxYzZlNzIyOTAzYTgwZDg3ZDFlYWM3ODM1YWY0Mzg5MDJjNDY0NzJhYjU4ZTAzMmEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.rqM3YOwrYdBCiwbBbEvalj7Tsfl-XWqgD1K9PeE46tI)](https://private-user-images.githubusercontent.com/46939827/413668612-96e81b08-6993-4a5e-96d0-3b05b50fbe44.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg2MTItOTZlODFiMDgtNjk5My00YTVlLTk2ZDAtM2IwNWI1MGZiZTQ0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJkNTUxODJiMDZhN2I2MDAxYzZlNzIyOTAzYTgwZDg3ZDFlYWM3ODM1YWY0Mzg5MDJjNDY0NzJhYjU4ZTAzMmEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.rqM3YOwrYdBCiwbBbEvalj7Tsfl-XWqgD1K9PeE46tI)
##### New Tag Colors + UI
- feat: expanded tag color system by [@CyanVoxel](https://github.com/CyanVoxel) in [#709](https://github.com/TagStudioDev/TagStudio/pull/709)
- fix(ui): use correct pink tag color by [@CyanVoxel](https://github.com/CyanVoxel) in [431efe4](https://github.com/TagStudioDev/TagStudio/commit/431efe4fe93213141c763e59ca9887215766fd42)
- fix(ui): use consistent tag outline colors by [@CyanVoxel](https://github.com/CyanVoxel) in [020a73d](https://github.com/TagStudioDev/TagStudio/commit/020a73d095c74283d6c80426d3c3db8874409952)
[![Screenshot 2025-01-04 at 04 23 43](https://private-user-images.githubusercontent.com/46939827/408753168-c8f82d89-ad7e-4be6-830e-b91cdc58e4c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MDg3NTMxNjgtYzhmODJkODktYWQ3ZS00YmU2LTgzMGUtYjkxY2RjNThlNGM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk1OWNhZGNkOTRiZGJhNGQxNGU1MjJhYTViYTc0OTNiNTA4NDUxODA4OTYxZDUwNzYxZDhmYWZkNzM4NTE3N2QmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.HLnwHyp3BYg8vXo3BtvqBoOqtpQTI1eykqa-L3chLUk)](https://private-user-images.githubusercontent.com/46939827/408753168-c8f82d89-ad7e-4be6-830e-b91cdc58e4c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MDg3NTMxNjgtYzhmODJkODktYWQ3ZS00YmU2LTgzMGUtYjkxY2RjNThlNGM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk1OWNhZGNkOTRiZGJhNGQxNGU1MjJhYTViYTc0OTNiNTA4NDUxODA4OTYxZDUwNzYxZDhmYWZkNzM4NTE3N2QmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.HLnwHyp3BYg8vXo3BtvqBoOqtpQTI1eykqa-L3chLUk)
##### New Tag Alias UI
- fix: preview panel aliases not staying up to date with database by [@DandyDev01](https://github.com/DandyDev01) in [#641](https://github.com/TagStudioDev/TagStudio/pull/641)
- fix: subtags/parent tags & aliases update the UI for building a tag by [@DandyDev01](https://github.com/DandyDev01) in [#534](https://github.com/TagStudioDev/TagStudio/pull/534)
#### Translations
TagStudio now has official translation support! Head to the new settings panel and select from one of the initial languages included. Note that many languages currently have incomplete translations.
Translation hosting generously provided by [Weblate](https://weblate.org/en/). Check out our [project page](https://hosted.weblate.org/projects/tagstudio/) to help translate TagStudio! Thank you to everyone who's helped contribute to the translations so far!
- translations: add string tokens for en.json by [@Bamowen](https://github.com/Bamowen) in [#507](https://github.com/TagStudioDev/TagStudio/pull/507)
- feat: translations by [@Computerdores](https://github.com/Computerdores) in [#662](https://github.com/TagStudioDev/TagStudio/pull/662)
- feat(ui): add language setting by [@CyanVoxel](https://github.com/CyanVoxel) in [#803](https://github.com/TagStudioDev/TagStudio/pull/803)
Initial Languages:
- Chinese (Traditional) (68%)
- [@brisu](https://github.com/brisu)
- Dutch (35%)
- [@Pheubel](https://github.com/Pheubel)
- Filipino (43%)
- [@searinminecraft](https://github.com/searinminecraft)
- French (100%)
- [@Bamowen](https://github.com/Bamowen), [@alessdangelo](https://github.com/alessdangelo), [@kitsumed](https://github.com/kitsumed), Obscaeris
- German (98%)
- [@Ryussei](https://github.com/Ryussei), [@Computerdores](https://github.com/Computerdores), Aaron M, [@JoeJoeTV](https://github.com/JoeJoeTV), [@Kurty00](https://github.com/Kurty00)
- Hungarian (100%)
- [@smileyhead](https://github.com/smileyhead)
- Norwegian Bokmål (16%)
- [@comradekingu](https://github.com/comradekingu)
- Polish (97%)
- Anonymous
- Portuguese (Brazil) (64%)
- [@LoboMetalurgico](https://github.com/LoboMetalurgico), [@SpaceFox1](https://github.com/SpaceFox1), [@DaviMarquezeli](https://github.com/DaviMarquezeli), [@viniciushelder](https://github.com/viniciushelder), Alexander Lennart Formiga Johnsson
- Russian (22%)
- [@The-Stolas](https://github.com/The-Stolas)
- Spanish (57%)
- [@gallegonovato](https://github.com/gallegonovato), [@Nginearing](https://github.com/Nginearing), [@noceno](https://github.com/noceno)
- Swedish (24%)
- [@adampawelec](https://github.com/adampawelec), [@mashed5894](https://github.com/mashed5894)
- Tamil (22%)
- [@VasigaranAndAngel](https://github.com/VasigaranAndAngel)
- Toki Pona (32%)
- [@goldstargloww](https://github.com/goldstargloww)
- Turkish (22%)
- [@Nyghl](https://github.com/Nyghl)
#### Miscellaneous
- feat: about section by [@mashed5894](https://github.com/mashed5894) in [#712](https://github.com/TagStudioDev/TagStudio/pull/712)
- feat(ui): add configurable splash screens by [@CyanVoxel](https://github.com/CyanVoxel) in [#703](https://github.com/TagStudioDev/TagStudio/pull/703)
- feat(ui): show filenames in thumbnail grid by [@CyanVoxel](https://github.com/CyanVoxel) in [#633](https://github.com/TagStudioDev/TagStudio/pull/633)
- feat(about): clickable links to docs/discord/etc in about modal by [@SkeleyM](https://github.com/SkeleyM) in [#799](https://github.com/TagStudioDev/TagStudio/pull/799)
### Fixed
- fix(ui): display all tags in panel during empty search by [@samuellieberman](https://github.com/samuellieberman) in [#328](https://github.com/TagStudioDev/TagStudio/pull/328)
- fix: avoid `KeyError` in `add_folders_to_tree()` (fix [#346](https://github.com/TagStudioDev/TagStudio/issues/346)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#347](https://github.com/TagStudioDev/TagStudio/pull/347)
- fix: error on closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#484](https://github.com/TagStudioDev/TagStudio/pull/484)
- fix: resolution info [#550](https://github.com/TagStudioDev/TagStudio/issues/550) by [@Roc25](https://github.com/Roc25) in [#551](https://github.com/TagStudioDev/TagStudio/pull/551)
- fix: remove queued thumnail jobs when closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#583](https://github.com/TagStudioDev/TagStudio/pull/583)
- fix: use absolute ffprobe path on macos (Fix [#511](https://github.com/TagStudioDev/TagStudio/issues/511)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#629](https://github.com/TagStudioDev/TagStudio/pull/629)
- fix(ui): prevent duplicate parent tags in UI by [@SkeleyM](https://github.com/SkeleyM) in [#665](https://github.com/TagStudioDev/TagStudio/pull/665)
- fix: fix -o flag not working if path has whitespace around it by [@python357-1](https://github.com/python357-1) in [#670](https://github.com/TagStudioDev/TagStudio/pull/670)
- fix: better file opening compatibility with non-ascii filenames by [@SkeleyM](https://github.com/SkeleyM) in [#667](https://github.com/TagStudioDev/TagStudio/pull/667)
- fix: restore environment before launching external programs by [@mashed5894](https://github.com/mashed5894) in [#707](https://github.com/TagStudioDev/TagStudio/pull/707)
- fix: have pydub use known ffmpeg + ffprobe locations by [@CyanVoxel](https://github.com/CyanVoxel) in [#724](https://github.com/TagStudioDev/TagStudio/pull/724)
- fix: add ".DS_Store" to `GLOBAL_IGNORE_SET` by [@CyanVoxel](https://github.com/CyanVoxel) in [b72a2f2](https://github.com/TagStudioDev/TagStudio/commit/b72a2f233141db4db6aa6be8796b626ebd3f0756)
- fix: don't add "._" files to libraries by [@CyanVoxel](https://github.com/CyanVoxel) in [eb1f634](https://github.com/TagStudioDev/TagStudio/commit/eb1f634d386cd8a5ecee1e6ff6a0b7d8811550fa)
### Changed
#### SQLite Save File Format
This was the main focus of this update, and where the majority of development time and resources have been spent since v9.4. These changes include everything that was done to migrate from the JSON format to SQLite starting from the initial SQLite PR, while re-implementing every feature from v9.4 as the initial SQLite PR was based on v9.3.x at the time.
- refactor!: use SQLite and SQLAlchemy for database backend by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#332](https://github.com/TagStudioDev/TagStudio/pull/332)
- feat: make search results more ergonomic by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#498](https://github.com/TagStudioDev/TagStudio/pull/498)
- feat: store `Entry` suffix separately by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#503](https://github.com/TagStudioDev/TagStudio/pull/503)
- feat: port thumbnail ([#390](https://github.com/TagStudioDev/TagStudio/pull/390)) and related features to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#522](https://github.com/TagStudioDev/TagStudio/pull/522)
- fix: don't check db version with new library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#536](https://github.com/TagStudioDev/TagStudio/pull/536)
- fix(ui): update ui when removing fields by [@DandyDev01](https://github.com/DandyDev01) in [#560](https://github.com/TagStudioDev/TagStudio/pull/560)
- feat(parity): backend for aliases and parent tags by [@DandyDev01](https://github.com/DandyDev01) in [#596](https://github.com/TagStudioDev/TagStudio/pull/596)
- fix: "open in explorer" opens correct folder by [@KirilBourakov](https://github.com/KirilBourakov) in [#603](https://github.com/TagStudioDev/TagStudio/pull/603)
- fix: ui/ux parity fixes for thumbnails and files by [@CyanVoxel](https://github.com/CyanVoxel) in [#608](https://github.com/TagStudioDev/TagStudio/pull/608)
- feat(parity): migrate json libraries to sqlite by [@CyanVoxel](https://github.com/CyanVoxel) in [#604](https://github.com/TagStudioDev/TagStudio/pull/604)
- fix: clear all setting values when opening a library by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#622](https://github.com/TagStudioDev/TagStudio/pull/622)
- fix: remove/rework windows path tests by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#625](https://github.com/TagStudioDev/TagStudio/pull/625)
- fix: add check to see if library is loaded in filter_items by [@Roc25](https://github.com/Roc25) in [#547](https://github.com/TagStudioDev/TagStudio/pull/547)
- fix: multiple macro errors by [@Computerdores](https://github.com/Computerdores) in [#612](https://github.com/TagStudioDev/TagStudio/pull/612)
- fix: don't allow blank tag alias values in db by [@CyanVoxel](https://github.com/CyanVoxel) in [#628](https://github.com/TagStudioDev/TagStudio/pull/628)
- feat: Reimplement drag drop files on sql migration by [@seakrueger](https://github.com/seakrueger) in [#528](https://github.com/TagStudioDev/TagStudio/pull/528)
- fix: stop sqlite db from being updated while running tests by [@python357-1](https://github.com/python357-1) in [#648](https://github.com/TagStudioDev/TagStudio/pull/648)
- fix: enter/return adds top result tag by [@SkeleyM](https://github.com/SkeleyM) in [#651](https://github.com/TagStudioDev/TagStudio/pull/651)
- fix: show correct unlinked files count by [@SkeleyM](https://github.com/SkeleyM) in [#653](https://github.com/TagStudioDev/TagStudio/pull/653)
- feat: implement parent tag search by [@Computerdores](https://github.com/Computerdores) in [#673](https://github.com/TagStudioDev/TagStudio/pull/673)
- fix: only close add tag menu with no search by [@SkeleyM](https://github.com/SkeleyM) in [#685](https://github.com/TagStudioDev/TagStudio/pull/685)
- fix: drag and drop no longer resets by [@SkeleyM](https://github.com/SkeleyM) in [#710](https://github.com/TagStudioDev/TagStudio/pull/710)
- feat(ui): port "create and add tag" to main branch by [@SkeleyM](https://github.com/SkeleyM) in [#711](https://github.com/TagStudioDev/TagStudio/pull/711)
- fix: don't add default title field, use proper phrasing for adding files by [@CyanVoxel](https://github.com/CyanVoxel) in [#701](https://github.com/TagStudioDev/TagStudio/pull/701)
- fix: preview panel + main window fixes and optimizations by [@CyanVoxel](https://github.com/CyanVoxel) in [#700](https://github.com/TagStudioDev/TagStudio/pull/700)
- fix: sort tag results by [@mashed5894](https://github.com/mashed5894) in [#721](https://github.com/TagStudioDev/TagStudio/pull/721)
- fix: restore opening last library on startup by [@SkeleyM](https://github.com/SkeleyM) in [#729](https://github.com/TagStudioDev/TagStudio/pull/729)
- fix(ui): don't always create tag on enter by [@SkeleyM](https://github.com/SkeleyM) in [#731](https://github.com/TagStudioDev/TagStudio/pull/731)
- fix: use tag aliases in tag search by [@CyanVoxel](https://github.com/CyanVoxel) in [#726](https://github.com/TagStudioDev/TagStudio/pull/726)
- fix: keep initial id order in `get_entries_full()` by [@CyanVoxel](https://github.com/CyanVoxel) in [#736](https://github.com/TagStudioDev/TagStudio/pull/736)
- fix: always catch db mismatch by [@CyanVoxel](https://github.com/CyanVoxel) in [#738](https://github.com/TagStudioDev/TagStudio/pull/738)
- fix: relink unlinked entry to existing entry without sql error by [@mashed5894](https://github.com/mashed5894) in [#730](https://github.com/TagStudioDev/TagStudio/issues/730)
- fix: refactor and fix bugs with missing_files.py by [@CyanVoxel](https://github.com/CyanVoxel) in [#739](https://github.com/TagStudioDev/TagStudio/pull/739)
- fix: dragging files references correct entry IDs [@CyanVoxel](https://github.com/CyanVoxel) in [44ff17c](https://github.com/TagStudioDev/TagStudio/commit/44ff17c0b3f05570e356c112f005dbc14c7cc05d)
- ui: port splash screen from Alpha-v9.4 by [@CyanVoxel](https://github.com/CyanVoxel) in [af760ee](https://github.com/TagStudioDev/TagStudio/commit/af760ee61a523c84bab0fb03a68d7465866d0e05)
- fix: tags created from tag database now add aliases by [@CyanVoxel](https://github.com/CyanVoxel) in [2903dd2](https://github.com/TagStudioDev/TagStudio/commit/2903dd22c45c02498687073d075bb88886de6b62)
- fix: check for tag name parity during JSON migration by [@CyanVoxel](https://github.com/CyanVoxel) in [#748](https://github.com/TagStudioDev/TagStudio/pull/748)
- feat(ui): re-implement tag display names on sql by [@CyanVoxel](https://github.com/CyanVoxel) in [#747](https://github.com/TagStudioDev/TagStudio/pull/747)
- fix(ui): restore Windows accent color on PySide 6.8.0.1 by [@CyanVoxel](https://github.com/CyanVoxel) in [#755](https://github.com/TagStudioDev/TagStudio/pull/755)
- fix(ui): (mostly) fix right-click search option on tags by [@CyanVoxel](https://github.com/CyanVoxel) in [#756](https://github.com/TagStudioDev/TagStudio/pull/756)
- feat: copy/paste fields and tags by [@mashed5894](https://github.com/mashed5894) in [#722](https://github.com/TagStudioDev/TagStudio/pull/722)
- perf: optimize query methods and reduce preview panel updates by [@CyanVoxel](https://github.com/CyanVoxel) in [#794](https://github.com/TagStudioDev/TagStudio/pull/794)
- feat: port file trashing ([#409](https://github.com/TagStudioDev/TagStudio/pull/409)) to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#792](https://github.com/TagStudioDev/TagStudio/pull/792)
- fix: prevent future library versions from being opened by [@CyanVoxel](https://github.com/CyanVoxel) in [bcf3b2f](https://github.com/TagStudioDev/TagStudio/commit/bcf3b2f96bc8b876ca4b0c1d1882ce14a190f249)
#### UI/UX
- feat(ui): pre-select default tag name in `BuildTagPanel` by [@Cool-Game-Dev](https://github.com/Cool-Game-Dev) in [#592](https://github.com/TagStudioDev/TagStudio/pull/592)
- feat(ui): keyboard navigation for editing tags by [@Computerdores](https://github.com/Computerdores) in [#407](https://github.com/TagStudioDev/TagStudio/pull/407)
- feat(ui): use tag query as default new tag name by [@CyanVoxel](https://github.com/CyanVoxel) in [29c0dfd](https://github.com/TagStudioDev/TagStudio/commit/29c0dfdb2d88e8f473e27c7f1fe7ede6e5bd0feb)
- feat(ui): shortcut to add tags to selected entries; change click behavior of tags to edit by [@CyanVoxel](https://github.com/CyanVoxel) in [#749](https://github.com/TagStudioDev/TagStudio/pull/749)
- fix(ui): use consistent dark mode colors for all systems by [@CyanVoxel](https://github.com/CyanVoxel) in [#752](https://github.com/TagStudioDev/TagStudio/pull/752)
- fix(ui): use camera white balance for raw images by [@CyanVoxel](https://github.com/CyanVoxel) in [6ee5304](https://github.com/TagStudioDev/TagStudio/commit/6ee5304b52f217af0f5df543fcb389649203d6b2)
- Mixed field editing has been limited due to various bugs in both the JSON and SQL implementations. This will be re-implemented in a future release.
- fix(ui): improve tagging ux by [@CyanVoxel](https://github.com/CyanVoxel) in [#633](https://github.com/TagStudioDev/TagStudio/pull/633)
- fix(ui): hide library actions when no library is open by [@CyanVoxel](https://github.com/CyanVoxel) in [#787](https://github.com/TagStudioDev/TagStudio/pull/787)
- refactor(ui): recycle tag list in TagSearchPanel by [@CyanVoxel](https://github.com/CyanVoxel) in [#788](https://github.com/TagStudioDev/TagStudio/pull/788)
- feat(ui): add tag view limit dropdown
- fix(ui): expand usage of esc and enter for modals by [@CyanVoxel](https://github.com/CyanVoxel) in [#793](https://github.com/TagStudioDev/TagStudio/pull/793)
#### Performance
- feat: improve performance of "Delete Missing Entries" by [@Toby222](https://github.com/Toby222) and [@Computerdores](https://github.com/Computerdores) in [#696](https://github.com/TagStudioDev/TagStudio/pull/696)
#### Internal Changes
- refactor: combine open launch args by [@UnusualEgg](https://github.com/UnusualEgg) in [#364](https://github.com/TagStudioDev/TagStudio/pull/364)
- feat: add date_created, date_modified, and date_added columns to entries table by [@CyanVoxel](https://github.com/CyanVoxel) in [#740](https://github.com/TagStudioDev/TagStudio/pull/740)
## [9.5.0 Pre-Release 4] - 2025-02-17
### Added
#### Custom User-Created Tag Colors ([@CyanVoxel](https://github.com/CyanVoxel) in [#801](https://github.com/TagStudioDev/TagStudio/pull/801))
Create your own custom tag colors via the new Tag Color Manager! Tag colors are assigned a namespace (group) and include a name, primary color, and optional secondary color. By default the secondary color is used for the tag text color, but this can also be toggled to apply to the border color as well!
[![Screenshot 2025-02-16 at 17 34 22](https://private-user-images.githubusercontent.com/46939827/413668576-b591f1fe-1c44-4d82-b6e5-d166590aeab1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg1NzYtYjU5MWYxZmUtMWM0NC00ZDgyLWI2ZTUtZDE2NjU5MGFlYWIxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBhYzUwZmExZjRlMWI4YzJjZTZmNDRiMjFiN2ZlZjg4ZjE3MWM4NzBkNmJlZWNjMzg2OWU5YTE2OWVmZTA2YTEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nEs6bk1euKTEkIqDipNJBrXHbHegb3PHWoW3tKI02_8)](https://private-user-images.githubusercontent.com/46939827/413668576-b591f1fe-1c44-4d82-b6e5-d166590aeab1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg1NzYtYjU5MWYxZmUtMWM0NC00ZDgyLWI2ZTUtZDE2NjU5MGFlYWIxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBhYzUwZmExZjRlMWI4YzJjZTZmNDRiMjFiN2ZlZjg4ZjE3MWM4NzBkNmJlZWNjMzg2OWU5YTE2OWVmZTA2YTEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nEs6bk1euKTEkIqDipNJBrXHbHegb3PHWoW3tKI02_8)
[![Screenshot 2025-02-16 at 17 32 56](https://private-user-images.githubusercontent.com/46939827/413668612-96e81b08-6993-4a5e-96d0-3b05b50fbe44.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg2MTItOTZlODFiMDgtNjk5My00YTVlLTk2ZDAtM2IwNWI1MGZiZTQ0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTlhOGU2NjA2ZjRhMjNjZGYxZDE1ZWYzZmVjN2RjM2Q0YzA1NTcwZGE5OGFkMjc2MDIzMTk1YTFlYjY2NTQxNmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.VD2trGcVKQVKUpzVog1UhZUM0JRcEHUhwGCiHpZ8zF0)](https://private-user-images.githubusercontent.com/46939827/413668612-96e81b08-6993-4a5e-96d0-3b05b50fbe44.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg2MTItOTZlODFiMDgtNjk5My00YTVlLTk2ZDAtM2IwNWI1MGZiZTQ0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTlhOGU2NjA2ZjRhMjNjZGYxZDE1ZWYzZmVjN2RjM2Q0YzA1NTcwZGE5OGFkMjc2MDIzMTk1YTFlYjY2NTQxNmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.VD2trGcVKQVKUpzVog1UhZUM0JRcEHUhwGCiHpZ8zF0)
#### Translations
TagStudio now has official translation support! Head to the new settings panel and select from one of the initial languages included. Note that many languages currently have incomplete translations.
Translation hosting generously provided by [Weblate](https://weblate.org/en/). Check out our [project page](https://hosted.weblate.org/projects/tagstudio/) to help translate TagStudio! Thank you to everyone who's helped contribute to the translations so far!
- translations: add string tokens for en.json by [@Bamowen](https://github.com/Bamowen) in [#507](https://github.com/TagStudioDev/TagStudio/pull/507)
- feat: translations by [@Computerdores](https://github.com/Computerdores) in [#662](https://github.com/TagStudioDev/TagStudio/pull/662)
- feat(ui): add language setting by [@CyanVoxel](https://github.com/CyanVoxel) in [#803](https://github.com/TagStudioDev/TagStudio/pull/803)
Initial Languages:
- Chinese (Traditional) (68%)
- [@brisu](https://github.com/brisu)
- Dutch (35%)
- [@Pheubel](https://github.com/Pheubel)
- Filipino (15%)
- [@searinminecraft](https://github.com/searinminecraft)
- French (89%)
- [@Bamowen](https://github.com/Bamowen), [@alessdangelo](https://github.com/alessdangelo), [@kitsumed](https://github.com/kitsumed), Obscaeris
- German (73%)
- [@Ryussei](https://github.com/Ryussei), [@Computerdores](https://github.com/Computerdores), Aaron M
- Hungarian (89%)
- [@smileyhead](https://github.com/smileyhead)
- Norwegian Bokmål (16%)
- [@comradekingu](https://github.com/comradekingu)
- Polish (76%)
- Anonymous
- Portuguese (Brazil) (22%)
- [@LoboMetalurgico](https://github.com/LoboMetalurgico), [@SpaceFox1](https://github.com/SpaceFox1)
- Russian (22%)
- [@The-Stolas](https://github.com/The-Stolas)
- Spanish (46%)
- [@gallegonovato](https://github.com/gallegonovato), [@Nginearing](https://github.com/Nginearing), [@noceno](https://github.com/noceno)
- Swedish (24%)
- [@adampawelec](https://github.com/adampawelec), [@mashed5894](https://github.com/mashed5894)
- Tamil (22%)
- [@VasigaranAndAngel](https://github.com/VasigaranAndAngel)
- Toki Pona (32%)
- [@goldstargloww](https://github.com/goldstargloww)
- Turkish (22%)
- [@Nyghl](https://github.com/Nyghl)
### Fixed
- feat(about): clickable links to docs/discord/etc in about modal by [@SkeleyM](https://github.com/SkeleyM) in [#799](https://github.com/TagStudioDev/TagStudio/pull/799)
### Internal Changes
This release increases the internal `DB_VERSION` to 8. Libraries created with this version of TagStudio can still be opened in earlier v9.5.0 pre-release versions, however the behavior of custom color borders will not be identical to the behavior in this PR. Otherwise it should still be possible to use any custom colors created in this version in these earlier pre-releases (but not really recommended).
## [9.5.0 Pre-Release 3] - 2025-02-10
### Added
##### [#743](https://github.com/TagStudioDev/TagStudio/pull/743) by [@CyanVoxel](https://github.com/CyanVoxel)
Added "Smartcase" and Globless Path Search
- `path: temp`: Returns all paths that have "temp" **(Case insensitive)** somewhere in the name.
- `path: Temp`: Returns all paths that have "Temp" **(Case sensitive)** somewhere in the name.
Glob Patterns w/ Smartcase
- `path: *temp*`: Returns all paths that have "temp" **(Case insensitive)** somewhere in the name.
- `path: *Temp*`: Returns all paths that have "Temp" **(Case sensitive)** somewhere in the name.
- `path: temp*`: Returns all paths that start with "temp" **(Case insensitive)** somewhere in the name.
- `path: Temp*`: Returns all paths that start with "Temp" **(Case sensitive)** somewhere in the name.
- `path: *temp`: Returns all paths that end with "temp" **(Case insensitive)** somewhere in the name.
- `path: *TEmP`: Returns all paths that end with "TEmP" **(Case sensitive)** somewhere in the name.
##### [#788](https://github.com/TagStudioDev/TagStudio/pull/788) by [@CyanVoxel](https://github.com/CyanVoxel)
- Added a "View Limit" dropdown to tag search boxes to limit the number of on-screen tags. Previously this limit was hardcoded to 100, but now options range from 25 to unlimited.
[![](https://private-user-images.githubusercontent.com/46939827/411701461-7f7da065-888d-4fe5-a4e7-f99447bcce98.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTE3MDE0NjEtN2Y3ZGEwNjUtODg4ZC00ZmU1LWE0ZTctZjk5NDQ3YmNjZTk4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWVlZjVhZjYyNWQwY2FmMjhmMWI1MzI5ZDdjYWMxMmM0N2M0Nzc4MmY1YjE0NWY4MjVhZmIyMDI1NzQzY2M0YmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.a3xUaH5r3HsDgOb6-lo3T-xSRRSy7dDOrln5i62KFP8)](https://private-user-images.githubusercontent.com/46939827/411701461-7f7da065-888d-4fe5-a4e7-f99447bcce98.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTE3MDE0NjEtN2Y3ZGEwNjUtODg4ZC00ZmU1LWE0ZTctZjk5NDQ3YmNjZTk4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWVlZjVhZjYyNWQwY2FmMjhmMWI1MzI5ZDdjYWMxMmM0N2M0Nzc4MmY1YjE0NWY4MjVhZmIyMDI1NzQzY2M0YmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.a3xUaH5r3HsDgOb6-lo3T-xSRRSy7dDOrln5i62KFP8)
### Changed
- fix(ui): expand usage of esc and enter for modals by [@CyanVoxel](https://github.com/CyanVoxel) in [#793](https://github.com/TagStudioDev/TagStudio/pull/793)
- perf: optimize query methods and reduce preview panel updates by [@CyanVoxel](https://github.com/CyanVoxel) in [#794](https://github.com/TagStudioDev/TagStudio/pull/794)
##### [#788](https://github.com/TagStudioDev/TagStudio/pull/788) by [@CyanVoxel](https://github.com/CyanVoxel)
- Improved performance of tag search boxes, including the tag manager
### Fixed
- fix(ui): hide library actions when no library is open by [@CyanVoxel](https://github.com/CyanVoxel) in [#787](https://github.com/TagStudioDev/TagStudio/pull/787)
- feat: port file trashing ([#409](https://github.com/TagStudioDev/TagStudio/pull/409)) to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#792](https://github.com/TagStudioDev/TagStudio/pull/792)
### Docs
- Added references to alternative POSIX shells, as well as pyenv to CONTRIBUTING.md by [@ChloeZamorano](https://github.com/ChloeZamorano) in [#791](https://github.com/TagStudioDev/TagStudio/pull/791)
## [9.5.0 Pre-Release 2] - 2025-02-03
### Added
##### [#784](https://github.com/TagStudioDev/TagStudio/pull/784) by [@CyanVoxel](https://github.com/CyanVoxel)
- Add Ctrl+M shortcut to open the "Tag Manager"
### Fixed
- fix: don't wrap field names too early by [@CyanVoxel](https://github.com/CyanVoxel) in [2215403](https://github.com/TagStudioDev/TagStudio/commit/2215403201e3b416a43ead0a322688180af6d71b) and [90a826d](https://github.com/TagStudioDev/TagStudio/commit/90a826d12804b3386a0b9003abb20f23f88ab3be)
- fix: save all tag attributes from "Create & Add" modal by [@SkeleyM](https://github.com/SkeleyM) in [#762](https://github.com/TagStudioDev/TagStudio/pull/762)
- fix: allow tag names with colons in search by [@SkeleyM](https://github.com/SkeleyM) in [#765](https://github.com/TagStudioDev/TagStudio/pull/765)
- fix: catch `ParsingError` by [@CyanVoxel](https://github.com/CyanVoxel) in [#779](https://github.com/TagStudioDev/TagStudio/pull/779)
- fix: patch incorrect description type & invalid disambiguation_id refs by [@CyanVoxel](https://github.com/CyanVoxel) in [#782](https://github.com/TagStudioDev/TagStudio/pull/782)
##### [#784](https://github.com/TagStudioDev/TagStudio/pull/784) by [@CyanVoxel](https://github.com/CyanVoxel)
- Reset tag search box and focus each time a tag search panel is opened
- Include tag parents in tag search results (v9.4 parity)
- Lowercase tag names now get properly sorted with uppercase ones
- Don't include tag display names in "closeness" factor when searching
- Escape "&" characters inside tag names so Qt doesn't treat them as mnemonics
- Set minimum tag width
- Fix "Add Tags" panel missing its window title when accessing from the keyboard shortcut
### Changed
##### [#784](https://github.com/TagStudioDev/TagStudio/pull/784) by [@CyanVoxel](https://github.com/CyanVoxel)
- The "use for disambiguation" button has been moved to the right-hand side of parent tags in order to prevent accidental clicks involving the left-hand "remove tag" button
- Add "Create & Add" button to the bottom of all non-whitespace searches, even if they return some tags
- The awkward "+" button next to tags in the "Add Tags" panel has been removed in favor of clicking on tags themselves
- Improved visual feedback for highlighting, keyboard focusing, and clicking tags
- The clickable area of the "-" button on tags has been increased and has visual feedback when you hover and click it
- You can now tab into the tag search list and add tags with a spacebar press (previously possible but very janky)
- In tag search panels, pressing the Esc key will return your focus to the search bar and highlight your previous query. If the search box is already highlighted, pressing Esc will close the modal
- In modals such as the "Add Tag" and "Edit Tag" panels, pressing Esc will cancel the operation and close the modal
### Internal Changes
- refactor: wrap migration_iterator lambda in a try/except block by [@CyanVoxel](https://github.com/CyanVoxel) in [#773](https://github.com/TagStudioDev/TagStudio/pull/773)
### Docs
- docs: update field and library pages by [@CyanVoxel](https://github.com/CyanVoxel) in [f5ff4d7](https://github.com/TagStudioDev/TagStudio/commit/f5ff4d78c1ad53134e9c64698886aee68c0f1dc1)
- docs: add information about "tag manager" by [@CyanVoxel](https://github.com/CyanVoxel) in [9bdbafa](https://github.com/TagStudioDev/TagStudio/commit/9bdbafa40c4274922f6533b5b5fcee9a4fe43030)
- docs: add note about glob searching in the readme by [@CyanVoxel](https://github.com/CyanVoxel) in [6e402ac](https://github.com/TagStudioDev/TagStudio/commit/6e402ac34d2d60e71fbd36ad234fe3914d5eb8e0)
- docs: add library_search page by [@CyanVoxel](https://github.com/CyanVoxel) in [5be7dfc](https://github.com/TagStudioDev/TagStudio/commit/5be7dfc314b21042c18b2f08893f2b452d12394a)
- docs: docs: add more links to index.md by [@CyanVoxel](https://github.com/CyanVoxel) in [d795889](https://github.com/TagStudioDev/TagStudio/commit/d7958892b7762586837204d686a6a2a993e3c26e)
- docs: fix typo for "category" in usage.md by [@pinheadtf2](https://github.com/pinheadtf2) in [#760](https://github.com/TagStudioDev/TagStudio/pull/760)
- fix(docs): fix screenshot sometimes not rendering by [@SkeleyM](https://github.com/SkeleyM) in [#775](https://github.com/TagStudioDev/TagStudio/pull/775)
## [9.5.0 Pre-Release 1] - 2025-01-31
### Added
#### Overhauled Search Engine
##### Boolean Operators
- feat: implement query language by [@Computerdores](https://github.com/Computerdores) in [#606](https://github.com/TagStudioDev/TagStudio/pull/606)
- feat: optimize AND queries by [@Computerdores](https://github.com/Computerdores) in [#679](https://github.com/TagStudioDev/TagStudio/pull/679)
##### Filetype, Mediatype, and Glob Path Searches
- fix: remove wildcard requirement for tags by [@Tyrannicodin](https://github.com/Tyrannicodin) in [#481](https://github.com/TagStudioDev/TagStudio/pull/481)
- feat: add filetype and mediatype searches by [@python357-1](https://github.com/python357-1) in [#575](https://github.com/TagStudioDev/TagStudio/pull/575)
- feat: make path search use globs by [@python357-1](https://github.com/python357-1) in [#582](https://github.com/TagStudioDev/TagStudio/pull/582)
- feat: implement search equivalence of "jpg" and "jpeg" filetypes by [@Computerdores](https://github.com/Computerdores) in [#649](https://github.com/TagStudioDev/TagStudio/pull/649)
##### Sortable Results
- feat: sort by "date added" in library by [@Computerdores](https://github.com/Computerdores) in [#674](https://github.com/TagStudioDev/TagStudio/pull/674)
##### Autocomplete
- feat: add autocomplete for search engine by [@python357-1](https://github.com/python357-1) in [#586](https://github.com/TagStudioDev/TagStudio/pull/586)
#### Replaced "Tag Fields" with Tag Categories
Instead of tags needing to be added to a tag field type such as "Meta Tags", "Content Tags", or just the "Tags" field, tags are now added directly to file entries with no intermediary step. While tag field types offered a way to further organize tags, it was cumbersome, inflexible, and simply not fully fleshed out. Tag Categories offer all of the previous (intentional) functionality while greatly increasing the ease of use and customization.
- feat!: tag categories by [@CyanVoxel](https://github.com/CyanVoxel) in [#655](https://github.com/TagStudioDev/TagStudio/pull/655)
[![Screenshot 2025-01-04 at 04 23 43](https://private-user-images.githubusercontent.com/46939827/400138597-0b92eca5-db8f-4e3e-954b-1b4f3795f073.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTUwODcsIm5iZiI6MTc0NjY1NDc4NywicGF0aCI6Ii80NjkzOTgyNy80MDAxMzg1OTctMGI5MmVjYTUtZGI4Zi00ZTNlLTk1NGItMWI0ZjM3OTVmMDczLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNTMwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTM1M2EwNGFjYmJiM2QwZjg2YzNmNTVjMGYwZDJkZGJkZTg5NjZjNDFhNTYyNDE0OGNlOWFhNzRkMzE3MjBkNzEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.B5NRZmHygdHlMy2ZnZtHjfOs83jjEliwfxoe3eMBnEQ)](https://private-user-images.githubusercontent.com/46939827/400138597-0b92eca5-db8f-4e3e-954b-1b4f3795f073.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTUwODcsIm5iZiI6MTc0NjY1NDc4NywicGF0aCI6Ii80NjkzOTgyNy80MDAxMzg1OTctMGI5MmVjYTUtZGI4Zi00ZTNlLTk1NGItMWI0ZjM3OTVmMDczLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNTMwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTM1M2EwNGFjYmJiM2QwZjg2YzNmNTVjMGYwZDJkZGJkZTg5NjZjNDFhNTYyNDE0OGNlOWFhNzRkMzE3MjBkNzEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.B5NRZmHygdHlMy2ZnZtHjfOs83jjEliwfxoe3eMBnEQ)
#### Thumbnails and File Previews
##### New Thumbnail Support
- feat: add svg thumbnail support (port [#442](https://github.com/TagStudioDev/TagStudio/pull/442)) by [@Tyrannicodin](https://github.com/Tyrannicodin) and [@CyanVoxel](https://github.com/CyanVoxel) in [#540](https://github.com/TagStudioDev/TagStudio/pull/540)
- feat: add pdf thumbnail support (port [#378](https://github.com/TagStudioDev/TagStudio/pull/378)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#543](https://github.com/TagStudioDev/TagStudio/pull/543)
- feat: add ePub thumbnail support (port [#387](https://github.com/TagStudioDev/TagStudio/pull/387)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#539](https://github.com/TagStudioDev/TagStudio/pull/539)
- feat: add OpenDocument thumbnail support (port [#366](https://github.com/TagStudioDev/TagStudio/pull/366)) by [@Joshua-Beatty](https://github.com/Joshua-Beatty) and [@CyanVoxel](https://github.com/CyanVoxel) in [#545](https://github.com/TagStudioDev/TagStudio/pull/545)
- feat: add JXL thumbnail and animated APNG + WEBP support (port [#344](https://github.com/TagStudioDev/TagStudio/pull/344) and partially port [#357](https://github.com/TagStudioDev/TagStudio/pull/357)) by [@BPplays](https://github.com/BPplays) and [@CyanVoxel](https://github.com/CyanVoxel) in [#549](https://github.com/TagStudioDev/TagStudio/pull/549)
- fix: catch ImportError for pillow_jxl module by [@CyanVoxel](https://github.com/CyanVoxel) in [a2f9685](https://github.com/TagStudioDev/TagStudio/commit/a2f9685bc0d744ea6f5334c6d2926aad3f6d375a)
##### Audio Playback
- feat: audio playback by [@csponge](https://github.com/csponge) in [#576](https://github.com/TagStudioDev/TagStudio/pull/576)
- feat(ui): add audio volume slider by [@SkeleyM](https://github.com/SkeleyM) in [#691](https://github.com/TagStudioDev/TagStudio/pull/691)
##### Thumbnail Caching
- feat(ui): add thumbnail caching by [@CyanVoxel](https://github.com/CyanVoxel) in [#694](https://github.com/TagStudioDev/TagStudio/pull/694)
#### Tags
##### Delete Tags _(Finally!)_
- feat: remove and create tags from tag database panel by [@DandyDev01](https://github.com/DandyDev01) in [#569](https://github.com/TagStudioDev/TagStudio/pull/569)
##### New Tag Colors + UI
- feat: expanded tag color system by [@CyanVoxel](https://github.com/CyanVoxel) in [#709](https://github.com/TagStudioDev/TagStudio/pull/709)
- fix(ui): use correct pink tag color by [@CyanVoxel](https://github.com/CyanVoxel) in [431efe4](https://github.com/TagStudioDev/TagStudio/commit/431efe4fe93213141c763e59ca9887215766fd42)
- fix(ui): use consistent tag outline colors by [@CyanVoxel](https://github.com/CyanVoxel) in [020a73d](https://github.com/TagStudioDev/TagStudio/commit/020a73d095c74283d6c80426d3c3db8874409952)
[![Screenshot 2025-01-04 at 04 23 43](https://private-user-images.githubusercontent.com/46939827/408753168-c8f82d89-ad7e-4be6-830e-b91cdc58e4c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTUwODcsIm5iZiI6MTc0NjY1NDc4NywicGF0aCI6Ii80NjkzOTgyNy80MDg3NTMxNjgtYzhmODJkODktYWQ3ZS00YmU2LTgzMGUtYjkxY2RjNThlNGM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNTMwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI3ZDk2MDg5ODVjYzA2ZGU2Njc1ODE5M2U4OWU4ZGMwY2MxNzUzM2M2YmM2ZmRiMTdlOTkyNGM3MjlhMWNiOWUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0RYUWgVW8VFvu2unzdWHtDCE4USYM77OcMvWeZkd8Hs)](https://private-user-images.githubusercontent.com/46939827/408753168-c8f82d89-ad7e-4be6-830e-b91cdc58e4c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTUwODcsIm5iZiI6MTc0NjY1NDc4NywicGF0aCI6Ii80NjkzOTgyNy80MDg3NTMxNjgtYzhmODJkODktYWQ3ZS00YmU2LTgzMGUtYjkxY2RjNThlNGM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNTMwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI3ZDk2MDg5ODVjYzA2ZGU2Njc1ODE5M2U4OWU4ZGMwY2MxNzUzM2M2YmM2ZmRiMTdlOTkyNGM3MjlhMWNiOWUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0RYUWgVW8VFvu2unzdWHtDCE4USYM77OcMvWeZkd8Hs)
##### New Tag Alias UI
- fix: preview panel aliases not staying up to date with database by [@DandyDev01](https://github.com/DandyDev01) in [#641](https://github.com/TagStudioDev/TagStudio/pull/641)
- fix: subtags/parent tags & aliases update the UI for building a tag by [@DandyDev01](https://github.com/DandyDev01) in [#534](https://github.com/TagStudioDev/TagStudio/pull/534)
#### Miscellaneous
- feat: about section by [@mashed5894](https://github.com/mashed5894) in [#712](https://github.com/TagStudioDev/TagStudio/pull/712)
- feat(ui): add configurable splash screens by [@CyanVoxel](https://github.com/CyanVoxel) in [#703](https://github.com/TagStudioDev/TagStudio/pull/703)
- feat(ui): show filenames in thumbnail grid by [@CyanVoxel](https://github.com/CyanVoxel) in [#633](https://github.com/TagStudioDev/TagStudio/pull/633)
### Fixed
- fix(ui): display all tags in panel during empty search by [@samuellieberman](https://github.com/samuellieberman) in [#328](https://github.com/TagStudioDev/TagStudio/pull/328)
- fix: avoid `KeyError` in `add_folders_to_tree()` (fix [#346](https://github.com/TagStudioDev/TagStudio/issues/346)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#347](https://github.com/TagStudioDev/TagStudio/pull/347)
- fix: error on closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#484](https://github.com/TagStudioDev/TagStudio/pull/484)
- fix: resolution info [#550](https://github.com/TagStudioDev/TagStudio/issues/550) by [@Roc25](https://github.com/Roc25) in [#551](https://github.com/TagStudioDev/TagStudio/pull/551)
- fix: remove queued thumnail jobs when closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#583](https://github.com/TagStudioDev/TagStudio/pull/583)
- fix: use absolute ffprobe path on macos (Fix [#511](https://github.com/TagStudioDev/TagStudio/issues/511)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#629](https://github.com/TagStudioDev/TagStudio/pull/629)
- fix(ui): prevent duplicate parent tags in UI by [@SkeleyM](https://github.com/SkeleyM) in [#665](https://github.com/TagStudioDev/TagStudio/pull/665)
- fix: fix -o flag not working if path has whitespace around it by [@python357-1](https://github.com/python357-1) in [#670](https://github.com/TagStudioDev/TagStudio/pull/670)
- fix: better file opening compatibility with non-ascii filenames by [@SkeleyM](https://github.com/SkeleyM) in [#667](https://github.com/TagStudioDev/TagStudio/pull/667)
- fix: restore environment before launching external programs by [@mashed5894](https://github.com/mashed5894) in [#707](https://github.com/TagStudioDev/TagStudio/pull/707)
- fix: have pydub use known ffmpeg + ffprobe locations by [@CyanVoxel](https://github.com/CyanVoxel) in [#724](https://github.com/TagStudioDev/TagStudio/pull/724)
- fix: add ".DS_Store" to `GLOBAL_IGNORE_SET` by [@CyanVoxel](https://github.com/CyanVoxel) in [b72a2f2](https://github.com/TagStudioDev/TagStudio/commit/b72a2f233141db4db6aa6be8796b626ebd3f0756)
- fix: don't add "._" files to libraries by [@CyanVoxel](https://github.com/CyanVoxel) in [eb1f634](https://github.com/TagStudioDev/TagStudio/commit/eb1f634d386cd8a5ecee1e6ff6a0b7d8811550fa)
### Changed
#### SQLite Save File Format
This was the main focus of this update, and where the majority of development time and resources have been spent since v9.4. These changes include everything that was done to migrate from the JSON format to SQLite starting from the initial SQLite PR, while re-implementing every feature from v9.4 as the initial SQLite PR was based on v9.3.x at the time.
- refactor!: use SQLite and SQLAlchemy for database backend by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#332](https://github.com/TagStudioDev/TagStudio/pull/332)
- feat: make search results more ergonomic by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#498](https://github.com/TagStudioDev/TagStudio/pull/498)
- feat: store `Entry` suffix separately by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#503](https://github.com/TagStudioDev/TagStudio/pull/503)
- feat: port thumbnail ([#390](https://github.com/TagStudioDev/TagStudio/pull/390)) and related features to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#522](https://github.com/TagStudioDev/TagStudio/pull/522)
- fix: don't check db version with new library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#536](https://github.com/TagStudioDev/TagStudio/pull/536)
- fix(ui): update ui when removing fields by [@DandyDev01](https://github.com/DandyDev01) in [#560](https://github.com/TagStudioDev/TagStudio/pull/560)
- feat(parity): backend for aliases and parent tags by [@DandyDev01](https://github.com/DandyDev01) in [#596](https://github.com/TagStudioDev/TagStudio/pull/596)
- fix: "open in explorer" opens correct folder by [@KirilBourakov](https://github.com/KirilBourakov) in [#603](https://github.com/TagStudioDev/TagStudio/pull/603)
- fix: ui/ux parity fixes for thumbnails and files by [@CyanVoxel](https://github.com/CyanVoxel) in [#608](https://github.com/TagStudioDev/TagStudio/pull/608)
- feat(parity): migrate json libraries to sqlite by [@CyanVoxel](https://github.com/CyanVoxel) in [#604](https://github.com/TagStudioDev/TagStudio/pull/604)
- fix: clear all setting values when opening a library by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#622](https://github.com/TagStudioDev/TagStudio/pull/622)
- fix: remove/rework windows path tests by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#625](https://github.com/TagStudioDev/TagStudio/pull/625)
- fix: add check to see if library is loaded in filter_items by [@Roc25](https://github.com/Roc25) in [#547](https://github.com/TagStudioDev/TagStudio/pull/547)
- fix: multiple macro errors by [@Computerdores](https://github.com/Computerdores) in [#612](https://github.com/TagStudioDev/TagStudio/pull/612)
- fix: don't allow blank tag alias values in db by [@CyanVoxel](https://github.com/CyanVoxel) in [#628](https://github.com/TagStudioDev/TagStudio/pull/628)
- feat: Reimplement drag drop files on sql migration by [@seakrueger](https://github.com/seakrueger) in [#528](https://github.com/TagStudioDev/TagStudio/pull/528)
- fix: stop sqlite db from being updated while running tests by [@python357-1](https://github.com/python357-1) in [#648](https://github.com/TagStudioDev/TagStudio/pull/648)
- fix: enter/return adds top result tag by [@SkeleyM](https://github.com/SkeleyM) in [#651](https://github.com/TagStudioDev/TagStudio/pull/651)
- fix: show correct unlinked files count by [@SkeleyM](https://github.com/SkeleyM) in [#653](https://github.com/TagStudioDev/TagStudio/pull/653)
- feat: implement parent tag search by [@Computerdores](https://github.com/Computerdores) in [#673](https://github.com/TagStudioDev/TagStudio/pull/673)
- fix: only close add tag menu with no search by [@SkeleyM](https://github.com/SkeleyM) in [#685](https://github.com/TagStudioDev/TagStudio/pull/685)
- fix: drag and drop no longer resets by [@SkeleyM](https://github.com/SkeleyM) in [#710](https://github.com/TagStudioDev/TagStudio/pull/710)
- feat(ui): port "create and add tag" to main branch by [@SkeleyM](https://github.com/SkeleyM) in [#711](https://github.com/TagStudioDev/TagStudio/pull/711)
- fix: don't add default title field, use proper phrasing for adding files by [@CyanVoxel](https://github.com/CyanVoxel) in [#701](https://github.com/TagStudioDev/TagStudio/pull/701)
- fix: preview panel + main window fixes and optimizations by [@CyanVoxel](https://github.com/CyanVoxel) in [#700](https://github.com/TagStudioDev/TagStudio/pull/700)
- fix: sort tag results by [@mashed5894](https://github.com/mashed5894) in [#721](https://github.com/TagStudioDev/TagStudio/pull/721)
- fix: restore opening last library on startup by [@SkeleyM](https://github.com/SkeleyM) in [#729](https://github.com/TagStudioDev/TagStudio/pull/729)
- fix(ui): don't always create tag on enter by [@SkeleyM](https://github.com/SkeleyM) in [#731](https://github.com/TagStudioDev/TagStudio/pull/731)
- fix: use tag aliases in tag search by [@CyanVoxel](https://github.com/CyanVoxel) in [#726](https://github.com/TagStudioDev/TagStudio/pull/726)
- fix: keep initial id order in `get_entries_full()` by [@CyanVoxel](https://github.com/CyanVoxel) in [#736](https://github.com/TagStudioDev/TagStudio/pull/736)
- fix: always catch db mismatch by [@CyanVoxel](https://github.com/CyanVoxel) in [#738](https://github.com/TagStudioDev/TagStudio/pull/738)
- fix: relink unlinked entry to existing entry without sql error by [@mashed5894](https://github.com/mashed5894) in [#730](https://github.com/TagStudioDev/TagStudio/issues/730)
- fix: refactor and fix bugs with missing_files.py by [@CyanVoxel](https://github.com/CyanVoxel) in [#739](https://github.com/TagStudioDev/TagStudio/pull/739)
- fix: dragging files references correct entry IDs [@CyanVoxel](https://github.com/CyanVoxel) in [44ff17c](https://github.com/TagStudioDev/TagStudio/commit/44ff17c0b3f05570e356c112f005dbc14c7cc05d)
- ui: port splash screen from Alpha-v9.4 by [@CyanVoxel](https://github.com/CyanVoxel) in [af760ee](https://github.com/TagStudioDev/TagStudio/commit/af760ee61a523c84bab0fb03a68d7465866d0e05)
- fix: tags created from tag database now add aliases by [@CyanVoxel](https://github.com/CyanVoxel) in [2903dd2](https://github.com/TagStudioDev/TagStudio/commit/2903dd22c45c02498687073d075bb88886de6b62)
- fix: check for tag name parity during JSON migration by [@CyanVoxel](https://github.com/CyanVoxel) in [#748](https://github.com/TagStudioDev/TagStudio/pull/748)
- feat(ui): re-implement tag display names on sql by [@CyanVoxel](https://github.com/CyanVoxel) in [#747](https://github.com/TagStudioDev/TagStudio/pull/747)
- fix(ui): restore Windows accent color on PySide 6.8.0.1 by [@CyanVoxel](https://github.com/CyanVoxel) in [#755](https://github.com/TagStudioDev/TagStudio/pull/755)
- fix(ui): (mostly) fix right-click search option on tags by [@CyanVoxel](https://github.com/CyanVoxel) in [#756](https://github.com/TagStudioDev/TagStudio/pull/756)
- feat: copy/paste fields and tags by [@mashed5894](https://github.com/mashed5894) in [#722](https://github.com/TagStudioDev/TagStudio/pull/722)
#### UI/UX
- feat(ui): pre-select default tag name in `BuildTagPanel` by [@Cool-Game-Dev](https://github.com/Cool-Game-Dev) in [#592](https://github.com/TagStudioDev/TagStudio/pull/592)
- feat(ui): keyboard navigation for editing tags by [@Computerdores](https://github.com/Computerdores) in [#407](https://github.com/TagStudioDev/TagStudio/pull/407)
- feat(ui): use tag query as default new tag name by [@CyanVoxel](https://github.com/CyanVoxel) in [29c0dfd](https://github.com/TagStudioDev/TagStudio/commit/29c0dfdb2d88e8f473e27c7f1fe7ede6e5bd0feb)
- feat(ui): shortcut to add tags to selected entries; change click behavior of tags to edit by [@CyanVoxel](https://github.com/CyanVoxel) in [#749](https://github.com/TagStudioDev/TagStudio/pull/749)
- fix(ui): use consistent dark mode colors for all systems by [@CyanVoxel](https://github.com/CyanVoxel) in [#752](https://github.com/TagStudioDev/TagStudio/pull/752)
- fix(ui): use camera white balance for raw images by [@CyanVoxel](https://github.com/CyanVoxel) in [6ee5304](https://github.com/TagStudioDev/TagStudio/commit/6ee5304b52f217af0f5df543fcb389649203d6b2)
- Mixed field editing has been limited due to various bugs in both the JSON and SQL implementations. This will be re-implemented in a future release.
#### Performance
- feat: improve performance of "Delete Missing Entries" by [@Toby222](https://github.com/Toby222) and [@Computerdores](https://github.com/Computerdores) in [#696](https://github.com/TagStudioDev/TagStudio/pull/696)
#### Internal Changes
- refactor: combine open launch args by [@UnusualEgg](https://github.com/UnusualEgg) in [#364](https://github.com/TagStudioDev/TagStudio/pull/364)
- feat: add date_created, date_modified, and date_added columns to entries table by [@CyanVoxel](https://github.com/CyanVoxel) in [#740](https://github.com/TagStudioDev/TagStudio/pull/740)
## [9.4.2] - 2024-12-01
### Added/Fixed
- Create auto-backup of library for use in save failures (Fix [#343](https://github.com/TagStudioDev/TagStudio/issues/343)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#554](https://github.com/TagStudioDev/TagStudio/pull/554)
## [9.4.1] - 2024-09-13
### Added
- Warn user if FFmpeg is not installed
- Support for `.raf` and `.orf` raw image thumbnails and previews
### Fixed
- Use `birthtime` for file creation time on Mac & Windows
- Use audio icon fallback when FFmpeg is not detected
- Retain search query upon directory refresh
### Changed
- Significantly improve file re-scanning performance
## [9.4.0] - 2024-09-03
### Added
- Copy and paste fields
- Add multiple fields at once
- Drag and drop files in/out of the program
- Files can be shared by dragging them from the thumbnail grid to other programs
- Files can be added to library folder by dragging them into the program
- Manage Python virtual environment in Nix flake
- Ability to create tag when adding tags
- Blender preview thumbnail support
- File deletion/trashing
- Added right-click option on thumbnails and preview panel to delete files
- Added Edit Menu option for deleting files
- Added <kbd>Delete</kbd> key shortcut for deleting files
- Font preview thumbnail support
- Short "Aa" previews for thumbnails
- Full alphabet preview for the preview pane
- Sort tags by alphabetical/color
- File explorer action follows OS naming
- Preview Source Engine files
- Expanded thumbnail and preview features
- Add album cover art thumbnails
- Add audio waveform thumbnails for audio files without embedded cover art
- Add new default file thumbnails, both for generic and specific file types
- Change the unlinked file icon to better convey its meaning
- Add dropdown for different thumbnail sizes
- Show File Creation and Modified dates; Restyle file path label
### Fixed
- Backslashes in f-string on file dupe widget
- Tags not shown when none searched
- Avoid error from eagerly grabbing data values
- Correct behavior for tag search options
- Load Gallery-DL sidecar files correctly
- Correct duplicate file matching
- GPU hardware acceleration in Nix flake
- Suppress command prompt windows for FFmpeg in builds
### Internal Changes
- Move type constants to media classes
- Combine open launch arguments
- Revamp Nix flake with devenv/direnv in cb4798b
- Remove impurity of Nix flake when used with direnv in bc38e56
## [9.3.2] - 2024-07-18
### Fixed
- Fix signal log warning
- Fix "Folders to Tags" feature
- Fix search ignoring case of extension list
### Internal Changes
- Add tests into CI by
- Create testing library files ad-hoc
- Refactoring: centralize field IDs
- Update to pyside6 version 6.7.1
## [9.3.1] - 2024-06-13
### Fixed
- Separately pin QT nixpkg version
- Bugfix for #252, don't attempt to read video file if invalid or 0 frames long
- Toggle Mouse Event Transparency on ItemThumbs
- Refactor `video_player.py`
## [9.3.0] - 2024-06-08
### Added
- Added playback previews for video files
- Added Boolean "and/or" search mode selection
- Added ability to scan and fix duplicate entries (not to be confused with duplicate files) from the "Fix Unlinked Entries" menu
- Added “Select All” (<kbd>Ctrl</kbd>+<kbd>A</kbd> / <kbd>⌘ Command</kbd>+<kbd>A</kbd>) hotkey for the library grid view
- Added "Clear Selection" hotkey (<kbd>Esc</kbd>) for the library grid view
- Added the ability to invert the file extension inclusion list into an exclusion list
- Added default landing page when no library is open
### Fixed
- TagStudio will no longer attempt to or allow you to reopen a library from a missing location
- Fixed `PermissionError` when attempting to access files with a higher permission level upon scanning the library directory
- Fixed RAW image previews sometimes not loadingand
- Fixed most non-UTF-8 encoded text files from not being able to be previewed
- Fixed "Refresh Directories"/"Fix Unlinked Entries" creating duplicate entries
- Other miscellaneous fixes
### Changed
- Renamed "Subtags" to "Parent Tags" to help better describe their function
- Increased number of tags shown by default in the "Add Tag" modal from 29 to 100
- Documentation is now split into individual linked files and updated to include future features
- Replaced use of `os.path` with `pathlib`
- `.cr2` files are now included in the list of RAW image file types
- Minimum supported macOS version raised to 12.0
## [9.2.1] - 2024-05-23
### Added
- Basic thumbnail/preview support for RAW images (currently `.raw`, `.dng`, `.rw2`, `.nef`, `.arw`, `.crw`, `.cr3`)
- NOTE: These previews are currently slow to load given the nature of rendering them. In the future once thumbnail caching is added, this process should only happen once.
- Thumbnail/preview support for HEIF images
### Fixed
- Fixed sidebar not expanding horizontally
- Fixed "Recent Library" list not updating when creating a new library
- Fixed palletized images not loading with alpha channels
- Low resolution images (such as pixel art) now render with crisp edges in thumbnails and previews
- Fixed visual bug where the edit icon would show for incorrect fields
## [9.2.0] - 2024-05-14
### Added
@@ -48,6 +847,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New application icons
### Known Issues
- Using and editing multiple entry fields of the same type may result in incorrect field(s) being updated
- Adding Favorite or Archived tags via the thumbnail badges may apply the tag(s) to incorrect fields
- Searching for tag names with spaces does not currently function as intended

View File

@@ -1,83 +1,56 @@
# Contributing to TagStudio
_Last Updated: January 30th, 2025_
_Last Updated: March 8th, 2025_
Thank you so much for showing interest in contributing to TagStudio! Here are a set of instructions and guidelines for contributing code or documentation to the project. This document will change over time, so make sure that your contributions still line up with the requirements here before submitting a pull request.
## Getting Started
- Check the [Feature Roadmap](/docs/updates/roadmap.md) page to see what priority features there are, the [FAQ](/README.md/#faq), as well as the open [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls).
- Check the [Feature Roadmap](/docs/updates/roadmap.md) page to see what priority features there are, the [FAQ](/README.md/#faq), as well as the project's [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls).
- If you'd like to add a feature that isn't on the feature roadmap or doesn't have an open issue, **PLEASE create a feature request** issue for it discussing your intentions so any feedback or important information can be given by the team first.
- We don't want you wasting time developing a feature or making a change that can't/won't be added for any reason ranging from pre-existing refactors to design philosophy differences.
- **Please don't** create pull requests that consist of large refactors, _especially_ without discussing them with us first. These end up doing more harm than good for the project by continuously delaying progress and disrupting everyone else's work.
- If you wish to discuss TagStudio further, feel free to join the [Discord Server](https://discord.com/invite/hRNnVKhF2G)
- If you wish to discuss TagStudio further, feel free to join the [Discord Server](https://discord.com/invite/hRNnVKhF2G)!
### Contribution Checklist
- I've read the [Feature Roadmap](/docs/updates/roadmap.md) page
- I've read the [FAQ](/README.md/#faq), including the "[Features I Likely Won't Add/Pull](/README.md/#features-i-likely-wont-addpull)" section
- I've checked the open [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls)
- I've checked the project's [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls)
- **I've created a new issue for my feature/fix _before_ starting work on it**, or have at least notified others in the relevant existing issue(s) of my intention to work on it
- I've set up my development environment including Ruff, Mypy, and PyTest
- I've read the [Code Guidelines](#code-guidelines) and/or [Documentation Guidelines](#documentation-guidelines)
- **_I mean it, I've found or created an issue for my feature/fix!_**
> [!NOTE]
> If the fix is small and self-explanatory (i.e. a typo), then it doesn't require an issue to be opened first. Issue tracking is supposed to make our lives easier, not harder. Please use your best judgement to minimize the amount of work involved for everyone involved.
> If the fix is small and self-explanatory (i.e. a typo), then it doesn't require an issue to be opened first. Issue tracking is supposed to make our lives easier, not harder. Please use your best judgement to minimize the amount of work for everyone involved.
## Creating a Development Environment
### Prerequisites
If you wish to develop for TagStudio, you'll need to create a development environment by installing the required dependencies. You have a number of options depending on your level of experience and familiarly with existing Python toolchains.
- [Python](https://www.python.org/downloads/) 3.12
- [Ruff](https://github.com/astral-sh/ruff) (Included in `requirements-dev.txt`)
- [Mypy](https://github.com/python/mypy) (Included in `requirements-dev.txt`)
- [PyTest](https://docs.pytest.org) (Included in `requirements-dev.txt`)
If you know what you're doing and have developed for Python projects in the past, you can get started quickly with the "Brief Instructions" below. Otherwise, please see the full instructions on the documentation website for "[Creating a Development Environment](https://docs.tagstud.io/install/#creating-a-development-environment)".
### Creating a Python Virtual Environment
### Brief Instructions
If you wish to launch the source version of TagStudio outside of your IDE:
1. Have [Python 3.12](https://www.python.org/downloads/) and PIP installed. Also have [FFmpeg](https://ffmpeg.org/download.html) installed if you wish to have audio/video playback and thumbnails.
2. Clone the repository to the folder of your choosing:
```
git clone https://github.com/TagStudioDev/TagStudio.git
```
3. Use a dependency manager such as [uv](https://docs.astral.sh/uv/) or [Poetry 2.0](https://python-poetry.org/blog/category/releases/) to install the required dependencies, or alternatively create and activate a [virtual environment](https://docs.tagstud.io/install/#manual-installation) with `venv`.
> [!IMPORTANT]
> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3` for consistency. You can check to see which alias your system uses and if it's for the correct Python version by typing `python3 --version` (or whichever alias) into your terminal.
4. If using a virtual environment instead of a dependency manager, install an editable version of the program and development dependencies with the following PIP command:
> [!TIP]
> On Linux and macOS, you can launch the `tagstudio.sh` script to skip the following process, minus the `requirements-dev.txt` installation step. _Using the script is fine if you just want to launch the program from source._
```
pip install -e ".[dev]"
```
1. In the root repository directory, create a python virtual environment:
`python3 -m venv .venv`
2. Activate your environment:
Otherwise, modify the command above for use with your dependency manager of choice. For example if using uv, you may use this:
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
- Linux/macOS: `source .venv/bin/activate`
3. Install the required packages:
- `pip install -r requirements.txt`
- If developing (includes Ruff and Mypy): `pip install -r requirements-dev.txt`
_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._
### Manually Launching (Outside of an IDE)
- **Windows** (start_win.bat)
- To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired.
- **Linux/macOS** (TagStudio.sh)
- Run the "TagStudio.sh" script and the program should launch! (Make sure that the script is marked as executable if on Linux). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `./TagStudio.sh`.
- **NixOS** (Nix Flake)
- Use the provided [Flake](https://nixos.wiki/wiki/Flakes) to create and enter a working environment by running `nix develop`. Then, run the program via `python3 tagstudio/tag_studio.py` from the root directory.
> [!WARNING]
> Support for NixOS is still a work in progress.
- **Any** (No Scripts)
- Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tag_studio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`.
```
uv pip install -e ".[dev]"
```
## Workflow Checks
@@ -108,31 +81,19 @@ Mypy is a static type checker for Python. It sure has a lot to say sometimes, bu
#### Running Locally
- **First time only:** Move into the `/tagstudio` directory with `cd tagstudio` and run the following:
- **(First time only)** Run the following:
- `mkdir -p .mypy_cache`
- `mypy --install-types --non-interactive`
- Check code by moving into the `/tagstudio` directory with `cd tagstudio` _(if you aren't already inside)_ and running `mypy --config-file ../pyproject.toml .`. _(Don't forget the `.` at the end!)_
- You can now check code by running `mypy --config-file pyproject.toml .` in the repository root. _(Don't forget the "." at the end!)_
Mypy is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=matangover.mypy), PyCharm [plugin](https://plugins.jetbrains.com/plugin/11086-mypy), and [more](https://plugins.jetbrains.com/plugin/11086-mypy).
### PyTest
- Run all tests by moving into the `/tagstudio` directory with `cd tagstudio` and running `pytest tests/`.
- Run all tests by running `pytest tests/` in the repository root.
## Code Style
Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older code may not be adhering to all of these guidelines, in which case _"do as I say, not as I do"..._
- Do your best to write clear, concise, and modular code.
- Keep a maximum column with of no more than **100** characters.
- Code comments should be used to help describe sections of code that can't speak for themselves.
- Use [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings for any classes and functions you add.
- If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;)
- Imports should be ordered alphabetically.
- Lists of values should be ordered using their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
- Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/tagstudio/src/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern.
- When writing text for window titles or form titles, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check.
- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)!
See the [Style Guide](/STYLE.md)
### Modules & Implementations
@@ -156,7 +117,7 @@ Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older
> [!IMPORTANT]
> Please do not force push if your PR is open for review!
>
> Force pushing makes it impossible to discern which changes have already been reviewed and which haven't. This means a reviewer will then have to rereview all the already reviewed code, which is a lot of unnecessary work for reviewers.
> Force pushing makes it impossible to discern which changes have already been reviewed and which haven't. This means a reviewer will then have to re-review all the already reviewed code, which is a lot of unnecessary work for reviewers.
> [!TIP]
> If you're unsure where to stop the scope of your PR, ask yourself: _"If I broke this up, could any parts of it still be used by the project in the meantime?"_
@@ -165,7 +126,7 @@ Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older
- Final code must function on supported versions of Windows, macOS, and Linux:
- Windows: 10, 11
- macOS: 12.0+
- macOS: 13.0+
- Linux: _Varies_
- Final code must **_NOT:_**
- Contain superfluous or unnecessary logging statements

View File

@@ -44,7 +44,7 @@ TagStudio is a photo & file organization application with an underlying tag-base
## Goals
- To achieve a portable, private, extensible, open-format, and feature-rich system of organizing and rediscovering files.
- To provide powerful methods for organization, notably the concept of tag inheritance, or taggable tags _(and in the near future, the combination of composition-based tags)._
- To provide powerful methods for organization, notably the concept of tag inheritance, or "taggable tags" _(and in the near future, the combination of composition-based tags)._
- To create an implementation of such a system that is resilient against a users actions outside the program (modifying, moving, or renaming files) while also not burdening the user with mandatory sidecar files or requiring them to change their existing file structures and workflows.
- To support a wide range of users spanning across different platforms, multi-user setups, and those with large (several terabyte) libraries.
- To make the dang thing look nice, too. Its 2025, not 1995.
@@ -75,14 +75,14 @@ Translation hosting generously provided by [Weblate](https://weblate.org/en/). C
- Add metadata to your library entries, including:
- Name, Author, Artist (Single-Line Text Fields)
- Description, Notes (Multiline Text Fields)
- Create rich tags composed of a name, color, a list of aliases, and a list of parent tags - these being tags in which these tags inherit values from.
- Create rich tags composed of a name, color, a list of aliases, and a list of "parent tags" - these being tags in which these tags inherit values from.
- Copy and paste tags and fields across file entries
- Automatically organize tags into groups based on parent tags marked as "categories"
- Generate tags from your existing folder structure with the "Folders to Tags" macro (NOTE: these tags do NOT sync with folders after they are created)
### Search
- Search for file entries based on tags, file path (`path:`), file types (`filetype:`), and even media types! (`mediatype:`)
- Search for file entries based on tags, file path (`path:`), file types (`filetype:`), and even media types! (`mediatype:`). Path searches currently use [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) syntax, so you may need to wrap your filename or filepath in asterisks while searching. This will not be strictly necessary in future versions of the program.
- Use and combine boolean operators (`AND`, `OR`, `NOT`) along with parentheses groups, quotation escaping, and underscore substitution to create detailed search queries
- Use special search conditions (`special:untagged` and `special:empty`) to find file entries without tags or fields, respectively
@@ -98,29 +98,21 @@ Translation hosting generously provided by [Weblate](https://weblate.org/en/). C
## Installation
To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system under the "Assets" section. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around.
To download executable builds of TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) page of the GitHub repository and download the latest release for your system under the "Assets" section at the bottom of the release.
**We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub releases page are _unofficial_ and not maintained by us.** Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk.
TagStudio has builds for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around.
> [!IMPORTANT]
> On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
For detailed instructions, installation help, and instructions for developing for TagStudio, please see the "[Installation](https://docs.tagstud.io/install/)" page on our documentation website.
> [!IMPORTANT]
> On Linux with non-Qt based Desktop Environments you may be unable to open TagStudio. You need to make sure that "xcb-cursor0" or "libxcb-cursor0" packages are installed. For more info check [Missing linux dependencies](https://github.com/TagStudioDev/TagStudio/discussions/182#discussioncomment-9452896)
<!-- prettier-ignore -->
> [!CAUTION]
> **We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub [Releases](https://github.com/TagStudioDev/TagStudio/releases) page are _unofficial_ and not maintained by us.**
>
> Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk!
### Third-Party Dependencies
- For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](/docs/help/ffmpeg.md) guide.
### Optional Arguments
Arguments available to pass to the program, either via the command line or a shortcut.
> `--open <path>` / `-o <path>`
> Path to a TagStudio Library folder to open on start.
>
> `--config-file <path>` / `-c <path>`
> Path to the TagStudio config file to load.
For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](/docs/help/ffmpeg.md) guide.
## Usage
@@ -136,13 +128,13 @@ Libraries under 10,000 files automatically scan for new or modified files when o
Access the "Add Tag" search box by either clicking on the "Add Tag" button at the bottom of the right sidebar, accessing the "Add Tags to Selected" option from the File menu, or by pressing <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd>.
From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the “+” button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the <kbd>Enter</kbd>/<kbd>Return</kbd> key to add the the topmost tag and reset the tag search. Press <kbd>Enter</kbd>/<kbd>Return</kbd> once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard!
From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the "+" button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the <kbd>Enter</kbd>/<kbd>Return</kbd> key to add the the topmost tag and reset the tag search. Press <kbd>Enter</kbd>/<kbd>Return</kbd> once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard!
To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.
### Adding Metadata to File Entries
To add a metadata field to a file entry, start by clicking the Add Field button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field youd like to add to the entry
To add a metadata field to a file entry, start by clicking the "Add Field" button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field youd like to add to the entry
### Editing Metadata Fields
@@ -164,15 +156,19 @@ Create a new tag by accessing the "New Tag" option from the Edit menu or by pres
- Parent tags with the disambiguation check next to them will be used to help disambiguate tag names that may not be unique.
- For example: If you had a tag for "Freddy Fazbear", you might add "Five Nights at Freddy's" as one of the parent tags. If the disambiguation box is checked next to "Five Nights at Freddy's" parent tag, then the tag "Freddy Fazbear" will display as "Freddy Fazbear (Five Nights at Freddy's)". Furthermore, if the "Five Nights at Freddy's" tag has a shorthand like "FNAF", then the "Freddy Fazbear" tag will display as "Freddy Fazbear (FNAF)".
- The **color** option lets you select an optional color palette to use for your tag.
- The **"Is Cagegory"** property lets you treat this tag as a category under which itself and any child tags inheriting from it will be sorted by inside the preview panel.
- The **"Is Category"** property lets you treat this tag as a category under which itself and any child tags inheriting from it will be sorted by inside the preview panel.
#### Tag Manager
You can manage your library of tags from opening the "Tag Manager" panel from Edit -> "Tag Manager". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
### Editing Tags
To edit a tag, click on it inside the preview panel or right-click the tag and select Edit Tag from the context menu.
To edit a tag, click on it inside the preview panel or right-click the tag and select "Edit Tag" from the context menu.
### Relinking Moved Files
Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to Search & Relink any unlinked file entries to their respective files, or Delete Unlinked Entries in the event the original files have been deleted and you no longer wish to keep their entries inside your library.
Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to "Search & Relink" any unlinked file entries to their respective files, or "Delete Unlinked Entries" in the event the original files have been deleted and you no longer wish to keep their entries inside your library.
> [!WARNING]
> There is currently no method to relink entries to files that have been renamed - only moved or deleted. This is a high priority for future releases.
@@ -190,7 +186,7 @@ These features were present in pre-public versions of TagStudio (9.0 and below)
#### Fix Duplicate Files
Load in a .dupeguru file generated by [dupeGuru](https://github.com/arsenetar/dupeguru/) and mirror metadata across entries marked as duplicates. After mirroring, return to dupeGuru to manage deletion of the duplicate files. After deletion, use the Fix Unlinked Entries feature in TagStudio to delete the duplicate set of entries for the now-deleted files
Load in a .dupeguru file generated by [dupeGuru](https://github.com/arsenetar/dupeguru/) and mirror metadata across entries marked as duplicates. After mirroring, return to dupeGuru to manage deletion of the duplicate files. After deletion, use the "Fix Unlinked Entries" feature in TagStudio to delete the duplicate set of entries for the now-deleted files
> [!CAUTION]
> While this feature is functional, its a pretty roundabout process and can be streamlined in the future.

84
STYLE.md Normal file
View File

@@ -0,0 +1,84 @@
# Code Style
Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older code may not be adhering to all of these guidelines, in which case _"do as I say, not as I do"..._
- Do your best to write clear, concise, and modular code.
- This should include making methods private by default (e.g. `__method()`)
- Methods should only be protected (e.g. `_method()`) or public (e.g. `method()`) when needed and warranted
- Keep a maximum column width of no more than **100** characters.
- Code comments should be used to help describe sections of code that can't speak for themselves.
- Use [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings for any classes and functions you add.
- If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;)
- Imports should be ordered alphabetically.
- Lists of values should be ordered using their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
- Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/src/tagstudio/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern.
- When writing text for window titles or form titles, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check.
- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)!
## QT
As of writing this section, the QT part of the code base is quite unstructured and the View and Controller parts are completely intermixed[^1]. This makes maintenance, fixes and general understanding of the code base quite challenging, because the interesting parts you are looking for are entangled in a bunch of repetitive UI setup code. To address this we are aiming to more strictly separate the view and controller aspects of the QT frontend.
The general structure of the QT code base should look like this:
```
qt
├── controller
│ ├── widgets
│ │ └── preview_panel_controller.py
│ └── main_window_controller.py
├── view
│ ├── widgets
│ │ └── preview_panel_view.py
│ └── main_window_view.py
├── ts_qt.py
└── mixed.py
```
In this structure there are the `view` and `controller` sub-directories. They have the exact same structure and for every `<component>_view.py` there is a `<component>_controller.py` at the same location in the other subdirectory and vice versa.
Typically the classes should look like this:
```py
# my_cool_widget_view.py
class MyCoolWidgetView(QWidget):
def __init__(self):
super().__init__()
self.__button = QPushButton()
self.__color_dropdown = QComboBox()
# ...
self.__connect_callbacks()
def __connect_callbacks(self):
self.__button.clicked.connect(self._button_click_callback)
self.__color_dropdown.currentIndexChanged.connect(
lambda idx: self._color_dropdown_callback(self.__color_dropdown.itemData(idx))
)
def _button_click_callback(self):
raise NotImplementedError()
```
```py
# my_cool_widget_controller.py
class MyCoolWidget(MyCoolWidgetView):
def __init__(self):
super().__init__()
def _button_click_callback(self):
print("Button was clicked!")
def _color_dropdown_callback(self, color: Color):
print(f"The selected color is now: {color}")
```
Observe the following key aspects of this example:
- The Controller is just called `MyCoolWidget` instead of `MyCoolWidgetController` as it will be directly used by other code
- The UI elements are in private variables
- This enforces that the controller shouldn't directly access UI elements
- Instead the view should provide a protected API (e.g. `_get_color()`) for things like setting/getting the value of a dropdown, etc.
- Instead of `_get_color()` there could also be a `_color` method marked with `@property`
- The callback methods are already defined as protected methods with NotImplementedErrors
- Defines the interface the callbacks
- Enforces that UI events be handled
> [!TIP]
> A good (non-exhaustive) rule of thumb is: If it requires a non-UI import, then it doesn't belong in the `*_view.py` file.
[^1]: For an explanation of the Model-View-Controller (MVC) Model, checkout this article: [MVC Framework Introduction](https://www.geeksforgeeks.org/mvc-framework-introduction/).

View File

@@ -1,7 +0,0 @@
#! /usr/bin/env bash
set -e
cd "$(dirname "$0")"
! [ -d .venv ] && python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python tagstudio/tag_studio.py

19
contrib/.envrc-nix Normal file
View File

@@ -0,0 +1,19 @@
# shellcheck shell=bash
# If you wish to use this file, symlink or copy it to `.envrc` for direnv to read it.
# This will use the Nix flake development shell.
#
# ln -s contrib/.envrc-nix .envrc
if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM="
fi
watch_file nix/shell.nix pyproject.toml
use flake
# Only watch now, or direnv will execute again if created or modified by itself.
watch_file "${UV_PROJECT_ENVIRONMENT:-.venv}"/bin/activate
# vi: ft=bash

32
contrib/.envrc-uv Normal file
View File

@@ -0,0 +1,32 @@
# shellcheck shell=bash
# If you wish to use this file, symlink or copy it to `.envrc` for direnv to read it.
# This will use a virtual environment created by uv.
#
# ln -s contrib/.envrc-uv .envrc
watch_file .python-version pyproject.toml uv.lock
venv="$(expand_path "${UV_PROJECT_ENVIRONMENT:-.venv}")"
if [ ! -f "${venv}"/bin/activate ]; then
printf '%s\n' 'Generating virtual environment...' >&2
rm -rf "${venv}"
uv venv "${venv}"
fi
# Only watch now, or direnv will execute again if created or modified by itself.
watch_file "${venv}"/bin/activate
# shellcheck disable=SC1091
source "${venv}"/bin/activate
if [ ! -f "${venv}"/pyproject.toml ] || ! diff --brief pyproject.toml "${venv}"/pyproject.toml >/dev/null; then
printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2
uv pip install --quiet --editable '.[dev]'
cp pyproject.toml "${venv}"/pyproject.toml
fi
pre-commit install
# vi: ft=bash

17
contrib/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "TagStudio",
"type": "python",
"request": "launch",
"program": "${workspaceRoot}/src/tagstudio/main.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": [
"-o",
"~/Documents/Example"
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

10
docs/assets/icon_mono.svg Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 739 739" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.986683,0,0,0.986683,-136.081,-136.081)">
<path d="M535.939,863.161C515.931,843.153 203.505,529.713 183.497,509.705C169.086,495.294 161.469,476.645 160.649,457.754C160.046,443.845 138.078,230.102 137.923,217.139C137.681,196.785 145.323,176.356 160.839,160.839C177.115,144.564 198.795,136.951 220.125,138.016C232.439,138.63 447.036,159.52 461.817,160.931C479.3,162.6 496.329,170.12 509.705,183.497C523.113,196.904 849.753,522.531 863.161,535.939C893.716,566.494 893.716,616.108 863.161,646.663L646.663,863.161C616.108,893.716 566.494,893.716 535.939,863.161ZM321.355,223.613C296.045,198.303 254.947,198.303 229.636,223.613C204.326,248.924 204.326,290.022 229.636,315.332C254.947,340.643 296.045,340.643 321.355,315.332C346.666,290.022 346.666,248.924 321.355,223.613ZM362.109,606.786C409.476,654.152 424.103,598.401 454.027,606.786C468.584,610.865 453.72,642.505 443.028,673.551C425.086,725.641 484.094,757.817 516.601,720.743C545.603,687.667 503.579,655.692 520.581,632.527C537.795,609.074 563.542,633.319 565.542,665.527C568.921,719.955 535.825,735.585 543.999,774.591C553.59,820.348 624.181,827.565 638,774.591C647.736,737.269 603.102,705.31 628.352,644.476C636.209,625.545 662.786,619.154 669.759,644.476C673.976,659.791 660.264,670.152 666.759,693.55C674.41,721.114 725.088,732.96 740.374,693.55C746.873,676.793 734.853,651.273 731.597,640.406C714.283,582.611 826.807,582.426 762.374,517.789C703.034,458.263 493.6,249.017 493.6,249.017C479.164,234.58 457.464,234.58 443.028,249.017L249.017,443.028C234.58,457.464 234.58,479.164 249.017,493.6C249.017,493.6 340.149,584.825 362.109,606.786Z" style="fill:white;"/>
</g>
<g transform="matrix(0.986683,0,0,0.986683,-136.081,-136.081)">
<path d="M733.962,560.164C740.987,553.139 740.987,541.733 733.962,534.708L482.173,282.92C475.148,275.895 463.742,275.895 456.717,282.92L431.261,308.376C424.237,315.4 424.237,326.807 431.261,333.831L683.05,585.62C690.075,592.645 701.481,592.645 708.506,585.62L733.962,560.164ZM439.639,559.207C446.664,552.182 446.664,540.776 439.639,533.751L335.491,429.602C328.466,422.578 317.059,422.578 310.035,429.602L284.579,455.058C277.554,462.083 277.554,473.489 284.579,480.514L388.728,584.663C395.752,591.688 407.159,591.688 414.184,584.663L439.639,559.207ZM584.306,556.624C591.331,549.599 591.331,538.192 584.306,531.168L409.115,355.977C402.091,348.953 390.684,348.953 383.66,355.977L358.204,381.433C351.179,388.458 351.179,399.864 358.204,406.889L533.394,582.079C540.419,589.104 551.825,589.104 558.85,582.079L584.306,556.624ZM298.425,246.543C311.081,259.198 311.081,279.747 298.425,292.402C285.77,305.058 265.221,305.058 252.566,292.402C239.911,279.747 239.911,259.198 252.566,246.543C265.221,233.888 285.77,233.888 298.425,246.543Z" style="fill:white;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 105 KiB

188
docs/develop.md Normal file
View File

@@ -0,0 +1,188 @@
# Developing
If you wish to develop for TagStudio, you'll need to create a development environment by installing the required dependencies. You have a number of options depending on your level of experience and familiarity with existing Python toolchains.
<!-- prettier-ignore -->
!!! tip "Contributing"
If you wish to contribute to TagStudio's development, please read our [CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md)!
## Installing Python
Python [3.12](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.12.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
<!-- prettier-ignore -->
!!! info "Python Aliases"
Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python` for consistency.
If you already have Python installed on your system, you can check the version by running the following command:
```sh
python --version
```
---
### Installing with pyenv
If you choose to install Python using pyenv, please refer to the following instructions:
1. Follow pyenv's [install instructions](https://github.com/pyenv/pyenv/?tab=readme-ov-file#installation) for your system.
2. Install the appropriate Python version with pyenv by running `pyenv install 3.12` (This will **not** mess with your existing Python installation).
3. Navigate to the repository root folder in your terminal and run `pyenv local 3.12`. You could alternatively use `pyenv shell 3.12` or `pyenv global 3.12` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended.
---
## Cloning from GitHub
The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio).
```sh
git clone https://github.com/TagStudioDev/TagStudio.git
```
## Installing Dependencies
To install the required dependencies, you can use a dependency manager such as [uv](https://docs.astral.sh/uv) or [Poetry 2.0](https://python-poetry.org). Alternatively you can create a virtual environment and manually install the dependencies yourself.
### Installing with uv
If using [uv](https://docs.astral.sh/uv), you can install the dependencies for TagStudio with the following command:
```sh
uv pip install -e ".[dev]"
```
A reference `.envrc` is provided for use with [direnv](#direnv), see [`contrib/.envrc-uv`](https://github.com/TagStudioDev/TagStudio/blob/main/contrib/.envrc-uv).
---
### Installing with Poetry
If using [Poetry](https://python-poetry.org), you can install the dependencies for TagStudio with the following command:
```sh
poetry install --with dev
```
---
### Manual Installation
If you choose to manually set up a virtual environment and install dependencies instead of using a dependency manager, please refer to the following instructions:
<!-- prettier-ignore -->
!!! tip "Virtual Environments"
Learn more about setting up a virtual environment with Python's [official tutorial](https://docs.python.org/3/tutorial/venv.html).
1. In the root repository directory, create a python virtual environment:
```sh
python -m venv .venv
```
2. Activate your environment:
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
- Linux/macOS: `source .venv/bin/activate`
<!-- prettier-ignore -->
!!! info "Supported Shells"
Depending on your system, the regular activation script _might_ not work on alternative shells. In this case, refer to the table below for supported shells:
| Shell | Script |
| ---------: | :------------------------ |
| Bash/ZSH | `.venv/bin/activate` |
| Fish | `.venv/bin/activate.fish` |
| CSH/TCSH | `.venv/bin/activate.csh` |
| PowerShell | `.venv/bin/activate.ps1` |
3. Use the following PIP command to create an editable installation and install the required development dependencies:
```sh
pip install -e ".[dev]"
```
## Nix(OS)
If using [Nix](https://nixos.org/), there is a development environment already provided in the [flake](https://wiki.nixos.org/wiki/Flakes) that is accessible with the following command:
```sh
nix develop
```
A reference `.envrc` is provided for use with [direnv](#direnv), see [`contrib/.envrc-nix`](https://github.com/TagStudioDev/TagStudio/blob/main/contrib/.envrc-nix).
## Tooling
### Editor Integration
The entry point for TagStudio is `src/tagstudio/main.py`. You can target this file from your IDE to run or connect a debug session. The example(s) below show off example launch scripts for different IDEs. Here you can also take advantage of [launch arguments](./usage.md/#launch-arguments) to pass your own test [libraries](./library/index.md) to use while developing. You can find more editor configurations in [`contrib`](https://github.com/TagStudioDev/TagStudio/tree/main/contrib).
<!-- prettier-ignore -->
=== "VS Code"
```json title=".vscode/launch.json"
{
"version": "0.2.0",
"configurations": [
{
"name": "TagStudio",
"type": "python",
"request": "launch",
"program": "${workspaceRoot}/src/tagstudio/main.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": ["-o", "~/Documents/Example"]
}
]
}
```
### pre-commit
There is a [pre-commit](https://pre-commit.com/) configuration that will run through some checks before code is committed. Namely, mypy and the Ruff linter and formatter will check your code, catching those nits right away.
Once you have pre-commit installed, just run:
```sh
pre-commit install
```
From there, Git will automatically run through the hooks during commit actions!
### direnv
You can automatically enter this development shell, and keep your user shell, with a tool like [direnv](https://direnv.net/). Some reference `.envrc` files are provided in the repository at [`contrib`](https://github.com/TagStudioDev/TagStudio/tree/main/contrib).
Two currently available are for [Nix](#nixos) and [uv](#installing-with-uv), to use one:
```sh
ln -s .envrc-$variant .envrc
```
You will have to allow usage of it.
<!-- prettier-ignore -->
!!! warning "direnv Security Framework"
These files are generally a good idea to check, as they execute commands on directory load. direnv has a security framework to only run `.envrc` files you have allowed, and does keep track on if it has changed. So, with that being said, the file may need to be allowed again if modifications are made.
```sh
cat .envrc # You are checking them, right?
direnv allow
```
## Building
To build your own executables of TagStudio, first follow the steps in "[Installing Dependencies](#installing-dependencies)." Once that's complete, run the following PyInstaller command:
```
pyinstaller tagstudio.spec
```
If you're on Windows or Linux and wish to build a portable executable, then pass the following flag:
```
pyinstaller tagstudio.spec -- --portable
```
The resulting executable file(s) will be located in a new folder named "dist".

View File

@@ -10,7 +10,8 @@ Pre-built binaries from trusted sources are available on the [FFmpeg website](ht
![Windows Download Location](../assets/ffmpeg_windows_download.png)
!!! note
<!-- prettier-ignore -->
!!! warning
Do NOT download the source code by mistake!
To Install:
@@ -19,10 +20,10 @@ To Install:
2. Move extracted contents to a unique folder (i.e; `c:\ffmpeg` or `c:\Program Files\ffmpeg`)
3. Add FFmpeg to your system PATH
1. In Windows, search for or go to "Edit the system environment variables" under the Control Panel
2. Under "User Variables", select "Path" then edit
3. Click new and add `<Your folder>\bin` (e.g; `c:\ffmpeg\bin` or `c:\Program Files\ffmpeg\bin`)
4. Click "Okay"
1. In Windows, search for or go to "Edit the system environment variables" under the Control Panel
2. Under "User Variables", select "Path" then edit
3. Click new and add `<Your folder>\bin` (e.g; `c:\ffmpeg\bin` or `c:\Program Files\ffmpeg\bin`)
4. Click "Okay"
### Package Managers
@@ -42,7 +43,7 @@ FFmpeg is available under the macOS section of the [FFmpeg website](https://www.
### Package Managers
FFmpeg may be installed by default on some Linux distributions, but if not, it is available via your distro's package manager of choice:
FFmpeg may be installed by default on some Linux distributions, but if not, it is available via your distribution package manager of choice:
1. Debian/Ubuntu (`sudo apt install ffmpeg`)
2. Fedora (`sudo dnf install ffmpeg-free`)

View File

@@ -4,46 +4,49 @@ title: Home
# Welcome to the TagStudio Documentation!
![TagStudio Alpha](assets/github_header.png)
![TagStudio Alpha](./assets/github_header.png)
TagStudio is a photo & 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.
<figure width="60%" markdown="span">
![TagStudio screenshot](assets/screenshot.png)
![TagStudio screenshot](./assets/screenshot.png)
<figcaption>TagStudio Alpha v9.5.0 running on macOS Sequoia.</figcaption>
</figure>
## Feature Roadmap
The [Feature Roadmap](updates/roadmap.md) lists all of the planned core features for TagStudio to be considered "feature complete" along with estimated release milestones. The development and testing of these features takes priority over all other requested or submitted features unless they are later added to this roadmap. This helps ensure that TagStudio eventually sees a full release and becomes more usable by more people more quickly.
The [Feature Roadmap](./updates/roadmap.md) lists all of the planned core features for TagStudio to be considered "feature complete" along with estimated release milestones. The development and testing of these features takes priority over all other requested or submitted features unless they are later added to this roadmap. This helps ensure that TagStudio eventually sees a full release and becomes more usable by more people more quickly.
## Current Features
### Libraries
- Create libraries/vaults centered around a system directory. Libraries contain a series of entries: the representations of your files combined with metadata fields. Each entry represents a file in your librarys directory, and is linked to its location.
- Create [libraries](./library/index.md) centered around a system directory. Libraries contain a series of entries: the representations of your files combined with metadata fields. Each entry represents a file in your librarys directory, and is linked to its location.
- Address moved, deleted, or otherwise "unlinked" files by using the "Fix Unlinked Entries" option in the Tools menu.
### Tagging + Custom Metadata
### Tagging + Metadata Fields
- Add custom powerful tags to your library entries
- Add metadata to your library entries, including:
- Add custom powerful [tags](./library/tag.md) to your library entries
- Add [metadata fields](./library/field.md) to your library entries, including:
- Name, Author, Artist (Single-Line Text Fields)
- Description, Notes (Multiline Text Fields)
- Create rich tags composed of a name, color, a list of aliases, and a list of parent tags - these being tags in which these tags inherit values from.
- Description, Notes (Multi-Line Text Fields)
- Create rich tags composed of a name, color, a list of aliases, and a list of "parent tags" - these being tags in which these tags inherit values from.
- Copy and paste tags and fields across file entries
- Automatically organize tags into groups based on parent tags marked as "categories"
- Generate tags from your existing folder structure with the "Folders to Tags" macro (NOTE: these tags do NOT sync with folders after they are created)
### Search
- Search for file entries based on tags, file path (`path:`), file types (`filetype:`), and even media types! (`mediatype:`)
- Use and combine boolean operators (`AND`, `OR`, `NOT`) along with parentheses groups, quotation escaping, and underscore substitution to create detailed search queries
- Use special search conditions (`special:untagged` and `special:empty`) to find file entries without tags or fields, respectively
- [Search](./library/library_search.md) for file entries based on tags, file path (`path:`), file types (`filetype:`), and even media types! (`mediatype:`)
- Use and combine Boolean operators (`AND`, `OR`, `NOT`) along with parentheses groups, quotation escaping, and underscore substitution to create detailed search queries
- Use special search conditions (`special:untagged`) to find file entries without tags or fields, respectively
### File Entries
- Nearly all file types are supported in TagStudio libraries - just not all have dedicated thumbnail support.
- Nearly all [file](./library/entry.md) types are supported in TagStudio libraries - just not all have dedicated thumbnail support.
- Preview most image file types, animated GIFs, videos, plain text documents, audio files, Blender projects, and more!
- Open files or file locations by right-clicking on thumbnails and previews and selecting the respective context menu options. You can also click on the preview panel image to open the file, and click the file path label to open its location.
- Delete files from both your library and drive by right-clicking the thumbnail(s) and selecting the "Move to Trash"/"Move to Recycle Bin" option.

View File

@@ -1,26 +1,212 @@
# Installation
To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system under the "Assets" section. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around.
TagStudio provides [releases](https://github.com/TagStudioDev/TagStudio/releases) as well as full access to its [source code](https://github.com/TagStudioDev/TagStudio) under the [GPLv3](https://github.com/TagStudioDev/TagStudio/blob/main/LICENSE) license.
**We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub releases page are _unofficial_ and not maintained by us.** Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk.
## Executables
To download executable builds of TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) page of the GitHub repository and download the latest release for your system under the "Assets" section at the bottom of the release.
!!! info "For macOS Users"
On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
TagStudio has builds for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around.
!!! info "For Linux Users"
On Linux with non-Qt based Desktop Environments you may be unable to open TagStudio. You need to make sure that "xcb-cursor0" or "libxcb-cursor0" packages are installed. For more info check [Missing linux dependencies](https://github.com/TagStudioDev/TagStudio/discussions/182#discussioncomment-9452896)
<!-- prettier-ignore -->
!!! info "Third-Party Dependencies"
You may need to install [third-party dependencies](#third-party-dependencies) such as [FFmpeg](https://ffmpeg.org/download.html) to use the full feature set of TagStudio.
<!-- prettier-ignore -->
!!! warning "For macOS Users"
On macOS, you may be met with a message saying "**"TagStudio" can't be opened because Apple cannot check it for malicious software.**" If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says "**"TagStudio" was blocked from use because it is not from an identified developer.**" Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
---
## Package Managers
<!-- prettier-ignore -->
!!! danger "Unofficial Releases"
**We do not currently publish TagStudio to _remote_ package repositories. Any TagStudio distributions outside of the [GitHub repository](https://github.com/TagStudioDev/TagStudio) are _unofficial_ and not maintained by us!**
Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk!
### Installing with PIP
TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.12 and PIP installed if you choose to install using this method.
The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio).
```sh
git clone https://github.com/TagStudioDev/TagStudio.git
```
Once cloned or downloaded, you can install TagStudio with the following PIP command:
```sh
pip install .
```
<!-- prettier-ignore -->
!!! note "Developer Dependencies"
If you wish to create an editable install with the additional dependencies required for developing TagStudio, use this modified PIP command instead:
```sh
pip install -e ".[dev]"
```
_See more under "[Developing](./develop.md)"_
TagStudio can now be launched via the `tagstudio` command in your terminal.
---
### Linux
Some external dependencies are required for TagStudio to execute. Below is a table of known packages that will be necessary.
<!-- prettier-ignore -->
| Package | Reason |
|--------------- | --------------- |
| [dbus](https://repology.org/project/dbus) | required for Qt; opening desktop applications |
| [ffmpeg](https://repology.org/project/ffmpeg) | audio/video playback |
| libstdc++ | required for Qt |
| [libva](https://repology.org/project/libva) | hardware rendering with [VAAPI](https://www.freedesktop.org/wiki/Software/vaapi) |
| [libvdpau](https://repology.org/project/libvdpau) | hardware rendering with [VDPAU](https://www.freedesktop.org/wiki/Software/VDPAU) |
| [libx11](https://repology.org/project/libx11) | required for Qt |
| libxcb-cursor OR [xcb-util-cursor](https://repology.org/project/xcb-util-cursor) | required for Qt |
| [libxkbcommon](https://repology.org/project/libxkbcommon) | required for Qt |
| [libxrandr](https://repology.org/project/libxrandr) | hardware rendering |
| [pipewire](https://repology.org/project/pipewire) | PipeWire audio support |
| [qt](https://repology.org/project/qt) | required |
| [qt-multimedia](https://repology.org/project/qt) | required |
| [qt-wayland](https://repology.org/project/qt) | Wayland support |
### Nix(OS)
For [Nix(OS)](https://nixos.org/), the TagStudio repository includes a [flake](https://wiki.nixos.org/wiki/Flakes) that provides some outputs such as a development shell and package.
Two packages are provided: `tagstudio` and `tagstudio-jxl`. The distinction was made because `tagstudio-jxl` has an extra compilation step for [JPEG-XL](https://jpeg.org/jpegxl) image support. To give either of them a test run, you can execute `nix run github:TagStudioDev/TagStudio#tagstudio`. If you are in a cloned repository and wish to run a package with the context of the repository, you can simply use `nix run` with no arguments.
`nix build` can be used in place of `nix run` if you only want to build. **The packages will only build if tests pass.**
<!-- prettier-ignore -->
!!! info "Nix Support"
Support for Nix is handled on a best-effort basis by one of our maintainers. Issues related to Nix may be slower to resolve, and could require further details.
Want to add TagStudio into your configuration?
This can be done by first adding the flake input into your `flake.nix`:
```nix title="flake.nix"
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
tagstudio = {
url = "github:TagStudioDev/TagStudio";
inputs.nixpkgs.follows = "nixpkgs"; # Use the same package set as your flake.
};
};
}
```
Then, make sure you add the `inputs` context to your configuration:
<!-- prettier-ignore-start -->
=== "NixOS with Home Manager"
```nix title="flake.nix"
{
outputs =
inputs@{ home-manager, nixpkgs, ... }:
{
nixosConfigurations.HOSTNAME = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
extraSpecialArgs = { inherit inputs; };
users.USER.imports = [
./home.nix
];
};
}
];
};
};
}
```
=== "NixOS"
```nix title="flake.nix"
{
outputs =
inputs@{ nixpkgs, ... }:
{
nixosConfigurations.HOSTNAME = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./configuration.nix
];
};
};
}
```
=== "Home Manager (standalone)"
```nix title="flake.nix"
{
outputs =
inputs@{ home-manager, nixpkgs, ... }:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
};
in
{
homeConfigurations.USER = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inherit inputs; };
modules = [
./home.nix
];
};
};
}
```
<!-- prettier-ignore-end -->
Finally, `inputs` can be used in a module to add the package to your packages list:
<!-- prettier-ignore-start -->
=== "Home Manager module"
```nix title="home.nix"
{ inputs, pkgs, ... }:
{
home.packages = [
inputs.tagstudio.packages.${pkgs.stdenv.hostPlatform.system}.tagstudio
];
}
```
=== "NixOS module"
```nix title="configuration.nix"
{ inputs, pkgs, ... }:
{
environment.systemPackages = [
inputs.tagstudio.packages.${pkgs.stdenv.hostPlatform.system}.tagstudio
];
}
```
<!-- prettier-ignore-end -->
Don't forget to rebuild!
## Third-Party Dependencies
- For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](/docs/help/ffmpeg.md) guide.
For audio/video thumbnails and playback you'll need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide.
## Optional Arguments
Optional arguments to pass to the program:
`--open <path>` / `-o <path>`
: Path to a TagStudio Library folder to open on start.
`--config-file <path>` / `-c <path>`
: Path to the TagStudio config file to load.
You can check to see if FFmpeg and FFprobe are correctly located by launching TagStudio and going to "About TagStudio" in the menu bar.

View File

@@ -1,20 +1,20 @@
# File Entries
File entries are the individual representations of your files inside a TagStudio [library](index.md). Each one corresponds one-to-one to a file on disk, and tracks all of the additional [tags](tag.md) and metadata that you attach to it inside TagStudio.
File entries are the individual representations of your files inside a TagStudio [library](./index.md). Each one corresponds one-to-one to a file on disk, and tracks all of the additional [tags](tag.md) and metadata that you attach to it inside TagStudio.
## Storage
File entry data is storied within the `ts_library.sqlite` file inside each library's `.TagStudio` folder. No modifications are made to your actual files on disk, and nothing like sidecar files are generated for your files.
File entry data is stored within the `ts_library.sqlite` file inside each library's `.TagStudio` folder. No modifications are made to your actual files on disk, and nothing like sidecar files are generated for your files.
## Appearance
File entries appear as file previews both inside the thumbnail grid. The preview panel shows a more detailed preview of the file, along with extra file stats and all attached TagStudio tags and fields.
File entries appear as thumbnails inside the grid display. The preview panel shows a more detailed preview of the file, along with extra file stats and all attached TagStudio tags and fields.
## Unlinked File Entries
If the file that an entry is referencing has been moved, renamed, or deleted on disk, then TagStudio will display a red chain-link icon for the thumbnail image. Certain uncached stats such as the file size and image dimensions will also be unavailable to see in the preview panel when a file becomes unlinked.
If the file that an entry is referencing has been moved, renamed, or deleted on disk, then TagStudio will display its unlinked status with a red chain-link icon instead of its thumbnail image. Certain uncached stats such as the file size and image dimensions will also be unavailable to see in the preview panel.
To fix file entries that have become unlinked, select the "Fix Unlinked Entries" option from the Tools menu. From there, refresh the unlinked entry count and choose whether to search and relink you files, and/or delete the file entires from your library. This will NOT delete or modify any files on disk.
To fix file entries that have become unlinked, select the "Fix Unlinked Entries" option from the Tools menu. From there, refresh the unlinked entry count and choose whether to search and relink you files, and/or delete the file entries from your library. This will NOT delete or modify any files on disk.
## Internal Structure
@@ -32,11 +32,11 @@ To fix file entries that have become unlinked, select the "Fix Unlinked Entries"
- `date_created` (`DATETIME`/`Datetime`)
- _Not currently used, will be implemented in an upcoming update._
- The creation date of the file (not the entry).
- Generates from `st_birthtime` on Windows and Mac, and `st_ctime` on Linux.
- Generated from `st_birthtime` on Windows and Mac, and `st_ctime` on Linux.
- `date_modified` (`DATETIME`/`Datetime`)
- _Not currently used, will be implemented in an upcoming update._
- The latest modification date of the file (not the entry).
- Generates from `st_mtime`.
- Generated from `st_mtime`.
- `date_added` (`DATETIME`/`Datetime`)
- The date the file entry was added to the TagStudio library.

View File

@@ -5,4 +5,4 @@ tags:
# Entry Groups
Entries can be grouped via tags marked as groups which when applied to different entries will signal TagStudio to treat those entries as a single group inside of searches and browsing.
Entries can be grouped via tags marked as "groups" which when applied to different entries will signal TagStudio to treat those entries as a single group inside of searches and browsing.

View File

@@ -1,34 +1,23 @@
# Field
# Fields
Fields are the building blocks of metadata stored in [entries](entry.md). Fields have several base types for representing different kinds of information, including:
Fields are additional types of metadata that you can attach to [file entries](./entry.md). Like [tags](./tag.md), fields are not stored inside files themselves nor in sidecar files, but rather inside the respective TagStudio [library](./index.md) save file.
#### `text_line`
## Field Types
- A string of text, displayed as a single line.
- e.g: Title, Author, Artist, URL, etc.
### Text Line
#### `text_box`
A string of text, displayed as a single line.
- A long string of text displayed as a box of text.
- e.g: Description, Notes, etc.
- e.g: Title, Author, Artist, URL, etc.
#### `tag_box`
### Text Box
- A box of [tags](tag.md) defined and added by the user.
- Multiple tag boxes can be used to separate classifications of tags.
- e.g: Content Tags, Meta Tags, etc.
A long string of text displayed as a box of text.
#### `datetime` [WIP]
- e.g: Description, Notes, etc.
- A date and time value.
- e.g: Date Created, Date Modified, Date Taken, etc.
### Datetime [WIP]
#### `checkbox` [WIP]
A date and time value.
- A simple two-state checkbox.
- Can be associated with a tag for quick organization.
- e.g: Archive, Favorite, etc.
#### `collation` [obsolete]
- Previously used for associating files to be used in a [collation](../utilities/macro.md#create-collage), will be removed in favor of a more flexible feature in future updates.
- e.g: Date Published, Date Taken, etc.

View File

@@ -1,4 +1,5 @@
# Library
The library is how TagStudio represents your chosen directory, with every file inside of it being displayed as an [entry](entry.md). You can have as many or few libraries as you wish, since each libraries' data is stored within a `.TagStudio` folder at its root.
Note that this means [tags](tag.md) you create only exist _per-library_.
The library is how TagStudio represents your chosen directory, with every file inside being represented by a [file entry](./entry.md). You can have as many or few libraries as you wish, since each libraries' data is stored within a `.TagStudio` folder at its root. From there the library save file itself is stored as `ts_library.sqlite`, with TagStudio versions 9.4 and below using a the legacy `ts_library.json` format.
Note that this means [tags](./tag.md) you create only exist _per-library_.

View File

@@ -0,0 +1,121 @@
# Library Search
TagStudio provides various methods to search your library, ranging from TagStudio data such as tags to inherent file data such as paths or media types.
## Boolean Operators
TagStudio allows you to use common [Boolean search](https://en.wikipedia.org/wiki/Full-text_search#Boolean_queries) operators when searching your library, along with [grouping](#grouping-and-nesting), [nesting](#grouping-and-nesting), and [character escaping](#escaping-characters). Note that you may need to use grouping in order to get the desired results you're looking for.
### AND
The `AND` operator will only return results that match **both** sides of the operator. `AND` is used implicitly when no Boolean operators are given. To use the `AND` operator explicitly, simply type "and" (case insensitive) in-between items of your search.
<!-- prettier-ignore -->
!!! example
Searching for "Tag1 Tag2" will be treated the same as "Tag1 `AND` Tag2" and will only return results that contain both Tag1 and Tag2.
### OR
The `OR` operator will return results that match **either** the left or right side of the operator. To use the `OR` operator simply type "or" (case insensitive) in-between items of your search.
<!-- prettier-ignore -->
!!! example
Searching for "Tag1 `OR` Tag2" will return results that contain either "Tag1", "Tag2", or both.
### NOT
The `NOT` operator will returns results where the condition on the right is **false.** To use the `NOT` operator simply type "not" (case insensitive) in-between items of your search. You can also begin your search with `NOT` to only view results that do not contain the next term that follows.
<!-- prettier-ignore -->
!!! example
Searching for "Tag1 `NOT` Tag2" will only return results that contain "Tag1" while also not containing "Tag2".
### Grouping and Nesting
Searches can be grouped and nested by using parentheses to surround parts of your search query.
<!-- prettier-ignore -->
!!! example
Searching for "(Tag1 `OR` Tag2) `AND` Tag3" will return any results that contain Tag3, plus one or the other (or both) of Tag1 and Tag2.
### Escaping Characters
Sometimes search queries have ambiguous characters and need to be "escaped". This is most common with tag names which contain spaces, or overlap with existing search keywords such as "[path:](#filename-and-path) of exile". To escape most search terms, surround the section of your search in plain quotes. Alternatively, spaces in tag names can be replaced by underscores.
#### Valid Escaped Tag Searches
- "Tag Name With Spaces"
- Tag_Name_With_Spaces
#### Invalid Escaped Tag Searches
- Tag Name With Spaces
- Reason: Ambiguity between a tag named "Tag Name With Spaces" and four individual tags called "Tag", "Name", "With", "Spaces".
## Tags
[Tag](#tags) search is the default mode of file entry search in TagStudio. No keyword prefix is required, however using `tag:` will also work. The tag search attempts to match tag [names](./tag.md#name), [shorthands](./tag.md#shorthand), [aliases](./tag.md#aliases), as well as allows for tags to [substitute](./tag.md#intuition-via-substitution) in for any of their [parent tags](./tag.md#parent-tags).
You may also see the `tag_id:` prefix keyword show up when using the right-click "Search for Tag" option on tags. This is meant for internal use, and eventually will not be displayed or accessible to the user.
## Fields
_[Field](./field.md) search is currently not in the program, however is coming in a future version._
## File Entry Search
### Filename and Path
Filename and path search is available via the `path:` keyword and comes in a few different styles. By default, any string that follows the `path:` keyword will be searched as a substring inside a file's complete filepath. This means that given a file `folder/my_file.txt`, searching for `path: my_file` or `path: folder` will both return results for that file.
#### Case Sensitivity
TagStudio uses a "[smartcase](https://neovim.io/doc/user/options.html#'smartcase')"-like system for case sensitivity. This means that a search term typed in `lowercase` will be treated as **case-insensitive**, while a term typed in any `MixedCase` will be treated as **case-sensitive**. This makes it quicker to type searches when case sensitivity isn't required, while also providing a simple option to leverage case sensitivity when desired. Note that this means there's technically no way to currently search for a lowercase term while respecting case sensitivity.
#### Glob Syntax
Optionally, you may use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) syntax to search filepaths.
#### Examples
Given a file "Artwork/Piece.jpg", the following searches will return results for it:
- `path: artwork/piece.jpg`
- `path: Artwork/Piece.jpg`
- `path: piece.jpg`
- `path: Piece.jpg`
- `path: artwork`
- `path: rtwor`
- `path: ece.jpg`
- `path: iec`
- `path: artwork/*`
- `path: Artwork/*`
- `path: *piece.jpg*`
- `path: *Piece.jpg*`
- `path: *artwork*`
- `path: *Artwork*`
- `path: *rtwor*`
- `path: *ece.jpg*`
- `path: *iec*`
- `path: *.jpg`
While the following searches will **NOT:**
- `path: ARTWORK/Piece.jpg` _(Reason: Mismatched case)_
- `path: *aRtWoRk/Piece*` _(Reason: Mismatched case)_
- `path: PieCe.jpg` _(Reason: Mismatched case)_
- `path: *PieCe.jpg*` _(Reason: Mismatched case)_
## Special Searches
Some predefined searches use the `special:` keyword prefix and give quick results for certain special search queries.
### Untagged
To see all your file entries which don't contain any tags, use the `special: untagged` search.
### Empty
**_NOTE:_** _Currently unavailable in v9.5.0_
To see all your file entries which don't contain any tags _and_ any fields, use the `special: empty` search.

View File

@@ -22,7 +22,7 @@ This is a special type of alias that's used for shortening the tag name under sp
Aliases are alternate names that the tag can go by. This may include individual first names for people, alternate spellings, shortened names, and more. If there's a common abbreviation or shortened name for your tag, it's recommended to use the [shorthand](#shorthand) field for this instead.
When searching for a tag, aliases (including the shorthand) can also be used to find the tag. This not only includes searching for tags themselves, but for tagged [file entries](entry.md) as well!
When searching for a tag, aliases (including the shorthand) can also be used to find the tag. This not only includes searching for tags themselves, but for tagged [file entries](./entry.md) as well!
### Automatic Disambiguation
@@ -32,7 +32,7 @@ Given a tag named "Freddy", we may confuse it with other "Freddy" tags in our li
![Tag Disambiguation Example](../assets/tag_disambiguation_example.png)
So if the "Five Night's at Fredddy's" tag is added as a parent tag on the "Freddy" tag, and the disambiguation box next to it is checked, then our tag name will automatically be displayed as "Freddy (Five Nights at Freddy's)". Better yet, if the "Five Night's at Fredddy's" tag has a shorthand such as "FNAF", then our "Freddy" tag will be displayed as "Freddy (FNAF)". This process preserves our base tag name ("Freddy") and provides an option to get a clean and consistent method to display disambiguating parent categories, rather than having to type this information in manually for each applicable tag.
So if the "Five Nights at Freddy's" tag is added as a parent tag on the "Freddy" tag, and the disambiguation box next to it is checked, then our tag name will automatically be displayed as "Freddy (Five Nights at Freddy's)". Better yet, if the "Five Nights at Freddy's" tag has a shorthand such as "FNAF", then our "Freddy" tag will be displayed as "Freddy (FNAF)". This process preserves our base tag name ("Freddy") and provides an option to get a clean and consistent method to display disambiguating parent categories, rather than having to type this information in manually for each applicable tag.
## Tag Relationships
@@ -52,7 +52,7 @@ In a system where tags have no relationships, you're required to add as many tag
#### Intuition via Substitution
Now when searching for for images that have `Dreamworks` and `Character`, any images or files originally just tagged with `Shrek` will appear as you would expect. A little bit of tag setup goes a long way not only saving so much time during tagging, but also to ensure an intuitive way to search your files!
Now when searching for images that have `Dreamworks` and `Character`, any images or files originally just tagged with `Shrek` will appear as you would expect. A little bit of tag setup goes a long way not only saving so much time during tagging, but also to ensure an intuitive way to search your files!
#### Rediscovery via Linking
@@ -60,23 +60,27 @@ Lastly, when searching your files with broader categories such as `Character` or
### Component Tags
**_[Coming in version 9.6](../updates/roadmap.md#96-alpha)_**
**_[Coming in version 9.6](../updates/roadmap.md#v96)_**
Component tags will be built from a composition-based, or "HAS" type relationship between tags. This takes care of instances where an attribute may "have" another attribute, but doesn't inherit from it. Shrek may be an `Orge`, he may be a `Character`, but he is NOT a `Leather Vest` - even if he's commonly seen _with_ it. Component tags, along with the upcoming [Tag Override](tag_overrides.md) feature, are built to handle these cases in a way that still simplifies the tagging process without adding too much undue complexity for the user.
Component tags will be built from a composition-based, or "HAS" type relationship between tags. This takes care of instances where an attribute may "have" another attribute, but doesn't inherit from it. Shrek may be an `Ogre`, he may be a `Character`, but he is NOT a `Leather Vest` - even if he's commonly seen _with_ it. Component tags, along with the upcoming [Tag Override](./tag_overrides.md) feature, are built to handle these cases in a way that still simplifies the tagging process without adding too much undue complexity for the user.
## Tag Appearance
### Color
Tags use a default uncolored appearance by default, however can take on a number of built-in and user-created\* colors and color palettes! Tag color palettes can be based on a single color value (see: TagStudio Standard, TagStudio Shades, TagStudio Pastels) or use an optional secondary color to override the border and text colors (see: TagStudio Neon).
Tags use a default uncolored appearance by default, however can take on a number of built-in and user-created colors and color palettes! Tag color palettes can be based on a single color value (see: TagStudio Standard, TagStudio Shades, TagStudio Pastels) or use an optional secondary color use for the text and optionally the tag border (e.g. TagStudio Neon).
![Tag Color Selection](../assets/tag_color_selection.png)
\*_Coming in the full version 9.5.0 release_
#### User-Created Colors
Custom palettes and colors can be created via the [Tag Color Manager](./tag_color.md). These colors will display alongside the built-in colors inside the tag selection window and are separated by their namespace names. Colors which use the secondary color for the tag border will be outlined in that color, otherwise they will only display the secondary color on the bottom of the swatch to indicate at a glance that the text colors are different.
![Custom Tag Color Selection](../assets/custom_tag_color_selection.png)
### Icon
**_[Coming in version 9.6](../updates/roadmap.md#96-alpha)_**
**_[Coming in version 9.6](../updates/roadmap.md#v96)_**
## Tag Properties
@@ -90,7 +94,7 @@ When the "Is Category" property is checked, this tag now acts as a category sepa
#### Is Hidden
**_[Coming in version 9.6](../updates/roadmap.md#96-alpha)_**
**_[Coming in version 9.6](../updates/roadmap.md#v96)_**
When the "Is Hidden" property is checked, any file entries tagged with this tag will not show up in searches by default. This property comes by default with the built-in "Archived" tag.

View File

@@ -4,7 +4,7 @@ tags:
# Tag Categories
The "Is Category" property of tags determines if a tag should be treated as a category itself when being organized inside the preview panel. Tags marked as categories will show themselves and all tags inheriting from it (including recursively) underneath a field-like section with the tag's name. This means that duplicates of tags can appear on entries if the tag inherits from multiple parent categories, however this is by design and reflects the nature multiple inheritance. Any tags not inheriting from a category tag will simply show under a default "Tag" section.
The "Is Category" property of tags determines if a tag should be treated as a category itself when being organized inside the preview panel. Tags marked as categories will show themselves and all tags inheriting from it (including recursively) underneath a field-like section with the tag's name. This means that duplicates of tags can appear on entries if the tag inherits from multiple parent categories, however this is by design and reflects the nature of multiple inheritance. Any tags not inheriting from a category tag will simply show under a default "Tag" section.
![Tag Categories Example](../assets/tag_categories_example.png)

71
docs/library/tag_color.md Normal file
View File

@@ -0,0 +1,71 @@
# Tag Colors
TagStudio features a variety of built-in tag colors, alongside the ability for users to create their own custom tag color palettes.
## Tag Color Manager
The Tag Color Manager is where you can create and manage your custom tag colors and associated namespaces. You can access the Tag Color Manager from the "File -> Manage Tag Colors" option in the menu bar.
![Tag Color Manager](../assets/tag_color_manager.png)
## Creating a Namespace
TagStudio uses namespaces to group colors into palettes. Namespaces are a way for you to use the same color name across multiple palettes without having to worry about [name collision](https://en.wikipedia.org/wiki/Name_collision) with other palettes. This is especially useful when sharing your color palettes with others!\*
_\* Color pack sharing coming in a future update_
To create your first namespace, either click the "New Namespace" button or the large button prompt underneath the built-in colors.
![Create Namespace](../assets/create_namespace.png)
### Name
The display name of the namespace, used for presentation.
### ID Slug
An internal ID for the namespace which is automatically derived from the namespace name.
Namespaces beginning with "tagstudio" are reserved by TagStudio and will automatically have their text changed.
<!-- prettier-ignore -->
!!! note
It's currently not possible to manually edit the Namespace ID Slug. This will be possible once sharable color packs are added.
## Creating a Color
Once you've created your first namespace, click the "+" button inside the namespace section to create a color. To edit a color that you've previously created, either click on the color name or right click and select "Edit Color" from the context menu.
![Create Color (Primary Color)](../assets/custom_color_primary_only.png)
### Name
The display name for the color, used for presentation. You may occasionally see the color name followed by the [namespace name](#name) in parentheses to disambiguate it from other colors with the same name.
### ID Slug
Similar to [Namespace ID Slugs](#id-slug), the ID Slug is used as an internal ID and is automatically derived from the tag color name.
<!-- prettier-ignore -->
!!! note
It's currently not possible to manually edit the Color ID Slug. This will be possible once sharable color packs are added.
### Primary Color
The primary color is used as the main tag color and by default is used as the background color with the text and border colors being derived from this color.
### Secondary Color
By default, the secondary color is only used as an optional override for the tag text color. This color can be cleared by clicking the adjacent "Reset" button.
![Create Color (Secondary Color)](../assets/custom_color_no_border.png)
The secondary color can also be used as the tag border color by checking the "Use Secondary Color for Border" box.
![Create Color (Use Secondary for Border)](../assets/custom_color_border.png)
## Using Colors
When editing a tag, click the tag color button to bring up the tag color selection panel. From here you can choose any built-in TagStudio color as well as any of your custom colors.
![Tag Color Selection](../assets/tag_color_selection.png)

View File

@@ -1,11 +1,11 @@
---
tags:
- Upcoming Feature
- Upcoming Feature
---
# Tag Overrides
Tag overrides are the ability to add or remove [parent tags](tag.md#subtags) from a [tag](tag.md) on a per- [entry](entry.md) basis.
Tag overrides are the ability to add or remove [parent tags](./tag.md#parent-tags) from a [tag](./tag.md) on a per-[entry](./entry.md) basis.
## Examples

View File

@@ -0,0 +1,99 @@
/* Dark Theme */
[data-md-color-scheme="slate"] {
--md-default-bg-color: #060617;
--md-default-fg-color: #eae1ff;
--md-default-fg-color--light: #b898ff;
--md-code-fg-color: #eae1ffcc;
--md-code-hl-string-color: rgb(92, 255, 228);
--md-code-hl-keyword-color: rgb(61, 155, 255);
--md-code-hl-constant-color: rgb(205, 78, 255);
--md-footer-bg-color--dark: #03030c;
--md-code-bg-color: #090a26;
}
/* Light Theme */
[data-md-color-scheme="default"] {
--md-default-fg-color--light: #090a26;
}
.md-header {
background: linear-gradient(
60deg,
rgb(205, 78, 255) 10%,
rgb(116, 123, 255) 50%,
rgb(72, 145, 255) 75%,
rgb(98, 242, 255) 100%
);
}
th,
td {
padding: 0.5em 1em 0.5em 1em !important;
}
.md-header {
border-style: solid;
border-width: 0 0 2px 0;
border-color: #ffffff33;
}
.md-header__title {
font-family: "Bai Jamjuree", Roboto, sans-serif;
font-weight: 500 !important;
font-size: 1.1rem;
letter-spacing: -0.05rem !important;
}
.md-nav__title,
h1,
h2,
.md-nav__item--section > .md-nav__link {
font-family: "Bai Jamjuree", Roboto, sans-serif;
font-weight: 500 !important;
font-size: 0.8rem;
letter-spacing: -0.05rem !important;
}
/* Add padding to stop text from cutting off early due to negative letter spacing */
.md-nav__item--section > .md-nav__link > .md-ellipsis {
padding-right: 0.5rem;
}
.md-header__title .md-header__topic .md-ellipsis {
padding-right: 0.5rem;
}
.md-header__button.md-logo {
padding-right: 0;
padding-bottom: 0.3rem;
margin-right: -0.8rem;
}
.md-search__form {
height: 1.5rem;
background-color: #00004444;
border-radius: 0.3rem !important;
border-style: solid;
border-width: 2px 1px 0 1px;
border-color: #00004422;
padding-bottom: 2px;
}
.md-search__form > .md-icon {
margin-top: -0.2rem;
}
.twemoji {
margin-top: 0.05rem;
}
/* Matches the palette used by mkdocs-material */
.priority-high {
color: #f1185a;
}
.priority-med {
color: #7c4eff;
}
.priority-low {
color: #28afff;
}

View File

@@ -1,6 +1,6 @@
# Feature Roadmap
This checklist details the current and remaining features required at a minimum for TagStudio to be considered Feature Complete. This list is _not_ a definitive list for additional feature requests and PRs as they come in, but rather an outline of my personal core feature set intended for TagStudio.
This checklist details the current and remaining features required at a minimum for TagStudio to be considered "Feature Complete". This list is _not_ a definitive list for additional feature requests and PRs as they come in, but rather an outline of my personal core feature set intended for TagStudio.
## Priorities
@@ -10,218 +10,132 @@ Features are broken up into the following priority levels, with nested prioritie
- [MEDIUM] - Important but not necessary
- [LOW] - Just nice to have
## Core Feature List
- [ ] Tags [HIGH]
- [x] ID-based, not string based [HIGH]
- [x] Tag name [HIGH]
- [x] Tag alias list, aka alternate names [HIGH]
- [x] Tag shorthand (specific short alias for displaying) [HIGH]
- [x] Parent/Inheritance subtags [HIGH]
- [ ] Composition/HAS subtags [HIGH]
- [x] Deleting Tags [HIGH]
- [ ] Merging Tags [HIGH]
- [ ] Tag Icons [HIGH]
- [ ] Small Icons [HIGH]
- [ ] Large Icons for Profiles [MEDIUM]
- [ ] Built-in Icon Packs (i.e. Boxicons) [HIGH]
- [ ] User Defined Icons [HIGH]
- [ ] Multiple Languages for Tag Strings [MEDIUM]
- [ ] User-defined tag colors [HIGH]
- [x] ID based, not string or hex [HIGH]
- [x] Color name [HIGH]
- [x] Color value (hex) [HIGH]
- [x] Existing colors are now a set of base colors [HIGH]
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
- [x] Title is tag name [HIGH]
- [ ] [Tag Overrides](../library/tag_overrides.md) [MEDIUM]
- [ ] Per-file overrides of subtags [HIGH]
- [ ] Tag Packs [MEDIUM]
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
- [ ] Importable [HIGH]
- [ ] Exportable [HIGH]
- [ ] Conflict resolution [HIGH]
- [ ] Color Packs [MEDIUM]
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
- [ ] Importable [HIGH]
- [ ] Exportable [HIGH]
- [ ] Exportable Library Data [HIGH]
- [ ] Standard notation format (i.e. JSON) contacting all library data [HIGH]
- [ ] [Macros](../utilities/macro.md) [HIGH]
- [ ] Sharable Macros [MEDIUM]
- [ ] Standard notation format (i.e. JSON) contacting macro instructions [HIGH]
- [ ] Exportable [HIGH]
- [ ] Importable [HIGH]
- [ ] Triggers [HIGH]
- [ ] On new file [HIGH]
- [ ] On library refresh [HIGH]
- [...]
- [ ] Actions [HIGH]
- [ ] Add tag(s) [HIGH]
- [ ] Add field(s) [HIGH]
- [ ] Set field content [HIGH]
- [ ] [...]
- [ ] Settings Menu [HIGH]
- [ ] Application Settings [HIGH]
- [ ] Stored in system user folder/designated folder [HIGH]
- [ ] Library Settings [HIGH]
- [ ] Stored in `.TagStudio` folder [HIGH]
- [ ] Multiple Root Directories per Library [HIGH]
- [ ] [Entry groups](../library/entry_groups.md) [HIGH]
- [ ] Groups for files/entries where the same entry can be in multiple groups [HIGH]
- [ ] Ability to number entries within group [HIGH]
- [ ] Ability to set sorting method for group [HIGH]
- [ ] Ability to set custom thumbnail for group [HIGH]
- [ ] Group is treated as entry with tags and metadata [HIGH]
- [ ] Nested groups [MEDIUM]
- [ ] Fields [HIGH]
- [x] Text Boxes [HIGH]
- [x] Text Lines [HIGH]
- [ ] Dates [HIGH]
- [ ] Custom field names [HIGH]
- [ ] Search engine [HIGH]
- [x] Boolean operators [HIGH]
- [ ] Tag objects + autocomplete [HIGH]
- [x] Filename search [HIGH]
- [x] Filetype search [HIGH]
- [x] Search by extension (e.g. ".jpg", ".png") [HIGH]
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW]
- [x] Search by media type (e.g. "image", "video", "document") [MEDIUM]
- [ ] Field content search [HIGH]
- [ ] HAS operator for composition tags [HIGH]
- [ ] OCR search [LOW]
- [ ] Fuzzy Search [LOW]
- [ ] Sortable results [HIGH]
- [ ] Sort by relevance [HIGH]
- [x] Sort by date added [HIGH]
- [ ] Sort by date created [HIGH]
- [ ] Sort by date modified [HIGH]
- [ ] Sort by date taken (photos) [MEDIUM]
- [ ] Sort by file size [HIGH]
- [ ] Sort by file dimension (images/video) [LOW]
- [ ] Automatic Entry Relinking [HIGH] [#36](https://github.com/TagStudioDev/TagStudio/issues/36)
- [ ] Detect Renames [HIGH]
- [ ] Detect Moves [HIGH]
- [ ] Detect Deletions [HIGH]
- [ ] Image Collages [LOW] [#91](https://github.com/TagStudioDev/TagStudio/issues/91)
- [ ] UI [HIGH]
- [ ] Tagging Panel [HIGH]
- [ ] Top Tags [HIGH]
- [ ] Recent Tags [HIGH]
- [ ] Tag Search [HIGH]
- [ ] Pinned Tags [HIGH]
- [ ] Configurable Thumbnails [MEDIUM]
- [ ] Custom thumbnail override [HIGH]
- [ ] Toggle File Extension Label [MEDIUM]
- [ ] Toggle Duration Label [MEDIUM]
- [ ] Custom Tag Badges [LOW]
- [ ] Thumbnails [HIGH]
- [ ] File Duration Label [HIGH]
- [ ] 3D Model Previews [MEDIUM]
- [ ] STL Previews [HIGH]
- [x] Drag and Drop [HIGH]
- [x] Drag files _to_ other programs [HIGH]
- [x] Drag files _to_ file explorer windows [MEDIUM]
- [x] Drag files _from_ file explorer windows [MEDIUM]
- [x] Drag files _from_ other programs [LOW]
- [ ] File Preview Panel [HIGH]
- [ ] Video Playback [HIGH]
- [x] Play/Pause [HIGH]
- [x] Loop [HIGH]
- [x] Toggle Autoplay [MEDIUM]
- [ ] Volume Control [HIGH]
- [x] Toggle Mute [HIGH]
- [ ] Timeline scrubber [HIGH]
- [ ] Fullscreen [MEDIUM]
- [x] Audio Playback [HIGH]
- [x] Play/Pause [HIGH]
- [ ] Loop [HIGH]
- [ ] Toggle Autoplay [MEDIUM]
- [x] Volume Control [HIGH]
- [x] Toggle Mute [HIGH]
- [x] Timeline scrubber [HIGH]
- [ ] Optimizations [HIGH]
- [x] Thumbnail caching [HIGH]
- [ ] File property caching/indexes [HIGH]
## Version Milestones
These version milestones are rough estimations for when the previous core features will be added. For a more definitive idea for when features are coming, please reference the current GitHub [milestones](https://github.com/TagStudioDev/TagStudio/milestones).
_(This list was created after the release of version 9.4)_
<!-- prettier-ignore -->
!!! note
This list was created after the release of version 9.4
### 9.5 (Alpha)
### v9.5
#### Core
- [x] SQL backend [HIGH]
#### Tags
- [x] Deleting Tags [HIGH]
- [ ] User-defined tag colors [HIGH]
- [x] ID based, not string or hex [HIGH]
- [x] Color name [HIGH]
- [x] Color value (hex) [HIGH]
- [x] Existing colors are now a set of base colors [HIGH]
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
#### Search
- [x] Boolean operators [HIGH]
- [x] Filename search [HIGH]
- [x] File type search [HIGH]
- [x] Search by extension (e.g. ".jpg", ".png") [HIGH]
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW]
- [x] Search by media type (e.g. "image", "video", "document") [MEDIUM]
- [x] Sort by date added [HIGH]
#### UI
- [x] Translations _(Any applicable)_ [MEDIUM]
- [ ] Tags [HIGH]
- [x] Deleting Tags [HIGH]
- [ ] User-defined tag colors [HIGH]
- [x] ID based, not string or hex [HIGH]
- [x] Color name [HIGH]
- [x] Color value (hex) [HIGH]
- [x] Existing colors are now a set of base colors [HIGH]
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
- [ ] Search engine [HIGH]
- [x] Boolean operators [HIGH]
- [ ] Tag objects + autocomplete [HIGH]
- [x] Filename search [HIGH]
- [x] Filetype search [HIGH]
- [x] Search by extension (e.g. ".jpg", ".png") [HIGH]
- [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW]
- [x] Search by media type (e.g. "image", "video", "document") [MEDIUM]
- [ ] Field content search [HIGH]
- [ ] Sortable results [HIGH]
- [x] Sort by date added [HIGH]
- [ ] Sort by date created [HIGH]
- [ ] Sort by date modified [HIGH]
- [ ] Settings Menu [HIGH]
- [ ] Application Settings [HIGH]
- [ ] Stored in system user folder/designated folder [HIGH]
- [ ] Library Settings [HIGH]
- [ ] Stored in `.TagStudio` folder [HIGH]
- [ ] Optimizations [HIGH]
- [x] Thumbnail caching [HIGH]
- [x] Unified Media Player [HIGH]
- [x] Auto-hiding player controls
- [x] Play/Pause [HIGH]
- [x] Loop [HIGH]
- [x] Toggle Autoplay [MEDIUM]
- [x] Volume Control [HIGH]
- [x] Toggle Mute [HIGH]
- [x] Timeline scrubber [HIGH]
- [ ] Fullscreen [MEDIUM]
- [x] Configurable page size [HIGH]
### 9.6 (Alpha)
#### Performance
- [x] Thumbnail caching [HIGH]
### v9.6
#### Core
- [ ] Cached file property table (media duration, word count, dimensions, etc.) [MEDIUM]
#### Library
- [ ] Tags [HIGH]
- [ ] Merging Tags [HIGH]
- [ ] Composition/HAS subtags [HIGH]
- [ ] Tag Icons [HIGH]
- [ ] Small Icons [HIGH]
- [ ] Large Icons for Profiles [MEDIUM]
- [ ] Built-in Icon Packs (i.e. Boxicons) [HIGH]
- [ ] User Defined Icons [HIGH]
- [ ] Multiple Languages for Tag Strings [MEDIUM]
- [ ] Title is tag name [HIGH]
- [ ] Title has tag color [MEDIUM]
- [ ] Tag marked as category does not display as a tag itself [HIGH]
- [ ] [Tag Overrides](../library/tag_overrides.md) [MEDIUM]
- [ ] Per-file overrides of subtags [HIGH]
- [ ] Fields [HIGH]
- [ ] Dates [HIGH]
- [ ] Custom field names [HIGH]
- [ ] Multiple Root Directories per Library [HIGH]
- [ ] `.ts_ignore` (`.gitignore`-style glob ignoring) [HIGH]
- [ ] Sharable Color Packs [MEDIUM]
- [ ] Human-readable (TOML) files containing tag data [HIGH]
- [ ] Importable [HIGH]
- [ ] Exportable [HIGH]
#### Tags
- [ ] Merging Tags [HIGH]
- [ ] [Component/HAS](../library/tag.md#component-tags) subtags [HIGH]
- [ ] Tag Icons [HIGH]
- [ ] Small Icons [HIGH]
- [ ] Large Icons for Profiles [MEDIUM]
- [ ] Built-in Icon Packs (i.e. Boxicons) [HIGH]
- [ ] User Defined Icons [HIGH]
- [ ] Multiple Languages for Tag Strings [MEDIUM]
- [ ] Title is tag name [HIGH]
- [ ] Title has tag color [MEDIUM]
- [ ] Tag marked as category does not display as a tag itself [HIGH]
- [ ] [Tag Overrides](../library/tag_overrides.md) [MEDIUM]
- [ ] Per-file overrides of subtags [HIGH]
#### Fields
- [ ] Datetime fields [HIGH]
- [ ] Custom field names [HIGH]
#### Search
- [ ] Field content search [HIGH]
- [ ] Sort by date created [HIGH]
- [ ] Sort by date modified [HIGH]
- [x] Sort by filename [HIGH]
- [ ] HAS operator for composition tags [HIGH]
- [ ] Search bar rework
- [ ] Improved tag autocomplete [HIGH]
- [ ] Tags appear as widgets in search bar [HIGH]
#### UI
- [ ] File duration on video thumbnails [HIGH]
- [ ] 3D Model Previews [MEDIUM]
- [ ] STL Previews [HIGH]
- [ ] Word count/line count on text thumbnails [LOW]
- [x] Settings Menu [HIGH]
- [x] Application Settings [HIGH]
- [x] Stored in system user folder/designated folder [HIGH]
- [ ] Library Settings [HIGH]
- [ ] Stored in `.TagStudio` folder [HIGH]
- [ ] Tagging Panel [HIGH]
Toggleable persistent main window panel or pop-out. Replaces the current tag manager.
- [ ] Top Tags [HIGH]
- [ ] Recent Tags [HIGH]
- [ ] Tag Search [HIGH]
- [ ] Pinned Tags [HIGH]
- [ ] Search engine [HIGH]
- [ ] HAS operator for composition tags [HIGH]
### 9.7 (Alpha)
- [ ] New tabbed tag building UI to support the new tag features [HIGH]
### v9.7
#### Library
- [ ] Configurable Thumbnails [MEDIUM]
- [ ] Toggle File Extension Label [MEDIUM]
- [ ] Toggle Duration Label [MEDIUM]
- [ ] Custom Tag Badges [LOW]
- [ ] Thumbnails [HIGH]
- [ ] File Duration Label [HIGH]
- [ ] [Entry groups](../library/entry_groups.md) [HIGH]
- [ ] Groups for files/entries where the same entry can be in multiple groups [HIGH]
- [ ] Ability to number entries within group [HIGH]
@@ -230,46 +144,74 @@ _(This list was created after the release of version 9.4)_
- [ ] Group is treated as entry with tags and metadata [HIGH]
- [ ] Nested groups [MEDIUM]
### 9.8 (Possible Beta)
#### Search
- [ ] Sort by relevance [HIGH]
- [ ] Sort by date taken (photos) [MEDIUM]
- [ ] Sort by file size [HIGH]
- [ ] Sort by file dimension (images/video) [LOW]
#### [Macros](../utilities/macro.md)
- [ ] Sharable Macros [MEDIUM]
- [ ] Standard notation format (TOML) contacting macro instructions [HIGH]
- [ ] Exportable [HIGH]
- [ ] Importable [HIGH]
- [ ] Triggers [HIGH]
- [ ] On new file [HIGH]
- [ ] On library refresh [HIGH]
- [ ] [...]
- [ ] Actions [HIGH]
- [ ] Add tag(s) [HIGH]
- [ ] Add field(s) [HIGH]
- [ ] Set field content [HIGH]
- [ ] [...]
#### UI
- [ ] Custom thumbnail overrides [MEDIUM]
- [ ] Toggle File Extension Label [MEDIUM]
- [ ] Toggle Duration Label [MEDIUM]
- [ ] Custom Tag Badges [LOW]
- [ ] Library list view [HIGH]
### v9.8
#### Library
- [ ] Automatic Entry Relinking [HIGH]
- [ ] Detect Renames [HIGH]
- [ ] Detect Moves [HIGH]
- [ ] Detect Deletions [HIGH]
- [ ] [Macros](../utilities/macro.md) [HIGH]
- [ ] Sharable Macros [MEDIUM]
- [ ] Standard notation format (i.e. JSON) contacting macro instructions [HIGH]
- [ ] Exportable [HIGH]
- [ ] Importable [HIGH]
- [ ] Triggers [HIGH]
- [ ] On new file [HIGH]
- [ ] On library refresh [HIGH]
- [...]
- [ ] Actions [HIGH]
- [ ] Add tag(s) [HIGH]
- [ ] Add field(s) [HIGH]
- [ ] Set field content [HIGH]
- [ ] [...]
### 9.9 (Possible Beta)
#### Search
- [ ] OCR search [LOW]
- [ ] Fuzzy Search [LOW]
### v9.9
#### Library
- [ ] Tag Packs [MEDIUM]
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
- [ ] Importable [HIGH]
- [ ] Exportable [HIGH]
- [ ] Conflict resolution [HIGH]
- [ ] Color Packs [MEDIUM]
- [ ] Human-readable (i.e. JSON) files containing tag data [HIGH]
- [ ] Importable [HIGH]
- [ ] Exportable [HIGH]
- [ ] Exportable Library Data [HIGH]
- [ ] Standard notation format (i.e. JSON) contacting all library data [HIGH]
### 10.0 (Possible Beta/Full Release)
#### Tags
- [ ] Tag Packs [MEDIUM]
- [ ] Human-readable (TOML) files containing tag data [HIGH]
- [ ] Multiple Languages for Tag Strings [MEDIUM]
- [ ] Importable [HIGH]
- [ ] Exportable [HIGH]
- [ ] Conflict resolution [HIGH]
### v10.0
- [ ] All remaining [HIGH] and optional [MEDIUM] features
### Post 10.0
### Post v10.0
#### Core
- [ ] Core Library/API
- [ ] Plugin Support

View File

@@ -0,0 +1,60 @@
# Save Format Changes
This page outlines the various changes made to the TagStudio save file format over time, sometimes referred to as the "database" or "database file".
---
## JSON
| Used From | Used Until | Format | Location |
| --------- | ----------------------------------------------------------------------- | ------ | --------------------------------------------- |
| v1.0.0 | [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2) | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
The legacy database format for public TagStudio releases [v9.1](https://github.com/TagStudioDev/TagStudio/tree/Alpha-v9.1) through [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2). Variations of this format had been used privately since v1.0.0.
Replaced by the new SQLite format introduced in TagStudio [v9.5.0 Pre-Release 1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1).
---
## DB_VERSION 6
| Used From | Used Until | Format | Location |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-PR1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | [v9.5.0-PR1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
The first public version of the SQLite save file format.
Migration from the legacy JSON format is provided via a walkthrough when opening a legacy library in TagStudio [v9.5.0 Pre-Release 1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) or later.
---
## DB_VERSION 7
| Used From | Used Until | Format | Location |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-PR2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | [v9.5.0-PR3](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr3) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Repairs "Description" fields to use a TEXT_LINE key instead of a TEXT_BOX key.
- Repairs tags that may have a disambiguation_id pointing towards a deleted tag.
---
## DB_VERSION 8
| Used From | Used Until | Format | Location |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-PR4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | [v9.5.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds the `color_border` column to the `tag_colors` table. Used for instructing the [secondary color](../library/tag_color.md#secondary-color) to apply to a tag's border as a new optional behavior.
- Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)".
- Updates Neon colors to use the new `color_border` property.
---
## DB_VERSION 9
| Used From | Used Until | Format | Location |
| ----------------------------------------------------------------------- | ---------- | ------ | ----------------------------------------------- |
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | _Current_ | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds the `filename` column to the `entries` table. Used for sorting entries by filename in search results.

View File

@@ -12,13 +12,13 @@ Libraries under 10,000 files automatically scan for new or modified files when o
Access the "Add Tag" search box by either clicking on the "Add Tag" button at the bottom of the right sidebar, accessing the "Add Tags to Selected" option from the File menu, or by pressing <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd>.
From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the “+” button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the <kbd>Enter</kbd>/<kbd>Return</kbd> key to add the the topmost tag and reset the tag search. Press <kbd>Enter</kbd>/<kbd>Return</kbd> once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard!
From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the "+" button next to any tags you want to the currently selected file entries. To quickly add the top result, press the <kbd>Enter</kbd>/<kbd>Return</kbd> key to add the topmost tag and reset the tag search. Press <kbd>Enter</kbd>/<kbd>Return</kbd> once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard!
To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.
## Adding Metadata to File Entries
To add a metadata field to a file entry, start by clicking the Add Field button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field youd like to add to the entry
To add a metadata field to a file entry, start by clicking the "Add Field" button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field youd like to add to the entry
## Editing Metadata Fields
@@ -33,26 +33,41 @@ Create a new tag by accessing the "New Tag" option from the Edit menu or by pres
- The tag **name** is the base name of the tag. **_This does NOT have to be unique!_**
- The tag **shorthand** is a special type of alias that displays in situations where screen space is more valuable, notably with name disambiguation.
- **Aliases** are alternate names for a tag. These let you search for terms other than the exact tag name in order to find the tag again.
- **Parent Tags** are tags in which this this tag can substitute for in searches. In other words, tags under this section are parents of this tag.
- **Parent Tags** are tags in which this tag can substitute for in searches. In other words, tags under this section are parents of this tag.
- Parent tags with the disambiguation check next to them will be used to help disambiguate tag names that may not be unique.
- For example: If you had a tag for "Freddy Fazbear", you might add "Five Nights at Freddy's" as one of the parent tags. If the disambiguation box is checked next to "Five Nights at Freddy's" parent tag, then the tag "Freddy Fazbear" will display as "Freddy Fazbear (Five Nights at Freddy's)". Furthermore, if the "Five Nights at Freddy's" tag has a shorthand like "FNAF", then the "Freddy Fazbear" tag will display as "Freddy Fazbear (FNAF)".
- The **color** option lets you select an optional color palette to use for your tag.
- The **"Is Cagegory"** property lets you treat this tag as a category under which itself and any child tags inheriting from it will be sorted by inside the preview panel.
- The **"Is Category"** property lets you treat this tag as a category under which itself and any child tags inheriting from it will be sorted by inside the preview panel.
### Tag Manager
You can manage your library of tags by opening the "Tag Manager" panel from Edit -> "Manage Tags". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
## Editing Tags
To edit a tag, click on it inside the preview panel or right-click the tag and select Edit Tag from the context menu.
To edit a tag, click on it inside the preview panel or right-click the tag and select "Edit Tag" from the context menu.
## Relinking Moved Files
Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to Search & Relink any unlinked file entries to their respective files, or Delete Unlinked Entries in the event the original files have been deleted and you no longer wish to keep their entries inside your library.
Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to "Search & Relink" any unlinked file entries to their respective files, or "Delete Unlinked Entries" in the event the original files have been deleted and you no longer wish to keep their entries inside your library.
<!-- prettier-ignore -->
!!! warning
There is currently no method to relink entries to files that have been renamed - only moved or deleted. This is a high priority for future releases.
<!-- prettier-ignore -->
!!! warning
If multiple matches for a moved file are found (matches are currently defined as files with a matching filename as the original), TagStudio will currently ignore the match groups. Adding a GUI for manual selection, as well as smarter automated relinking, are high priorities for future versions.
### Saving the Library
As of version 9.5, libraries are saved automatically as you go. To save a backup of your library, select File -> Save Library Backup from the menu bar.
## Launch Arguments
There are a handful of launch arguments you can pass to TagStudio via the command line or a desktop shortcut.
| Argument | Short | Description |
| ---------------------- | ----- | ---------------------------------------------------- |
| `--open <path>` | `-o` | Path to a TagStudio Library folder to open on start. |
| `--config-file <path>` | `-c` | Path to the TagStudio config file to load. |

View File

@@ -35,12 +35,12 @@ This tool is a preview of an upcoming feature. When selected, TagStudio will gen
### Auto-fill [WIP]
Tool is in development and will be documented in future update.
Tool is in development and will be documented in a future update.
### Sort fields
Tool is in development, will allow for user-defined sorting of [fields](../library/field.md).
Tool is in development. Will allow for user-defined sorting of [fields](../library/field.md).
### Folders to Tags
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](../library/tag.md#subtags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](../library/tag.md#parent-tags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.

515
flake.lock generated
View File

@@ -1,132 +1,5 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": "devenv_2",
"flake-compat": [
"devenv",
"flake-compat"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"pre-commit-hooks": [
"devenv",
"pre-commit-hooks"
]
},
"locked": {
"lastModified": 1712055811,
"narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=",
"owner": "cachix",
"repo": "cachix",
"rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "cachix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat_2",
"nix": "nix_2",
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1724763216,
"narHash": "sha256-oW2bwCrJpIzibCNK6zfIDaIQw765yMAuMSG2gyZfGv0=",
"owner": "cachix",
"repo": "devenv",
"rev": "1e4ef61205b9aa20fe04bf1c468b6a316281c4f1",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"devenv-root": {
"flake": false,
"locked": {
"narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=",
"type": "file",
"url": "file:///dev/null"
},
"original": {
"type": "file",
"url": "file:///dev/null"
}
},
"devenv_2": {
"inputs": {
"flake-compat": [
"devenv",
"cachix",
"flake-compat"
],
"nix": "nix",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix",
"pre-commit-hooks": [
"devenv",
"cachix",
"pre-commit-hooks"
]
},
"locked": {
"lastModified": 1708704632,
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
"owner": "cachix",
"repo": "devenv",
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "python-rewrite",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
@@ -134,11 +7,11 @@
]
},
"locked": {
"lastModified": 1725024810,
"narHash": "sha256-ODYRm8zHfLTH3soTFWE452ydPYz2iTvr9T8ftDMUQ3E=",
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "af510d4a62d071ea13925ce41c95e3dec816c01d",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
@@ -147,354 +20,27 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688870561,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix2container": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1724996935,
"narHash": "sha256-njRK9vvZ1JJsP8oV2OgkBrpJhgQezI03S7gzskCcHos=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "fa6bb0a1159f55d071ba99331355955ae30b3401",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"type": "github"
}
},
"nix_2": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression_2"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1692808169,
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
"lastModified": 1744932701,
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-qt6": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-regression_2": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1710695816,
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713361204,
"narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1724819573,
"narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "71e91c409d1e654808b2621f28a327acfdad8dc2",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1692876271,
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-utils": "flake-utils_2",
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1713775815,
"narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"devenv-root": "devenv-root",
"flake-parts": "flake-parts",
"nix2container": "nix2container",
"nixpkgs": "nixpkgs_3",
"nixpkgs-qt6": "nixpkgs-qt6",
"systems": "systems_4"
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
@@ -511,51 +57,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
}
},
"root": "root",

245
flake.nix
View File

@@ -2,209 +2,88 @@
description = "TagStudio";
inputs = {
devenv.url = "github:cachix/devenv";
devenv-root = {
url = "file+file:///dev/null";
flake = false;
};
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
nix2container = {
url = "github:nlewo/nix2container";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
# Pinned to Qt version 6.7.1
nixpkgs-qt6.url = "github:NixOS/nixpkgs/e6cea36f83499eb4e9cd184c8a8e823296b50ad5";
systems.url = "github:nix-systems/default-linux";
systems.url = "github:nix-systems/default";
};
outputs =
{
inputs@{
flake-parts,
nixpkgs,
nixpkgs-qt6,
self,
systems,
...
}@inputs:
}:
let
inherit (nixpkgs) lib;
in
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.devenv.flakeModule ];
systems = import systems;
systems = import inputs.systems;
perSystem =
{ pkgs, ... }:
{
config,
pkgs,
system,
...
}:
let
inherit (nixpkgs) lib;
packages =
let
python3Packages = pkgs.python312Packages;
qt6Pkgs = import nixpkgs-qt6 { inherit system; };
in
{
formatter = pkgs.nixfmt-rfc-style;
devenv.shells = rec {
default = tagstudio;
tagstudio =
let
cfg = config.devenv.shells.tagstudio;
in
{
# NOTE: many things were simply transferred over from previous,
# there must be additional work in ensuring all relevant dependencies
# are in place (and no extraneous). I have already spent much
# work making this in the first place and just need to get it out
# there, especially after my promises. Would appreciate any help
# (possibly PRs!) on taking care of this. Otherwise, just expect
# this to get ironed out over time.
#
# Thank you! -Xarvex
devenv.root =
let
devenvRoot = builtins.readFile inputs.devenv-root.outPath;
in
# If not overriden (/dev/null), --impure is necessary.
pkgs.lib.mkIf (devenvRoot != "") devenvRoot;
name = "TagStudio";
# Derived from previous flake iteration.
packages =
(with pkgs; [
cmake
binutils
coreutils
dbus
fontconfig
freetype
gdb
glib
libGL
libGLU
libgcc
libxkbcommon
mypy
ruff
xorg.libxcb
xorg.libX11
zstd
])
++ (with qt6Pkgs; [
qt6.full
qt6.qtbase
qt6.qtwayland
qtcreator
]);
enterShell =
let
setQtEnv =
pkgs.runCommand "set-qt-env"
{
buildInputs = with qt6Pkgs.qt6; [
qtbase
];
nativeBuildInputs =
(with pkgs; [
makeShellWrapper
])
++ (with qt6Pkgs.qt6; [
wrapQtAppsHook
]);
}
''
makeShellWrapper "$(type -p sh)" "$out" "''${qtWrapperArgs[@]}"
sed "/^exec/d" -i "$out"
'';
in
''
source ${setQtEnv}
'';
scripts.tagstudio.exec = ''
python ${cfg.devenv.root}/tagstudio/tag_studio.py
'';
env = {
QT_QPA_PLATFORM = "wayland;xcb";
# Derived from previous flake iteration.
# Not desired given LD_LIBRARY_PATH pollution.
# See supposed alternative below, further research required.
LD_LIBRARY_PATH = lib.makeLibraryPath (
(with pkgs; [
dbus
fontconfig
freetype
gcc-unwrapped
glib
libglvnd
libkrb5
libpulseaudio
libva
libxkbcommon
openssl
stdenv.cc.cc.lib
wayland
xorg.libxcb
xorg.libX11
xorg.libXrandr
zlib
zstd
])
++ (with qt6Pkgs.qt6; [
qtbase
qtwayland
full
])
);
};
languages.python = {
enable = true;
venv = {
enable = true;
quiet = true;
requirements =
let
excludeDeps =
req: deps:
builtins.concatStringsSep "\n" (
builtins.filter (line: !(lib.any (elem: lib.hasPrefix elem line) deps)) (lib.splitString "\n" req)
);
in
''
${builtins.readFile ./requirements.txt}
${excludeDeps (builtins.readFile ./requirements-dev.txt) [
"mypy"
"ruff"
]}
'';
};
# Should be able to replace LD_LIBRARY_PATH?
# Was not quite able to get working,
# will be consulting cachix community. -Xarvex
# libraries = with pkgs; [ ];
};
pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix {
inherit (pkgs) cmake;
inherit pyexiv2;
};
pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
vtf2img = python3Packages.callPackage ./nix/package/vtf2img.nix { };
in
rec {
default = tagstudio;
tagstudio = pkgs.callPackage ./nix/package {
# HACK: Remove when PySide6 is bumped to 6.9.x.
# Sourced from https://github.com/NixOS/nixpkgs/commit/2f9c1ad5e19a6154d541f878774a9aacc27381b7.
pyside6 =
if lib.versionAtLeast python3Packages.pyside6.version "6.9.0" then
(python3Packages.pyside6.override {
shiboken6 = python3Packages.shiboken6.overrideAttrs {
version = "6.8.0.2";
src = pkgs.fetchurl {
url = "mirror://qt/official_releases/QtForPython/shiboken6/PySide6-6.8.0.2-src/pyside-setup-everywhere-src-6.8.0.tar.xz";
hash = "sha256-Ghohmo8yfjQNJYJ1+tOp8mG48EvFcEF0fnPdatJStOE=";
};
sourceRoot = "pyside-setup-everywhere-src-6.8.0/sources/shiboken6";
patches = [ ./nix/package/shiboken6-fix-include-qt-headers.patch ];
};
}).overrideAttrs
{ sourceRoot = "pyside-setup-everywhere-src-6.8.0/sources/pyside6"; }
else
python3Packages.pyside6;
inherit pillow-jxl-plugin vtf2img;
};
tagstudio-jxl = tagstudio.override { withJXLSupport = true; };
inherit pillow-jxl-plugin pyexiv2 vtf2img;
};
devShells = rec {
default = tagstudio;
tagstudio = import ./nix/shell.nix {
inherit
inputs
lib
pkgs
self
;
};
};
formatter = pkgs.nixfmt-rfc-style;
};
};
}

View File

@@ -9,6 +9,7 @@
# To run the preview server:
# mkdocs serve
---
site_name: TagStudio
site_description: "A User-Focused Photo & File Management System"
site_url: https://docs.tagstud.io/
@@ -25,58 +26,60 @@ extra:
tags:
Upcoming Feature: upcoming
# by default the navigation is an alphanumerically sorted,
# nested list of all the Markdown files found within the /docs directory
# where index files are always first
# uncomment the following to configure the navigation manually:
# nav:
# - Home:
# - index.md
# - install.md
# - usage.md
# - Library:
# - library/index.md
# - library/entry.md
# - library/entry_groups.md
# - library/field.md
# - library/tag.md
# - library/tag_categories.md
# - library/tag_overrides.md
# - Utilities:
# - utilities/macro.md
# - Updates:
# - updates/changelog.md
# - updates/roadmap.md
# - updates/db_migration.md
nav:
- Home:
- index.md
- install.md
- usage.md
- develop.md
- Help:
- help/ffmpeg.md
- Library:
- library/index.md
- library/entry.md
- library/entry_groups.md
- library/field.md
- library/library_search.md
- library/tag.md
- library/tag_categories.md
- library/tag_color.md
- library/tag_overrides.md
- Utilities:
- utilities/macro.md
- Updates:
- updates/changelog.md
- updates/roadmap.md
- updates/schema_changes.md
theme:
name: material
custom_dir: overrides
palette:
# Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
icon: material/lightbulb-auto-outline
name: Switch to Light Mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
- media: "(prefers-color-scheme: light)"
scheme: default
primary: deep purple
accent: deep purple
toggle:
icon: material/brightness-7
name: Switch to dark mode
icon: material/lightbulb
name: Switch to Dark Mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: deep purple
accent: deep purple
toggle:
icon: material/brightness-4
name: SSwitch to system preference
logo: assets/icon.png
icon: material/lightbulb-night-outline
name: Switch to System Preference
logo: assets/icon_mono.svg
favicon: assets/icon.ico
font: false # use system fonts
font:
code: Jetbrains Mono
language: en
features:
- navigation.instant
@@ -84,11 +87,9 @@ theme:
- navigation.tracking
- navigation.expand
- navigation.sections
#- navigation.tabs
#- content.tabs.link
#- navigation.top
- search.suggest
- content.code.annotate
- content.code.copy
- content.action.edit
icon:
repo: fontawesome/brands/github
@@ -134,5 +135,9 @@ markdown_extensions:
plugins:
- search
- tags
- social: # social embed cards
enabled: !ENV [CI, false] # enabled only when running in CI (eg GitHub Actions)
- social: # social embed cards
enabled: !ENV [CI, false] # enabled only when running in CI (eg GitHub Actions)
extra_css:
- stylesheets/extra.css
- https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap

136
nix/package/default.nix Normal file
View File

@@ -0,0 +1,136 @@
{
ffmpeg-headless,
lib,
pipewire,
python3Packages,
qt6,
stdenv,
wrapGAppsHook,
pillow-jxl-plugin,
pyside6,
vtf2img,
withJXLSupport ? false,
}:
let
pyproject = (lib.importTOML ../../pyproject.toml).project;
in
python3Packages.buildPythonApplication {
pname = pyproject.name;
inherit (pyproject) version;
pyproject = true;
src = ../../.;
nativeBuildInputs = [
python3Packages.pythonRelaxDepsHook
qt6.wrapQtAppsHook
# INFO: Should be unnecessary once PR is pulled.
# PR: https://github.com/NixOS/nixpkgs/pull/271037
# Issue: https://github.com/NixOS/nixpkgs/issues/149812
wrapGAppsHook
];
buildInputs = [
qt6.qtbase
qt6.qtmultimedia
];
nativeCheckInputs = with python3Packages; [
pytest-qt
pytest-xdist
pytestCheckHook
syrupy
];
# TODO: Install more icon resolutions when available.
preInstall = ''
mkdir -p $out/share/applications $out/share/icons/hicolor/512x512/apps
cp $src/src/tagstudio/resources/tagstudio.desktop $out/share/applications
cp $src/src/tagstudio/resources/icon.png $out/share/icons/hicolor/512x512/apps/tagstudio.png
'';
makeWrapperArgs =
[ "--prefix PATH : ${lib.makeBinPath [ ffmpeg-headless ]}" ]
++ lib.optional stdenv.hostPlatform.isLinux "--prefix LD_LIBRARY_PATH : ${
lib.makeLibraryPath [ pipewire ]
}";
pythonRemoveDeps = lib.optional (!withJXLSupport) [ "pillow_jxl" ];
pythonRelaxDeps = [
"numpy"
"pillow"
"pillow-heif"
"pillow-jxl-plugin"
"structlog"
"typing-extensions"
];
pythonImportsCheck = [ "tagstudio" ];
build-system = with python3Packages; [ hatchling ];
dependencies =
with python3Packages;
[
chardet
ffmpeg-python
humanfriendly
mutagen
numpy
opencv-python
pillow
pillow-heif
pydantic
pydub
pyside6
rawpy
send2trash
sqlalchemy
structlog
toml
ujson
vtf2img
]
++ lib.optional withJXLSupport pillow-jxl-plugin;
disabledTests = [
# INFO: These tests require modifications to a library, which does not work
# in a read-only environment.
"test_build_tag_panel_add_alias_callback"
"test_build_tag_panel_add_aliases"
"test_build_tag_panel_add_sub_tag_callback"
"test_build_tag_panel_build_tag"
"test_build_tag_panel_remove_alias_callback"
"test_build_tag_panel_remove_subtag_callback"
"test_build_tag_panel_set_aliases"
"test_build_tag_panel_set_parent_tags"
"test_build_tag_panel_set_tag"
"test_json_migration"
"test_library_migrations"
"test_add_same_tag_to_selection_single"
"test_add_tag_to_selection_multiple"
"test_add_tag_to_selection_single"
"test_custom_tag_category"
"test_file_path_display"
"test_meta_tag_category"
"test_update_selection_empty"
"test_update_selection_empty"
"test_update_selection_multiple"
"test_update_selection_single"
# INFO: This test requires modification of a configuration file.
"test_filepath_setting"
];
meta = {
inherit (pyproject) description;
homepage = "https://docs.tagstud.io/";
license = lib.licenses.gpl3Only;
maintainers = with lib.maintainers; [ xarvex ];
mainProgram = "tagstudio";
platforms = lib.platforms.unix;
};
}

View File

@@ -0,0 +1,67 @@
{
buildPythonPackage,
cmake,
fetchPypi,
lib,
numpy,
packaging,
pillow,
pyexiv2,
pytestCheckHook,
rustPlatform,
}:
buildPythonPackage rec {
pname = "pillow-jxl-plugin";
version = "1.3.2";
pyproject = true;
src = fetchPypi {
pname = builtins.replaceStrings [ "-" ] [ "_" ] pname;
inherit version;
hash = "sha256-efBoek8yUFR+ArhS55lm9F2XhkZ7/I3GsScQEe8U/2I=";
};
cargoDeps = rustPlatform.fetchCargoVendor {
inherit src;
hash = "sha256-vZHrwGfgo3fIIOY7p0vy4XIKiHoddPDdJggkBen+w/A=";
};
nativeBuildInputs = [
cmake
rustPlatform.cargoSetupHook
rustPlatform.maturinBuildHook
];
nativeCheckInputs = [
numpy
pyexiv2
pytestCheckHook
];
# INFO: Working directory takes precedence in the Python path. Remove
# `pillow_jxl` to prevent it from being loaded during pytest, rather than the
# built module, as it includes a `pillow_jxl.pillow_jxl` .so that is imported.
# See: https://github.com/NixOS/nixpkgs/issues/255262
# See: https://github.com/NixOS/nixpkgs/pull/255471
preCheck = ''
rm -r pillow_jxl
'';
dontUseCmakeConfigure = true;
pythonImportsCheck = [ "pillow_jxl" ];
dependencies = [
packaging
pillow
];
meta = {
description = "Pillow plugin for JPEG-XL, using Rust for bindings.";
homepage = "https://github.com/Isotr0py/pillow-jpegxl-plugin";
license = lib.licenses.gpl3;
maintainers = with lib.maintainers; [ xarvex ];
platforms = lib.platforms.unix;
};
}

32
nix/package/pyexiv2.nix Normal file
View File

@@ -0,0 +1,32 @@
{
autoPatchelfHook,
buildPythonPackage,
exiv2,
fetchFromGitHub,
lib,
}:
buildPythonPackage rec {
pname = "pyexiv2";
version = "2.15.3";
src = fetchFromGitHub {
owner = "LeoHsiao1";
repo = pname;
rev = "v${version}";
hash = "sha256-83bFMaoXncvhRJNcCgkkC7B29wR5pjuLO/EdkQdqxxo=";
};
nativeBuildInputs = [ autoPatchelfHook ];
buildInputs = [ exiv2.lib ];
pythonImportsCheck = [ "pyexiv2" ];
meta = {
description = "Read and write image metadata, including EXIF, IPTC, XMP, ICC Profile.";
homepage = "https://github.com/LeoHsiao1/pyexiv2";
license = lib.licenses.gpl3;
maintainers = with lib.maintainers; [ xarvex ];
platforms = with lib.platforms; darwin ++ linux ++ windows;
};
}

View File

@@ -0,0 +1,81 @@
Sourced from https://github.com/NixOS/nixpkgs/blob/5ba0f1ea90b0afa2abc23a43edb63af51d932e6d/pkgs/development/python-modules/shiboken6/fix-include-qt-headers.patch.
--- a/ApiExtractor/clangparser/compilersupport.cpp
+++ b/ApiExtractor/clangparser/compilersupport.cpp
@@ -16,6 +16,7 @@
#include <QtCore/QStandardPaths>
#include <QtCore/QStringList>
#include <QtCore/QVersionNumber>
+#include <QtCore/QRegularExpression>
#include <clang-c/Index.h>
@@ -341,6 +342,13 @@ QByteArrayList emulatedCompilerOptions()
{
QByteArrayList result;
HeaderPaths headerPaths;
+
+ bool isNixDebug = qgetenv("NIX_DEBUG").toInt() > 0;
+ // examples:
+ // /nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-qtsensors-6.4.2-dev/include
+ // /nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-qtbase-6.4.2-dev/include
+ QRegularExpression qtHeaderRegex(uR"(/[0-9a-z]{32}-qt[a-z0-9]+-)"_s);
+
switch (compiler()) {
case Compiler::Msvc:
result.append(QByteArrayLiteral("-fms-compatibility-version=19.26.28806"));
@@ -352,9 +360,30 @@ QByteArrayList emulatedCompilerOptions()
appendClangBuiltinIncludes(&headerPaths);
break;
case Compiler::Clang:
- headerPaths.append(gppInternalIncludePaths(compilerFromCMake(u"clang++"_s)));
+ // fix: error: cannot jump from switch statement to this case label: case Compiler::Gpp
+ // note: jump bypasses variable initialization: const HeaderPaths clangPaths =
+ {
+ //headerPaths.append(gppInternalIncludePaths(compilerFromCMake(u"clang++"_s)));
+ // fix: qt.shiboken: x is specified in typesystem, but not defined. This could potentially lead to compilation errors.
+ // PySide requires that Qt headers are not -isystem
+ // https://bugreports.qt.io/browse/PYSIDE-787
+ const HeaderPaths clangPaths = gppInternalIncludePaths(compilerFromCMake(u"clang++"_qs));
+ for (const HeaderPath &h : clangPaths) {
+ auto match = qtHeaderRegex.match(QString::fromUtf8(h.path));
+ if (!match.hasMatch()) {
+ if (isNixDebug)
+ qDebug() << "shiboken compilersupport.cpp: found non-qt header: " << h.path;
+ // add using -isystem
+ headerPaths.append(h);
+ } else {
+ if (isNixDebug)
+ qDebug() << "shiboken compilersupport.cpp: found qt header: " << h.path;
+ headerPaths.append({h.path, HeaderType::Standard});
+ }
+ }
result.append(noStandardIncludeOption());
break;
+ }
case Compiler::Gpp:
if (needsClangBuiltinIncludes())
appendClangBuiltinIncludes(&headerPaths);
@@ -363,8 +392,20 @@ QByteArrayList emulatedCompilerOptions()
// <type_traits> etc (g++ 11.3).
const HeaderPaths gppPaths = gppInternalIncludePaths(compilerFromCMake(u"g++"_qs));
for (const HeaderPath &h : gppPaths) {
- if (h.path.contains("c++") || h.path.contains("sysroot"))
+ // fix: qt.shiboken: x is specified in typesystem, but not defined. This could potentially lead to compilation errors.
+ // PySide requires that Qt headers are not -isystem
+ // https://bugreports.qt.io/browse/PYSIDE-787
+ auto match = qtHeaderRegex.match(QString::fromUtf8(h.path));
+ if (!match.hasMatch()) {
+ if (isNixDebug)
+ qDebug() << "shiboken compilersupport.cpp: found non-qt header: " << h.path;
+ // add using -isystem
headerPaths.append(h);
+ } else {
+ if (isNixDebug)
+ qDebug() << "shiboken compilersupport.cpp: found qt header: " << h.path;
+ headerPaths.append({h.path, HeaderType::Standard});
+ }
}
break;
}
--
2.39.0

29
nix/package/vtf2img.nix Normal file
View File

@@ -0,0 +1,29 @@
{
buildPythonPackage,
fetchPypi,
lib,
pillow,
}:
buildPythonPackage rec {
pname = "vtf2img";
version = "0.1.0";
src = fetchPypi {
inherit pname version;
hash = "sha256-YmWs8673d72wH4nTOXP4AFGs2grIETln4s1MD5PfE0A=";
};
pythonImportsCheck = [ "vtf2img" ];
dependencies = [ pillow ];
meta = {
description = "A Python library to convert Valve Texture Format (VTF) files to images.";
homepage = "https://github.com/julienc91/vtf2img";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ xarvex ];
mainProgram = "vtf2img";
platforms = lib.platforms.unix;
};
}

104
nix/shell.nix Normal file
View File

@@ -0,0 +1,104 @@
{ lib, pkgs, ... }:
let
# INFO: PySide6 from PIP is compiled for 6.8.0 of Qt, must pin to match version.
# Long term this can be solved with an alternative like uv2nix for the devshell.
qt6Pkgs = import (builtins.fetchTarball {
name = "nixos-unstable-qt6-pinned";
url = "https://github.com/NixOS/nixpkgs/archive/93ff48c9be84a76319dac293733df09bbbe3f25c.tar.gz";
sha256 = "038m7932v4z0zn2lk7k7mbqbank30q84r1viyhchw9pcm3aq3q23";
}) { inherit (pkgs) system; };
pythonLibraryPath = lib.makeLibraryPath (
(with pkgs; [
fontconfig.lib
freetype
glib
stdenv.cc.cc.lib
zstd
])
++ (with qt6Pkgs.qt6; [ qtbase ])
++ lib.optionals (!pkgs.stdenv.isDarwin) (
(with pkgs; [
dbus.lib
libGL
libdrm
libpulseaudio
libva
libxkbcommon
pipewire
xorg.libX11
xorg.libXrandr
])
++ (with qt6Pkgs.qt6; [ qtwayland ])
)
);
libraryPath = "${lib.optionalString pkgs.stdenv.isDarwin "DY"}LD_LIBRARY_PATH";
python = pkgs.python312;
pythonWrapped = pkgs.symlinkJoin {
inherit (python)
name
pname
version
meta
;
paths = [ python ];
nativeBuildInputs = (with pkgs; [ makeWrapper ]) ++ (with qt6Pkgs.qt6; [ wrapQtAppsHook ]);
buildInputs = with qt6Pkgs.qt6; [ qtbase ];
postBuild = ''
wrapProgram $out/bin/python3.12 \
--prefix ${libraryPath} : ${pythonLibraryPath} \
"''${qtWrapperArgs[@]}"
'';
};
in
pkgs.mkShellNoCC {
nativeBuildInputs = with pkgs; [
coreutils
uv
ruff
];
buildInputs = [ pythonWrapped ] ++ (with pkgs; [ ffmpeg-headless ]);
env = {
QT_QPA_PLATFORM = "wayland;xcb";
UV_NO_SYNC = "1";
UV_PYTHON_DOWNLOADS = "never";
};
shellHook =
let
python = lib.getExe pythonWrapped;
in
# bash
''
venv="''${UV_PROJECT_ENVIRONMENT:-.venv}"
if [ ! -f "''${venv}"/bin/activate ] || [ "$(readlink -f "''${venv}"/bin/python)" != "$(readlink -f ${python})" ]; then
printf '%s\n' 'Regenerating virtual environment, Python interpreter changed...' >&2
rm -rf "''${venv}"
uv venv --python ${python} "''${venv}"
fi
source "''${venv}"/bin/activate
if [ ! -f "''${venv}"/pyproject.toml ] || ! diff --brief pyproject.toml "''${venv}"/pyproject.toml >/dev/null; then
printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2
uv pip install --quiet --editable '.[mkdocs,mypy,pre-commit,pytest]'
cp pyproject.toml "''${venv}"/pyproject.toml
fi
pre-commit install
'';
meta = {
maintainers = with lib.maintainers; [ xarvex ];
platforms = lib.platforms.unix;
};
}

View File

@@ -0,0 +1,122 @@
<!--
Copyright (c) 2016-2025 Martin Donath <martin.donath@squidfunk.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-->
<!-- Determine classes -->
{% set class = "md-header" %}
{% if "navigation.tabs.sticky" in features %}
{% set class = class ~ " md-header--shadow md-header--lifted" %}
{% elif "navigation.tabs" not in features %}
{% set class = class ~ " md-header--shadow" %}
{% endif %}
<!-- Header -->
<header class="{{ class }}" data-md-component="header">
<nav
class="md-header__inner md-grid"
aria-label="{{ lang.t('header') }}"
>
<!-- Link to home -->
<a
href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
title="{{ config.site_name | e }}"
class="md-header__button md-logo"
aria-label="{{ config.site_name }}"
data-md-component="logo"
>
{% include "partials/logo.html" %}
</a>
<!-- Button to open drawer -->
<label class="md-header__button md-icon" for="__drawer">
{% set icon = config.theme.icon.menu or "material/menu" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</label>
<!-- Header title -->
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
<a style="font-weight: 900;">Tag</a><a style="font-weight: 500;"><i>Studio</i></a>
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
{% if page.meta and page.meta.title %}
{{ page.meta.title }}
{% else %}
{{ page.title }}
{% endif %}
</span>
</div>
</div>
</div>
<!-- Color palette toggle -->
{% if config.theme.palette %}
{% if not config.theme.palette is mapping %}
{% include "partials/palette.html" %}
{% endif %}
{% endif %}
<!-- User preference: color palette -->
{% if not config.theme.palette is mapping %}
{% include "partials/javascripts/palette.html" %}
{% endif %}
<!-- Site language selector -->
{% if config.extra.alternate %}
{% include "partials/alternate.html" %}
{% endif %}
<!-- Button to open search modal -->
{% if "material/search" in config.plugins %}
{% set search = config.plugins["material/search"] | attr("config") %}
<!-- Check if search is actually enabled - see https://t.ly/DT_0V -->
{% if search.enabled %}
<label class="md-header__button md-icon" for="__search">
{% set icon = config.theme.icon.search or "material/magnify" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</label>
<!-- Search interface -->
{% include "partials/search.html" %}
{% endif %}
{% endif %}
<!-- Repository information -->
{% if config.repo_url %}
<div class="md-header__source">
{% include "partials/source.html" %}
</div>
{% endif %}
</nav>
<!-- Navigation tabs (sticky) -->
{% if "navigation.tabs.sticky" in features %}
{% if "navigation.tabs" in features %}
{% include "partials/tabs.html" %}
{% endif %}
{% endif %}
</header>

View File

@@ -0,0 +1,69 @@
<!--
Copyright (c) 2016-2025 Martin Donath <martin.donath@squidfunk.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-->
{% import "partials/nav-item.html" as item with context %}
<!-- Determine classes -->
{% set class = "md-nav md-nav--primary" %}
{% if "navigation.tabs" in features %}
{% set class = class ~ " md-nav--lifted" %}
{% endif %}
{% if "toc.integrate" in features %}
{% set class = class ~ " md-nav--integrated" %}
{% endif %}
<!-- Navigation -->
<nav
class="{{ class }}"
aria-label="{{ lang.t('nav') }}"
data-md-level="0"
>
<!-- Site title -->
<!-- <label class="md-nav__title" for="__drawer">
<a
href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
title="{{ config.site_name | e }}"
class="md-nav__button md-logo"
aria-label="{{ config.site_name }}"
data-md-component="logo"
>
{% include "partials/logo.html" %}
</a>
{{ config.site_name }}
</label> -->
<!-- Repository information -->
{% if config.repo_url %}
<div class="md-nav__source">
{% include "partials/source.html" %}
</div>
{% endif %}
<!-- Navigation list -->
<ul class="md-nav__list" data-md-scrollfix>
{% for nav_item in nav %}
{% set path = "__nav_" ~ loop.index %}
{{ item.render(nav_item, path, 1) }}
{% endfor %}
</ul>
</nav>

View File

@@ -1,62 +1,113 @@
[tool.ruff]
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"]
line-length = 100
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff.lint.per-file-ignores]
"tagstudio/tests/**" = ["D", "E402"]
"tagstudio/src/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint]
select = [
"B",
"D",
"E",
"F",
"FBT003",
"I",
"N",
"SIM",
"T20",
"UP",
[project]
name = "TagStudio"
description = "A User-Focused Photo & File Management System."
version = "9.5.3"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.13"
dependencies = [
"chardet~=5.2",
"ffmpeg-python~=0.2",
"humanfriendly==10.*",
"mutagen~=1.47",
"numpy~=2.2",
"opencv_python~=4.11",
"Pillow>=10.2,<=12.0",
"pillow-heif~=0.22",
"pillow-jxl-plugin~=1.3",
"pydantic~=2.10",
"pydub~=0.25",
"PySide6==6.8.0.*",
"rawpy~=0.24",
"Send2Trash~=1.8",
"SQLAlchemy~=2.0",
"srctools~=2.6",
"structlog~=25.3",
"toml~=0.10",
"typing_extensions~=4.13",
"ujson~=5.10",
]
ignore = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
[project.optional-dependencies]
dev = ["tagstudio[mkdocs,mypy,pre-commit,pyinstaller,pytest,ruff]"]
mkdocs = ["mkdocs-material[imaging]>=9.6.14"]
mypy = ["mypy==1.15.0", "mypy-extensions==1.*", "types-ujson~=5.10"]
pre-commit = ["pre-commit~=4.2"]
pyinstaller = ["Pyinstaller~=6.13"]
pytest = [
"pytest==8.3.5",
"pytest-cov==6.1.1",
"pytest-qt==4.4.0",
"syrupy==4.9.1",
]
ruff = ["ruff==0.11.8"]
[project.gui-scripts]
tagstudio = "tagstudio.main:main"
[tool.hatch.build.targets.wheel]
packages = ["src/tagstudio"]
[tool.mypy]
strict_optional = false
disable_error_code = ["func-returns-value", "import-untyped"]
mypy_path = ["src/tagstudio"]
disable_error_code = [
"annotation-unchecked",
"func-returns-value",
"import-untyped",
]
explicit_package_bases = true
ignore_missing_imports = true
implicit_optional = true
strict_optional = false
warn_unused_ignores = true
check_untyped_defs = true
mypy_path = ["tagstudio"]
exclude = ["build", "dist"]
[[tool.mypy.overrides]]
module = "tests.*"
module = "tagstudio.qt.main_window"
ignore_errors = true
[[tool.mypy.overrides]]
module = "src.qt.main_window"
module = "tagstudio.qt.ui.home_ui"
ignore_errors = true
[[tool.mypy.overrides]]
module = "src.qt.ui.home_ui"
ignore_errors = true
[[tool.mypy.overrides]]
module = "src.core.ts_core"
module = "tagstudio.core.ts_core"
ignore_errors = true
[tool.pytest.ini_options]
#addopts = "-m 'not qt'"
qt_api = "pyside6"
[tool.pyright]
ignore = [".venv/**"]
include = ["src/tagstudio", "tests"]
extraPaths = ["src/tagstudio", "tests"]
reportAny = false
reportIgnoreCommentWithoutRule = false
reportImplicitStringConcatenation = false
reportMissingTypeArgument = false
# reportOptionalMemberAccess = false
reportUnannotatedClassAttribute = false
reportUnknownArgumentType = false
reportUnknownLambdaType = false
reportUnknownMemberType = false
reportUnusedCallResult = false
[tool.ruff]
exclude = ["home_ui.py", "resources.py", "resources_rc.py"]
line-length = 100
[tool.ruff.lint]
select = ["B", "D", "E", "F", "FBT003", "I", "N", "SIM", "T20", "UP"]
ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["D", "E402"]
"src/tagstudio/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
[tool.ruff.lint.pydocstyle]
convention = "google"

View File

@@ -1,8 +0,0 @@
ruff==0.8.1
pre-commit==3.7.0
pytest==8.2.0
Pyinstaller==6.6.0
mypy==1.11.2
syrupy==4.7.1
pytest-qt==4.4.0
pytest-cov==5.0.0

View File

@@ -1,19 +0,0 @@
chardet==5.2.0
ffmpeg-python==0.2.0
humanfriendly==10.0
mutagen==1.47.0
numpy==2.1.0
opencv_python==4.10.0.84
pillow-heif==0.16.0
pillow-jxl-plugin==1.3.0
Pillow==10.3.0
pydub==0.25.1
PySide6_Addons==6.8.0.1
PySide6_Essentials==6.8.0.1
PySide6==6.8.0.1
rawpy==0.22.0
SQLAlchemy==2.0.34
structlog==24.4.0
typing_extensions>=3.10.0.0,<=4.11.0
ujson>=5.8.0,<=5.9.0
vtf2img==0.1.0

View File

@@ -2,8 +2,8 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
VERSION: str = "9.5.0" # Major.Minor.Patch
VERSION_BRANCH: str = "Pre-Release 1" # Usually "" or "Pre-Release"
VERSION: str = "9.5.3" # Major.Minor.Patch
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
# The folder & file names where TagStudio keeps its data relative to a library.
TS_FOLDER_NAME: str = ".TagStudio"
@@ -25,3 +25,5 @@ TAG_FAVORITE = 1
TAG_META = 2
RESERVED_TAG_START = 0
RESERVED_TAG_END = 999
RESERVED_NAMESPACE_PREFIX = "tagstudio"

View File

@@ -2,15 +2,18 @@ from pathlib import Path
import structlog
from PySide6.QtCore import QSettings
from src.core.constants import TS_FOLDER_NAME
from src.core.enums import SettingItems
from src.core.library.alchemy.library import LibraryStatus
from tagstudio.core.constants import TS_FOLDER_NAME
from tagstudio.core.enums import SettingItems
from tagstudio.core.global_settings import GlobalSettings
from tagstudio.core.library.alchemy.library import LibraryStatus
logger = structlog.get_logger(__name__)
class DriverMixin:
settings: QSettings
cached_values: QSettings
settings: GlobalSettings
def evaluate_path(self, open_path: str | None) -> LibraryStatus:
"""Check if the path of library is valid."""
@@ -20,17 +23,17 @@ class DriverMixin:
if not library_path.exists():
logger.error("Path does not exist.", open_path=open_path)
return LibraryStatus(success=False, message="Path does not exist.")
elif self.settings.value(
SettingItems.START_LOAD_LAST, defaultValue=True, type=bool
) and self.settings.value(SettingItems.LAST_LIBRARY):
library_path = Path(str(self.settings.value(SettingItems.LAST_LIBRARY)))
elif self.settings.open_last_loaded_on_startup and self.cached_values.value(
SettingItems.LAST_LIBRARY
):
library_path = Path(str(self.cached_values.value(SettingItems.LAST_LIBRARY)))
if not (library_path / TS_FOLDER_NAME).exists():
logger.error(
"TagStudio folder does not exist.",
library_path=library_path,
ts_folder=TS_FOLDER_NAME,
)
self.settings.setValue(SettingItems.LAST_LIBRARY, "")
self.cached_values.setValue(SettingItems.LAST_LIBRARY, "")
# dont consider this a fatal error, just skip opening the library
library_path = None

View File

@@ -10,15 +10,29 @@ from uuid import uuid4
class SettingItems(str, enum.Enum):
"""List of setting item names."""
START_LOAD_LAST = "start_load_last"
LAST_LIBRARY = "last_library"
LIBS_LIST = "libs_list"
WINDOW_SHOW_LIBS = "window_show_libs"
SHOW_FILENAMES = "show_filenames"
AUTOPLAY = "autoplay_videos"
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
class ShowFilepathOption(int, enum.Enum):
"""Values representing the options for the "show_filenames" setting."""
SHOW_FULL_PATHS = 0
SHOW_RELATIVE_PATHS = 1
SHOW_FILENAMES_ONLY = 2
DEFAULT = SHOW_RELATIVE_PATHS
class TagClickActionOption(int, enum.Enum):
"""Values representing the options for the "tag_click_action" setting."""
OPEN_EDIT = 0
SET_SEARCH = 1
ADD_TO_SEARCH = 2
DEFAULT = OPEN_EDIT
class Theme(str, enum.Enum):
COLOR_BG_DARK = "#65000000"
COLOR_BG_LIGHT = "#22000000"
@@ -69,6 +83,5 @@ class LibraryPrefs(DefaultEnum):
"""Library preferences with default value accessible via .default property."""
IS_EXCLUDE_LIST = True
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
PAGE_SIZE: int = 500
DB_VERSION: int = 6
EXTENSION_LIST = [".json", ".xmp", ".aae"]
DB_VERSION = 9

View File

@@ -0,0 +1,102 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import platform
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import override
import structlog
import toml
from pydantic import BaseModel, Field
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
if platform.system() == "Windows":
DEFAULT_GLOBAL_SETTINGS_PATH = (
Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml"
)
else:
DEFAULT_GLOBAL_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml"
logger = structlog.get_logger(__name__)
class TomlEnumEncoder(toml.TomlEncoder):
@override
def dump_value(self, v):
if isinstance(v, Enum):
return super().dump_value(v.value)
return super().dump_value(v)
class Theme(Enum):
DARK = 0
LIGHT = 1
SYSTEM = 2
DEFAULT = SYSTEM
# NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings
# properties to be overwritten with environment variables. as tagstudio is not currently using
# environment variables, i did not base it on that, but that may be useful in the future.
class GlobalSettings(BaseModel):
language: str = Field(default="en")
open_last_loaded_on_startup: bool = Field(default=True)
autoplay: bool = Field(default=True)
loop: bool = Field(default=True)
show_filenames_in_grid: bool = Field(default=True)
page_size: int = Field(default=100)
show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT)
theme: Theme = Field(default=Theme.SYSTEM)
tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT)
date_format: str = Field(default="%x")
hour_format: bool = Field(default=True)
zero_padding: bool = Field(default=True)
loaded_from: Path = Field(default=DEFAULT_GLOBAL_SETTINGS_PATH, exclude=True)
@staticmethod
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings":
if path.exists():
with open(path) as file:
filecontents = file.read()
if len(filecontents.strip()) != 0:
logger.info("[Settings] Reading Global Settings File", path=path)
settings_data = toml.loads(filecontents)
settings = GlobalSettings(**settings_data, loaded_from=path)
return settings
return GlobalSettings(loaded_from=path)
def save(self, path: Path | None = None) -> None:
if path is None:
path = self.loaded_from
if not path.parent.exists():
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
toml.dump(self.model_dump(), f, encoder=TomlEnumEncoder())
@property
def datetime_format(self) -> str:
date_format = self.date_format
is_24h = self.hour_format
hour_format = "%H:%M:%S" if is_24h else "%I:%M:%S %p"
zero_padding = self.zero_padding
zero_padding_symbol = ""
if not zero_padding:
zero_padding_symbol = "#" if platform.system() == "Windows" else "-"
date_format = date_format.replace("%d", f"%{zero_padding_symbol}d").replace(
"%m", f"%{zero_padding_symbol}m"
)
hour_format = hour_format.replace("%H", f"%{zero_padding_symbol}H").replace(
"%I", f"%{zero_padding_symbol}I"
)
return f"{date_format}, {hour_format}"
def format_datetime(self, dt: datetime) -> str:
return datetime.strftime(dt, self.datetime_format)

View File

@@ -2,13 +2,15 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
import structlog
from sqlalchemy import Dialect, Engine, String, TypeDecorator, create_engine, text
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import DeclarativeBase
from src.core.constants import RESERVED_TAG_END
from tagstudio.core.constants import RESERVED_TAG_END
logger = structlog.getLogger(__name__)

View File

@@ -5,7 +5,7 @@
import structlog
from .models import Namespace, TagColorGroup
from tagstudio.core.library.alchemy.models import Namespace, TagColorGroup
logger = structlog.get_logger(__name__)
@@ -307,6 +307,12 @@ def pastels() -> list[TagColorGroup]:
def shades() -> list[TagColorGroup]:
burgundy = TagColorGroup(
slug="burgundy",
namespace="tagstudio-shades",
name="Burgundy",
primary="#6E1C24",
)
auburn = TagColorGroup(
slug="auburn",
namespace="tagstudio-shades",
@@ -319,19 +325,31 @@ def shades() -> list[TagColorGroup]:
name="Olive",
primary="#4C652E",
)
dark_teal = TagColorGroup(
slug="dark-teal",
namespace="tagstudio-shades",
name="Dark Teal",
primary="#1F5E47",
)
navy = TagColorGroup(
slug="navy",
namespace="tagstudio-shades",
name="Navy",
primary="#104B98",
)
dark_lavender = TagColorGroup(
slug="dark_lavender",
namespace="tagstudio-shades",
name="Dark Lavender",
primary="#3D3B6C",
)
berry = TagColorGroup(
slug="berry",
namespace="tagstudio-shades",
name="Berry",
primary="#9F2AA7",
)
return [auburn, olive, navy, berry]
return [burgundy, auburn, olive, dark_teal, navy, dark_lavender, berry]
def earth_tones() -> list[TagColorGroup]:
@@ -421,6 +439,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Red",
primary="#180607",
secondary="#E22C3C",
color_border=True,
)
neon_red_orange = TagColorGroup(
slug="neon-red-orange",
@@ -428,6 +447,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Red Orange",
primary="#220905",
secondary="#E83726",
color_border=True,
)
neon_orange = TagColorGroup(
slug="neon-orange",
@@ -435,6 +455,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Orange",
primary="#1F0D05",
secondary="#ED6022",
color_border=True,
)
neon_amber = TagColorGroup(
slug="neon-amber",
@@ -442,6 +463,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Amber",
primary="#251507",
secondary="#FA9A2C",
color_border=True,
)
neon_yellow = TagColorGroup(
slug="neon-yellow",
@@ -449,6 +471,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Yellow",
primary="#2B1C0B",
secondary="#FFD63D",
color_border=True,
)
neon_lime = TagColorGroup(
slug="neon-lime",
@@ -456,6 +479,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Lime",
primary="#1B220C",
secondary="#92E649",
color_border=True,
)
neon_green = TagColorGroup(
slug="neon-green",
@@ -463,6 +487,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Green",
primary="#091610",
secondary="#45D649",
color_border=True,
)
neon_teal = TagColorGroup(
slug="neon-teal",
@@ -470,6 +495,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Teal",
primary="#09191D",
secondary="#22D589",
color_border=True,
)
neon_cyan = TagColorGroup(
slug="neon-cyan",
@@ -477,6 +503,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Cyan",
primary="#0B191C",
secondary="#3DDBDB",
color_border=True,
)
neon_blue = TagColorGroup(
slug="neon-blue",
@@ -484,6 +511,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Blue",
primary="#09101C",
secondary="#3B87F0",
color_border=True,
)
neon_indigo = TagColorGroup(
slug="neon-indigo",
@@ -491,6 +519,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Indigo",
primary="#150B24",
secondary="#874FF5",
color_border=True,
)
neon_purple = TagColorGroup(
slug="neon-purple",
@@ -498,6 +527,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Purple",
primary="#1E0B26",
secondary="#BB4FF0",
color_border=True,
)
neon_magenta = TagColorGroup(
slug="neon-magenta",
@@ -505,6 +535,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Magenta",
primary="#220A13",
secondary="#F64680",
color_border=True,
)
neon_pink = TagColorGroup(
slug="neon-pink",
@@ -512,6 +543,7 @@ def neon() -> list[TagColorGroup]:
name="Neon Pink",
primary="#210E15",
secondary="#FF62AF",
color_border=True,
)
neon_white = TagColorGroup(
slug="neon-white",
@@ -519,6 +551,7 @@ def neon() -> list[TagColorGroup]:
name="Neon White",
primary="#131315",
secondary="#F2F1F8",
color_border=True,
)
return [
neon_red,

View File

@@ -2,11 +2,15 @@ import enum
from dataclasses import dataclass, replace
from pathlib import Path
from src.core.query_lang import AST as Query # noqa: N811
from src.core.query_lang import Constraint, ConstraintType, Parser
import structlog
from tagstudio.core.query_lang.ast import AST
from tagstudio.core.query_lang.parser import Parser
MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140
logger = structlog.get_logger(__name__)
class TagColorEnum(enum.IntEnum):
DEFAULT = 1
@@ -63,67 +67,67 @@ class ItemType(enum.Enum):
class SortingModeEnum(enum.Enum):
DATE_ADDED = "file.date_added"
FILE_NAME = "generic.filename"
PATH = "file.path"
@dataclass
class FilterState:
class BrowsingState:
"""Represent a state of the Library grid view."""
# these should remain
page_index: int | None = 0
page_size: int | None = 500
page_index: int = 0
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
ascending: bool = True
# these should be erased on update
query: str | None = None
# Abstract Syntax Tree Of the current Search Query
ast: Query = None
@property
def limit(self):
return self.page_size
@property
def offset(self):
return self.page_size * self.page_index
def ast(self) -> AST | None:
if self.query is None:
return None
return Parser(self.query).parse()
@classmethod
def show_all(cls) -> "FilterState":
return FilterState()
def show_all(cls) -> "BrowsingState":
return BrowsingState()
@classmethod
def from_search_query(cls, search_query: str) -> "FilterState":
return cls(ast=Parser(search_query).parse())
def from_search_query(cls, search_query: str) -> "BrowsingState":
return cls(query=search_query)
@classmethod
def from_tag_id(cls, tag_id: int | str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []))
def from_tag_id(cls, tag_id: int | str) -> "BrowsingState":
return cls(query=f"tag_id:{str(tag_id)}")
@classmethod
def from_path(cls, path: Path | str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []))
def from_path(cls, path: Path | str) -> "BrowsingState":
return cls(query=f'path:"{str(path).strip()}"')
@classmethod
def from_mediatype(cls, mediatype: str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []))
def from_mediatype(cls, mediatype: str) -> "BrowsingState":
return cls(query=f"mediatype:{mediatype}")
@classmethod
def from_filetype(cls, filetype: str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.FileType, filetype, []))
def from_filetype(cls, filetype: str) -> "BrowsingState":
return cls(query=f"filetype:{filetype}")
@classmethod
def from_tag_name(cls, tag_name: str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []))
def from_tag_name(cls, tag_name: str) -> "BrowsingState":
return cls(query=f'tag:"{tag_name}"')
def with_page_size(self, page_size: int) -> "FilterState":
return replace(self, page_size=page_size)
def with_page_index(self, index: int) -> "BrowsingState":
return replace(self, page_index=index)
def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState":
def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
return replace(self, sorting_mode=mode)
def with_sorting_direction(self, ascending: bool) -> "FilterState":
def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
return replace(self, ascending=ascending)
def with_search_query(self, search_query: str) -> "BrowsingState":
return replace(self, query=search_query)
class FieldTypeEnum(enum.Enum):
TEXT_LINE = "Text Line"

View File

@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from __future__ import annotations
from dataclasses import dataclass, field
@@ -11,11 +12,11 @@ from typing import TYPE_CHECKING, Any
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
from .db import Base
from .enums import FieldTypeEnum
from tagstudio.core.library.alchemy.db import Base
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
if TYPE_CHECKING:
from .models import Entry, ValueType
from tagstudio.core.library.alchemy.models import Entry, ValueType
class BaseField(Base):
@@ -113,7 +114,7 @@ class _FieldID(Enum):
AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE)
ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE)
URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE)
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_LINE)
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_BOX)
NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX)
COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE)
DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME)

View File

@@ -2,10 +2,11 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from .db import Base
from tagstudio.core.library.alchemy.db import Base
class TagParent(Base):

View File

@@ -2,22 +2,22 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import datetime as dt
from datetime import datetime as dt
from pathlib import Path
from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ...constants import TAG_ARCHIVED, TAG_FAVORITE
from .db import Base, PathType
from .fields import (
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from tagstudio.core.library.alchemy.db import Base, PathType
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
from tagstudio.core.library.alchemy.fields import (
BaseField,
BooleanField,
DatetimeField,
FieldTypeEnum,
TextField,
)
from .joins import TagParent
from tagstudio.core.library.alchemy.joins import TagParent
class Namespace(Base):
@@ -63,6 +63,7 @@ class TagColorGroup(Base):
name: Mapped[str] = mapped_column()
primary: Mapped[str] = mapped_column(nullable=False)
secondary: Mapped[str | None]
color_border: Mapped[bool] = mapped_column(nullable=False, default=False)
# TODO: Determine if slug and namespace can be optional and generated/added here if needed.
def __init__(
@@ -72,6 +73,7 @@ class TagColorGroup(Base):
name: str,
primary: str,
secondary: str | None = None,
color_border: bool = False,
):
self.slug = slug
self.namespace = namespace
@@ -79,6 +81,7 @@ class TagColorGroup(Base):
self.primary = primary
if secondary:
self.secondary = secondary
self.color_border = color_border
super().__init__()
@@ -184,10 +187,11 @@ class Entry(Base):
folder: Mapped[Folder] = relationship("Folder")
path: Mapped[Path] = mapped_column(PathType, unique=True)
filename: Mapped[str] = mapped_column()
suffix: Mapped[str] = mapped_column()
date_created: Mapped[dt.datetime | None]
date_modified: Mapped[dt.datetime | None]
date_added: Mapped[dt.datetime | None]
date_created: Mapped[dt | None]
date_modified: Mapped[dt | None]
date_added: Mapped[dt | None]
tags: Mapped[set[Tag]] = relationship(secondary="tag_entries")
@@ -222,13 +226,14 @@ class Entry(Base):
folder: Folder,
fields: list[BaseField],
id: int | None = None,
date_created: dt.datetime | None = None,
date_modified: dt.datetime | None = None,
date_added: dt.datetime | None = None,
date_created: dt | None = None,
date_modified: dt | None = None,
date_added: dt | None = None,
) -> None:
self.path = path
self.folder = folder
self.id = id
self.filename = path.name
self.suffix = path.suffix.lstrip(".").lower()
# The date the file associated with this entry was created.
@@ -301,7 +306,7 @@ class ValueType(Base):
def slugify_field_key(mapper, connection, target):
"""Slugify the field key before inserting into the database."""
if not target.key:
from .library import slugify
from tagstudio.core.library.alchemy.library import slugify
target.key = slugify(target.tag)

View File

@@ -2,32 +2,41 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import re
from typing import TYPE_CHECKING
import structlog
from sqlalchemy import ColumnElement, and_, distinct, func, or_, select, text
from sqlalchemy.orm import Session
from src.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories
from src.core.query_lang import BaseVisitor
from src.core.query_lang.ast import ANDList, Constraint, ConstraintType, Not, ORList, Property
from sqlalchemy.sql.operators import ilike_op
from .joins import TagEntry
from .models import Entry, Tag, TagAlias
from tagstudio.core.library.alchemy.joins import TagEntry
from tagstudio.core.library.alchemy.models import Entry, Tag, TagAlias
from tagstudio.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories
from tagstudio.core.query_lang.ast import (
ANDList,
BaseVisitor,
Constraint,
ConstraintType,
Not,
ORList,
Property,
)
# workaround to have autocompletion in the Editor
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
from .library import Library
from tagstudio.core.library.alchemy.library import Library
else:
Library = None # don't import .library because of circular imports
Library = None # don't import library because of circular imports
logger = structlog.get_logger(__name__)
# TODO: Reevaluate after subtags -> parent tags name change
CHILDREN_QUERY = text("""
TAG_CHILDREN_ID_QUERY = text("""
-- Note for this entire query that tag_parents.child_id is the parent id and tag_parents.parent_id is the child id due to bad naming
WITH RECURSIVE ChildTags AS (
SELECT :tag_id AS child_id
UNION ALL
UNION
SELECT tp.parent_id AS child_id
FROM tag_parents tp
INNER JOIN ChildTags c ON tp.child_id = c.child_id
@@ -97,7 +106,29 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
elif node.type == ConstraintType.TagID:
return self.__entry_matches_tag_ids([int(node.value)])
elif node.type == ConstraintType.Path:
return Entry.path.op("GLOB")(node.value)
ilike = False
glob = False
# Smartcase check
if node.value == node.value.lower():
ilike = True
if node.value.startswith("*") or node.value.endswith("*"):
glob = True
if ilike and glob:
logger.info("ConstraintType.Path", ilike=True, glob=True)
return func.lower(Entry.path).op("GLOB")(f"{node.value.lower()}")
elif ilike:
logger.info("ConstraintType.Path", ilike=True, glob=False)
return ilike_op(Entry.path, f"%{node.value}%")
elif glob:
logger.info("ConstraintType.Path", ilike=False, glob=True)
return Entry.path.op("GLOB")(node.value)
else:
logger.info(
"ConstraintType.Path", ilike=False, glob=False, re=re.escape(node.value)
)
return Entry.path.regexp_match(re.escape(node.value))
elif node.type == ConstraintType.MediaType:
extensions: set[str] = set[str]()
for media_cat in MediaCategories.ALL_CATEGORIES:
@@ -116,7 +147,7 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
# raise exception if Constraint stays unhandled
raise NotImplementedError("This type of constraint is not implemented yet")
def visit_property(self, node: Property) -> None:
def visit_property(self, node: Property) -> ColumnElement[bool]:
raise NotImplementedError("This should never be reached!")
def visit_not(self, node: Not) -> ColumnElement[bool]:
@@ -151,7 +182,7 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
return tag_ids
outp = []
for tag_id in tag_ids:
outp.extend(list(session.scalars(CHILDREN_QUERY, {"tag_id": tag_id})))
outp.extend(list(session.scalars(TAG_CHILDREN_ID_QUERY, {"tag_id": tag_id})))
return outp
def __entry_has_all_tags(self, tag_ids: list[int]) -> ColumnElement[bool]:

View File

@@ -7,29 +7,28 @@
"""The Library object and related methods for TagStudio."""
import datetime
from enum import Enum
import os
from pathlib import Path
import time
import traceback
from typing import Generator, cast
import xml.etree.ElementTree as ET
import structlog
from typing_extensions import Self
import ujson
from enum import Enum
from pathlib import Path
from typing import cast, Generator
from typing_extensions import Self
from .fields import DEFAULT_FIELDS, TEXT_FIELDS
from src.core.enums import OpenStatus
from src.core.utils.str import strip_punctuation
from src.core.utils.web import strip_web_protocol
from src.core.constants import (
from tagstudio.core.constants import (
BACKUP_FOLDER_NAME,
COLLAGE_FOLDER_NAME,
TS_FOLDER_NAME,
VERSION,
)
from tagstudio.core.enums import OpenStatus
from tagstudio.core.library.json.fields import DEFAULT_FIELDS, TEXT_FIELDS
from tagstudio.core.utils.str import strip_punctuation
from tagstudio.core.utils.web import strip_web_protocol
TYPE = ["file", "meta", "alt", "mask"]
@@ -128,7 +127,7 @@ class Entry:
if field_index >= 0 and field_index == i:
t: list[int] = library.get_field_attr(f, "content")
logger.info(
f't:{tag_id}, i:{i}, idx:{field_index}, c:{library.get_field_attr(f, "content")}'
f"t:{tag_id}, i:{i}, idx:{field_index}, c:{library.get_field_attr(f, 'content')}"
)
t.remove(tag_id)
elif field_index < 0:
@@ -208,9 +207,9 @@ class Tag:
"""Returns a formatted tag name intended for displaying."""
if self.subtag_ids:
if library.get_tag(self.subtag_ids[0]).shorthand:
return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).shorthand})"
return f"{self.name} ({library.get_tag(self.subtag_ids[0]).shorthand})"
else:
return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).name})"
return f"{self.name} ({library.get_tag(self.subtag_ids[0]).name})"
else:
return f"{self.name}"
@@ -759,7 +758,7 @@ class Library:
logger.info(f"[LIBRARY] Saving Library Backup to Disk...")
start_time = time.time()
filename = f'ts_library_backup_{datetime.datetime.utcnow().strftime("%F_%T").replace(":", "")}.json'
filename = f"ts_library_backup_{datetime.datetime.utcnow().strftime('%F_%T').replace(':', '')}.json"
self.verify_ts_folders()
with open(

View File

@@ -1,50 +1,64 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import logging
import mimetypes
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
logging.basicConfig(format="%(message)s", level=logging.INFO)
import structlog
FILETYPE_EQUIVALENTS = [set(["jpg", "jpeg"])]
logger = structlog.get_logger(__name__)
FILETYPE_EQUIVALENTS = [
set(["aif", "aiff", "aifc"]),
set(["html", "htm", "xhtml", "shtml", "dhtml"]),
set(["jfif", "jpeg_large", "jpeg", "jpg_large", "jpg"]),
set(["json", "jsonc", "json5"]),
set(["md", "markdown", "mkd", "rmd"]),
set(["tar.gz", "tgz"]),
set(["xml", "xul"]),
set(["yaml", "yml"]),
]
class MediaType(str, Enum):
"""Names of media types."""
ADOBE_PHOTOSHOP: str = "adobe_photoshop"
AFFINITY_PHOTO: str = "affinity_photo"
ARCHIVE: str = "archive"
AUDIO_MIDI: str = "audio_midi"
AUDIO: str = "audio"
BLENDER: str = "blender"
DATABASE: str = "database"
DISK_IMAGE: str = "disk_image"
DOCUMENT: str = "document"
EBOOK: str = "ebook"
FONT: str = "font"
IMAGE_ANIMATED: str = "image_animated"
IMAGE_RAW: str = "image_raw"
IMAGE_VECTOR: str = "image_vector"
IMAGE: str = "image"
INSTALLER: str = "installer"
MATERIAL: str = "material"
MODEL: str = "model"
OPEN_DOCUMENT: str = "open_document"
PACKAGE: str = "package"
PDF: str = "pdf"
PLAINTEXT: str = "plaintext"
PRESENTATION: str = "presentation"
PROGRAM: str = "program"
SHORTCUT: str = "shortcut"
SOURCE_ENGINE: str = "source_engine"
SPREADSHEET: str = "spreadsheet"
TEXT: str = "text"
VIDEO: str = "video"
ADOBE_PHOTOSHOP = "adobe_photoshop"
AFFINITY_PHOTO = "affinity_photo"
ARCHIVE = "archive"
AUDIO_MIDI = "audio_midi"
AUDIO = "audio"
BLENDER = "blender"
CODE = "code"
DATABASE = "database"
DISK_IMAGE = "disk_image"
DOCUMENT = "document"
EBOOK = "ebook"
FONT = "font"
IMAGE_ANIMATED = "image_animated"
IMAGE_RAW = "image_raw"
IMAGE_VECTOR = "image_vector"
IMAGE = "image"
INSTALLER = "installer"
IWORK = "iwork"
MATERIAL = "material"
MODEL = "model"
OPEN_DOCUMENT = "open_document"
PACKAGE = "package"
PDF = "pdf"
PLAINTEXT = "plaintext"
PRESENTATION = "presentation"
PROGRAM = "program"
SHADER = "shader"
SHORTCUT = "shortcut"
SOURCE_ENGINE = "source_engine"
SPREADSHEET = "spreadsheet"
TEXT = "text"
VIDEO = "video"
@dataclass(frozen=True)
@@ -67,6 +81,21 @@ class MediaCategory:
name: str
is_iana: bool = False
def contains(self, ext: str, mime_fallback: bool = False) -> bool:
"""Check if an extension is a member of this MediaCategory.
Args:
ext (str): File extension with a leading "." and in all lowercase.
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
"""
if ext in self.extensions:
return True
elif mime_fallback and self.is_iana:
mime_type: str | None = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
if mime_type is not None and mime_type.startswith(self.media_type.value):
return True
return False
class MediaCategories:
"""Contain pre-made MediaCategory objects as well as methods to interact with them."""
@@ -80,6 +109,7 @@ class MediaCategories:
".psd",
}
_AFFINITY_PHOTO_SET: set[str] = {".afphoto"}
_KRITA_SET: set[str] = {".kra", ".krz"}
_ARCHIVE_SET: set[str] = {
".7z",
".gz",
@@ -96,6 +126,7 @@ class MediaCategories:
_AUDIO_SET: set[str] = {
".aac",
".aif",
".aifc",
".aiff",
".alac",
".flac",
@@ -143,9 +174,71 @@ class MediaCategories:
".blend31",
".blend32",
}
_CODE_SET: set[str] = {
".bat",
".cfg",
".conf",
".cpp",
".cs",
".csh",
".css",
".d",
".dhtml",
".fgd",
".fish",
".gitignore",
".h",
".hpp",
".htm",
".html",
".inf",
".ini",
".js",
".json",
".json5",
".jsonc",
".jsx",
".kv3",
".lua",
".meta",
".nix",
".nu",
".nut",
".php",
".plist",
".prefs",
".ps1",
".py",
".pyi",
".qml",
".qrc",
".qss",
".rs",
".sh",
".shtml",
".sip",
".spec",
".tcl",
".timestamp",
".toml",
".ts",
".tsx",
".vcfg",
".vdf",
".vmt",
".vqlayout",
".vsc",
".vsnd_template",
".xhtml",
".xml",
".xul",
".yaml",
".yml",
}
_DATABASE_SET: set[str] = {
".accdb",
".mdb",
".pdb",
".sqlite",
".sqlite3",
}
@@ -166,23 +259,23 @@ class MediaCategories:
".wps",
}
_EBOOK_SET: set[str] = {
".azw",
".azw3",
".cb7",
".cba",
".cbr",
".cbt",
".cbz",
".djvu",
".epub",
# ".azw",
# ".azw3",
# ".cb7",
# ".cba",
# ".cbr",
# ".cbt",
# ".cbz",
# ".djvu",
# ".fb2",
# ".ibook",
# ".inf",
# ".kfx",
# ".lit",
# ".mobi",
# ".pdb"
# ".prc",
".fb2",
".ibook",
".inf",
".kfx",
".lit",
".mobi",
".pdb",
".prc",
}
_FONT_SET: set[str] = {
".fon",
@@ -209,7 +302,7 @@ class MediaCategories:
".raw",
".rw2",
}
_IMAGE_VECTOR_SET: set[str] = {".svg"}
_IMAGE_VECTOR_SET: set[str] = {".eps", ".epsf", ".epsi", ".svg", ".svgz"}
_IMAGE_RASTER_SET: set[str] = {
".apng",
".avif",
@@ -218,6 +311,7 @@ class MediaCategories:
".gif",
".heic",
".heif",
".icns",
".j2k",
".jfif",
".jp2",
@@ -235,6 +329,7 @@ class MediaCategories:
".webp",
}
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
_MATERIAL_SET: set[str] = {".mtl"}
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
_OPEN_DOCUMENT_SET: set[str] = {
@@ -248,6 +343,7 @@ class MediaCategories:
".odp",
".ods",
".odt",
".ora",
}
_PACKAGE_SET: set[str] = {
".aab",
@@ -258,51 +354,21 @@ class MediaCategories:
".pkg",
".xapk",
}
_PDF_SET: set[str] = {
".pdf",
}
_PDF_SET: set[str] = {".pdf"}
_PLAINTEXT_SET: set[str] = {
".bat",
".cfg",
".conf",
".cpp",
".cs",
".css",
".csv",
".fgd",
".gi",
".h",
".hpp",
".htm",
".html",
".inf",
".ini",
".js",
".json",
".jsonc",
".kv3",
".lua",
".i3u",
".lang",
".lock",
".log",
".markdown",
".md",
".nut",
".php",
".plist",
".prefs",
".py",
".pyc",
".qss",
".sh",
".toml",
".ts",
".mkd",
".rmd",
".txt",
".vcfg",
".vdf",
".vmt",
".vqlayout",
".vsc",
".vsnd_template",
".xml",
".yaml",
".yml",
"contributing",
"license",
"readme",
}
_PRESENTATION_SET: set[str] = {
".key",
@@ -310,9 +376,16 @@ class MediaCategories:
".ppt",
".pptx",
}
_PROGRAM_SET: set[str] = {".app", ".exe"}
_SOURCE_ENGINE_SET: set[str] = {
".vtf",
_PROGRAM_SET: set[str] = {".app", ".bin", ".exe"}
_SOURCE_ENGINE_SET: set[str] = {".vtf"}
_SHADER_SET: set[str] = {
".effect",
".frag",
".fsh",
".glsl",
".shader",
".vert",
".vsh",
}
_SHORTCUT_SET: set[str] = {".desktop", ".lnk", ".url"}
_SPREADSHEET_SET: set[str] = {
@@ -337,188 +410,212 @@ class MediaCategories:
".wmv",
}
ADOBE_PHOTOSHOP_TYPES: MediaCategory = MediaCategory(
ADOBE_PHOTOSHOP_TYPES = MediaCategory(
media_type=MediaType.ADOBE_PHOTOSHOP,
extensions=_ADOBE_PHOTOSHOP_SET,
is_iana=False,
name="photoshop",
)
AFFINITY_PHOTO_TYPES: MediaCategory = MediaCategory(
AFFINITY_PHOTO_TYPES = MediaCategory(
media_type=MediaType.AFFINITY_PHOTO,
extensions=_AFFINITY_PHOTO_SET,
is_iana=False,
name="affinity photo",
)
ARCHIVE_TYPES: MediaCategory = MediaCategory(
ARCHIVE_TYPES = MediaCategory(
media_type=MediaType.ARCHIVE,
extensions=_ARCHIVE_SET,
is_iana=False,
name="archive",
)
AUDIO_MIDI_TYPES: MediaCategory = MediaCategory(
AUDIO_MIDI_TYPES = MediaCategory(
media_type=MediaType.AUDIO_MIDI,
extensions=_AUDIO_MIDI_SET,
is_iana=False,
name="audio midi",
)
AUDIO_TYPES: MediaCategory = MediaCategory(
AUDIO_TYPES = MediaCategory(
media_type=MediaType.AUDIO,
extensions=_AUDIO_SET | _AUDIO_MIDI_SET,
is_iana=True,
name="audio",
)
BLENDER_TYPES: MediaCategory = MediaCategory(
BLENDER_TYPES = MediaCategory(
media_type=MediaType.BLENDER,
extensions=_BLENDER_SET,
is_iana=False,
name="blender",
)
DATABASE_TYPES: MediaCategory = MediaCategory(
CODE_TYPES = MediaCategory(
media_type=MediaType.CODE,
extensions=_CODE_SET,
is_iana=False,
name="code",
)
DATABASE_TYPES = MediaCategory(
media_type=MediaType.DATABASE,
extensions=_DATABASE_SET,
is_iana=False,
name="database",
)
DISK_IMAGE_TYPES: MediaCategory = MediaCategory(
DISK_IMAGE_TYPES = MediaCategory(
media_type=MediaType.DISK_IMAGE,
extensions=_DISK_IMAGE_SET,
is_iana=False,
name="disk image",
)
DOCUMENT_TYPES: MediaCategory = MediaCategory(
DOCUMENT_TYPES = MediaCategory(
media_type=MediaType.DOCUMENT,
extensions=_DOCUMENT_SET,
is_iana=False,
name="document",
)
EBOOK_TYPES: MediaCategory = MediaCategory(
EBOOK_TYPES = MediaCategory(
media_type=MediaType.EBOOK,
extensions=_EBOOK_SET,
is_iana=False,
name="ebook",
)
FONT_TYPES: MediaCategory = MediaCategory(
FONT_TYPES = MediaCategory(
media_type=MediaType.FONT,
extensions=_FONT_SET,
is_iana=True,
name="font",
)
IMAGE_ANIMATED_TYPES: MediaCategory = MediaCategory(
IMAGE_ANIMATED_TYPES = MediaCategory(
media_type=MediaType.IMAGE_ANIMATED,
extensions=_IMAGE_ANIMATED_SET,
is_iana=False,
name="animated image",
)
IMAGE_RAW_TYPES: MediaCategory = MediaCategory(
IMAGE_RAW_TYPES = MediaCategory(
media_type=MediaType.IMAGE_RAW,
extensions=_IMAGE_RAW_SET,
is_iana=False,
name="raw image",
)
IMAGE_VECTOR_TYPES: MediaCategory = MediaCategory(
IMAGE_VECTOR_TYPES = MediaCategory(
media_type=MediaType.IMAGE_VECTOR,
extensions=_IMAGE_VECTOR_SET,
is_iana=False,
name="vector image",
)
IMAGE_RASTER_TYPES: MediaCategory = MediaCategory(
IMAGE_RASTER_TYPES = MediaCategory(
media_type=MediaType.IMAGE,
extensions=_IMAGE_RASTER_SET,
is_iana=False,
name="raster image",
)
IMAGE_TYPES: MediaCategory = MediaCategory(
IMAGE_TYPES = MediaCategory(
media_type=MediaType.IMAGE,
extensions=_IMAGE_RASTER_SET | _IMAGE_RAW_SET | _IMAGE_VECTOR_SET,
is_iana=True,
name="image",
)
INSTALLER_TYPES: MediaCategory = MediaCategory(
INSTALLER_TYPES = MediaCategory(
media_type=MediaType.INSTALLER,
extensions=_INSTALLER_SET,
is_iana=False,
name="installer",
)
MATERIAL_TYPES: MediaCategory = MediaCategory(
IWORK_TYPES = MediaCategory(
media_type=MediaType.IWORK,
extensions=_IWORK_SET,
is_iana=False,
name="iwork",
)
MATERIAL_TYPES = MediaCategory(
media_type=MediaType.MATERIAL,
extensions=_MATERIAL_SET,
is_iana=False,
name="material",
)
MODEL_TYPES: MediaCategory = MediaCategory(
MODEL_TYPES = MediaCategory(
media_type=MediaType.MODEL,
extensions=_MODEL_SET,
is_iana=True,
name="model",
)
OPEN_DOCUMENT_TYPES: MediaCategory = MediaCategory(
OPEN_DOCUMENT_TYPES = MediaCategory(
media_type=MediaType.OPEN_DOCUMENT,
extensions=_OPEN_DOCUMENT_SET,
is_iana=False,
name="open document",
)
PACKAGE_TYPES: MediaCategory = MediaCategory(
PACKAGE_TYPES = MediaCategory(
media_type=MediaType.PACKAGE,
extensions=_PACKAGE_SET,
is_iana=False,
name="package",
)
PDF_TYPES: MediaCategory = MediaCategory(
PDF_TYPES = MediaCategory(
media_type=MediaType.PDF,
extensions=_PDF_SET,
is_iana=False,
name="pdf",
)
PLAINTEXT_TYPES: MediaCategory = MediaCategory(
PLAINTEXT_TYPES = MediaCategory(
media_type=MediaType.PLAINTEXT,
extensions=_PLAINTEXT_SET,
extensions=_PLAINTEXT_SET | _CODE_SET,
is_iana=False,
name="plaintext",
)
PRESENTATION_TYPES: MediaCategory = MediaCategory(
PRESENTATION_TYPES = MediaCategory(
media_type=MediaType.PRESENTATION,
extensions=_PRESENTATION_SET,
is_iana=False,
name="presentation",
)
PROGRAM_TYPES: MediaCategory = MediaCategory(
PROGRAM_TYPES = MediaCategory(
media_type=MediaType.PROGRAM,
extensions=_PROGRAM_SET,
is_iana=False,
name="program",
)
SHORTCUT_TYPES: MediaCategory = MediaCategory(
SHADER_TYPES = MediaCategory(
media_type=MediaType.SHADER,
extensions=_SHADER_SET,
is_iana=False,
name="shader",
)
SHORTCUT_TYPES = MediaCategory(
media_type=MediaType.SHORTCUT,
extensions=_SHORTCUT_SET,
is_iana=False,
name="shortcut",
)
SOURCE_ENGINE_TYPES: MediaCategory = MediaCategory(
SOURCE_ENGINE_TYPES = MediaCategory(
media_type=MediaType.SOURCE_ENGINE,
extensions=_SOURCE_ENGINE_SET,
is_iana=False,
name="source engine",
)
SPREADSHEET_TYPES: MediaCategory = MediaCategory(
SPREADSHEET_TYPES = MediaCategory(
media_type=MediaType.SPREADSHEET,
extensions=_SPREADSHEET_SET,
is_iana=False,
name="spreadsheet",
)
TEXT_TYPES: MediaCategory = MediaCategory(
TEXT_TYPES = MediaCategory(
media_type=MediaType.TEXT,
extensions=_DOCUMENT_SET | _PLAINTEXT_SET,
is_iana=True,
name="text",
)
VIDEO_TYPES: MediaCategory = MediaCategory(
VIDEO_TYPES = MediaCategory(
media_type=MediaType.VIDEO,
extensions=_VIDEO_SET,
is_iana=True,
name="video",
)
KRITA_TYPES = MediaCategory(
media_type=MediaType.IMAGE,
extensions=_KRITA_SET,
is_iana=False,
name="krita",
)
ALL_CATEGORIES: list[MediaCategory] = [
ALL_CATEGORIES = [
ADOBE_PHOTOSHOP_TYPES,
AFFINITY_PHOTO_TYPES,
ARCHIVE_TYPES,
@@ -535,6 +632,7 @@ class MediaCategories:
IMAGE_TYPES,
IMAGE_VECTOR_TYPES,
INSTALLER_TYPES,
IWORK_TYPES,
MATERIAL_TYPES,
MODEL_TYPES,
OPEN_DOCUMENT_TYPES,
@@ -543,11 +641,14 @@ class MediaCategories:
PLAINTEXT_TYPES,
PRESENTATION_TYPES,
PROGRAM_TYPES,
CODE_TYPES,
SHADER_TYPES,
SHORTCUT_TYPES,
SOURCE_ENGINE_TYPES,
SPREADSHEET_TYPES,
TEXT_TYPES,
VIDEO_TYPES,
KRITA_TYPES,
]
@staticmethod
@@ -559,16 +660,11 @@ class MediaCategories:
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
"""
media_types: set[MediaType] = set()
# mime_guess: bool = False
for cat in MediaCategories.ALL_CATEGORIES:
if ext in cat.extensions:
if cat.contains(ext, mime_fallback):
media_types.add(cat.media_type)
elif mime_fallback and cat.is_iana:
mime_type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
if mime_type and mime_type.startswith(cat.media_type.value):
media_types.add(cat.media_type)
# mime_guess = True
return media_types
@staticmethod
@@ -580,10 +676,4 @@ class MediaCategories:
media_cat (MediaCategory): The MediaCategory to to check for extension membership.
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
"""
if ext in media_cat.extensions:
return True
elif mime_fallback and media_cat.is_iana:
mime_type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
if mime_type and mime_type.startswith(media_cat.media_type.value):
return True
return False
return media_cat.contains(ext, mime_fallback)

View File

@@ -1,12 +1,14 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import traceback
from enum import IntEnum
from typing import Any
import structlog
from src.core.library.alchemy.enums import TagColorEnum
from tagstudio.core.library.alchemy.enums import TagColorEnum
logger = structlog.get_logger(__name__)

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Generic, TypeVar
from typing import Generic, TypeVar, Union
class ConstraintType(Enum):
@@ -12,7 +12,7 @@ class ConstraintType(Enum):
Special = 5
@staticmethod
def from_string(text: str) -> "ConstraintType":
def from_string(text: str) -> Union["ConstraintType", None]:
return {
"tag": ConstraintType.Tag,
"tag_id": ConstraintType.TagID,
@@ -24,7 +24,7 @@ class ConstraintType(Enum):
class AST:
parent: "AST" = None
parent: Union["AST", None] = None
def __str__(self):
class_name = self.__class__.__name__

View File

@@ -1,6 +1,14 @@
from .ast import AST, ANDList, Constraint, Not, ORList, Property
from .tokenizer import ConstraintType, Token, Tokenizer, TokenType
from .util import ParsingError
from tagstudio.core.query_lang.ast import (
AST,
ANDList,
Constraint,
ConstraintType,
Not,
ORList,
Property,
)
from tagstudio.core.query_lang.tokenizer import Token, Tokenizer, TokenType
from tagstudio.core.query_lang.util import ParsingError
class Parser:

View File

@@ -1,8 +1,8 @@
from enum import Enum
from typing import Any
from .ast import ConstraintType
from .util import ParsingError
from tagstudio.core.query_lang.ast import ConstraintType
from tagstudio.core.query_lang.util import ParsingError
class TokenType(Enum):
@@ -26,19 +26,19 @@ class Token:
start: int
end: int
def __init__(self, type: TokenType, value: Any, start: int = None, end: int = None) -> None:
def __init__(self, type: TokenType, value: Any, start: int, end: int) -> None:
self.type = type
self.value = value
self.start = start
self.end = end
@staticmethod
def from_type(type: TokenType, pos: int = None) -> "Token":
def from_type(type: TokenType, pos: int) -> "Token":
return Token(type, None, pos, pos)
@staticmethod
def EOF() -> "Token": # noqa: N802
return Token.from_type(TokenType.EOF)
def EOF(pos: int) -> "Token": # noqa: N802
return Token.from_type(TokenType.EOF, pos)
def __str__(self) -> str:
return f"Token({self.type}, {self.value}, {self.start}, {self.end})" # pragma: nocover
@@ -50,7 +50,7 @@ class Token:
class Tokenizer:
text: str
pos: int
current_char: str
current_char: str | None
ESCAPABLE_CHARS = ["\\", '"', '"']
NOT_IN_ULITERAL = [":", " ", "[", "]", "(", ")", "=", ","]
@@ -63,7 +63,7 @@ class Tokenizer:
def get_next_token(self) -> Token:
self.__skip_whitespace()
if self.current_char is None:
return Token.EOF()
return Token.EOF(self.pos)
if self.current_char in ("'", '"'):
return self.__quoted_string()
@@ -93,22 +93,23 @@ class Tokenizer:
start = self.pos
while self.current_char not in self.NOT_IN_ULITERAL and self.current_char is not None:
while self.current_char is not None:
if self.current_char in self.NOT_IN_ULITERAL:
if self.current_char == ":":
if len(out) == 0:
raise ParsingError(self.pos, self.pos)
constraint_type = ConstraintType.from_string(out)
if constraint_type is not None:
self.__advance()
return Token(TokenType.CONSTRAINTTYPE, constraint_type, start, self.pos)
else:
break
out += self.current_char
self.__advance()
end = self.pos - 1
if self.current_char == ":":
if len(out) == 0:
raise ParsingError(self.pos, self.pos)
self.__advance()
constraint_type = ConstraintType.from_string(out)
if constraint_type is None:
raise ParsingError(start, end, f'Invalid ContraintType "{out}"')
return Token(TokenType.CONSTRAINTTYPE, constraint_type, start, end)
else:
return Token(TokenType.ULITERAL, out, start, end)
return Token(TokenType.ULITERAL, out, start, end)
def __quoted_string(self) -> Token:
start = self.pos
@@ -118,6 +119,8 @@ class Tokenizer:
out = ""
while escape or self.current_char != quote:
if self.current_char is None:
raise ParsingError(start, self.pos, "Unterminated quoted string")
if escape:
escape = False
if self.current_char not in Tokenizer.ESCAPABLE_CHARS:

View File

@@ -7,10 +7,11 @@
import json
from pathlib import Path
from src.core.constants import TS_FOLDER_NAME
from src.core.library import Entry, Library
from src.core.library.alchemy.fields import _FieldID
from src.core.utils.missing_files import logger
from tagstudio.core.constants import TS_FOLDER_NAME
from tagstudio.core.library.alchemy.fields import _FieldID
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.missing_files import logger
class TagStudioCore:

View File

@@ -3,8 +3,10 @@ from dataclasses import dataclass, field
from pathlib import Path
import structlog
from src.core.library import Entry, Library
from src.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
logger = structlog.get_logger()
@@ -50,14 +52,15 @@ class DupeRegistry:
continue
results = self.library.search_library(
FilterState.from_path(path_relative),
BrowsingState.from_path(path_relative), 500
)
entries = self.library.get_entries(results.ids)
if not results:
# file not in library
continue
files.append(results[0])
files.append(entries[0])
if not len(files) > 1:
# only one file in the group, nothing to do
@@ -77,7 +80,7 @@ class DupeRegistry:
)
for i, entries in enumerate(self.groups):
remove_ids = [x.id for x in entries[1:]]
remove_ids = entries[1:]
logger.info("Removing entries group", ids=remove_ids)
self.library.remove_entries(remove_ids)
yield i - 1 # The -1 waits for the next step to finish

View File

@@ -3,8 +3,10 @@ from dataclasses import dataclass, field
from pathlib import Path
import structlog
from src.core.library import Entry, Library
from src.core.utils.refresh_dir import GLOBAL_IGNORE_SET
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.refresh_dir import GLOBAL_IGNORE_SET
logger = structlog.get_logger()
@@ -25,7 +27,7 @@ class MissingRegistry:
"""Track the number of entries that point to an invalid filepath."""
logger.info("[refresh_missing_files] Refreshing missing files...")
self.missing_file_entries = []
for i, entry in enumerate(self.library.get_entries()):
for i, entry in enumerate(self.library.all_entries()):
full_path = self.library.library_dir / entry.path
if not full_path.exists() or not full_path.is_file():
self.missing_file_entries.append(entry)

View File

@@ -1,12 +1,14 @@
import datetime as dt
from collections.abc import Iterator
from dataclasses import dataclass, field
from datetime import datetime as dt
from pathlib import Path
from time import time
import structlog
from src.core.constants import TS_FOLDER_NAME
from src.core.library import Entry, Library
from tagstudio.core.constants import TS_FOLDER_NAME
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
logger = structlog.get_logger(__name__)
@@ -42,7 +44,7 @@ class RefreshDirTracker:
path=entry_path,
folder=self.library.folder,
fields=[],
date_added=dt.datetime.now(),
date_added=dt.now(),
)
for entry_path in self.files_not_in_library
]

View File

@@ -3,18 +3,17 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
"""TagStudio launcher."""
import argparse
import logging
import traceback
import structlog
from src.qt.ts_qt import QtDriver
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
def main():
@@ -31,11 +30,18 @@ def main():
help="Path to a TagStudio Library folder to open on start.",
)
parser.add_argument(
"-c",
"--config-file",
dest="config_file",
"-s",
"--settings-file",
dest="settings_file",
type=str,
help="Path to a TagStudio .ini or .plist config file to use.",
help="Path to a TagStudio .toml global settings file to use.",
)
parser.add_argument(
"-c",
"--cache-file",
dest="cache_file",
type=str,
help="Path to a TagStudio .ini or .plist cache file to use.",
)
# parser.add_argument('--browse', dest='browse', action='store_true',
@@ -48,16 +54,9 @@ def main():
action="store_true",
help="Reveals additional internal data useful for debugging.",
)
parser.add_argument(
"--ui",
dest="ui",
type=str,
help="User interface option for TagStudio. Options: qt, cli (Default: qt)",
)
args = parser.parse_args()
from src.core.library import alchemy as backend
driver = QtDriver(backend, args)
driver = QtDriver(args)
ui_name = "Qt"
# Run the chosen frontend driver.
@@ -65,7 +64,7 @@ def main():
driver.start()
except Exception:
traceback.print_exc()
logging.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
logger.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
input()

View File

@@ -9,15 +9,14 @@ from datetime import datetime as dt
from pathlib import Path
import structlog
from PIL import (
Image,
)
from src.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
from src.core.singleton import Singleton
from PIL import Image
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
from tagstudio.core.singleton import Singleton
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.core.library import Library
from tagstudio.core.library import Library
logger = structlog.get_logger(__name__)
@@ -33,7 +32,7 @@ class CacheManager(metaclass=Singleton):
self.last_lib_path: Path | None = None
@staticmethod
def clear_cache(library_dir: Path) -> bool:
def clear_cache(library_dir: Path | None) -> bool:
"""Clear all files and folders within the cached folder.
Returns:
@@ -142,8 +141,7 @@ class CacheManager(metaclass=Singleton):
self.last_lib_path = self.lib.library_dir
CacheManager.folder_dict.clear()
# NOTE: The /dev/null check is a workaround for current test assumptions.
if self.last_lib_path and self.last_lib_path != Path("/dev/null"):
if self.last_lib_path:
# Ensure thumbnail cache path exists.
self.cache_dir().mkdir(exist_ok=True)
# Registers any existing folders and counts the capacity of the most recent one.

View File

@@ -0,0 +1,93 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from typing import TYPE_CHECKING, override
import structlog
from PySide6.QtCore import Signal
from tagstudio.core.enums import TagClickActionOption
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.qt.modals.build_tag import BuildTagPanel
from tagstudio.qt.view.components.tag_box_view import TagBoxWidgetView
from tagstudio.qt.widgets.panel import PanelModal
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
class TagBoxWidget(TagBoxWidgetView):
on_update = Signal()
__entries: list[int] = []
def __init__(self, title: str, driver: "QtDriver"):
super().__init__(title, driver)
self.__driver = driver
def set_entries(self, entries: list[int]) -> None:
self.__entries = entries
@override
def _on_click(self, tag: Tag) -> None: # type: ignore[misc]
match self.__driver.settings.tag_click_action:
case TagClickActionOption.OPEN_EDIT:
self._on_edit(tag)
case TagClickActionOption.SET_SEARCH:
self.__driver.update_browsing_state(BrowsingState.from_tag_id(tag.id))
case TagClickActionOption.ADD_TO_SEARCH:
# NOTE: modifying the ast and then setting that would be nicer
# than this string manipulation, but also much more complex,
# due to needing to implement a visitor that turns an AST to a string
# So if that exists when you read this, change the following accordingly.
current = self.__driver.browsing_history.current
suffix = BrowsingState.from_tag_id(tag.id).query
assert suffix is not None
self.__driver.update_browsing_state(
current.with_search_query(
f"{current.query} {suffix}" if current.query else suffix
)
)
@override
def _on_remove(self, tag: Tag) -> None: # type: ignore[misc]
logger.info(
"[TagBoxWidget] remove_tag",
selected=self.__entries,
)
for entry_id in self.__entries:
self.__driver.lib.remove_tags_from_entries(entry_id, tag.id)
self.on_update.emit()
@override
def _on_edit(self, tag: Tag) -> None: # type: ignore[misc]
build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag)
edit_modal = PanelModal(
build_tag_panel,
self.__driver.lib.tag_display_name(tag.id),
"Edit Tag",
done_callback=self.on_update.emit,
has_save=True,
)
# TODO - this was update_tag()
edit_modal.saved.connect(
lambda: self.__driver.lib.update_tag(
build_tag_panel.build_tag(),
parent_ids=set(build_tag_panel.parent_ids),
alias_names=set(build_tag_panel.alias_names),
alias_ids=set(build_tag_panel.alias_ids),
)
)
edit_modal.show()
@override
def _on_search(self, tag: Tag) -> None: # type: ignore[misc]
self.__driver.main_window.search_field.setText(f"tag_id:{tag.id}")
self.__driver.update_browsing_state(BrowsingState.from_tag_id(tag.id))

View File

@@ -0,0 +1,155 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import io
from pathlib import Path
from typing import TYPE_CHECKING
import cv2
import rawpy
import structlog
from PIL import Image, UnidentifiedImageError
from PIL.Image import DecompressionBombError
from PySide6.QtCore import QSize
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.media_types import MediaCategories
from tagstudio.qt.helpers.file_opener import open_file
from tagstudio.qt.helpers.file_tester import is_readable_video
from tagstudio.qt.view.widgets.preview.preview_thumb_view import PreviewThumbView
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
Image.MAX_IMAGE_PIXELS = None
class PreviewThumb(PreviewThumbView):
__current_file: Path
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__(library, driver)
self.__driver: QtDriver = driver
def __get_image_stats(self, filepath: Path) -> FileAttributeData:
"""Get width and height of an image as dict."""
stats = FileAttributeData()
ext = filepath.suffix.lower()
if MediaCategories.IMAGE_RAW_TYPES.contains(ext, mime_fallback=True):
try:
with rawpy.imread(str(filepath)) as raw:
rgb = raw.postprocess()
image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
stats.width = image.width
stats.height = image.height
except (
rawpy._rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
FileNotFoundError,
):
pass
elif MediaCategories.IMAGE_RASTER_TYPES.contains(ext, mime_fallback=True):
try:
image = Image.open(str(filepath))
stats.width = image.width
stats.height = image.height
except (
DecompressionBombError,
FileNotFoundError,
NotImplementedError,
UnidentifiedImageError,
) as e:
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
elif MediaCategories.IMAGE_VECTOR_TYPES.contains(ext, mime_fallback=True):
pass # TODO
return stats
def __get_gif_data(self, filepath: Path) -> tuple[bytes, tuple[int, int]] | None:
"""Loads an animated image and returns gif data and size, if successful."""
ext = filepath.suffix.lower()
try:
image: Image.Image = Image.open(filepath)
if ext == ".apng":
image_bytes_io = io.BytesIO()
image.save(
image_bytes_io,
"GIF",
lossless=True,
save_all=True,
loop=0,
disposal=2,
)
image.close()
image_bytes_io.seek(0)
return (image_bytes_io.read(), (image.width, image.height))
else:
image.close()
with open(filepath, "rb") as f:
return (f.read(), (image.width, image.height))
except (UnidentifiedImageError, FileNotFoundError) as e:
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
return None
def __get_video_res(self, filepath: str) -> tuple[bool, QSize]:
video = cv2.VideoCapture(filepath, cv2.CAP_FFMPEG)
success, frame = video.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(frame)
return (success, QSize(image.width, image.height))
def display_file(self, filepath: Path) -> FileAttributeData:
"""Render a single file preview."""
self.__current_file = filepath
ext = filepath.suffix.lower()
# Video
if MediaCategories.VIDEO_TYPES.contains(ext, mime_fallback=True) and is_readable_video(
filepath
):
size: QSize | None = None
try:
success, size = self.__get_video_res(str(filepath))
if not success:
size = None
except cv2.error as e:
logger.error("[PreviewThumb] Could not play video", filepath=filepath, error=e)
return self._display_video(filepath, size)
# Audio
elif MediaCategories.AUDIO_TYPES.contains(ext, mime_fallback=True):
return self._display_audio(filepath)
# Animated Images
elif MediaCategories.IMAGE_ANIMATED_TYPES.contains(ext, mime_fallback=True):
if (ret := self.__get_gif_data(filepath)) and (
stats := self._display_gif(ret[0], ret[1])
) is not None:
return stats
else:
self._display_image(filepath)
return self.__get_image_stats(filepath)
# Other Types (Including Images)
else:
self._display_image(filepath)
return self.__get_image_stats(filepath)
def _open_file_action_callback(self):
open_file(self.__current_file)
def _open_explorer_action_callback(self):
open_file(self.__current_file, file_manager=True)
def _delete_action_callback(self):
if bool(self.__current_file):
self.__driver.delete_files_callback(self.__current_file)
def _button_wrapper_callback(self):
open_file(self.__current_file)

View File

@@ -0,0 +1,47 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import typing
from warnings import catch_warnings
from PySide6.QtWidgets import QListWidgetItem
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.modals.add_field import AddFieldModal
from tagstudio.qt.modals.tag_search import TagSearchModal
from tagstudio.qt.view.widgets.preview_panel_view import PreviewPanelView
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
class PreviewPanel(PreviewPanelView):
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__(library, driver)
self.__add_field_modal = AddFieldModal(self.lib)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
def _add_field_button_callback(self):
self.__add_field_modal.show()
def _add_tag_button_callback(self):
self.__add_tag_modal.show()
def _set_selection_callback(self):
with catch_warnings(record=True):
self.__add_field_modal.done.disconnect()
self.__add_tag_modal.tsp.tag_chosen.disconnect()
self.__add_field_modal.done.connect(self._add_field_to_selected)
self.__add_tag_modal.tsp.tag_chosen.connect(self._add_tag_to_selected)
def _add_field_to_selected(self, field_list: list[QListWidgetItem]):
self._fields.add_field_to_selected(field_list)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])
def _add_tag_to_selected(self, tag_id: int):
self._fields.add_tags_to_selected(tag_id)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])

View File

@@ -2,26 +2,31 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x."""
from typing import Literal, override
from PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt
from PySide6.QtWidgets import QLayout, QSizePolicy, QWidget
from PySide6.QtWidgets import QLayout, QLayoutItem, QSizePolicy, QWidget
IGNORE_SIZE = "ignore_size"
class FlowWidget(QWidget):
def __init__(self, parent=None) -> None:
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.ignore_size: bool = False
self.setProperty(IGNORE_SIZE, False) # noqa: FBT003
class FlowLayout(QLayout):
def __init__(self, parent=None):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
if parent is not None:
self.setContentsMargins(QMargins(0, 0, 0, 0))
self._item_list = []
self._item_list: list[QLayoutItem] = []
self.grid_efficiency = False
def __del__(self):
@@ -29,46 +34,56 @@ class FlowLayout(QLayout):
while item:
item = self.takeAt(0)
def addItem(self, item): # noqa: N802
self._item_list.append(item)
@override
def addItem(self, arg__1: QLayoutItem) -> None:
self._item_list.append(arg__1)
def count(self):
@override
def count(self) -> int:
return len(self._item_list)
def itemAt(self, index): # noqa: N802
@override
def itemAt(self, index: int) -> QLayoutItem | None: # pyright: ignore[reportIncompatibleMethodOverride]
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
def takeAt(self, index): # noqa: N802
@override
def takeAt(self, index: int) -> QLayoutItem | None: # pyright: ignore[reportIncompatibleMethodOverride]
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
return None
def expandingDirections(self): # noqa: N802
return Qt.Orientation(0)
@override
def expandingDirections(self) -> Qt.Orientation:
return Qt.Orientation.Horizontal
def hasHeightForWidth(self): # noqa: N802
@override
def hasHeightForWidth(self) -> Literal[True]:
return True
def heightForWidth(self, width): # noqa: N802
height = self._do_layout(QRect(0, 0, width, 0), test_only=True)
return height
@override
def heightForWidth(self, arg__1: int) -> int:
height = self._do_layout(QRect(0, 0, arg__1, 0), test_only=True)
return int(height)
def setGeometry(self, rect): # noqa: N802
super().setGeometry(rect)
self._do_layout(rect, test_only=False)
@override
def setGeometry(self, arg__1: QRect) -> None:
super().setGeometry(arg__1)
self._do_layout(arg__1, test_only=False)
def enable_grid_optimizations(self, value: bool):
def enable_grid_optimizations(self, value: bool) -> None:
"""Enable or Disable efficiencies when all objects are equally sized."""
self.grid_efficiency = value
def sizeHint(self): # noqa: N802
@override
def sizeHint(self) -> QSize:
return self.minimumSize()
def minimumSize(self): # noqa: N802
@override
def minimumSize(self) -> QSize:
if self.grid_efficiency:
if self._item_list:
return self._item_list[0].minimumSize()
@@ -88,8 +103,8 @@ class FlowLayout(QLayout):
y = rect.y()
line_height = 0
spacing = self.spacing()
layout_spacing_x = None
layout_spacing_y = None
layout_spacing_x = 0
layout_spacing_y = 0
if self.grid_efficiency and self._item_list:
item = self._item_list[0]
@@ -107,12 +122,12 @@ class FlowLayout(QLayout):
for item in self._item_list:
skip_count = 0
if issubclass(type(item.widget()), FlowWidget) and item.widget().ignore_size:
ignore_size: bool | None = item.widget().property(IGNORE_SIZE)
if ignore_size:
skip_count += 1
if (issubclass(type(item.widget()), FlowWidget) and not item.widget().ignore_size) or (
not issubclass(type(item.widget()), FlowWidget)
):
else:
if not self.grid_efficiency:
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(

View File

@@ -29,10 +29,7 @@ import os
import struct
from io import BufferedReader
from PIL import (
Image,
ImageOps,
)
from PIL import Image, ImageOps
def blend_extract_thumb(path):

View File

@@ -6,7 +6,8 @@
from PIL import Image
from PySide6.QtCore import Qt
from PySide6.QtGui import QGuiApplication
from src.qt.helpers.gradient import linear_gradient
from tagstudio.qt.helpers.gradient import linear_gradient
# TODO: Consolidate the built-in QT theme values with the values
# here, in enums.py, and in palette.py.
@@ -34,7 +35,7 @@ def theme_fg_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image:
return _apply_overlay(image, im)
def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image:
def gradient_overlay(image: Image.Image, gradient: list[str]) -> Image.Image:
"""Overlay a color gradient onto an image.
Args:

Some files were not shown because too many files have changed in this diff Show More