Compare commits

...

78 Commits

Author SHA1 Message Date
Travis Abendshien
7c5b8e51e6 chore: bump version to v9.5.2 2025-03-31 15:39:46 -07:00
Weblate (bot)
efb4c4a6ed translations: update Hungarian, French, Toki Pona (#891)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (312 of 312 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (309 of 309 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (312 of 312 strings)

Translated using Weblate (French)

Currently translated at 100.0% (309 of 309 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 80.9% (250 of 309 strings)

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

---------

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

* type annotations and parameter name changes

* Update src/tagstudio/qt/flowlayout.py

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

* Update src/tagstudio/qt/flowlayout.py

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

* ruff format

---------

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

* text_wrapper.py type fix and minor improvements

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

* feat: add iwork and powerpoint thumb support

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

* fix: remove decompression bomb check and catch others

* feat: add .aiff file equivalencies

* ui: update database icon

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

* fix: correct malformed or missing media types

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

* fix: catch BadZipFile error for iWork thumbs

* chore: add type hints to thumb_renderer dicts

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

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

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

* fix: center widgets in preview area

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

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

* fix: resolve commit suggestions.

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

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

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

* fix: start media muted

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

Remove code causing mypy issue.

Add new method for getting slider click state.

* refactor: use layouts instead of manual positioning.

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

Change the volume slider orientation at smaller
media sizes.

* fix: color position label white

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

* fix: allow dragging slider after click

* Apply suggestions from code review

fix: apply suggestions from code review.

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

* fix: remove references to legacy video player.

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

Fix several issues after incorrectly implementing suggestions.

* fix: add loop setting and other actions.

* refactor: simplify widget state management.

Make a single method to control widget state.

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

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

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

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

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

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

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

* build: be more selective about the data files.

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

---------

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

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

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

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

* refactor: move languages dict to translations.py

* refactor: move build of Settings Modal to SettingsPanel class

* feat: hide title label

* feat: global settings class

* fix: initialise settings

* fix: properly store grid files changes

* fix: placeholder text for library settings

* feat: add ui elements for remaining global settings

* feat: add page size setting

* fix: version mismatch between pydantic and typing_extensions

* fix: update test_driver.py

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

* feat: setting for dark mode

* fix: only show restart_label when necessary

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

* feat: add test for GlobalSettings

* docs: mark roadmap item as completed

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

* Update src/tagstudio/main.py

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

* fix: address review suggestions

* fix: page size setting

* feat: change dark mode option to theme dropdown

* fix: test was expecting wrong behaviour

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

* fix: test fr fr

* fix: tests fr fr fr

* fix: tests fr fr fr fr

* fix: update test

* fix: tests fr fr fr fr fr

* fix: select all was selecting hidden entries

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

Currently translated at 100.0% (302 of 302 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (302 of 302 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (302 of 302 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 75.8% (229 of 302 strings)

Translated using Weblate (Toki Pona)

Currently translated at 75.9% (227 of 299 strings)

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

---------

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

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

* refactor: rework ffmpeg_checker.py

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

* fix: stop ffmpeg cmd windows, fix version outputs

* chore: ensure stdout is cast to str

---------

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

Currently translated at 89.9% (269 of 299 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 52.8% (158 of 299 strings)

Added translation using Weblate (Japanese)

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

* Translated using Weblate (Czech)

Currently translated at 15.7% (47 of 299 strings)

Translated using Weblate (Czech)

Currently translated at 14.3% (43 of 299 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (299 of 299 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (299 of 299 strings)

Translated using Weblate (Spanish)

Currently translated at 96.9% (290 of 299 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (299 of 299 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 67.8% (203 of 299 strings)

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

* Translated using Weblate (Viossa)

Currently translated at 21.0% (63 of 299 strings)

Translated using Weblate (Viossa)

Currently translated at 21.0% (63 of 299 strings)

Added translation using Weblate (Viossa)

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

---------

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

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

* fix(ts_qt): remove duplicate logic

* fix(ts_qt): add fallback values

---------

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

* feat(contrib): define developer configurations

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

* fix(docs): typo

* docs: split develop and install, document integrations

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

* docs: move third-party dependencies to install page

* nit(flake): use pythonPackages variable

---------

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

* feat: add file path sorting to dropdown

* feat: implement path sorting

* feat: add filename column and bump db version

* feat: implement filename sorting

* doc: tick off roadmap item for filename sorting

* fix: use existing filename translation instead

* fix: populate Entry.filename in constructor

* fix: add missing assertion in search_library fixture

* fix: update search test library

* feat: add db migration test

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

Currently translated at 100.0% (294 of 294 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 89.1% (262 of 294 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (294 of 294 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (294 of 294 strings)

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

---------

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

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

* feat: implement file path option for file attributes

---------

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

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

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

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

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

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

style: formatted code changes with ruff  (#16)

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

enhancement: formated using RUFF

test: addeded test to check title bar update

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

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

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

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

* refactor: update import paths to reflect recent reformat

* format: run ruff format

* translation: add translations for filepath setting

* refactor: store filepath options in integer enum

---------

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

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

* refactor: split translation keys for about screen

* ui: change form field style

* fix: split new translation keys

* fix: remove unused key

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

Currently translated at 100.0% (289 of 289 strings)

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

* Translated using Weblate (Danish)

Currently translated at 4.4% (13 of 289 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (289 of 289 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 63.3% (183 of 289 strings)

Translated using Weblate (Toki Pona)

Currently translated at 57.4% (166 of 289 strings)

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

---------

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

Currently translated at 100.0% (289 of 289 strings)

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

* Translated using Weblate (Filipino)

Currently translated at 59.5% (172 of 289 strings)

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

* Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (289 of 289 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.5% (256 of 289 strings)

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

* Translated using Weblate (German)

Currently translated at 99.3% (287 of 289 strings)

Translated using Weblate (German)

Currently translated at 98.6% (285 of 289 strings)

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

* Translated using Weblate (Russian)

Currently translated at 90.3% (261 of 289 strings)

Translated using Weblate (Russian)

Currently translated at 82.3% (238 of 289 strings)

Translated using Weblate (Russian)

Currently translated at 79.2% (229 of 289 strings)

Translated using Weblate (Russian)

Currently translated at 78.8% (228 of 289 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 95.8% (277 of 289 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 49.4% (143 of 289 strings)

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

---------

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

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

chore(ci): remove legacy scripts

chore: format with new mypy rules; fix translation test

wip(ci/mypy): remove config flag

fix(pyinstaller): use correct dict access

fix(resources): usage in ts_qt.py

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

fix(nix/package): remove old dependency patch

feat(nix): support Darwin

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

fix(nix/shell): typo

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

fix(pyproject): specify mypy-extensions

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

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

fix(nix/shell): provide FFmpeg on runtime

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

fix(nix/package): load pipewire

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

fix: remove extra noqa comment

docs: update installation docs

docs: shrink table size on docs site

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

docs: update commands, environment, setup

fix: use consistent possessives

chore: format with prettier, add ignore flags

fix(pyinstaller): consume from pyproject

Revert "fix(pyinstaller): consume from pyproject"

This reverts commit 398cd4e5630a3e83d22d15286d7ac59b4c07c5d6.

refactor: use icon from resource manager

Also fixes incorrect path currently used in ts_qt.py.

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

docs: add build section

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

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

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

* feat: parametrise format key test

* feat: parametrise completeness test

* fix: include more useful information in the error message

* refactor: remove unused method

* refactor: extract translation loading to a separate function

* fix: include more useful information in the error message

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

Currently translated at 24.9% (72 of 289 strings)

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

* Translated using Weblate (Filipino)

Currently translated at 44.6% (129 of 289 strings)

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

* Translated using Weblate (Tamil)

Currently translated at 24.9% (72 of 289 strings)

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

* Translated using Weblate (Danish)

Currently translated at 3.8% (11 of 289 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 96.1% (278 of 289 strings)

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

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 17.9% (52 of 289 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 36.3% (105 of 289 strings)

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

---------

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

Currently translated at 22.4% (65 of 289 strings)

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

* Translated using Weblate (Filipino)

Currently translated at 43.9% (127 of 289 strings)

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

* Translated using Weblate (Tamil)

Currently translated at 22.4% (65 of 289 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.9% (257 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 88.9% (257 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.7% (187 of 289 strings)

Co-authored-by: Helder Lima <vinicius-helder@hotmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/
Translation: TagStudio/Strings

* Translated using Weblate (German)

Currently translated at 98.9% (286 of 289 strings)

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

* Translated using Weblate (Danish)

Currently translated at 3.8% (11 of 289 strings)

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

* Translated using Weblate (Russian)

Currently translated at 73.3% (212 of 289 strings)

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

* Translated using Weblate (Polish)

Currently translated at 97.5% (282 of 289 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 95.8% (277 of 289 strings)

Translated using Weblate (Spanish)

Currently translated at 95.8% (277 of 289 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Aless <dev@aless.ch>
Co-authored-by: Bamowen <mathieu.monsauret@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Swedish)

Currently translated at 25.2% (73 of 289 strings)

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

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

Currently translated at 68.8% (199 of 289 strings)

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

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 16.9% (49 of 289 strings)

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

* Translated using Weblate (Toki Pona)

Currently translated at 33.2% (96 of 289 strings)

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

---------

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

* fix: set "Create & Add" button text

* refactor: rename "translate_formatted" to "format"

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

Currently translated at 43.9% (127 of 289 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.7% (187 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 63.3% (183 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.4% (143 of 289 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.4% (143 of 289 strings)

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

* Translated using Weblate (German)

Currently translated at 98.9% (286 of 289 strings)

Translated using Weblate (German)

Currently translated at 98.9% (286 of 289 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (289 of 289 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (Polish)

Currently translated at 97.5% (282 of 289 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 57.7% (167 of 289 strings)

Translated using Weblate (Spanish)

Currently translated at 55.0% (159 of 289 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

Translated using Weblate (French)

Currently translated at 100.0% (289 of 289 strings)

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

---------

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

* feat: add further translated widget types

* refactor: replace usages of translate_qobject with appropriate classes

* refactor: remove wrapper classes

* refactor: remove unnecessary used of Translations.formatted

* refactor: remove further unnecessary format calls

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

* refactor: remove TranslatedString class

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

* tests: add sqlite db migration tests

* tests: fix and refactor migration tests

* fix: apply db8 schema changes before repairing db6

* docs: add save file format change log

* chore: remove db version explanations from docstrings
2025-02-24 15:14:54 -08:00
Travis Abendshien
f9ca743b64 docs: add custom color info 2025-02-20 15:14:51 -08:00
Travis Abendshien
04bd1bc027 fix(docs): restore broken links, ignore callout formatting 2025-02-20 14:24:12 -08:00
Haider Ali
ecc0bb57fb docs: change "Tag Manager" to "Manage Tags" (#806) 2025-02-17 20:42:48 -08:00
Travis Abendshien
d11b514bab fix: use remove() instead of difference() for self collision checks 2025-02-17 15:39:43 -08:00
334 changed files with 7603 additions and 5042 deletions

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
---
name: Release
on:
@@ -19,15 +20,27 @@ jobs:
suffix: _portable
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -Ur requirements.txt pyinstaller
- run: pyinstaller tagstudio.spec -- ${{ matrix.build-flag }}
- run: tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio
- uses: actions/upload-artifact@v4
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pyinstaller]
- name: Execute PyInstaller
run: |
pyinstaller tagstudio.spec -- ${{ matrix.build-flag }}
tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tagstudio_linux_x86_64${{ matrix.suffix }}
path: dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz
@@ -41,20 +54,35 @@ jobs:
arch: x86_64
- os-version: '14'
arch: aarch64
runs-on: macos-${{ matrix.os-version }}
env:
# even though we run on 12, target towards compatibility
# INFO: Even though we run on 13, target towards compatibility
MACOSX_DEPLOYMENT_TARGET: '11.0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -Ur requirements.txt pyinstaller
- run: pyinstaller tagstudio.spec
- run: tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app
- uses: actions/upload-artifact@v4
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pyinstaller]
- name: Execute PyInstaller
run: |
pyinstaller tagstudio.spec
tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tagstudio_macos_${{ matrix.arch }}
path: dist/tagstudio_macos_${{ matrix.arch }}.tar.gz
@@ -72,30 +100,51 @@ jobs:
build-flag: --portable
suffix: _portable
file-end: .exe
runs-on: windows-2019
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

3
.gitignore vendored
View File

@@ -265,3 +265,6 @@ TagStudio.ini
.envrc
.direnv
.devenv
result
result-*

View File

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

View File

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

View File

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

View File

@@ -1,100 +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. Make sure you're using the correct Python version:
- If the output matches `Python 3.12.x` (where the x is any number) then you're using the correct Python version and can skip to step 2. Otherwise, you can install the correct Python version from the [Python](https://www.python.org/downloads/) website, or you can use a tool like [pyenv](https://github.com/pyenv/pyenv/) to install the correct version without changes to your system:
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.
Otherwise, modify the command above for use with your dependency manager of choice. For example if using uv, you may use this:
2. In the root repository directory, create a python virtual environment:
`python3 -m venv .venv`
3. Activate your environment:
- Windows w/Powershell: `.venv\Scripts\Activate.ps1`
- Windows w/Command Prompt: `.venv\Scripts\activate.bat`
- Linux/macOS: `source .venv/bin/activate`
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` |
|PWSH |`.venv/bin/activate.ps1` |
4. 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)
If you encounter errors about the Python version, or seemingly vague script errors, [pyenv](https://github.com/pyenv/pyenv/) may solve your issue. See step 1 of [Creating a Python Virtual Environment](#creating-a-python-virtual-environment).
- **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
@@ -125,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
@@ -147,7 +103,7 @@ Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older
- If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;)
- Imports should be ordered alphabetically.
- Lists of values should be ordered using their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
- Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/tagstudio/src/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern.
- Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/src/tagstudio/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern.
- When writing text for window titles or form titles, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check.
- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)!
@@ -173,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?"_
@@ -182,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,7 +75,7 @@ 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)
@@ -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,7 +156,7 @@ 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
@@ -172,11 +164,11 @@ You can manage your library of tags from opening the "Tag Manager" panel from Ed
### 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.
@@ -194,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

19
contrib/.envrc-nix Normal file
View File

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

32
contrib/.envrc-uv Normal file
View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

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

180
docs/develop.md Normal file
View File

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

View File

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

View File

@@ -4,21 +4,21 @@ 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
@@ -32,8 +32,8 @@ The [Feature Roadmap](updates/roadmap.md) lists all of the planned core features
- 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)
@@ -41,7 +41,7 @@ The [Feature Roadmap](updates/roadmap.md) lists all of the planned core features
### Search
- [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 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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# Fields
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.
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.
## Field Types

View File

@@ -1,5 +1,5 @@
# 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.
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_.
Note that this means [tags](./tag.md) you create only exist _per-library_.

View File

@@ -4,35 +4,43 @@ TagStudio provides various methods to search your library, ranging from TagStudi
## 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.
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.
The `AND` operator will only return results that match **both** sides of the operator. `AND` is used implicitly when no Boolean operators are given. To use the `AND` operator explicitly, simply type "and" (case insensitive) in-between items of your search.
> For example, searching for "Tag1 Tag2" will be treated the same as "Tag1 `AND` Tag2" and will only return results that contain both Tag1 and Tag2.
<!-- prettier-ignore -->
!!! example
Searching for "Tag1 Tag2" will be treated the same as "Tag1 `AND` Tag2" and will only return results that contain both Tag1 and Tag2.
### OR
The `OR` operator will return results that match **either** the left or right side of the operator. To use the `OR` operator simply type "or" (case insensitive) in-between items of your search.
> For example, searching for "Tag1 `OR` Tag2" will return results that contain either "Tag1", "Tag2", or both.
<!-- prettier-ignore -->
!!! example
Searching for "Tag1 `OR` Tag2" will return results that contain either "Tag1", "Tag2", or both.
### NOT
The `NOT` operator will returns results where the condition on the right is **false.** To use the `NOT` operator simply type "not" (case insensitive) in-between items of your search. You can also begin your search with `NOT` to only view results that do not contain the next term that follows.
> For example, searching for "Tag1 `NOT` Tag2" will only return results that contain "Tag1" while also not containing "Tag2".
<!-- prettier-ignore -->
!!! example
Searching for "Tag1 `NOT` Tag2" will only return results that contain "Tag1" while also not containing "Tag2".
### Grouping and Nesting
Searches can be grouped and nested by using parentheses to surround parts of your search query.
> For example, searching for "(Tag1 `OR` Tag2) `AND` Tag3" will return results any results that contain Tag3, plus one or the other (or both) of Tag1 and Tag2.
<!-- prettier-ignore -->
!!! example
Searching for "(Tag1 `OR` Tag2) `AND` Tag3" will return any results that contain Tag3, plus one or the other (or both) of Tag1 and Tag2.
### Escaping Characters
Sometimes search queries have ambiguous characters and need to be "escaped". This is most common with tag names which contain spaces, or overlap with existing search keywords such as "[path:](#filename--filepath) of exile". To escape most search terms, surround the section of your search in plain quotes. Alternatively, spaces in tag names can be replaced by underscores.
Sometimes search queries have ambiguous characters and need to be "escaped". This is most common with tag names which contain spaces, or overlap with existing search keywords such as "[path:](#filename-and-path) of exile". To escape most search terms, surround the section of your search in plain quotes. Alternatively, spaces in tag names can be replaced by underscores.
#### Valid Escaped Tag Searches
@@ -46,17 +54,17 @@ Sometimes search queries have ambiguous characters and need to be "escaped". Thi
## 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).
[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.
You may also see the `tag_id:` prefix keyword show up when using the right-click "Search for Tag" option on tags. This is meant for internal use, and eventually will not be displayed or accessible to the user.
## Fields
_[Field](field.md) search is currently not in the program, however is coming in a future version._
_[Field](./field.md) search is currently not in the program, however is coming in a future version._
## File Entry Search
### Filename + Path
### 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.
@@ -66,7 +74,7 @@ TagStudio uses a "[smartcase](https://neovim.io/doc/user/options.html#'smartcase
#### Glob Syntax
Optionally, you may use [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) syntax to search filepaths.
Optionally, you may use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) syntax to search filepaths.
#### Examples

View File

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,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
@@ -14,6 +14,7 @@ Features are broken up into the following priority levels, with nested prioritie
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).
<!-- prettier-ignore -->
!!! note
This list was created after the release of version 9.4
@@ -38,7 +39,7 @@ These version milestones are rough estimations for when the previous core featur
- [x] Boolean operators [HIGH]
- [x] Filename search [HIGH]
- [x] Filetype 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]
@@ -93,6 +94,7 @@ These version milestones are rough estimations for when the previous core featur
- [ ] Field content search [HIGH]
- [ ] Sort by date created [HIGH]
- [ ] Sort by date modified [HIGH]
- [x] Sort by filename [HIGH]
- [ ] HAS operator for composition tags [HIGH]
- [ ] Search bar rework
- [ ] Improved tag autocomplete [HIGH]
@@ -104,14 +106,14 @@ These version milestones are rough estimations for when the previous core featur
- [ ] 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]
- [x] Settings Menu [HIGH]
- [x] Application Settings [HIGH]
- [x] Stored in system user folder/designated folder [HIGH]
- [ ] Library Settings [HIGH]
- [ ] Stored in `.TagStudio` folder [HIGH]
- [ ] Tagging Panel [HIGH]
Togglebale persistent main window panel or popout. Replaces the current tag manager.
Toggleable persistent main window panel or pop-out. Replaces the current tag manager.
- [ ] Top Tags [HIGH]
- [ ] Recent Tags [HIGH]

View File

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

View File

@@ -12,13 +12,13 @@ Libraries under 10,000 files automatically scan for new or modified files when o
Access the "Add Tag" search box by either clicking on the "Add Tag" button at the bottom of the right sidebar, accessing the "Add Tags to Selected" option from the File menu, or by pressing <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd>.
From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the “+” button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the <kbd>Enter</kbd>/<kbd>Return</kbd> key to add the the topmost tag and reset the tag search. Press <kbd>Enter</kbd>/<kbd>Return</kbd> once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard!
From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the "+" button next to any tags you want to the currently selected file entries. To quickly add the top result, press the <kbd>Enter</kbd>/<kbd>Return</kbd> key to add the topmost tag and reset the tag search. Press <kbd>Enter</kbd>/<kbd>Return</kbd> once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard!
To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.
## Adding Metadata to File Entries
To add a metadata field to a file entry, start by clicking the Add Field button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field youd like to add to the entry
To add a metadata field to a file entry, start by clicking the "Add Field" button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field youd like to add to the entry
## Editing Metadata Fields
@@ -33,7 +33,7 @@ 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.
@@ -41,22 +41,33 @@ Create a new tag by accessing the "New Tag" option from the Edit menu or by pres
### Tag Manager
You can manage your library of tags from opening the "Tag Manager" panel from Edit -> "Tag Manager". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
You can manage your library of tags by opening the "Tag Manager" panel from Edit -> "Manage Tags". From here you can create, search for, edit, and permanently delete any tags you've created in your library.
## Editing Tags
To edit a tag, click on it inside the preview panel or right-click the tag and select Edit Tag from the context menu.
To edit a tag, click on it inside the preview panel or right-click the tag and select "Edit Tag" from the context menu.
## Relinking Moved Files
Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to Search & Relink any unlinked file entries to their respective files, or Delete Unlinked Entries in the event the original files have been deleted and you no longer wish to keep their entries inside your library.
Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to "Search & Relink" any unlinked file entries to their respective files, or "Delete Unlinked Entries" in the event the original files have been deleted and you no longer wish to keep their entries inside your library.
<!-- prettier-ignore -->
!!! warning
There is currently no method to relink entries to files that have been renamed - only moved or deleted. This is a high priority for future releases.
<!-- prettier-ignore -->
!!! warning
If multiple matches for a moved file are found (matches are currently defined as files with a matching filename as the original), TagStudio will currently ignore the match groups. Adding a GUI for manual selection, as well as smarter automated relinking, are high priorities for future versions.
### Saving the Library
As of version 9.5, libraries are saved automatically as you go. To save a backup of your library, select File -> Save Library Backup from the menu bar.
## Launch Arguments
There are a handful of launch arguments you can pass to TagStudio via the command line or a desktop shortcut.
| Argument | Short | Description |
| ---------------------- | ----- | ---------------------------------------------------- |
| `--open <path>` | `-o` | Path to a TagStudio Library folder to open on start. |
| `--config-file <path>` | `-c` | Path to the TagStudio config file to load. |

View File

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

509
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,27 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688870561,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix2container": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1724996935,
"narHash": "sha256-njRK9vvZ1JJsP8oV2OgkBrpJhgQezI03S7gzskCcHos=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "fa6bb0a1159f55d071ba99331355955ae30b3401",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"type": "github"
}
},
"nix_2": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression_2"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1692808169,
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
"lastModified": 1741173522,
"narHash": "sha256-k7VSqvv0r1r53nUI/IfPHCppkUAddeXn843YlAC5DR0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
"rev": "d69ab0d71b22fa1ce3dbeff666e6deb4917db049",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-qt6": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-regression_2": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1710695816,
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713361204,
"narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1724819573,
"narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "71e91c409d1e654808b2621f28a327acfdad8dc2",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1692876271,
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-utils": "flake-utils_2",
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1713775815,
"narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"devenv-root": "devenv-root",
"flake-parts": "flake-parts",
"nix2container": "nix2container",
"nixpkgs": "nixpkgs_3",
"nixpkgs-qt6": "nixpkgs-qt6",
"systems": "systems_4"
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
@@ -511,51 +57,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
}
},
"root": "root",

224
flake.nix
View File

@@ -2,209 +2,67 @@
description = "TagStudio";
inputs = {
devenv.url = "github:cachix/devenv";
devenv-root = {
url = "file+file:///dev/null";
flake = false;
};
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
nix2container = {
url = "github:nlewo/nix2container";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
# Pinned to Qt version 6.7.1
nixpkgs-qt6.url = "github:NixOS/nixpkgs/e6cea36f83499eb4e9cd184c8a8e823296b50ad5";
systems.url = "github:nix-systems/default-linux";
systems.url = "github:nix-systems/default";
};
outputs =
{
inputs@{
flake-parts,
nixpkgs,
nixpkgs-qt6,
self,
systems,
...
}@inputs:
}:
let
inherit (nixpkgs) lib;
in
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.devenv.flakeModule ];
systems = import systems;
systems = import inputs.systems;
perSystem =
{ pkgs, ... }:
{
config,
pkgs,
system,
...
}:
let
inherit (nixpkgs) lib;
packages =
let
pythonPackages = 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 = pythonPackages.callPackage ./nix/package/pillow-jxl-plugin.nix {
inherit (pkgs) cmake;
inherit pyexiv2;
inherit (pkgs) rustPlatform;
};
pyexiv2 = pythonPackages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
vtf2img = pythonPackages.callPackage ./nix/package/vtf2img.nix { };
in
rec {
default = tagstudio;
tagstudio = pythonPackages.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

@@ -9,6 +9,7 @@
# To run the preview server:
# mkdocs serve
---
site_name: TagStudio
site_description: "A User-Focused Photo & File Management System"
site_url: https://docs.tagstud.io/
@@ -25,30 +26,30 @@ extra:
tags:
Upcoming Feature: upcoming
# by default the navigation is an alphanumerically sorted,
# nested list of all the Markdown files found within the /docs directory
# where index files are always first
# uncomment the following to configure the navigation manually:
# nav:
# - Home:
# - index.md
# - install.md
# - usage.md
# - Library:
# - library/index.md
# - library/entry.md
# - library/entry_groups.md
# - library/field.md
# - library/tag.md
# - library/tag_categories.md
# - library/tag_overrides.md
# - Utilities:
# - utilities/macro.md
# - Updates:
# - updates/changelog.md
# - updates/roadmap.md
# - updates/db_migration.md
nav:
- Home:
- index.md
- install.md
- usage.md
- develop.md
- Help:
- help/ffmpeg.md
- Library:
- library/index.md
- library/entry.md
- library/entry_groups.md
- library/field.md
- library/library_search.md
- library/tag.md
- library/tag_categories.md
- library/tag_color.md
- library/tag_overrides.md
- Utilities:
- utilities/macro.md
- Updates:
- updates/changelog.md
- updates/roadmap.md
- updates/schema_changes.md
theme:
name: material
@@ -59,7 +60,7 @@ theme:
icon: material/brightness-auto
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
- media: "(prefers-color-scheme: light)"
scheme: default
primary: deep purple
accent: deep purple
@@ -67,7 +68,7 @@ theme:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: deep purple
accent: deep purple
@@ -76,7 +77,7 @@ theme:
name: SSwitch to system preference
logo: assets/icon.png
favicon: assets/icon.ico
font: false # use system fonts
font: false # use system fonts
language: en
features:
- navigation.instant
@@ -84,11 +85,9 @@ theme:
- navigation.tracking
- navigation.expand
- navigation.sections
#- navigation.tabs
#- content.tabs.link
#- navigation.top
- search.suggest
- content.code.annotate
- content.code.copy
- content.action.edit
icon:
repo: fontawesome/brands/github
@@ -134,5 +133,8 @@ markdown_extensions:
plugins:
- search
- tags
- social: # social embed cards
enabled: !ENV [CI, false] # enabled only when running in CI (eg GitHub Actions)
- social: # social embed cards
enabled: !ENV [CI, false] # enabled only when running in CI (eg GitHub Actions)
extra_css:
- stylesheets/extra.css

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

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

View File

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

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

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

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

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

104
nix/shell.nix Normal file
View File

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

View File

@@ -1,60 +1,111 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "TagStudio"
description = "A User-Focused Photo & File Management System."
version = "9.5.0-pre4"
version = "9.5.2"
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",
"toml==0.10.2",
"pydantic==2.9.2",
]
[tool.ruff]
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"]
line-length = 100
[project.optional-dependencies]
dev = ["tagstudio[mkdocs,mypy,pre-commit,pyinstaller,pytest,ruff]"]
mkdocs = ["mkdocs-material[imaging]==9.*"]
mypy = ["mypy==1.11.2", "mypy-extensions==1.*", "types-ujson>=5.8.0,<5.9.0"]
pre-commit = ["pre-commit==3.7.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"]
[tool.ruff.lint.per-file-ignores]
"tagstudio/tests/**" = ["D", "E402"]
"tagstudio/src/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"]
[project.gui-scripts]
tagstudio = "tagstudio.main:main"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint]
select = ["B", "D", "E", "F", "FBT003", "I", "N", "SIM", "T20", "UP"]
ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"]
[tool.pyright]
ignore = [".venv/**"]
include = ["tagstudio/**"]
reportAny = false
reportImplicitStringConcatenation = false
# reportOptionalMemberAccess = false
reportUnannotatedClassAttribute = false
reportUnknownArgumentType = false
reportUnknownMemberType = false
reportUnusedCallResult = false
[tool.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
reportIgnoreCommentWithoutRule = false
reportImplicitStringConcatenation = false
reportMissingTypeArgument = false
# reportOptionalMemberAccess = false
reportUnannotatedClassAttribute = false
reportUnknownArgumentType = false
reportUnknownLambdaType = 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,20 +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
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

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 4" # Usually "" or "Pre-Release"
VERSION: str = "9.5.2" # 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"

View File

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

View File

@@ -10,14 +10,18 @@ from uuid import uuid4
class SettingItems(str, enum.Enum):
"""List of setting item names."""
START_LOAD_LAST = "start_load_last"
LAST_LIBRARY = "last_library"
LIBS_LIST = "libs_list"
WINDOW_SHOW_LIBS = "window_show_libs"
SHOW_FILENAMES = "show_filenames"
AUTOPLAY = "autoplay_videos"
THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit"
LANGUAGE = "language"
class ShowFilepathOption(int, enum.Enum):
"""Values representing the options for the "show_filenames" setting."""
SHOW_FULL_PATHS = 0
SHOW_RELATIVE_PATHS = 1
SHOW_FILENAMES_ONLY = 2
DEFAULT = SHOW_RELATIVE_PATHS
class Theme(str, enum.Enum):
@@ -70,6 +74,5 @@ class LibraryPrefs(DefaultEnum):
"""Library preferences with default value accessible via .default property."""
IS_EXCLUDE_LIST = True
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
PAGE_SIZE: int = 500
DB_VERSION: int = 8
EXTENSION_LIST = [".json", ".xmp", ".aae"]
DB_VERSION = 9

View File

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

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

View File

@@ -3,8 +3,9 @@ from dataclasses import dataclass, replace
from pathlib import Path
import structlog
from src.core.query_lang import AST as Query # noqa: N811
from src.core.query_lang import Constraint, ConstraintType, Parser
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
@@ -66,6 +67,8 @@ class ItemType(enum.Enum):
class SortingModeEnum(enum.Enum):
DATE_ADDED = "file.date_added"
FILE_NAME = "generic.filename"
PATH = "file.path"
@dataclass
@@ -73,14 +76,14 @@ class FilterState:
"""Represent a state of the Library grid view."""
# these should remain
page_index: int | None = 0
page_size: int | None = 500
page_size: int
page_index: int = 0
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
ascending: bool = True
# these should be erased on update
# Abstract Syntax Tree Of the current Search Query
ast: Query = None
ast: AST | None = None
@property
def limit(self):
@@ -91,35 +94,32 @@ class FilterState:
return self.page_size * self.page_index
@classmethod
def show_all(cls) -> "FilterState":
return FilterState()
def show_all(cls, page_size: int) -> "FilterState":
return FilterState(page_size=page_size)
@classmethod
def from_search_query(cls, search_query: str) -> "FilterState":
return cls(ast=Parser(search_query).parse())
def from_search_query(cls, search_query: str, page_size: int) -> "FilterState":
return cls(ast=Parser(search_query).parse(), page_size=page_size)
@classmethod
def from_tag_id(cls, tag_id: int | str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []))
def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size)
@classmethod
def from_path(cls, path: Path | str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []))
def from_path(cls, path: Path | str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size)
@classmethod
def from_mediatype(cls, mediatype: str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []))
def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size)
@classmethod
def from_filetype(cls, filetype: str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.FileType, filetype, []))
def from_filetype(cls, filetype: str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size)
@classmethod
def from_tag_name(cls, tag_name: str) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []))
def with_page_size(self, page_size: int) -> "FilterState":
return replace(self, page_size=page_size)
def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size)
def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState":
return replace(self, sorting_mode=mode)

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

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
@@ -43,10 +44,8 @@ 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,
@@ -57,19 +56,35 @@ 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
@@ -81,7 +96,7 @@ 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
UNION
SELECT tp.parent_id AS child_id
FROM tag_parents tp
INNER JOIN ChildTags c ON tp.child_id = c.child_id
@@ -184,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
@@ -191,7 +207,7 @@ class Library:
"""Class for the Library object, and all CRUD operations made upon it."""
library_dir: Path | None = None
storage_path: Path | str | None
storage_path: Path | None
engine: Engine | None = None
folder: Folder | None
included_files: set[Path] = set()
@@ -290,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:
@@ -315,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
@@ -323,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():
@@ -364,16 +379,13 @@ class Library:
select(Preferences).where(Preferences.key == LibraryPrefs.DB_VERSION.name)
)
if db_result:
db_version = db_result.value # type: ignore
db_version = db_result.value
if db_version < 6: # NOTE: DB_VERSION 6 is the first supported SQL DB version.
mismatch_text = Translations.translate_formatted(
"status.library_version_mismatch"
)
found_text = Translations.translate_formatted("status.library_version_found")
expected_text = Translations.translate_formatted(
"status.library_version_expected"
)
# NOTE: DB_VERSION 6 is the first supported SQL DB version.
if db_version < 6 or db_version > LibraryPrefs.DB_VERSION.default:
mismatch_text = Translations["status.library_version_mismatch"]
found_text = Translations["status.library_version_found"]
expected_text = Translations["status.library_version_expected"]
return LibraryStatus(
success=False,
message=(
@@ -460,10 +472,25 @@ class Library:
# Apply any post-SQL migration patches.
if not is_new:
# save backup if patches will be applied
if LibraryPrefs.DB_VERSION.default != db_version:
self.library_dir = library_dir
self.save_library_backup_to_disk()
self.library_dir = None
# schema changes first
if db_version < 8:
self.apply_db8_schema_changes(session)
if db_version < 9:
self.apply_db9_schema_changes(session)
# now the data changes
if db_version == 6:
self.apply_db6_patches(session)
self.apply_repairs_for_db6(session)
if db_version >= 6 and db_version < 8:
self.apply_db7_patches(session)
self.apply_db8_default_data(session)
if db_version < 9:
self.apply_db9_filename_population(session)
# Update DB_VERSION
if LibraryPrefs.DB_VERSION.default > db_version:
@@ -473,11 +500,8 @@ class Library:
self.library_dir = library_dir
return LibraryStatus(success=True, library_path=library_dir)
def apply_db6_patches(self, session: Session):
"""Apply migration patches to a library with DB_VERSION 6.
DB_VERSION 6 was only used in v9.5.0-pr1.
"""
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.
@@ -501,11 +525,8 @@ class Library:
session.commit()
def apply_db7_patches(self, session: Session):
"""Apply migration patches to a library with DB_VERSION 7 or earlier.
DB_VERSION 7 was used from v9.5.0-pr2 to v9.5.0-pr3.
"""
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(
@@ -522,6 +543,8 @@ class Library:
)
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()
@@ -570,6 +593,29 @@ class Library:
)
session.rollback()
def apply_db9_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 9."""
add_filename_column = text(
"ALTER TABLE entries ADD COLUMN filename TEXT NOT NULL DEFAULT ''"
)
try:
session.execute(add_filename_column)
session.commit()
logger.info("[Library][Migration] Added filename column to entries table")
except Exception as e:
logger.error(
"[Library][Migration] Could not create filename column in entries table!",
error=e,
)
session.rollback()
def apply_db9_filename_population(self, session: Session):
"""Populate the filename column introduced in DB_VERSION 9."""
for entry in self.get_entries():
session.merge(entry).filename = entry.path.name
session.commit()
logger.info("[Library][Migration] Populated filename column in entries table")
@property
def default_fields(self) -> list[BaseField]:
with Session(self.engine) as session:
@@ -632,7 +678,7 @@ class Library:
end_time = time.time()
logger.info(
f"[Library] Time it took to get entry: "
f"{format_timespan(end_time-start_time, max_units=5)}",
f"{format_timespan(end_time - start_time, max_units=5)}",
with_fields=with_fields,
with_tags=with_tags,
)
@@ -842,14 +888,18 @@ class Library:
statement = statement.distinct(Entry.id)
start_time = time.time()
query_count = select(func.count()).select_from(statement.alias("entries"))
count_all: int = session.execute(query_count).scalar()
count_all: int = session.execute(query_count).scalar() or 0
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:
case SortingModeEnum.DATE_ADDED:
sort_on = Entry.id
case SortingModeEnum.FILE_NAME:
sort_on = func.lower(Entry.filename)
case SortingModeEnum.PATH:
sort_on = func.lower(Entry.path)
statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on))
statement = statement.limit(search.limit).offset(search.offset)
@@ -1164,7 +1214,7 @@ 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()
@@ -1191,7 +1241,6 @@ class Library:
f"{RESERVED_NAMESPACE_PREFIX}",
namespace=namespace,
)
logger.error("Should not see me")
namespace_obj = Namespace(
namespace=slug,
@@ -1353,7 +1402,7 @@ class Library:
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
@@ -1362,6 +1411,8 @@ class Library:
target_path,
)
logger.info("Library backup saved to disk.", path=target_path)
return target_path
def get_tag(self, tag_id: int) -> Tag | None:

View File

@@ -8,16 +8,16 @@ 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):
@@ -187,6 +187,7 @@ class Entry(Base):
folder: Mapped[Folder] = relationship("Folder")
path: Mapped[Path] = mapped_column(PathType, unique=True)
filename: Mapped[str] = mapped_column()
suffix: Mapped[str] = mapped_column()
date_created: Mapped[dt | None]
date_modified: Mapped[dt | None]
@@ -232,6 +233,7 @@ class Entry(Base):
self.path = path
self.folder = folder
self.id = id
self.filename = path.name
self.suffix = path.suffix.lstrip(".").lower()
# The date the file associated with this entry was created.
@@ -304,7 +306,7 @@ class ValueType(Base):
def slugify_field_key(mapper, connection, target):
"""Slugify the field key before inserting into the database."""
if not target.key:
from .library import slugify
from tagstudio.core.library.alchemy.library import slugify
target.key = slugify(target.tag)

View File

@@ -9,18 +9,25 @@ import structlog
from sqlalchemy import ColumnElement, and_, distinct, func, or_, select, text
from sqlalchemy.orm import Session
from sqlalchemy.sql.operators import ilike_op
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 .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,
)
# 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__)
@@ -29,7 +36,7 @@ TAG_CHILDREN_ID_QUERY = text("""
-- Note for this entire query that tag_parents.child_id is the parent id and tag_parents.parent_id is the child id due to bad naming
WITH RECURSIVE ChildTags AS (
SELECT :tag_id AS child_id
UNION ALL
UNION
SELECT tp.parent_id AS child_id
FROM tag_parents tp
INNER JOIN ChildTags c ON tp.child_id = c.child_id

View File

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

View File

@@ -1,7 +1,8 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import logging
import mimetypes
from dataclasses import dataclass
@@ -10,41 +11,53 @@ from pathlib import Path
logging.basicConfig(format="%(message)s", level=logging.INFO)
FILETYPE_EQUIVALENTS = [set(["jpg", "jpeg"])]
FILETYPE_EQUIVALENTS = [
set(["aif", "aiff", "aifc"]),
set(["html", "htm", "xhtml", "shtml", "dhtml"]),
set(["jfif", "jpeg_large", "jpeg", "jpg_large", "jpg"]),
set(["json", "jsonc", "json5"]),
set(["md", "markdown", "mkd", "rmd"]),
set(["tar.gz", "tgz"]),
set(["xml", "xul"]),
set(["yaml", "yml"]),
]
class MediaType(str, Enum):
"""Names of media types."""
ADOBE_PHOTOSHOP: str = "adobe_photoshop"
AFFINITY_PHOTO: str = "affinity_photo"
ARCHIVE: str = "archive"
AUDIO_MIDI: str = "audio_midi"
AUDIO: str = "audio"
BLENDER: str = "blender"
DATABASE: str = "database"
DISK_IMAGE: str = "disk_image"
DOCUMENT: str = "document"
EBOOK: str = "ebook"
FONT: str = "font"
IMAGE_ANIMATED: str = "image_animated"
IMAGE_RAW: str = "image_raw"
IMAGE_VECTOR: str = "image_vector"
IMAGE: str = "image"
INSTALLER: str = "installer"
MATERIAL: str = "material"
MODEL: str = "model"
OPEN_DOCUMENT: str = "open_document"
PACKAGE: str = "package"
PDF: str = "pdf"
PLAINTEXT: str = "plaintext"
PRESENTATION: str = "presentation"
PROGRAM: str = "program"
SHORTCUT: str = "shortcut"
SOURCE_ENGINE: str = "source_engine"
SPREADSHEET: str = "spreadsheet"
TEXT: str = "text"
VIDEO: str = "video"
ADOBE_PHOTOSHOP = "adobe_photoshop"
AFFINITY_PHOTO = "affinity_photo"
ARCHIVE = "archive"
AUDIO_MIDI = "audio_midi"
AUDIO = "audio"
BLENDER = "blender"
CODE = "code"
DATABASE = "database"
DISK_IMAGE = "disk_image"
DOCUMENT = "document"
EBOOK = "ebook"
FONT = "font"
IMAGE_ANIMATED = "image_animated"
IMAGE_RAW = "image_raw"
IMAGE_VECTOR = "image_vector"
IMAGE = "image"
INSTALLER = "installer"
IWORK = "iwork"
MATERIAL = "material"
MODEL = "model"
OPEN_DOCUMENT = "open_document"
PACKAGE = "package"
PDF = "pdf"
PLAINTEXT = "plaintext"
PRESENTATION = "presentation"
PROGRAM = "program"
SHADER = "shader"
SHORTCUT = "shortcut"
SOURCE_ENGINE = "source_engine"
SPREADSHEET = "spreadsheet"
TEXT = "text"
VIDEO = "video"
@dataclass(frozen=True)
@@ -96,6 +109,7 @@ class MediaCategories:
_AUDIO_SET: set[str] = {
".aac",
".aif",
".aifc",
".aiff",
".alac",
".flac",
@@ -143,9 +157,71 @@ class MediaCategories:
".blend31",
".blend32",
}
_CODE_SET: set[str] = {
".bat",
".cfg",
".conf",
".cpp",
".cs",
".csh",
".css",
".d",
".dhtml",
".fgd",
".fish",
".gitignore",
".h",
".hpp",
".htm",
".html",
".inf",
".ini",
".js",
".json",
".json5",
".jsonc",
".jsx",
".kv3",
".lua",
".meta",
".nix",
".nu",
".nut",
".php",
".plist",
".prefs",
".ps1",
".py",
".pyi",
".qml",
".qrc",
".qss",
".rs",
".sh",
".shtml",
".sip",
".spec",
".tcl",
".timestamp",
".toml",
".ts",
".tsx",
".vcfg",
".vdf",
".vmt",
".vqlayout",
".vsc",
".vsnd_template",
".xhtml",
".xml",
".xul",
".yaml",
".yml",
}
_DATABASE_SET: set[str] = {
".accdb",
".mdb",
".pdb",
".sqlite",
".sqlite3",
}
@@ -166,23 +242,23 @@ class MediaCategories:
".wps",
}
_EBOOK_SET: set[str] = {
".azw",
".azw3",
".cb7",
".cba",
".cbr",
".cbt",
".cbz",
".djvu",
".epub",
# ".azw",
# ".azw3",
# ".cb7",
# ".cba",
# ".cbr",
# ".cbt",
# ".cbz",
# ".djvu",
# ".fb2",
# ".ibook",
# ".inf",
# ".kfx",
# ".lit",
# ".mobi",
# ".pdb"
# ".prc",
".fb2",
".ibook",
".inf",
".kfx",
".lit",
".mobi",
".pdb",
".prc",
}
_FONT_SET: set[str] = {
".fon",
@@ -209,7 +285,7 @@ class MediaCategories:
".raw",
".rw2",
}
_IMAGE_VECTOR_SET: set[str] = {".svg"}
_IMAGE_VECTOR_SET: set[str] = {".eps", ".epsf", ".epsi", ".svg", ".svgz"}
_IMAGE_RASTER_SET: set[str] = {
".apng",
".avif",
@@ -218,6 +294,7 @@ class MediaCategories:
".gif",
".heic",
".heif",
".icns",
".j2k",
".jfif",
".jp2",
@@ -235,6 +312,7 @@ class MediaCategories:
".webp",
}
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
_MATERIAL_SET: set[str] = {".mtl"}
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
_OPEN_DOCUMENT_SET: set[str] = {
@@ -258,51 +336,21 @@ class MediaCategories:
".pkg",
".xapk",
}
_PDF_SET: set[str] = {
".pdf",
}
_PDF_SET: set[str] = {".pdf"}
_PLAINTEXT_SET: set[str] = {
".bat",
".cfg",
".conf",
".cpp",
".cs",
".css",
".csv",
".fgd",
".gi",
".h",
".hpp",
".htm",
".html",
".inf",
".ini",
".js",
".json",
".jsonc",
".kv3",
".lua",
".i3u",
".lang",
".lock",
".log",
".markdown",
".md",
".nut",
".php",
".plist",
".prefs",
".py",
".pyc",
".qss",
".sh",
".toml",
".ts",
".mkd",
".rmd",
".txt",
".vcfg",
".vdf",
".vmt",
".vqlayout",
".vsc",
".vsnd_template",
".xml",
".yaml",
".yml",
"contributing",
"license",
"readme",
}
_PRESENTATION_SET: set[str] = {
".key",
@@ -310,9 +358,16 @@ class MediaCategories:
".ppt",
".pptx",
}
_PROGRAM_SET: set[str] = {".app", ".exe"}
_SOURCE_ENGINE_SET: set[str] = {
".vtf",
_PROGRAM_SET: set[str] = {".app", ".bin", ".exe"}
_SOURCE_ENGINE_SET: set[str] = {".vtf"}
_SHADER_SET: set[str] = {
".effect",
".frag",
".fsh",
".glsl",
".shader",
".vert",
".vsh",
}
_SHORTCUT_SET: set[str] = {".desktop", ".lnk", ".url"}
_SPREADSHEET_SET: set[str] = {
@@ -337,188 +392,206 @@ class MediaCategories:
".wmv",
}
ADOBE_PHOTOSHOP_TYPES: MediaCategory = MediaCategory(
ADOBE_PHOTOSHOP_TYPES = MediaCategory(
media_type=MediaType.ADOBE_PHOTOSHOP,
extensions=_ADOBE_PHOTOSHOP_SET,
is_iana=False,
name="photoshop",
)
AFFINITY_PHOTO_TYPES: MediaCategory = MediaCategory(
AFFINITY_PHOTO_TYPES = MediaCategory(
media_type=MediaType.AFFINITY_PHOTO,
extensions=_AFFINITY_PHOTO_SET,
is_iana=False,
name="affinity photo",
)
ARCHIVE_TYPES: MediaCategory = MediaCategory(
ARCHIVE_TYPES = MediaCategory(
media_type=MediaType.ARCHIVE,
extensions=_ARCHIVE_SET,
is_iana=False,
name="archive",
)
AUDIO_MIDI_TYPES: MediaCategory = MediaCategory(
AUDIO_MIDI_TYPES = MediaCategory(
media_type=MediaType.AUDIO_MIDI,
extensions=_AUDIO_MIDI_SET,
is_iana=False,
name="audio midi",
)
AUDIO_TYPES: MediaCategory = MediaCategory(
AUDIO_TYPES = MediaCategory(
media_type=MediaType.AUDIO,
extensions=_AUDIO_SET | _AUDIO_MIDI_SET,
is_iana=True,
name="audio",
)
BLENDER_TYPES: MediaCategory = MediaCategory(
BLENDER_TYPES = MediaCategory(
media_type=MediaType.BLENDER,
extensions=_BLENDER_SET,
is_iana=False,
name="blender",
)
DATABASE_TYPES: MediaCategory = MediaCategory(
CODE_TYPES = MediaCategory(
media_type=MediaType.CODE,
extensions=_CODE_SET,
is_iana=False,
name="code",
)
DATABASE_TYPES = MediaCategory(
media_type=MediaType.DATABASE,
extensions=_DATABASE_SET,
is_iana=False,
name="database",
)
DISK_IMAGE_TYPES: MediaCategory = MediaCategory(
DISK_IMAGE_TYPES = MediaCategory(
media_type=MediaType.DISK_IMAGE,
extensions=_DISK_IMAGE_SET,
is_iana=False,
name="disk image",
)
DOCUMENT_TYPES: MediaCategory = MediaCategory(
DOCUMENT_TYPES = MediaCategory(
media_type=MediaType.DOCUMENT,
extensions=_DOCUMENT_SET,
is_iana=False,
name="document",
)
EBOOK_TYPES: MediaCategory = MediaCategory(
EBOOK_TYPES = MediaCategory(
media_type=MediaType.EBOOK,
extensions=_EBOOK_SET,
is_iana=False,
name="ebook",
)
FONT_TYPES: MediaCategory = MediaCategory(
FONT_TYPES = MediaCategory(
media_type=MediaType.FONT,
extensions=_FONT_SET,
is_iana=True,
name="font",
)
IMAGE_ANIMATED_TYPES: MediaCategory = MediaCategory(
IMAGE_ANIMATED_TYPES = MediaCategory(
media_type=MediaType.IMAGE_ANIMATED,
extensions=_IMAGE_ANIMATED_SET,
is_iana=False,
name="animated image",
)
IMAGE_RAW_TYPES: MediaCategory = MediaCategory(
IMAGE_RAW_TYPES = MediaCategory(
media_type=MediaType.IMAGE_RAW,
extensions=_IMAGE_RAW_SET,
is_iana=False,
name="raw image",
)
IMAGE_VECTOR_TYPES: MediaCategory = MediaCategory(
IMAGE_VECTOR_TYPES = MediaCategory(
media_type=MediaType.IMAGE_VECTOR,
extensions=_IMAGE_VECTOR_SET,
is_iana=False,
name="vector image",
)
IMAGE_RASTER_TYPES: MediaCategory = MediaCategory(
IMAGE_RASTER_TYPES = MediaCategory(
media_type=MediaType.IMAGE,
extensions=_IMAGE_RASTER_SET,
is_iana=False,
name="raster image",
)
IMAGE_TYPES: MediaCategory = MediaCategory(
IMAGE_TYPES = MediaCategory(
media_type=MediaType.IMAGE,
extensions=_IMAGE_RASTER_SET | _IMAGE_RAW_SET | _IMAGE_VECTOR_SET,
is_iana=True,
name="image",
)
INSTALLER_TYPES: MediaCategory = MediaCategory(
INSTALLER_TYPES = MediaCategory(
media_type=MediaType.INSTALLER,
extensions=_INSTALLER_SET,
is_iana=False,
name="installer",
)
MATERIAL_TYPES: MediaCategory = MediaCategory(
IWORK_TYPES = MediaCategory(
media_type=MediaType.IWORK,
extensions=_IWORK_SET,
is_iana=False,
name="iwork",
)
MATERIAL_TYPES = MediaCategory(
media_type=MediaType.MATERIAL,
extensions=_MATERIAL_SET,
is_iana=False,
name="material",
)
MODEL_TYPES: MediaCategory = MediaCategory(
MODEL_TYPES = MediaCategory(
media_type=MediaType.MODEL,
extensions=_MODEL_SET,
is_iana=True,
name="model",
)
OPEN_DOCUMENT_TYPES: MediaCategory = MediaCategory(
OPEN_DOCUMENT_TYPES = MediaCategory(
media_type=MediaType.OPEN_DOCUMENT,
extensions=_OPEN_DOCUMENT_SET,
is_iana=False,
name="open document",
)
PACKAGE_TYPES: MediaCategory = MediaCategory(
PACKAGE_TYPES = MediaCategory(
media_type=MediaType.PACKAGE,
extensions=_PACKAGE_SET,
is_iana=False,
name="package",
)
PDF_TYPES: MediaCategory = MediaCategory(
PDF_TYPES = MediaCategory(
media_type=MediaType.PDF,
extensions=_PDF_SET,
is_iana=False,
name="pdf",
)
PLAINTEXT_TYPES: MediaCategory = MediaCategory(
PLAINTEXT_TYPES = MediaCategory(
media_type=MediaType.PLAINTEXT,
extensions=_PLAINTEXT_SET,
extensions=_PLAINTEXT_SET | _CODE_SET,
is_iana=False,
name="plaintext",
)
PRESENTATION_TYPES: MediaCategory = MediaCategory(
PRESENTATION_TYPES = MediaCategory(
media_type=MediaType.PRESENTATION,
extensions=_PRESENTATION_SET,
is_iana=False,
name="presentation",
)
PROGRAM_TYPES: MediaCategory = MediaCategory(
PROGRAM_TYPES = MediaCategory(
media_type=MediaType.PROGRAM,
extensions=_PROGRAM_SET,
is_iana=False,
name="program",
)
SHORTCUT_TYPES: MediaCategory = MediaCategory(
SHADER_TYPES = MediaCategory(
media_type=MediaType.SHADER,
extensions=_SHADER_SET,
is_iana=False,
name="shader",
)
SHORTCUT_TYPES = MediaCategory(
media_type=MediaType.SHORTCUT,
extensions=_SHORTCUT_SET,
is_iana=False,
name="shortcut",
)
SOURCE_ENGINE_TYPES: MediaCategory = MediaCategory(
SOURCE_ENGINE_TYPES = MediaCategory(
media_type=MediaType.SOURCE_ENGINE,
extensions=_SOURCE_ENGINE_SET,
is_iana=False,
name="source engine",
)
SPREADSHEET_TYPES: MediaCategory = MediaCategory(
SPREADSHEET_TYPES = MediaCategory(
media_type=MediaType.SPREADSHEET,
extensions=_SPREADSHEET_SET,
is_iana=False,
name="spreadsheet",
)
TEXT_TYPES: MediaCategory = MediaCategory(
TEXT_TYPES = MediaCategory(
media_type=MediaType.TEXT,
extensions=_DOCUMENT_SET | _PLAINTEXT_SET,
is_iana=True,
name="text",
)
VIDEO_TYPES: MediaCategory = MediaCategory(
VIDEO_TYPES = MediaCategory(
media_type=MediaType.VIDEO,
extensions=_VIDEO_SET,
is_iana=True,
name="video",
)
ALL_CATEGORIES: list[MediaCategory] = [
ALL_CATEGORIES = [
ADOBE_PHOTOSHOP_TYPES,
AFFINITY_PHOTO_TYPES,
ARCHIVE_TYPES,
@@ -535,6 +608,7 @@ class MediaCategories:
IMAGE_TYPES,
IMAGE_VECTOR_TYPES,
INSTALLER_TYPES,
IWORK_TYPES,
MATERIAL_TYPES,
MODEL_TYPES,
OPEN_DOCUMENT_TYPES,
@@ -543,6 +617,8 @@ class MediaCategories:
PLAINTEXT_TYPES,
PRESENTATION_TYPES,
PROGRAM_TYPES,
CODE_TYPES,
SHADER_TYPES,
SHORTCUT_TYPES,
SOURCE_ENGINE_TYPES,
SPREADSHEET_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):

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()
@@ -50,7 +52,7 @@ class DupeRegistry:
continue
results = self.library.search_library(
FilterState.from_path(path_relative),
FilterState.from_path(path_relative, page_size=500),
)
if not results:

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

@@ -5,8 +5,10 @@ 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__)

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),
@@ -31,11 +33,18 @@ def main():
help="Path to a TagStudio Library folder to open on start.",
)
parser.add_argument(
"-c",
"--config-file",
dest="config_file",
"-s",
"--settings-file",
dest="settings_file",
type=str,
help="Path to a TagStudio .ini or .plist config file to use.",
help="Path to a TagStudio .toml global settings file to use.",
)
parser.add_argument(
"-c",
"--cache-file",
dest="cache_file",
type=str,
help="Path to a TagStudio .ini or .plist cache file to use.",
)
# parser.add_argument('--browse', dest='browse', action='store_true',
@@ -48,16 +57,9 @@ def main():
action="store_true",
help="Reveals additional internal data useful for debugging.",
)
parser.add_argument(
"--ui",
dest="ui",
type=str,
help="User interface option for TagStudio. Options: qt, cli (Default: qt)",
)
args = parser.parse_args()
from src.core.library import alchemy as backend
driver = QtDriver(backend, args)
driver = QtDriver(args)
ui_name = "Qt"
# Run the chosen frontend driver.

View File

@@ -9,15 +9,14 @@ from datetime import datetime as dt
from pathlib import Path
import structlog
from PIL import (
Image,
)
from src.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
from src.core.singleton import Singleton
from PIL import Image
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
from tagstudio.core.singleton import Singleton
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.core.library import Library
from tagstudio.core.library import Library
logger = structlog.get_logger(__name__)
@@ -33,7 +32,7 @@ class CacheManager(metaclass=Singleton):
self.last_lib_path: Path | None = None
@staticmethod
def clear_cache(library_dir: Path) -> bool:
def clear_cache(library_dir: Path | None) -> bool:
"""Clear all files and folders within the cached folder.
Returns:

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,14 @@ import subprocess
import sys
import traceback
from pathlib import Path
from typing import override
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel
from src.qt.helpers.silent_popen import silent_Popen
from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import QLabel, QWidget
from tagstudio.qt.helpers.silent_popen import silent_Popen
logger = structlog.get_logger(__name__)
@@ -114,15 +117,17 @@ class FileOpenerHelper:
class FileOpenerLabel(QLabel):
def __init__(self, parent=None):
def __init__(self, parent: QWidget | None = None) -> None:
"""Initialize the FileOpenerLabel.
Args:
parent (QWidget, optional): The parent widget. Defaults to None.
"""
self.filepath: str | Path | None = None
super().__init__(parent)
def set_file_path(self, filepath):
def set_file_path(self, filepath: str | Path) -> None:
"""Set the filepath to open.
Args:
@@ -130,20 +135,22 @@ class FileOpenerLabel(QLabel):
"""
self.filepath = filepath
def mousePressEvent(self, event): # noqa: N802
@override
def mousePressEvent(self, ev: QMouseEvent) -> None:
"""Handle mouse press events.
On a left click, open the file in the default file explorer.
On a right click, show a context menu.
Args:
event (QMouseEvent): The mouse press event.
ev (QMouseEvent): The mouse press event.
"""
super().mousePressEvent(event)
if event.button() == Qt.MouseButton.LeftButton:
if ev.button() == Qt.MouseButton.LeftButton:
assert self.filepath is not None, "File path is not set"
opener = FileOpenerHelper(self.filepath)
opener.open_explorer()
elif event.button() == Qt.MouseButton.RightButton:
elif ev.button() == Qt.MouseButton.RightButton:
# Show context menu
pass
else:
super().mousePressEvent(ev)

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):
@@ -18,8 +19,8 @@ def is_readable_video(filepath: Path | str):
filepath (Path | str): The filepath of the video to check.
"""
try:
probe = _probe(Path(filepath))
for stream in probe["streams"]:
result = probe(Path(filepath))
for stream in result["streams"]:
# DRM check
if stream.get("codec_tag_string") in [
"drma",

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
@@ -48,8 +49,8 @@ def four_corner_gradient(
def linear_gradient(
size=tuple[int, int],
colors=list[str],
size: tuple[int, int],
colors: list[str],
interpolation: Image.Resampling = Image.Resampling.BICUBIC,
) -> Image.Image:
seed: Image.Image = Image.new(mode="RGBA", size=(len(colors), 1), color="#000000")

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

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