Compare commits

...

74 Commits

Author SHA1 Message Date
Travis Abendshien
f7f4da8acd refactor: port #455 functionality to main
Co-Authored-By: Tyrannicodin <tyrannicodin@gmail.com>
2025-03-11 23:49:59 -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
315 changed files with 8524 additions and 4176 deletions

View File

@@ -1,16 +1,18 @@
# 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="
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
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
if [ -f .venv/bin/activate ]; then
# If the file is watched when it does not exist,
# direnv will execute again when it gets created.
watch_file .venv/bin/activate
fi
rm "${devenv_root_file}"
unset devenv_root_file
watch_file nix/shell.nix
watch_file pyproject.toml
use flake
# vi: ft=bash

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
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: chartboost/ruff-action@v1
with:
version: 0.6.4
args: 'format --check'
version: 0.8.1
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: chartboost/ruff-action@v1
with:
version: 0.6.4
args: 'check'
version: 0.8.1
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,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

@@ -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,16 +81,16 @@ 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
@@ -156,7 +129,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 +138,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.

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

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

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

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,383 @@
# 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.
## Releases
**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 provides executable [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.
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.
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.
<!-- prettier-ignore -->
!!! info "For macOS Users"
On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application.
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.
!!! 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)
---
### 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 "[Creating a Development Environment](#creating-a-development-environment)"_
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!
---
## Creating a Development Environment
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.
<!-- 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)!
### Install 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.
<!-- 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.
---
### 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]
```
---
#### 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
```
---
#### Installing with Nix
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
```
You can automatically enter this development shell, and keep your user shell, with a tool like [direnv](https://direnv.net/). A reference `.envrc` is provided in the repository; to use it:
```sh
ln -s .envrc.recommended .envrc
```
You will have to allow usage of it.
<!-- prettier-ignore -->
!!! warning "`.envrc` Security"
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
```
---
#### 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]
```
### Launching
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](#launch-arguments) to pass your own test [libraries](./library/index.md) to use while developing.
<!-- prettier-ignore -->
=== "VS Code"
```json title=".vscode/launch.json"
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "TagStudio",
"type": "python",
"request": "launch",
"program": "${workspaceRoot}/src/tagstudio/main.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": ["-o", "~/Documents/Example"]
}
]
}
```
## Building
To build your own executables of TagStudio, first follow the steps in "[Installing with PIP](#installing-with-pip)" including the developer dependencies step. 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".
## 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 also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide.
## Optional Arguments
You can check to see if FFmpeg and FFprobe are correctly located by launching TagStudio and going to "About TagStudio" in the menu bar.
Optional arguments to pass to the program:
## Launch Arguments
`--open <path>` / `-o <path>`
: Path to a TagStudio Library folder to open on start.
There are a handful of launch arguments you can pass to TagStudio via the command line or a desktop shortcut.
`--config-file <path>` / `-c <path>`
: Path to the TagStudio config file to load.
| 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

@@ -1,6 +1,6 @@
# 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
@@ -14,7 +14,7 @@ File entries appear as file previews both inside the thumbnail grid. The preview
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.
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

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 results any results that contain Tag3, plus one or the other (or both) of Tag1 and Tag2.
### Escaping Characters
Sometimes search queries have ambiguous characters and need to be "escaped". This is most common with tag names which contain spaces, or overlap with existing search keywords such as "[path:](#filename-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 with 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

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

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. To open the Tag Color Manager, go to "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,3 @@
th, td {
padding: 0.5em 1em 0.5em 1em !important;
}

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

View File

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

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 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,23 +33,29 @@ 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 from opening the "Tag Manager" panel from Edit -> "Manage Tags". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
## Editing Tags
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.

View File

@@ -43,4 +43,4 @@ Tool is in development, will allow for user-defined sorting of [fields](../libra
### Folders to Tags
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](../library/tag.md#subtags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.
Creates tags from the existing folder structure in the library, which are previewed in a hierarchy view for the user to confirm. A tag will be created for each folder and applied to all entries, with each subfolder being linked to the parent folder as a [parent tag](../library/tag.md#parent-tags). Tags will initially be named after the folders, but can be fully edited and customized afterwards.

502
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": [
@@ -147,354 +20,44 @@
"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": 1741173522,
"narHash": "sha256-k7VSqvv0r1r53nUI/IfPHCppkUAddeXn843YlAC5DR0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
"rev": "d69ab0d71b22fa1ce3dbeff666e6deb4917db049",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-qt6": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"lastModified": 1734856068,
"narHash": "sha256-Q+CB1ajsJg4Z9HGHTBAGY1q18KpnnkmF/eCTLUY6FQ0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"rev": "93ff48c9be84a76319dac293733df09bbbe3f25c",
"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",
"rev": "93ff48c9be84a76319dac293733df09bbbe3f25c",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"devenv-root": "devenv-root",
"flake-parts": "flake-parts",
"nix2container": "nix2container",
"nixpkgs": "nixpkgs_3",
"nixpkgs": "nixpkgs",
"nixpkgs-qt6": "nixpkgs-qt6",
"systems": "systems_4"
"systems": "systems"
}
},
"systems": {
@@ -511,51 +74,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",

224
flake.nix
View File

@@ -2,209 +2,69 @@
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";
nixpkgs-qt6.url = "github:NixOS/nixpkgs/93ff48c9be84a76319dac293733df09bbbe3f25c";
# 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
python = 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 = python.callPackage ./nix/package/pillow-jxl-plugin.nix {
inherit (pkgs) cmake;
inherit pyexiv2;
inherit (pkgs) rustPlatform;
};
pyexiv2 = python.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
vtf2img = python.callPackage ./nix/package/vtf2img.nix { };
in
rec {
default = tagstudio;
tagstudio = pkgs.python312Packages.callPackage ./nix/package {
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

@@ -89,6 +89,7 @@ theme:
#- navigation.top
- search.suggest
- content.code.annotate
- content.code.copy
- content.action.edit
icon:
repo: fontawesome/brands/github
@@ -136,3 +137,6 @@ plugins:
- tags
- social: # social embed cards
enabled: !ENV [CI, false] # enabled only when running in CI (eg GitHub Actions)
extra_css:
- stylesheets/extra.css

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

@@ -0,0 +1,114 @@
{
buildPythonApplication,
chardet,
ffmpeg-headless,
ffmpeg-python,
hatchling,
humanfriendly,
lib,
mutagen,
numpy,
opencv-python,
pillow,
pillow-heif,
pillow-jxl-plugin,
pipewire,
pydub,
pyside6,
pytest-qt,
pytest-xdist,
pytestCheckHook,
pythonRelaxDepsHook,
qt6,
rawpy,
send2trash,
sqlalchemy,
stdenv,
structlog,
syrupy,
ujson,
vtf2img,
withJXLSupport ? false,
}:
let
pyproject = (lib.importTOML ../../pyproject.toml).project;
in
buildPythonApplication {
pname = pyproject.name;
inherit (pyproject) version;
pyproject = true;
src = ../../.;
nativeBuildInputs = [
pythonRelaxDepsHook
qt6.wrapQtAppsHook
];
buildInputs = [
qt6.qtbase
qt6.qtmultimedia
];
nativeCheckInputs = [
pytest-qt
pytest-xdist
pytestCheckHook
syrupy
];
makeWrapperArgs =
[ "--prefix PATH : ${lib.makeBinPath [ ffmpeg-headless ]}" ]
++ lib.optional stdenv.hostPlatform.isLinux "--prefix LD_LIBRARY_PATH : ${
lib.makeLibraryPath [ pipewire ]
}";
pythonRemoveDeps = true;
pythonImportsCheck = [ "tagstudio" ];
build-system = [ hatchling ];
dependencies = [
chardet
ffmpeg-python
humanfriendly
mutagen
numpy
opencv-python
pillow
pillow-heif
pydub
pyside6
rawpy
send2trash
sqlalchemy
structlog
ujson
vtf2img
] ++ lib.optional withJXLSupport pillow-jxl-plugin;
# INFO: These tests require modifications to a library, which does not work
# in a read-only environment.
disabledTests = [
"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"
];
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;
};
}

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;
};
}

93
nix/shell.nix Normal file
View File

@@ -0,0 +1,93 @@
{
inputs,
lib,
pkgs,
...
}:
let
qt6Pkgs = import inputs.nixpkgs-qt6 { 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
ruff
];
buildInputs = [ pythonWrapped ] ++ (with pkgs; [ ffmpeg-headless ]);
env.QT_QPA_PLATFORM = "wayland;xcb";
shellHook =
let
python = lib.getExe pythonWrapped;
in
# bash
''
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
${python} -m venv .venv
fi
source .venv/bin/activate
if [ ! -f .venv/pyproject.toml ] || [ "$(cat .venv/pyproject.toml)" != "$(cat pyproject.toml)" ]; then
printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2
pip install --quiet --editable '.[mkdocs,mypy,pytest]'
cp pyproject.toml .venv/pyproject.toml
fi
'';
meta = {
maintainers = with lib.maintainers; [ xarvex ];
platforms = lib.platforms.unix;
};
}

View File

@@ -1,62 +1,105 @@
[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.1"
license = "GPL-3.0-only"
readme = "README.md"
dependencies = [
"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==10.3.0",
"pillow-heif==0.16.0",
"pillow-jxl-plugin==1.3.0",
"pydub==0.25.1",
"PySide6==6.8.0.1",
"rawpy==0.22.0",
"Send2Trash==1.8.3",
"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",
]
ignore = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
[project.optional-dependencies]
dev = ["pre-commit==3.7.0", "tagstudio[mkdocs,mypy,pyinstaller,pytest,ruff]"]
mkdocs = ["mkdocs-material[imaging]==9.*"]
mypy = ["mypy==1.11.2", "mypy-extensions==1.*", "types-ujson>=5.8.0,<5.9.0"]
pyinstaller = ["Pyinstaller==6.6.0"]
pytest = [
"pytest==8.2.0",
"pytest-cov==5.0.0",
"pytest-qt==4.4.0",
"syrupy==4.7.1",
]
ruff = ["ruff==0.8.1"]
[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/**"]
reportAny = false
reportImplicitStringConcatenation = false
# reportOptionalMemberAccess = false
reportUnannotatedClassAttribute = false
reportUnknownArgumentType = false
reportUnknownMemberType = false
reportUnusedCallResult = false
[tool.ruff]
exclude = ["main_window.py", "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.1" # 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,9 +2,10 @@ 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.library.alchemy.library import LibraryStatus
logger = structlog.get_logger(__name__)

View File

@@ -17,6 +17,7 @@ class SettingItems(str, enum.Enum):
SHOW_FILENAMES = "show_filenames"
AUTOPLAY = "autoplay_videos"
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
LANGUAGE = "language"
class Theme(str, enum.Enum):
@@ -69,6 +70,6 @@ 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"]
PAGE_SIZE = 500
DB_VERSION = 8

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, Constraint, ConstraintType
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
@@ -77,7 +81,7 @@ class FilterState:
# these should be erased on update
# Abstract Syntax Tree Of the current Search Query
ast: Query = None
ast: AST = None
@property
def limit(self):

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,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import re
import shutil
import time
@@ -11,6 +12,7 @@ from dataclasses import dataclass
from datetime import UTC, datetime
from os import makedirs
from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4
from warnings import catch_warnings
@@ -31,6 +33,7 @@ from sqlalchemy import (
func,
or_,
select,
text,
update,
)
from sqlalchemy.exc import IntegrityError
@@ -41,12 +44,11 @@ from sqlalchemy.orm import (
make_transient,
selectinload,
)
from src.core.library.json.library import Library as JsonLibrary # type: ignore
from src.qt.translations import Translations
from ...constants import (
from tagstudio.core.constants import (
BACKUP_FOLDER_NAME,
LEGACY_TAG_FIELD_IDS,
RESERVED_NAMESPACE_PREFIX,
RESERVED_TAG_END,
RESERVED_TAG_START,
TAG_ARCHIVED,
@@ -54,24 +56,65 @@ from ...constants import (
TAG_META,
TS_FOLDER_NAME,
)
from ...enums import LibraryPrefs
from . import default_color_groups
from .db import make_tables
from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum
from .fields import (
from tagstudio.core.enums import LibraryPrefs
from tagstudio.core.library.alchemy import default_color_groups
from tagstudio.core.library.alchemy.db import make_tables
from tagstudio.core.library.alchemy.enums import (
MAX_SQL_VARIABLES,
FieldTypeEnum,
FilterState,
SortingModeEnum,
)
from tagstudio.core.library.alchemy.fields import (
BaseField,
DatetimeField,
TextField,
_FieldID,
)
from .joins import TagEntry, TagParent
from .models import Entry, Folder, Namespace, Preferences, Tag, TagAlias, TagColorGroup, ValueType
from .visitors import SQLBoolExpressionBuilder
from tagstudio.core.library.alchemy.joins import TagEntry, TagParent
from tagstudio.core.library.alchemy.models import (
Entry,
Folder,
Namespace,
Preferences,
Tag,
TagAlias,
TagColorGroup,
ValueType,
)
from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder
from tagstudio.core.library.json.library import Library as JsonLibrary
from tagstudio.qt.translations import Translations
if TYPE_CHECKING:
from sqlalchemy import Select
logger = structlog.get_logger(__name__)
TAG_CHILDREN_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
SELECT tp.parent_id AS child_id
FROM tag_parents tp
INNER JOIN ChildTags c ON tp.child_id = c.child_id
)
SELECT * FROM ChildTags;
""") # noqa: E501
def slugify(input_string: str) -> str:
class ReservedNamespaceError(Exception):
"""Raise during an unauthorized attempt to create or modify a reserved namespace value.
Reserved namespace prefix: "tagstudio".
"""
pass
def slugify(input_string: str, allow_reserved: bool = False) -> str:
# Convert to lowercase and normalize unicode characters
slug = unicodedata.normalize("NFKD", input_string.lower())
@@ -81,6 +124,9 @@ def slugify(input_string: str) -> str:
# Replace spaces with hyphens
slug = re.sub(r"[-\s]+", "-", slug)
if not allow_reserved and slug.startswith(RESERVED_NAMESPACE_PREFIX):
raise ReservedNamespaceError
return slug
@@ -153,6 +199,7 @@ class LibraryStatus:
success: bool
library_path: Path | None = None
message: str | None = None
msg_description: str | None = None
json_migration_req: bool = False
@@ -160,8 +207,8 @@ class Library:
"""Class for the Library object, and all CRUD operations made upon it."""
library_dir: Path | None = None
storage_path: Path | str | None
engine: Engine | None
storage_path: Path | None
engine: Engine | None = None
folder: Folder | None
included_files: set[Path] = set()
@@ -246,7 +293,7 @@ class Library:
for k, v in field.items():
# Old tag fields get added as tags
if k in LEGACY_TAG_FIELD_IDS:
self.add_tags_to_entry(entry_id=entry.id + 1, tag_ids=v)
self.add_tags_to_entries(entry_ids=entry.id + 1, tag_ids=v)
else:
self.add_field_to_entry(
entry_id=(entry.id + 1), # JSON IDs start at 0 instead of 1
@@ -259,7 +306,7 @@ class Library:
self.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, json_lib.is_exclude_list)
end_time = time.time()
logger.info(f"Library Converted! ({format_timespan(end_time-start_time)})")
logger.info(f"Library Converted! ({format_timespan(end_time - start_time)})")
def get_field_name_from_id(self, field_id: int) -> _FieldID:
for f in _FieldID:
@@ -284,7 +331,7 @@ class Library:
else:
return tag.name
def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus:
def open_library(self, library_dir: Path, storage_path: Path | None = None) -> LibraryStatus:
is_new: bool = True
if storage_path == ":memory:":
self.storage_path = storage_path
@@ -292,7 +339,6 @@ class Library:
return self.open_sqlite_library(library_dir, is_new)
else:
self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME
if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()):
json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME
if json_path.exists():
@@ -318,6 +364,7 @@ class Library:
# https://docs.sqlalchemy.org/en/20/changelog/migration_07.html
# Under -> sqlite-the-sqlite-dialect-now-uses-nullpool-for-file-based-databases
poolclass = None if self.storage_path == ":memory:" else NullPool
db_version: int = 0
logger.info(
"[Library] Opening SQLite Library",
@@ -328,31 +375,29 @@ class Library:
with Session(self.engine) as session:
# dont check db version when creating new library
if not is_new:
db_version = session.scalar(
db_result = session.scalar(
select(Preferences).where(Preferences.key == LibraryPrefs.DB_VERSION.name)
)
if db_result:
db_version = db_result.value
if not db_version or db_version.value != LibraryPrefs.DB_VERSION.default:
mismatch_text = Translations.translate_formatted(
"status.library_version_mismatch"
)
found_text = Translations.translate_formatted("status.library_version_found")
expected_text = Translations.translate_formatted(
"status.library_version_expected"
)
# NOTE: DB_VERSION 6 is the first supported SQL DB version.
if db_version < 6 or db_version > LibraryPrefs.DB_VERSION.default:
mismatch_text = Translations["status.library_version_mismatch"]
found_text = Translations["status.library_version_found"]
expected_text = Translations["status.library_version_expected"]
return LibraryStatus(
success=False,
message=(
f"{mismatch_text}\n"
f"{found_text} v{0 if not db_version else db_version.value}, "
f"{found_text} v{db_version}, "
f"{expected_text} v{LibraryPrefs.DB_VERSION.default}"
),
)
logger.info(f"[Library] DB_VERSION: {db_version}")
make_tables(self.engine)
# TODO: Determine a good way of updating built-in data after updates.
# Add default tag color namespaces.
if is_new:
namespaces = default_color_groups.namespaces()
@@ -371,12 +416,13 @@ class Library:
tag_colors += default_color_groups.grayscale()
tag_colors += default_color_groups.earth_tones()
tag_colors += default_color_groups.neon()
try:
session.add_all(tag_colors)
session.commit()
except IntegrityError as e:
logger.error("[Library] Couldn't add default tag colors", error=e)
session.rollback()
if is_new:
try:
session.add_all(tag_colors)
session.commit()
except IntegrityError as e:
logger.error("[Library] Couldn't add default tag colors", error=e)
session.rollback()
# Add default tags.
if is_new:
@@ -421,14 +467,119 @@ class Library:
)
session.add(folder)
session.expunge(folder)
session.commit()
self.folder = folder
# Apply any post-SQL migration patches.
if not is_new:
if db_version < 8:
self.apply_db8_schema_changes(session)
if db_version == 6:
self.apply_repairs_for_db6(session)
if db_version >= 6 and db_version < 8:
self.apply_db8_default_data(session)
# Update DB_VERSION
if LibraryPrefs.DB_VERSION.default > db_version:
self.set_prefs(LibraryPrefs.DB_VERSION, LibraryPrefs.DB_VERSION.default)
# everything is fine, set the library path
self.library_dir = library_dir
return LibraryStatus(success=True, library_path=library_dir)
def apply_repairs_for_db6(self, session: Session):
"""Apply database repairs introduced in DB_VERSION 7."""
logger.info("[Library][Migration] Applying patches to DB_VERSION: 6 library...")
with session:
# Repair "Description" fields with a TEXT_LINE key instead of a TEXT_BOX key.
desc_stmd = (
update(ValueType)
.where(ValueType.key == _FieldID.DESCRIPTION.name)
.values(type=FieldTypeEnum.TEXT_BOX.name)
)
session.execute(desc_stmd)
session.flush()
# Repair tags that may have a disambiguation_id pointing towards a deleted tag.
all_tag_ids: set[int] = {tag.id for tag in self.tags}
disam_stmt = (
update(Tag)
.where(Tag.disambiguation_id.not_in(all_tag_ids))
.values(disambiguation_id=None)
)
session.execute(disam_stmt)
session.flush()
session.commit()
def apply_db8_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 8."""
# TODO: Use Alembic for this part instead
# Add the missing color_border column to the TagColorGroups table.
color_border_stmt = text(
"ALTER TABLE tag_colors ADD COLUMN color_border BOOLEAN DEFAULT FALSE NOT NULL"
)
try:
session.execute(color_border_stmt)
session.commit()
logger.info("[Library][Migration] Added color_border column to tag_colors table")
except Exception as e:
logger.error(
"[Library][Migration] Could not create color_border column in tag_colors table!",
error=e,
)
session.rollback()
def apply_db8_default_data(self, session: Session):
"""Apply default data changes introduced in DB_VERSION 8."""
tag_colors: list[TagColorGroup] = default_color_groups.standard()
tag_colors += default_color_groups.pastels()
tag_colors += default_color_groups.shades()
tag_colors += default_color_groups.grayscale()
tag_colors += default_color_groups.earth_tones()
# tag_colors += default_color_groups.neon() # NOTE: Neon is handled separately
# Add any new default colors introduced in DB_VERSION 8
for color in tag_colors:
try:
session.add(color)
logger.info(
"[Library][Migration] Migrated tag color to DB_VERSION 8+",
color_name=color.name,
)
session.commit()
except IntegrityError:
session.rollback()
# Update Neon colors to use the the color_border property
for color in default_color_groups.neon():
try:
neon_stmt = (
update(TagColorGroup)
.where(
and_(
TagColorGroup.namespace == color.namespace,
TagColorGroup.slug == color.slug,
)
)
.values(
slug=color.slug,
namespace=color.namespace,
name=color.name,
primary=color.primary,
secondary=color.secondary,
color_border=color.color_border,
)
)
session.execute(neon_stmt)
session.commit()
except IntegrityError as e:
logger.error(
"[Library] Could not migrate Neon colors to DB_VERSION 8+!",
error=e,
)
session.rollback()
@property
def default_fields(self) -> list[BaseField]:
with Session(self.engine) as session:
@@ -460,30 +611,49 @@ class Library:
self, entry_id: int, with_fields: bool = True, with_tags: bool = True
) -> Entry | None:
"""Load entry and join with all joins and all tags."""
# NOTE: TODO: Currently this method makes multiple separate queries to the db and combines
# those into a final Entry object (if using "with" args). This was done due to it being
# much more efficient than the existing join query, however there likely exists a single
# query that can accomplish the same task without exhibiting the same slowdown.
with Session(self.engine) as session:
statement = select(Entry).where(Entry.id == entry_id)
tags: set[Tag] | None = None
tag_stmt: Select[tuple[Tag]]
entry_stmt = select(Entry).where(Entry.id == entry_id).limit(1)
if with_fields:
statement = (
statement.outerjoin(Entry.text_fields)
entry_stmt = (
entry_stmt.outerjoin(Entry.text_fields)
.outerjoin(Entry.datetime_fields)
.options(selectinload(Entry.text_fields), selectinload(Entry.datetime_fields))
)
# if with_tags:
# entry_stmt = entry_stmt.outerjoin(Entry.tags).options(selectinload(Entry.tags))
if with_tags:
statement = (
statement.outerjoin(Entry.tags)
.outerjoin(TagAlias)
.options(
selectinload(Entry.tags).options(
joinedload(Tag.aliases),
joinedload(Tag.parent_tags),
)
tag_stmt = select(Tag).where(
and_(
TagEntry.tag_id == Tag.id,
TagEntry.entry_id == entry_id,
)
)
entry = session.scalar(statement)
start_time = time.time()
entry = session.scalar(entry_stmt)
if with_tags:
tags = set(session.scalars(tag_stmt)) # pyright: ignore [reportPossiblyUnboundVariable]
end_time = time.time()
logger.info(
f"[Library] Time it took to get entry: "
f"{format_timespan(end_time - start_time, max_units=5)}",
with_fields=with_fields,
with_tags=with_tags,
)
if not entry:
return None
session.expunge(entry)
make_transient(entry)
# Recombine the separately queried tags with the base entry object.
if with_tags and tags:
entry.tags = tags
return entry
def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]:
@@ -639,12 +809,15 @@ class Library:
with Session(self.engine) as session:
return session.query(exists().where(Entry.path == path)).scalar()
def get_paths(self, glob: str | None = None) -> list[str]:
def get_paths(self, glob: str | None = None, limit: int = -1) -> list[str]:
path_strings: list[str] = []
with Session(self.engine) as session:
paths = session.scalars(select(Entry.path)).unique()
path_strings: list[str] = list(map(lambda x: x.as_posix(), paths))
return path_strings
if limit > 0:
paths = session.scalars(select(Entry.path).limit(limit)).unique()
else:
paths = session.scalars(select(Entry.path)).unique()
path_strings = list(map(lambda x: x.as_posix(), paths))
return path_strings
def search_library(
self,
@@ -681,7 +854,7 @@ class Library:
query_count = select(func.count()).select_from(statement.alias("entries"))
count_all: int = session.execute(query_count).scalar()
end_time = time.time()
logger.info(f"finished counting ({format_timespan(end_time-start_time)})")
logger.info(f"finished counting ({format_timespan(end_time - start_time)})")
sort_on: ColumnExpressionArgument = Entry.id
match search.sorting_mode:
@@ -711,19 +884,16 @@ class Library:
return res
def search_tags(
self,
name: str | None,
) -> list[Tag]:
def search_tags(self, name: str | None, limit: int = 100) -> list[set[Tag]]:
"""Return a list of Tag records matching the query."""
tag_limit = 100
with Session(self.engine) as session:
query = select(Tag).outerjoin(TagAlias)
query = select(Tag).outerjoin(TagAlias).order_by(func.lower(Tag.name))
query = query.options(
selectinload(Tag.parent_tags),
selectinload(Tag.aliases),
).limit(tag_limit)
)
if limit > 0:
query = query.limit(limit)
if name:
query = query.where(
@@ -734,12 +904,28 @@ class Library:
)
)
tags = session.scalars(query)
res = list(set(tags))
direct_tags = set(session.scalars(query))
ancestor_tag_ids: list[Tag] = []
for tag in direct_tags:
ancestor_tag_ids.extend(
list(session.scalars(TAG_CHILDREN_QUERY, {"tag_id": tag.id}))
)
ancestor_tags = session.scalars(
select(Tag)
.where(Tag.id.in_(ancestor_tag_ids))
.options(selectinload(Tag.parent_tags), selectinload(Tag.aliases))
)
res = [
direct_tags,
{at for at in ancestor_tags if at not in direct_tags},
]
logger.info(
"searching tags",
search=name,
limit=limit,
statement=str(query),
results=len(res),
)
@@ -792,6 +978,14 @@ class Library:
session.delete(child_tag)
session.expunge(child_tag)
disam_stmt = (
update(Tag)
.where(Tag.disambiguation_id == tag.id)
.values(disambiguation_id=None)
)
session.execute(disam_stmt)
session.flush()
session.delete(tag)
session.commit()
session.expunge(tag)
@@ -980,13 +1174,86 @@ class Library:
if tag:
tags.append(tag.id)
else:
new = session.add(Tag(name=string))
new = session.add(Tag(name=string)) # type: ignore
if new:
tags.append(new.id)
session.flush()
session.commit()
return tags
def add_namespace(self, namespace: Namespace) -> bool:
"""Add a namespace value to the library.
Args:
namespace(str): The namespace slug. No special characters
"""
with Session(self.engine) as session:
if not namespace.namespace:
logger.warning("[LIBRARY][add_namespace] Namespace slug must not be empty")
return False
slug = namespace.namespace
try:
slug = slugify(namespace.namespace)
except ReservedNamespaceError:
logger.error(
f"[LIBRARY][add_namespace] Will not add a namespace with the reserved prefix:"
f"{RESERVED_NAMESPACE_PREFIX}",
namespace=namespace,
)
namespace_obj = Namespace(
namespace=slug,
name=namespace.name,
)
try:
session.add(namespace_obj)
session.commit()
return True
except IntegrityError:
session.rollback()
logger.error("IntegrityError")
return False
def delete_namespace(self, namespace: Namespace | str):
"""Delete a namespace and any connected data from the library."""
if isinstance(namespace, str):
if namespace.startswith(RESERVED_NAMESPACE_PREFIX):
raise ReservedNamespaceError
else:
if namespace.namespace.startswith(RESERVED_NAMESPACE_PREFIX):
raise ReservedNamespaceError
with Session(self.engine, expire_on_commit=False) as session:
try:
namespace_: Namespace | None = None
if isinstance(namespace, str):
namespace_ = session.scalar(
select(Namespace).where(Namespace.namespace == namespace)
)
else:
namespace_ = namespace
if not namespace_:
raise Exception
session.delete(namespace_)
session.flush()
colors = session.scalars(
select(TagColorGroup).where(TagColorGroup.namespace == namespace_.namespace)
)
for color in colors:
session.delete(color)
session.flush()
session.commit()
except IntegrityError as e:
logger.error(e)
session.rollback()
return None
def add_tag(
self,
tag: Tag,
@@ -1014,41 +1281,49 @@ class Library:
session.rollback()
return None
def add_tags_to_entry(self, entry_id: int, tag_ids: int | list[int] | set[int]) -> bool:
"""Add one or more tags to an entry."""
tag_ids = [tag_ids] if isinstance(tag_ids, int) else tag_ids
def add_tags_to_entries(
self, entry_ids: int | list[int], tag_ids: int | list[int] | set[int]
) -> bool:
"""Add one or more tags to one or more entries."""
entry_ids_ = [entry_ids] if isinstance(entry_ids, int) else entry_ids
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
with Session(self.engine, expire_on_commit=False) as session:
for tag_id in tag_ids:
try:
session.add(TagEntry(tag_id=tag_id, entry_id=entry_id))
session.flush()
except IntegrityError:
session.rollback()
for tag_id in tag_ids_:
for entry_id in entry_ids_:
try:
session.add(TagEntry(tag_id=tag_id, entry_id=entry_id))
session.flush()
except IntegrityError:
session.rollback()
try:
session.commit()
except IntegrityError as e:
logger.warning("[add_tags_to_entry]", warning=e)
logger.warning("[Library][add_tags_to_entries]", warning=e)
session.rollback()
return False
return True
def remove_tags_from_entry(self, entry_id: int, tag_ids: int | list[int] | set[int]) -> bool:
"""Remove one or more tags from an entry."""
def remove_tags_from_entries(
self, entry_ids: int | list[int], tag_ids: int | list[int] | set[int]
) -> bool:
"""Remove one or more tags from one or more entries."""
entry_ids_ = [entry_ids] if isinstance(entry_ids, int) else entry_ids
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
with Session(self.engine, expire_on_commit=False) as session:
try:
for tag_id in tag_ids_:
tag_entry = session.scalars(
select(TagEntry).where(
and_(
TagEntry.tag_id == tag_id,
TagEntry.entry_id == entry_id,
for entry_id in entry_ids_:
tag_entry = session.scalars(
select(TagEntry).where(
and_(
TagEntry.tag_id == tag_id,
TagEntry.entry_id == entry_id,
)
)
)
).first()
if tag_entry:
session.delete(tag_entry)
session.commit()
).first()
if tag_entry:
session.delete(tag_entry)
session.flush()
session.commit()
return True
except IntegrityError as e:
@@ -1056,11 +1331,38 @@ class Library:
session.rollback()
return False
def add_color(self, color_group: TagColorGroup) -> TagColorGroup | None:
with Session(self.engine, expire_on_commit=False) as session:
try:
session.add(color_group)
session.commit()
session.expunge(color_group)
return color_group
except IntegrityError as e:
logger.error(
"[Library] Could not add color, trying to update existing value instead.",
error=e,
)
session.rollback()
return None
def delete_color(self, color: TagColorGroup):
with Session(self.engine, expire_on_commit=False) as session:
try:
session.delete(color)
session.commit()
except IntegrityError as e:
logger.error(e)
session.rollback()
return None
def save_library_backup_to_disk(self) -> Path:
assert isinstance(self.library_dir, Path)
makedirs(str(self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
filename = f'ts_library_backup_{datetime.now(UTC).strftime("%Y_%m_%d_%H%M%S")}.sqlite'
filename = f"ts_library_backup_{datetime.now(UTC).strftime('%Y_%m_%d_%H%M%S')}.sqlite"
target_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
@@ -1171,6 +1473,55 @@ class Library:
"""Edit a Tag in the Library."""
self.add_tag(tag, parent_ids, alias_names, alias_ids)
def update_color(self, old_color_group: TagColorGroup, new_color_group: TagColorGroup) -> None:
"""Update a TagColorGroup in the Library. If it doesn't already exist, create it."""
with Session(self.engine) as session:
existing_color = session.scalar(
select(TagColorGroup).where(
and_(
TagColorGroup.namespace == old_color_group.namespace,
TagColorGroup.slug == old_color_group.slug,
)
)
)
if existing_color:
update_color_stmt = (
update(TagColorGroup)
.where(
and_(
TagColorGroup.namespace == old_color_group.namespace,
TagColorGroup.slug == old_color_group.slug,
)
)
.values(
slug=new_color_group.slug,
namespace=new_color_group.namespace,
name=new_color_group.name,
primary=new_color_group.primary,
secondary=new_color_group.secondary,
color_border=new_color_group.color_border,
)
)
session.execute(update_color_stmt)
session.flush()
update_tags_stmt = (
update(Tag)
.where(
and_(
Tag.color_namespace == old_color_group.namespace,
Tag.color_slug == old_color_group.slug,
)
)
.values(
color_namespace=new_color_group.namespace,
color_slug=new_color_group.slug,
)
)
session.execute(update_tags_stmt)
session.commit()
else:
self.add_color(new_color_group)
def update_aliases(self, tag, alias_ids, alias_names, session):
prev_aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)).all()
@@ -1256,7 +1607,7 @@ class Library:
value=field.value,
)
tag_ids = [tag.id for tag in from_entry.tags]
self.add_tags_to_entry(into_entry.id, tag_ids)
self.add_tags_to_entries(into_entry.id, tag_ids)
self.remove_entries([from_entry.id])
@property
@@ -1270,7 +1621,28 @@ class Library:
color_groups[color.namespace] = []
color_groups[color.namespace].append(color)
session.expunge(color)
return color_groups
# Add empty namespaces that are available for use.
empty_namespaces = session.scalars(
select(Namespace)
.where(Namespace.namespace.not_in(color_groups.keys()))
.order_by(asc(Namespace.namespace))
)
for en in empty_namespaces:
if not color_groups.get(en.namespace):
color_groups[en.namespace] = []
session.expunge(en)
return dict(
sorted(color_groups.items(), key=lambda kv: self.get_namespace_name(kv[0]).lower())
)
@property
def namespaces(self) -> list[Namespace]:
"""Return every Namespace in the library."""
with Session(self.engine) as session:
namespaces = session.scalars(select(Namespace).order_by(asc(Namespace.name)))
return list(namespaces)
def get_namespace_name(self, namespace: str) -> str:
with Session(self.engine) as session:

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__()
@@ -185,9 +188,9 @@ class Entry(Base):
path: Mapped[Path] = mapped_column(PathType, unique=True)
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,9 +225,9 @@ 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
@@ -301,7 +304,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,28 +2,37 @@
# 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
@@ -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:
@@ -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

@@ -16,35 +16,35 @@ FILETYPE_EQUIVALENTS = [set(["jpg", "jpeg"])]
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"
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"
MATERIAL = "material"
MODEL = "model"
OPEN_DOCUMENT = "open_document"
PACKAGE = "package"
PDF = "pdf"
PLAINTEXT = "plaintext"
PRESENTATION = "presentation"
PROGRAM = "program"
SHORTCUT = "shortcut"
SOURCE_ENGINE = "source_engine"
SPREADSHEET = "spreadsheet"
TEXT = "text"
VIDEO = "video"
@dataclass(frozen=True)
@@ -337,188 +337,188 @@ 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(
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(
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,
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(
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",
)
ALL_CATEGORIES: list[MediaCategory] = [
ALL_CATEGORIES = [
ADOBE_PHOTOSHOP_TYPES,
AFFINITY_PHOTO_TYPES,
ARCHIVE_TYPES,

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,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):
@@ -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

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 FilterState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
logger = structlog.get_logger()

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

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,6 +3,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
"""TagStudio launcher."""
import argparse
@@ -10,7 +11,8 @@ import logging
import traceback
import structlog
from src.qt.ts_qt import QtDriver
from tagstudio.qt.ts_qt import QtDriver
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
@@ -55,9 +57,8 @@ def main():
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.

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

View File

@@ -2,6 +2,7 @@
# 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 PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt

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.

View File

@@ -0,0 +1,8 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
def escape_text(text: str):
"""Escapes characters that are problematic in Qt widgets."""
return text.replace("&", "&&")

View File

@@ -0,0 +1,31 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import logging
from pathlib import Path
from send2trash import send2trash
logging.basicConfig(format="%(message)s", level=logging.INFO)
def delete_file(path: str | Path) -> bool:
"""Send a file to the system trash.
Args:
path (str | Path): The path of the file to delete.
"""
_path = Path(path)
try:
logging.info(f"[delete_file] Sending to Trash: {_path}")
send2trash(_path)
return True
except PermissionError as e:
logging.error(f"[delete_file][ERROR] PermissionError: {e}")
except FileNotFoundError:
logging.error(f"[delete_file][ERROR] File Not Found: {_path}")
except Exception as e:
logging.error(e)
return False

View File

@@ -11,7 +11,8 @@ from pathlib import Path
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel
from src.qt.helpers.silent_popen import silent_Popen
from tagstudio.qt.helpers.silent_popen import silent_Popen
logger = structlog.get_logger(__name__)

View File

@@ -6,7 +6,8 @@
from pathlib import Path
import ffmpeg
from src.qt.helpers.vendored.ffmpeg import _probe
from tagstudio.qt.helpers.vendored.ffmpeg import _probe
def is_readable_video(filepath: Path | str):

View File

@@ -1,6 +1,8 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from collections.abc import Callable
from PySide6.QtCore import QObject, Signal

View File

@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PIL import Image

View File

@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import numpy as np
from PIL import Image

View File

@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtWidgets import QPushButton

View File

@@ -4,10 +4,9 @@
# https://creativecommons.org/licenses/by-sa/4.0/
# Modified for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtGui import QBrush, QColor, QPainter, QPixmap
from PySide6.QtWidgets import (
QProxyStyle,
)
from PySide6.QtWidgets import QProxyStyle
class RoundedPixmapStyle(QProxyStyle):

View File

@@ -1,6 +1,8 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import os
import subprocess
import sys

View File

@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PIL import Image, ImageDraw, ImageFont

View File

@@ -9,7 +9,8 @@ import subprocess
import ffmpeg
import structlog
from src.qt.helpers.silent_popen import silent_Popen
from tagstudio.qt.helpers.silent_popen import silent_Popen
logger = structlog.get_logger(__name__)

View File

@@ -17,7 +17,8 @@ from tempfile import NamedTemporaryFile
from pydub.logging_utils import log_conversion, log_subprocess_output
from pydub.utils import fsdecode
from src.qt.helpers.vendored.ffmpeg import FFMPEG_CMD
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD
try:
from itertools import izip
@@ -40,8 +41,9 @@ from pydub.utils import (
get_array_type,
ratio_to_db,
)
from src.qt.helpers.silent_popen import silent_Popen
from src.qt.helpers.vendored.pydub.utils import _mediainfo_json
from tagstudio.qt.helpers.silent_popen import silent_Popen
from tagstudio.qt.helpers.vendored.pydub.utils import _mediainfo_json
basestring = str
xrange = range
@@ -361,11 +363,11 @@ class _AudioSegment:
"""Permit use of sum() builtin with an iterable of AudioSegments."""
if rarg == 0:
return self
raise TypeError("Gains must be the second addend after the " "AudioSegment")
raise TypeError("Gains must be the second addend after the AudioSegment")
def __sub__(self, arg):
if isinstance(arg, _AudioSegment):
raise TypeError("AudioSegment objects can't be subtracted from " "each other")
raise TypeError("AudioSegment objects can't be subtracted from each other")
else:
return self.apply_gain(-arg)
@@ -1345,8 +1347,7 @@ class _AudioSegment:
"""
if None not in [duration, end, start]:
raise TypeError(
'Only two of the three arguments, "start", '
'"end", and "duration" may be specified'
'Only two of the three arguments, "start", "end", and "duration" may be specified'
)
# no fade == the same audio

View File

@@ -7,8 +7,9 @@ from pydub.utils import (
fsdecode,
get_extra_info,
)
from src.qt.helpers.silent_popen import silent_Popen
from src.qt.helpers.vendored.ffmpeg import FFPROBE_CMD
from tagstudio.qt.helpers.silent_popen import silent_Popen
from tagstudio.qt.helpers.vendored.ffmpeg import FFPROBE_CMD
def _mediainfo_json(filepath, read_ahead_limit=-1):

View File

@@ -5,21 +5,34 @@
import logging
import typing
from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt, QStringListModel)
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout,
QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow,
QPushButton, QScrollArea, QSizePolicy,
QStatusBar, QWidget, QSplitter, QCheckBox,
QSpacerItem, QCompleter)
from src.qt.pagination import Pagination
from src.qt.widgets.landing import LandingWidget
from src.qt.translations import Translations
from PySide6.QtCore import QMetaObject, QRect, QSize, QStringListModel, Qt
from PySide6.QtWidgets import (
QComboBox,
QCompleter,
QFrame,
QGridLayout,
QHBoxLayout,
QLayout,
QLineEdit,
QMainWindow,
QPushButton,
QScrollArea,
QSizePolicy,
QSpacerItem,
QSplitter,
QStatusBar,
QVBoxLayout,
QWidget,
)
from tagstudio.qt.pagination import Pagination
from tagstudio.qt.translations import Translations
from tagstudio.qt.widgets.landing import LandingWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
from tagstudio.qt.ts_qt import QtDriver
logging.basicConfig(format="%(message)s", level=logging.INFO)
@@ -74,7 +87,7 @@ class Ui_MainWindow(QMainWindow):
# Thumbnail Size placeholder
self.thumb_size_combobox = QComboBox(self.centralwidget)
self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox")
Translations.translate_with_setter(self.thumb_size_combobox.setPlaceholderText, "home.thumbnail_size")
self.thumb_size_combobox.setPlaceholderText(Translations["home.thumbnail_size"])
self.thumb_size_combobox.setCurrentText("")
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -142,7 +155,7 @@ class Ui_MainWindow(QMainWindow):
self.horizontalLayout_2.addWidget(self.forwardButton)
self.searchField = QLineEdit(self.centralwidget)
Translations.translate_with_setter(self.searchField.setPlaceholderText, "home.search_entries")
self.searchField.setPlaceholderText(Translations["home.search_entries"])
self.searchField.setObjectName(u"searchField")
self.searchField.setMinimumSize(QSize(0, 32))
@@ -152,8 +165,7 @@ class Ui_MainWindow(QMainWindow):
self.searchField.setCompleter(self.searchFieldCompleter)
self.horizontalLayout_2.addWidget(self.searchField)
self.searchButton = QPushButton(self.centralwidget)
Translations.translate_qobject(self.searchButton, "home.search")
self.searchButton = QPushButton(Translations["home.search"], self.centralwidget)
self.searchButton.setObjectName(u"searchButton")
self.searchButton.setMinimumSize(QSize(0, 32))
@@ -193,4 +205,4 @@ class Ui_MainWindow(QMainWindow):
self.landing_widget.setHidden(True)
self.landing_widget.set_status_label("")
self.scrollArea.setHidden(False)

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