Compare commits

...

18 Commits

Author SHA1 Message Date
Hosted Weblate 08da66aa07 Translated using Weblate (Spanish)
Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (422 of 422 strings)

Co-authored-by: 2004milenadiaz-source <2004.milena.diaz@gmail.com>
Co-authored-by: Joan <joancanalscrehuet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Application Strings
2026-07-10 01:31:56 +02:00
Travis Abendshien 4397e087ef docs: update install instructions 2026-07-09 16:31:41 -07:00
Travis Abendshien fee9480cc3 docs: update changelog 2026-07-09 16:30:48 -07:00
Travis Abendshien 1f6aae98ed ci: update paths for python workflows 2026-07-09 16:30:26 -07:00
Travis Abendshien 8edbb7a258 docs: update README badges 2026-07-08 23:42:26 -07:00
Travis Abendshien 9c3f76ce16 docs: update changelog 2026-07-08 23:13:29 -07:00
Sola-ris c755894c84 feat: render .pxd thumbnails (#1430)
* feat: render .pxd thumbnails.

* move .pxd to _DOCUMENT_SET, rename method, fix doc and typos.

* add missing break.
2026-07-08 23:05:48 -07:00
Xarvex 377593b190 feat(ci)!: complete workflows revamp (#1437)
* chore(ci): move spec-file into scripts

* feat(ci)!: rework workflows and build processes

All Python setups route through one local action, and checks are much
more conditional. There is more to be done, but this is a significant
improvement in processing time and redundancy.

As well, nightly builds are made (though not made as GitHub releases..
yet).

* ci: remove symlinked CHANGELONG.md from build_docs.yml paths

* docs: update location of tagstudio.spec

* ci: enforce single quotes in github yaml files

* ci(tests): remove macOS pytest runner

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
2026-07-08 22:49:57 -07:00
Xarvex 993eeff0fc fix(deps): replace optional dependencies with dependency groups (#1435)
Definition: https://packaging.python.org/en/latest/specifications/dependency-groups

The optional dependency table ([project.optional-dependencies]) is
supposed to be user-facing for installs of TagStudio. Meanwhile, the
replacement dependency groups is explicitly meant for extra development
tools, so lets use that.

This does mean the command for a full development environment has changed
from:
```sh
pip install -e '.[dev]'
```

To:
```sh
pip install -e . --group all
```

The documentation has been adjusted for a quick replacement, but
probably should have more information on a later time.
2026-07-07 14:36:02 -07:00
Xarvex faf88a912e nit(dev): make supplementary configuratios CC0
This has the approval of myself and @CyanVoxel, whom are the only
contributors to these files.
2026-07-06 15:30:59 -05:00
Xarvex 3e26b990f7 nit(dev): put filetype clause on top 2026-07-06 15:15:28 -05:00
Xarvex 65cb49e472 fix(dev): use indent_size of 4 by default 2026-07-06 15:05:44 -05:00
Travis Abendshien 3593acdd59 docs: add v9.6.1 changelog 2026-07-06 11:42:30 -07:00
Weblate (bot) 858d433b6f translations: update from Hosted Weblate (#1402)
* Translated using Weblate (German)

Currently translated at 85.7% (362 of 422 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 0.7% (3 of 418 strings)

Added translation using Weblate (Estonian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/et/
Translation: TagStudio/Application Strings

* Translated using Weblate (Russian)

Currently translated at 93.7% (392 of 418 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 86.1% (360 of 418 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (418 of 418 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (388 of 388 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.7% (387 of 388 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (387 of 387 strings)

Update translation files

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

Update translation files

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

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

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: romandobra <romanreachless@gmail.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>
2026-07-06 10:25:38 -07:00
TheBobBobs 2965d2056b fix: Thumbnail Grid not resizing to fill width (#1433)
* fix: update thumb_grid when row width changes

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

* fix: selecting a single item out of a selecting

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

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

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

* fix: revert errant license change

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

* refactor: implement review feedback

* feat(ui): add tooltips for module locations on about screen
2026-07-05 15:25:56 -07:00
57 changed files with 1227 additions and 640 deletions
+2 -4
View File
@@ -10,13 +10,11 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 100
trim_trailing_whitespace = true
[*.{css,json,md}]
indent_size = 4
[*.{yaml,yml}]
[*.{nix,yaml,yml}]
indent_size = 2
+59
View File
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
---
name: Setup Python
description: Combination of Python setup actions
inputs:
skip-setup:
default: 'false'
description: Whether to skip setup actions and only execute installs
groups:
description: Newline-separated list of dependency groups to install
runs:
using: composite
steps:
- name: Setup Python install
if: inputs.skip-setup != 'true'
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Setup uv install
if: inputs.skip-setup != 'true'
uses: astral-sh/setup-uv@v8.2.0
- name: Run pip install (Windows)
if: runner.os == 'Windows'
env:
INPUT_GROUPS: ${{ inputs.groups }}
UV_PYTHON_DOWNLOADS: never
UV_SYSTEM_PYTHON: '1'
shell: pwsh
run: |
if (![string]::IsNullOrEmpty(${env:INPUT_GROUPS})) {
$groupArgs = -split ${env:INPUT_GROUPS} -replace '^', '--group='
}
uv pip install . ${groupArgs}
- name: Run pip install (non-Windows)
if: runner.os != 'Windows'
env:
INPUT_GROUPS: ${{ inputs.groups }}
UV_PYTHON_DOWNLOADS: never
UV_SYSTEM_PYTHON: '1'
shell: bash
run: |
extra_args=()
if [ -n "${INPUT_GROUPS}" ]; then
while IFS= read -r group; do
if [ -n "${group}" ]; then
extra_args+=("--group=${group}")
fi
done <<<"${INPUT_GROUPS}"
fi
uv pip install . "${extra_args[@]}"
+203
View File
@@ -0,0 +1,203 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
# TODO: This will be invoked by a 'Release' workflow in the future.
---
name: Build
on:
push:
tags:
- v*
schedule:
# From: https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule
# To decrease the chance of delay, schedule your workflow to run at a different time of the hour.
- cron: 42 3 * * *
timezone: America/Los_Angeles
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: sh
jobs:
meta:
name: Metadata
runs-on: ubuntu-latest
outputs:
type: ${{ steps.meta.outputs.type }}
version: ${{ steps.meta.outputs.version }}
steps:
- name: Set metadata
id: meta
run: |
type=Nightly
if [ "${GITHUB_REF_TYPE}" = tag ]; then
case ${GITHUB_REF_NAME} in
v*)
type=Release
case ${GITHUB_REF_NAME} in
*-pr*) type=Pre-${type} ;;
esac
;;
esac
fi
if [ ${type} = Nightly ]; then
version=dev+$(date --iso-8601 | tr -d -)
else
version=${GITHUB_REF_NAME}
fi
cat <<EOF >>"${GITHUB_OUTPUT}"
type=${type}
version=${version}
EOF
build:
strategy:
fail-fast: ${{ needs.meta.outputs.type != 'Nightly' }}
matrix:
include:
- os: ubuntu-22.04
os-label: Linux x86
- os: macos-15-intel
os-label: macOS x86
- os: macos-15
os-label: macOS ARM
- os: windows-2022
os-label: Windows x86
name: Build${{ needs.meta.outputs.type && format(' {0}', needs.meta.outputs.type) }}${{ matrix.os-label && format(' ({0})', matrix.os-label) }}
needs: meta
runs-on: ${{ matrix.os }}
env:
BUILD_PREFIX: tagstudio_${{ needs.meta.outputs.version }}
steps:
- name: Prepare build (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$dir = "${env:RUNNER_TEMP}/upload"
New-Item -Path ${dir} -ItemType Directory -Force
$os = ${env:RUNNER_OS}.ToLower()
$arch = uname -m
$commonPath = "${dir}/${env:BUILD_PREFIX}_${os}_${arch}"
Add-Content -Path ${env:GITHUB_ENV} -Value @"
BUILD_ARTIFACT_DEFAULT=${commonPath}.zip
BUILD_ARTIFACT_PORTABLE=${commonPath}_portable.zip
"@ -Encoding utf8
- name: Prepare build (non-Windows)
if: runner.os != 'Windows'
run: |
dir=${RUNNER_TEMP}/upload
mkdir -p "${dir}"
os=$(
tr '[:upper:]' '[:lower:]' <<EOF
${RUNNER_OS}
EOF
)
arch=$(uname -m)
common_path=${dir}/${BUILD_PREFIX}_${os}_${arch}
artifact_default=${common_path}.tar.gz
if [ "${RUNNER_OS}" = macOS ]; then
artifact_portable=
else
artifact_portable=${common_path}_portable.tar.gz
fi
cat <<EOF >>"${GITHUB_ENV}"
BUILD_ARTIFACT_DEFAULT=${artifact_default}
${artifact_portable:+BUILD_ARTIFACT_PORTABLE=${artifact_portable}}
EOF
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Python
uses: ./.github/actions/setup-python
with:
groups: build
- parallel:
- name: Run PyInstaller (Linux)
if: runner.os == 'Linux'
run: |
pyinstaller --distpath dist/default --workpath build/default scripts/tagstudio.spec
tar czf "${BUILD_ARTIFACT_DEFAULT}" -C dist/default/tagstudio .
- name: Run PyInstaller (Linux portable)
if: runner.os == 'Linux'
run: |
pyinstaller --distpath dist/portable --workpath build/portable scripts/tagstudio.spec -- --portable
tar czf "${BUILD_ARTIFACT_PORTABLE}" -C dist/portable tagstudio
- name: Run PyInstaller (macOS)
if: runner.os == 'macOS'
run: |
scripts/tagstudio.spec
tar czf "${BUILD_ARTIFACT_DEFAULT}" -C dist TagStudio.app
- name: Run PyInstaller (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
PyInstaller --distpath dist/default --workpath build/default scripts/tagstudio.spec
Compress-Archive -Path dist/default/TagStudio -DestinationPath ${env:BUILD_ARTIFACT_DEFAULT}
- name: Run PyInstaller (Windows portable)
if: runner.os == 'Windows'
shell: pwsh
run: |
PyInstaller --distpath dist/portable --workpath build/portable scripts/tagstudio.spec -- --portable
Compress-Archive -Path dist/portable/TagStudio.exe -DestinationPath ${env:BUILD_ARTIFACT_PORTABLE}
- parallel:
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
path: ${{ env.BUILD_ARTIFACT_DEFAULT }}
if-no-files-found: error
# Short retention for 'Pre-Release' and 'Release' because they get uploaded to a GitHub release.
retention-days: &upload-artifact-retention-days ${{ case(needs.meta.outputs.type == 'Nightly', '7', '1') }}
archive: false
- name: Upload build artifact (portable)
if: env.BUILD_ARTIFACT_PORTABLE != ''
uses: actions/upload-artifact@v7
with:
path: ${{ env.BUILD_ARTIFACT_PORTABLE }}
if-no-files-found: error
retention-days: *upload-artifact-retention-days
archive: false
upload:
permissions:
contents: write
name: Upload Builds to Release
needs: [meta, build]
if: needs.meta.outputs.type != 'Nightly'
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/download
merge-multiple: 'true'
skip-decompress: 'true'
- name: Upload build artifacts to release
uses: softprops/action-gh-release@v3
with:
files: ${{ runner.temp }}/download/*
+56
View File
@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
# TODO: In the future, docs will be built (but not published) for PRs.
---
name: Build Docs
on:
push:
branches:
- main
paths:
- .github/actions/setup-python/action.yml
- .github/workflows/build_docs.yml
- docs/**
- mkdocs.yml
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: sh
jobs:
publish:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
permissions:
contents: write
name: Publish Docs
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Python
uses: ./.github/actions/setup-python
with:
groups: docs
- name: Use cache
uses: actions/cache@v6
with:
key: mkdocs-material-${{ github.run_id }}
path: .cache
restore-keys: |
mkdocs-material-
- name: Run mkdocs
env:
DISABLE_MKDOCS_2_WARNING: 'true'
run: mkdocs gh-deploy --force
+168
View File
@@ -0,0 +1,168 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
---
name: Checks (Python)
on:
pull_request:
paths: &on_paths
- .github/actions/setup-python/action.yml
- .github/workflows/checks_python.yml
- .editorconfig
- pyproject.toml
- uv.lock
- '**.py'
- '**.pyi'
- 'src/tagstudio/resources/**'
push:
paths: *on_paths
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: sh
jobs:
run-conditions:
name: Run Conditions
runs-on: ubuntu-latest
outputs:
pyright: ${{ steps.run-conditions.outputs.pyright }}
pytest: ${{ steps.run-conditions.outputs.pytest }}
ruff: ${{ steps.run-conditions.outputs.ruff }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v47.0.6
with:
files_yaml: |
generic:
- .github/workflows/checks_python.yml
- pyproject.toml
- uv.lock
- '**.py'
- '**.pyi'
pyright:
- .github/actions/setup-python/action.yml
pytest:
- .github/actions/setup-python/action.yml
- 'src/tagstudio/resources/**'
ruff:
- .editorconfig
- name: Set run conditions
id: run-conditions
run: |
cat <<EOF >>"${GITHUB_OUTPUT}"
pyright=${{ steps.changed-files.outputs.generic_any_changed || steps.changed-files.outputs.pyright_any_changed }}
pytest=${{ steps.changed-files.outputs.generic_any_changed || steps.changed-files.outputs.pytest_any_changed }}
ruff=${{ steps.changed-files.outputs.generic_any_changed || steps.changed-files.outputs.ruff_any_changed }}
EOF
check-pyright:
name: Pyright
needs: run-conditions
if: needs.run-conditions.outputs.pyright == 'true'
runs-on: ubuntu-latest
steps:
- &checkout
name: Checkout repository
uses: actions/checkout@v7
- name: Setup Python
uses: ./.github/actions/setup-python
with:
groups: |
pyright
pytest
- name: Execute Pyright
run: pyright
check-pytest:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
os-label: Linux
- os: windows-2022
os-label: Windows
name: pytest${{ matrix.os-label && format(' ({0})', matrix.os-label) }}
needs: run-conditions
if: needs.run-conditions.outputs.pytest == 'true'
runs-on: ${{ matrix.os }}
steps:
- *checkout
- name: Setup Python
uses: ./.github/actions/setup-python
with:
groups: pytest
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 \
libgl1 \
libopengl0 \
libpulse0 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-xinerama0 \
libxkbcommon-x11-0 \
libyaml-dev \
ripgrep \
x11-utils
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: brew install ripgrep
- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: choco install ripgrep
- name: Execute pytest (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: pytest
- name: Execute pytest (non-Windows)
if: runner.os != 'Windows'
run: pytest
check-ruff:
name: Ruff
needs: run-conditions
if: needs.run-conditions.outputs.ruff == 'true'
runs-on: ubuntu-latest
steps:
- *checkout
- parallel:
- name: Run Ruff linter
uses: astral-sh/ruff-action@v4.0.0
- name: Run Ruff formatter
uses: astral-sh/ruff-action@v4.0.0
with:
args: format --check
-51
View File
@@ -1,51 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
---
name: Publish Docs
on:
push:
branches:
- main
paths:
- .github/workflows/publish_docs.yaml
- docs/**
- mkdocs.yml
- CHANGELOG.md
permissions:
contents: write
concurrency:
group: publish-docs
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
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-
- name: Execute mkdocs
run: mkdocs gh-deploy --force
-31
View File
@@ -1,31 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
---
name: Pyright
on: [push, pull_request]
jobs:
pyright:
name: Run Pyright
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pyright,pytest]
- name: Execute Pyright
uses: jordemort/action-pyright@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: ${{ github.event_name == 'pull_request' && 'github-pr-review' || 'github-check' }}
-70
View File
@@ -1,70 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
---
name: pytest
on: [push, pull_request]
jobs:
pytest-linux:
name: Run pytest (Linux)
runs-on: ubuntu-24.04
steps:
- &checkout
name: Checkout repo
uses: actions/checkout@v4
- &setup-python
name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- &install-dependencies
name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pytest]
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 \
libgl1 \
libopengl0 \
libpulse0 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-xinerama0 \
libxkbcommon-x11-0 \
libyaml-dev \
ripgrep \
x11-utils
- name: Execute pytest
run: |
xvfb-run pytest --cov-report xml --cov=tagstudio
pytest-windows:
name: Run pytest (Windows)
runs-on: windows-2025
steps:
- *checkout
- *setup-python
- *install-dependencies
- name: Install system dependencies
run: |
choco install ripgrep
- name: Execute pytest
run: |
pytest
-156
View File
@@ -1,156 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
---
name: Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
linux:
strategy:
matrix:
build-type: ["", portable]
include:
- build-type: ""
build-flag: ""
suffix: ""
- build-type: portable
build-flag: --portable
suffix: _portable
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
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
macos:
strategy:
matrix:
os-version: ["14", "15"]
include:
- os-version: "14"
arch: x86_64
- os-version: "15"
arch: aarch64
runs-on: macos-${{ matrix.os-version }}
env:
# INFO: Even though we run on 14, target towards compatibility
MACOSX_DEPLOYMENT_TARGET: "11.0"
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
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
windows:
strategy:
matrix:
build-type: ["", portable]
include:
- build-type: ""
build-flag: ""
suffix: ""
file-end: ""
- build-type: portable
build-flag: --portable
suffix: _portable
file-end: .exe
runs-on: windows-2022
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
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:
- 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/*
tagstudio_linux_x86_64_portable/*
tagstudio_macos_x86_64/*
tagstudio_macos_aarch64/*
tagstudio_windows_x86_64/*
tagstudio_windows_x86_64_portable/*
-12
View File
@@ -1,12 +0,0 @@
# SPDX-FileCopyrightText: 2020 Free Software Foundation Europe e.V.
# SPDX-License-Identifier: CC0-1.0
name: REUSE compliance check
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v6
+25
View File
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
---
name: REUSE Compliance Check
on: [pull_request, push]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: sh
jobs:
check:
name: REUSE
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Run REUSE
uses: fsfe/reuse-action@v6
-33
View File
@@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
---
name: Ruff
on: [push, pull_request]
jobs:
ruff-format:
name: Run Ruff format
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Execute Ruff format
uses: astral-sh/ruff-action@v3
with:
version: 0.15.17
args: format --check
ruff-check:
name: Run Ruff check
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Execute Ruff check
uses: astral-sh/ruff-action@v3
with:
version: 0.15.17
args: check
+2
View File
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
---
repos:
- repo: local
+5
View File
@@ -8,3 +8,8 @@ bracketSpacing = true
objectWrap = "preserve"
quoteProps = "as-needed"
singleQuote = false
[[overrides]]
files = [".github/**/*.yml", ".github/**/*.yaml"]
[overrides.options]
singleQuote = true
+3 -4
View File
@@ -3,11 +3,10 @@
# TagStudio: A User-Focused Photo & File Management System
[![Downloads](https://img.shields.io/github/downloads/TagStudioDev/TagStudio/total.svg?maxAge=2592001)](https://github.com/TagStudioDev/TagStudio/releases)
[![Downloads](https://img.shields.io/github/downloads/TagStudioDev/TagStudio/total.svg)](https://github.com/TagStudioDev/TagStudio/releases)
[![Translations](https://hosted.weblate.org/widget/tagstudio/strings/svg-badge.svg)](https://hosted.weblate.org/projects/tagstudio/strings/)
[![PyTest](https://github.com/TagStudioDev/TagStudio/actions/workflows/pytest.yaml/badge.svg)](https://github.com/TagStudioDev/TagStudio/actions/workflows/pytest.yaml)
[![MyPy](https://github.com/TagStudioDev/TagStudio/actions/workflows/mypy.yaml/badge.svg)](https://github.com/TagStudioDev/TagStudio/actions/workflows/mypy.yaml)
[![Ruff](https://github.com/TagStudioDev/TagStudio/actions/workflows/ruff.yaml/badge.svg)](https://github.com/TagStudioDev/TagStudio/actions/workflows/ruff.yaml)
[![REUSE Status](https://api.reuse.software/badge/github.com/TagStudioDev/TagStudio)](https://api.reuse.software/info/github.com/TagStudioDev/TagStudio)
[![Python Checks](https://github.com/TagStudioDev/TagStudio/actions/workflows/checks_python.yml/badge.svg)](https://github.com/TagStudioDev/TagStudio/actions/workflows/checks_python.yml)
<p align="center">
<img width="60%" src="src/tagstudio/resources/qt/images/tagstudio_logo-text_color.png">
-3
View File
@@ -4,8 +4,6 @@ version = 1
path = [
"tests/fixtures/**",
"contrib/.envrc-nix",
"contrib/.envrc-uv",
"contrib/.vscode/launch.json",
"docs/CNAME",
"docs/assets/**",
@@ -23,7 +21,6 @@ path = [
".github/ISSUE_TEMPLATE/**",
".github/PULL_REQUEST_TEMPLATE.md",
".gitignore",
".pre-commit-config.yaml",
"flake.lock",
]
SPDX-FileCopyrightText = "(c) TagStudio Contributors"
+3 -2
View File
@@ -1,4 +1,7 @@
# vi: ft=bash
# shellcheck shell=bash
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
# 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.
@@ -15,5 +18,3 @@ 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
+4 -3
View File
@@ -1,4 +1,7 @@
# vi: ft=sh
# shellcheck shell=bash
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
# 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.
@@ -23,10 +26,8 @@ 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]'
uv pip install --quiet --editable . --group all
cp pyproject.toml "${venv}"/pyproject.toml
fi
pre-commit install
# vi: ft=bash
+37
View File
@@ -9,6 +9,43 @@ toc_depth: 2
# :material-script-text: Changelog
## 9.6.1 <small>July 9th, 2026</small>
### Added
- feat(ui): render .pxd thumbnails by @Sola-ris in #1430
### Changed
- feat(ui): organize settings into panels by @purpletennisball in #1425
- feat(ui): left click to edit tag in tag manager by @purpletennisball in #1416
### Fixed
- fix: fix issues with updating tag aliases causing freezes by @CyanVoxel in #1412
- fix: make TagStudio python package executable by @Computerdores in #1414
- fix: use optimized SQL when selecting non-hidden entries by @racerand in #1240
- fix: fix ripgrep not being located for macOS builds by @CyanVoxel in #1427
- fix: remove invalid child_id relationships from tag_parents by @CyanVoxel in #1423
- fix(ui): fix "search for tag" function in tag manager by @CyanVoxel in #1411
- fix(ui): mouse down selects thumbnail instead of mouse up by @purpletennisball in #1420
- fix(ui): thumbnail grid not resizing to fill width by @TheBobBobs in #1433
#### Internal Changes
- fix(nix): add pyright to devshell by @Computerdores in #1415
- fix(deps): replace optional dependencies with dependency groups by @Xarvex in #1435
- feat(ci)!: complete workflows revamp by @Xarvex in #1437
#### Translations
- **French** updated by @kitsumed
- **Hungarian** updated by @smileyhead
- **Russian** updated by @romandobra
- **Spanish** updated by @JCC1998, @2004milenadiaz-source
---
## 9.6.0 <small>June 29th, 2026</small>
<p align="center">
+7 -7
View File
@@ -57,7 +57,7 @@ To install the required dependencies, you can use a dependency manager such as [
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]"
uv pip install -e . --group all
```
TagStudio should now be runnable using the `tagstudio` command.
@@ -109,7 +109,7 @@ If you choose to manually set up a virtual environment and install dependencies
3. Use the following PIP command to create an editable installation and install the required development dependencies:
```sh
pip install -e ".[dev]"
pip install -e . --group all
```
4. TagStudio should now be runnable using the `tagstudio` command.
@@ -161,7 +161,7 @@ The entry point for TagStudio is `src/tagstudio/main.py`. You can target this fi
[Ruff](https://github.com/astral-sh/ruff) is a Python linter and code formatter that helps enforce a consistent formatting style across our codebase.
Ruff is installed alongside the `pip install -e ".[dev]"` command, but is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff), PyCharm [plugin](https://plugins.jetbrains.com/plugin/20574-ruff), and [more](https://docs.astral.sh/ruff/integrations/).
Ruff is installed alongside the `pip install -e . --group all` command, but is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff), PyCharm [plugin](https://plugins.jetbrains.com/plugin/20574-ruff), and [more](https://docs.astral.sh/ruff/integrations/).
```sh title="Lint Code"
ruff check
@@ -180,7 +180,7 @@ Ruff should automatically discover the configuration options inside the [pyproje
[Pyright](https://github.com/microsoft/pyright) is a static type checker for Python that helps enforce type strictness and prevent easy-to-miss errors across our codebase.
Pyright is installed alongside the `pip install -e ".[dev]"` command, but is also available as VS Code extensions (see the [Pyright](https://marketplace.visualstudio.com/items?itemName=ms-pyright.pyright), [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance), and [basedpyright](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) extensions), a PyCharm [setting](https://www.jetbrains.com/help/pycharm/lsp-tools.html#pyright), or in the form of forks such as [basedpyright](https://docs.basedpyright.com/latest/).
Pyright is installed alongside the `pip install -e . --group all` command, but is also available as VS Code extensions (see the [Pyright](https://marketplace.visualstudio.com/items?itemName=ms-pyright.pyright), [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance), and [basedpyright](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) extensions), a PyCharm [setting](https://www.jetbrains.com/help/pycharm/lsp-tools.html#pyright), or in the form of forks such as [basedpyright](https://docs.basedpyright.com/latest/).
```sh title="Run Checks"
pyright
@@ -192,7 +192,7 @@ Pyright/basedpyright should automatically discover the configuration options ins
[Pytest](https://github.com/pytest-dev/pytest) runs our Python code against the tests inside the [`tests/`](https://github.com/TagStudioDev/TagStudio/tree/main/tests) directory.
Pytest is installed alongside the `pip install -e ".[dev]"` command.
Pytest is installed alongside the `pip install -e . --group all` command.
```sh title="Run Tests"
pytest tests/
@@ -236,13 +236,13 @@ direnv allow
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
pyinstaller scripts/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
pyinstaller scripts/tagstudio.spec -- --portable
```
The resulting executable file(s) will be located in a new folder named "dist".
+21 -24
View File
@@ -1,12 +1,12 @@
---
title: Installation
title: Installing
icon: material/download
---
<!-- SPDX-FileCopyrightText: (c) TagStudio Contributors -->
<!-- SPDX-License-Identifier: GPL-3.0-only -->
# :material-download: Installation
# :material-download: Installing
TagStudio provides executable [releases](https://github.com/TagStudioDev/TagStudio/releases) as well as full access to its [source code](https://github.com/TagStudioDev/TagStudio) under the [GPLv3](https://github.com/TagStudioDev/TagStudio/blob/main/LICENSE) license.
@@ -17,8 +17,8 @@ To download executable builds of TagStudio, visit the [Releases](https://github.
TagStudio has builds for :fontawesome-brands-windows: **Windows**, :fontawesome-brands-apple: **macOS** _(Apple Silicon & Intel)_, and :material-penguin: **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around.
<!-- 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.
!!! info "Optional Dependencies"
You may need to install [optional third-party dependencies](#optional-dependencies) such as [FFmpeg](https://ffmpeg.org/download.html) to use the full feature set of TagStudio.
<!-- prettier-ignore -->
!!! warning ":fontawesome-brands-apple: macOS "Privacy & Security" Popup"
@@ -54,7 +54,7 @@ pip install .
!!! 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]"
pip install -e . --group all
```
_See more under "[Developing](developing.md)"_
@@ -82,6 +82,19 @@ Some external dependencies are required for TagStudio to execute. Below is a tab
| [qt-multimedia](https://repology.org/project/qt) | required |
| [qt-wayland](https://repology.org/project/qt) | Wayland support |
<!-- prettier-ignore -->
!!! bug "Missing Linux Dependency Example"
An error message such as the following indicates that you're missing the `libxcb-cursor` or `xcb-util-cursor` library, depending on your distro:
```
qt.qpa.plugin: From 6.5.0, xcb-cursor0 or libxcb-cursor0 is needed to load the Qt xcb platform plugin.
qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "/tmp/_MEIayuTiW/cv2/qt/plugins" even though it was found.
This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.
Available platform plugins are: vnc, wayland-egl, offscreen, wayland, linuxfb, minimalegl, eglfs, minimal, vkkhrdisplay, xcb.
Aborted (core dumped)
```
### :material-nix: 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.
@@ -212,7 +225,9 @@ Finally, `inputs` can be used in a module to add the package to your packages li
Don't forget to rebuild!
## Third-Party Dependencies
## Optional Dependencies
Some TagStudio functionality such as multimedia thumbnails and playback, RAR archive thumbnails, and improved directory scanning performance will require installing optional third-party dependencies. Depending on your system, you may already have one or more of these installed. To check
<!-- prettier-ignore -->
!!! tip
@@ -242,21 +257,3 @@ To generate thumbnails for RAR-based files (like `.cbr`) you'll need an extracto
### ripgrep
A recommended tool to improve the performance of directory scanning is [`ripgrep`](https://github.com/BurntSushi/ripgrep), a Rust-based directory walker that natively integrates with our [`.ts_ignore`](ignore.md) (`.gitignore`-style) pattern matching system for excluding files and directories. Ripgrep is already pre-installed on some Linux distributions and also available from several package managers.
## Common Error Messages
### Could not load the Qt platform plugin "xcb"
If you get an error message like this one:
```
qt.qpa.plugin: From 6.5.0, xcb-cursor0 or libxcb-cursor0 is needed to load the Qt xcb platform plugin.
qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "/tmp/_MEIayuTiW/cv2/qt/plugins" even though it was found.
This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.
Available platform plugins are: vnc, wayland-egl, offscreen, wayland, linuxfb, minimalegl, eglfs, minimal, vkkhrdisplay, xcb.
Aborted (core dumped)
```
Make sure you installed `libxcb-cursor` or `xcb-util-cursor`.
+3 -3
View File
@@ -196,8 +196,8 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 202
| Added in Commit | Introduced in Release | Format |
| --------------- | ----------------------------------------------------------------------- | ------ |
| | [v9.6.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.1) | SQLite |
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 95e2fe7b4449951c385e35a2e13f0c1925f1f98e | [v9.6.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.1) | SQLite |
- Applies repairs to the `tag_parents` table, removing rows that reference child tags that have been deleted.
+20 -19
View File
@@ -47,7 +47,7 @@ Images will generate thumbnails the first time they are viewed or since the last
### :material-movie-open: Videos
Video thumbnails will default to the closest viable frame from the middle of the video. Both thumbnail generation and video playback in the Preview Panel requires [FFmpeg](install.md#third-party-dependencies) installed on your system.
Video thumbnails will default to the closest viable frame from the middle of the video. Both thumbnail generation and video playback in the Preview Panel requires [FFmpeg](install.md#optional-dependencies) installed on your system.
| Filetype | Extensions | Dependencies |
| --------------------- | ----------------------- | :----------: |
@@ -65,7 +65,7 @@ Video thumbnails will default to the closest viable frame from the middle of the
### :material-sine-wave: Audio
Audio thumbnails will default to embedded cover art (if any) and fallback to generated waveform thumbnails. Audio file playback is supported in the Preview Panel if you have [FFmpeg](install.md#third-party-dependencies) installed on your system. Audio waveforms are currently not cached.
Audio thumbnails will default to embedded cover art (if any) and fallback to generated waveform thumbnails. Audio file playback is supported in the Preview Panel if you have [FFmpeg](install.md#optional-dependencies) installed on your system. Audio waveforms are currently not cached.
| Filetype | Extensions | Dependencies |
| ------------------- | ------------------------ | :----------: |
@@ -82,23 +82,24 @@ Audio thumbnails will default to embedded cover art (if any) and fallback to gen
Preview support for office documents or well-known project file formats varies by the format and whether or not embedded thumbnails are available to be read from. OpenDocument-based files are typically supported.
| Filetype | Extensions | Preview Type |
| ------------------------------------ | --------------------- | -------------------------------------------------------------------------- |
| Blender | `.blend`, `.blend<#>` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Clip Studio Paint | `.clip` | Embedded thumbnail |
| Keynote (Apple iWork) | `.key` | Embedded thumbnail |
| Krita[^3] | `.kra`, `.krz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Mdipack (FireAlpaca, Medibang Paint) | `.mdp` | Embedded thumbnail |
| MuseScore | `.mscz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Numbers (Apple iWork) | `.numbers` | Embedded thumbnail |
| OpenDocument Presentation | `.odp`, `.fodp` | Embedded thumbnail |
| OpenDocument Spreadsheet | `.ods`, `.fods` | Embedded thumbnail |
| OpenDocument Text | `.odt`, `.fodt` | Embedded thumbnail |
| Pages (Apple iWork) | `.pages` | Embedded thumbnail |
| Paint.NET | `.pdn` | Embedded thumbnail |
| PDF | `.pdf` | First page render |
| Photoshop | `.psd` | Flattened image render |
| PowerPoint (Microsoft Office) | `.pptx`, `.ppt` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Filetype | Extensions | Preview Type |
| ------------------------------------- | --------------------- | -------------------------------------------------------------------------- |
| Blender | `.blend`, `.blend<#>` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Clip Studio Paint | `.clip` | Embedded thumbnail |
| Keynote (Apple iWork) | `.key` | Embedded thumbnail |
| Krita[^3] | `.kra`, `.krz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Mdipack (FireAlpaca, Medibang Paint) | `.mdp` | Embedded thumbnail |
| MuseScore | `.mscz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Numbers (Apple iWork) | `.numbers` | Embedded thumbnail |
| OpenDocument Presentation | `.odp`, `.fodp` | Embedded thumbnail |
| OpenDocument Spreadsheet | `.ods`, `.fods` | Embedded thumbnail |
| OpenDocument Text | `.odt`, `.fodt` | Embedded thumbnail |
| Pages (Apple iWork) | `.pages` | Embedded thumbnail |
| Paint.NET | `.pdn` | Embedded thumbnail |
| PDF | `.pdf` | First page render |
| Photoshop | `.psd` | Flattened image render |
| Pixelmator Pro (Apple Creator Studio) | `.pxd` | Embedded thumbnail |
| PowerPoint (Microsoft Office) | `.pptx`, `.ppt` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
### :material-archive: Archives
-1
View File
@@ -1,7 +1,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
{
description = "TagStudio";
+2 -2
View File
@@ -32,9 +32,8 @@ extra:
nav:
- Home:
- index.md
- Getting Started:
- Installing:
- install.md
- usage.md
- Developers:
- developing.md
- contributing.md
@@ -42,6 +41,7 @@ nav:
- Help:
- help/ffmpeg.md
- Using TagStudio:
- usage.md
- libraries.md
- entries.md
- preview-support.md
-1
View File
@@ -1,7 +1,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
{
buildPythonPackage,
cmake,
-1
View File
@@ -1,7 +1,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
{
autoPatchelfHook,
buildPythonPackage,
+1 -1
View File
@@ -120,7 +120,7 @@ pkgs.mkShellNoCC {
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]'
uv pip install --quiet --editable . --group docs --group extra --group test
cp pyproject.toml "''${venv}"/pyproject.toml
fi
+21 -9
View File
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
[project]
name = "TagStudio"
description = "A User-Focused Photo & File Management System."
version = "9.6.0"
version = "9.6.1"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.14"
@@ -42,12 +42,28 @@ dependencies = [
"semver~=3.0.4",
]
[project.optional-dependencies]
dev = ["tagstudio[mkdocs,pyright,pre-commit,pyinstaller,pytest,ruff]"]
[project.gui-scripts]
tagstudio = "tagstudio.main:main"
[dependency-groups]
all = [
{ include-group = "check" },
{ include-group = "build" },
{ include-group = "docs" },
{ include-group = "extra" },
]
check = [{ include-group = "lint" }, { include-group = "test" }]
build = [{ include-group = "pyinstaller" }]
docs = [{ include-group = "mkdocs" }]
extra = [{ include-group = "pre-commit" }]
lint = [{ include-group = "pyright" }, { include-group = "ruff" }]
test = [{ include-group = "pytest" }]
mkdocs = ["mkdocs-material[imaging]>=9.7", "mkdocs-redirects~=1.2"]
pyright = ["pyright~=1.1.409"]
pre-commit = ["pre-commit~=4.2"]
pyinstaller = ["Pyinstaller~=6.21"]
pyright = ["pyright~=1.1.409"]
pytest = [
"pytest==9.0.3",
"pytest-cov==6.1.1",
@@ -57,14 +73,10 @@ pytest = [
]
ruff = ["ruff==0.15.17"]
[project.gui-scripts]
tagstudio = "tagstudio.main:main"
[tool.hatch.build.targets.wheel]
packages = ["src/tagstudio"]
[tool.pytest.ini_options]
[tool.pytest]
qt_api = "pyside6"
pythonpath = ["src"]
filterwarnings = [
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env perl
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: CC0-1.0
use strict;
use warnings;
use feature 'say';
# From: https://regex101.com/r/vkijKf/1
if (($ARGV[0] // <STDIN>) =~ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/) {
foreach my $index (0 .. $#{^CAPTURE}) {
say ${^CAPTURE}[$index] // "";
}
}
+15 -10
View File
@@ -1,16 +1,22 @@
#!/usr/bin/env -S python -m PyInstaller
# vi: ft=python
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
import argparse
import platform
from argparse import ArgumentParser
from pathlib import Path
from tomllib import load
from PyInstaller.building.api import COLLECT, EXE, PYZ
from PyInstaller.building.build_main import Analysis
from PyInstaller.building.osx import BUNDLE
from tomllib import load
parser = ArgumentParser()
# HACK: Without this, the script will fail if empty arguments are passed.
parser.add_argument("_", nargs="*", help=argparse.SUPPRESS)
parser.add_argument("--portable", action="store_true")
options = parser.parse_args()
@@ -19,23 +25,24 @@ with open("pyproject.toml", "rb") as file:
system = platform.system()
project_root = Path("..", "src/tagstudio")
name = pyproject["name"] if system == "Windows" else "tagstudio"
icon = None
if system == "Windows":
icon = "src/tagstudio/resources/icon.ico"
icon = Path(project_root, "resources/icon.ico")
elif system == "Darwin":
icon = "src/tagstudio/resources/icon.icns"
icon = Path(project_root, "resources/icon.icns")
datafiles = [
("src/tagstudio/qt/*.json", "tagstudio/qt"),
("src/tagstudio/qt/*.qrc", "tagstudio/qt"),
("src/tagstudio/resources", "tagstudio/resources"),
(f"{project_root}/qt/*.json", "tagstudio/qt"),
(f"{project_root}/qt/*.qrc", "tagstudio/qt"),
(f"{project_root}/resources", "tagstudio/resources"),
]
a = Analysis(
["src/tagstudio/main.py"],
pathex=["src"],
[Path(project_root, "main.py")],
pathex=[],
binaries=[],
datas=datafiles,
hiddenimports=[],
@@ -100,5 +107,3 @@ if system == "Darwin":
"NSPrincipalClass": "NSApplication",
},
)
# vi: ft=python
+2 -1
View File
@@ -1,8 +1,9 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from importlib.metadata import version
VERSION: str = "9.6.0" # Major.Minor.Patch
VERSION: str = version("tagstudio") # Major.Minor.Patch
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
COPYRIGHT_YEARS: str = "2021-2026"
COPYRIGHT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien & TagStudio Contributors"
+11 -10
View File
@@ -12,14 +12,14 @@ import structlog
logger = structlog.get_logger(__name__)
FILETYPE_EQUIVALENTS = [
set(["aif", "aiff", "aifc"]),
set(["html", "htm", "xhtml", "shtml", "dhtml"]),
set(["jfif", "jpeg_large", "jpeg", "jpg_large", "jpg"]),
set(["json", "jsonc", "json5"]),
set(["md", "markdown", "mkd", "rmd"]),
set(["tar.gz", "tgz"]),
set(["xml", "xul"]),
set(["yaml", "yml"]),
{"aif", "aiff", "aifc"},
{"html", "htm", "xhtml", "shtml", "dhtml"},
{"jfif", "jpeg_large", "jpeg", "jpg_large", "jpg"},
{"json", "jsonc", "json5"},
{"md", "markdown", "mkd", "rmd"},
{"tar.gz", "tgz"},
{"xml", "xul"},
{"yaml", "yml"},
]
@@ -75,7 +75,7 @@ class MediaCategory:
extensions (set[str]): The set of file extensions associated with this category.
Includes leading ".", all lowercase, and does not need to be unique to this category.
is_iana (bool): Represents whether or not this is an IANA registered category.
is_iana (bool): Represents whether this is an IANA registered category.
"""
media_type: MediaType
@@ -257,6 +257,7 @@ class MediaCategories:
".odt",
".pages",
".pdf",
".pxd",
".rtf",
".tex",
".wpd",
@@ -336,7 +337,7 @@ class MediaCategories:
".webp",
}
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
_IWORK_SET: set[str] = {".key", ".numbers", ".pages"}
_MATERIAL_SET: set[str] = {".mtl"}
_MDIPACK_SET: set[str] = {".mdp"}
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
+59
View File
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import contextlib
from typing import override
import structlog
from tagstudio.core.utils.module_status import ModuleStatus
from tagstudio.core.utils.silent_subprocess import (
silent_run, # pyright: ignore[reportUnknownVariableType]
)
logger = structlog.get_logger(__name__)
class _FfModuleStatus(ModuleStatus):
"""A base class that implements common logic for reading FFmpeg/FFprobe version output."""
_FFMPEG = "ffmpeg"
_FFPROBE = "ffprobe"
@classmethod
def ff_version(cls, command: str):
ff_cmd = cls._which(command)
if ff_cmd:
out = silent_run([ff_cmd, "-version"], shell=False, capture_output=True, text=True)
if out.returncode == 0:
with contextlib.suppress(Exception):
return str(out.stdout).split(" ")[2]
class FfmpegStatus(_FfModuleStatus):
"""Class for getting the location and version of FFmpeg, if it exists."""
@override
@classmethod
def which(cls):
return cls._which(cls._FFMPEG)
@override
@classmethod
def _version(cls):
return cls.ff_version(cls._FFMPEG)
class FfprobeStatus(_FfModuleStatus):
"""Class for getting the location and version of FFprobe, if it exists."""
@override
@classmethod
def which(cls):
return cls._which(cls._FFPROBE)
@override
@classmethod
def _version(cls):
return cls.ff_version(cls._FFPROBE)
+75
View File
@@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import os
import platform
from shutil import which
import structlog
logger = structlog.get_logger(__name__)
user = os.environ.get("USER", None)
# NOTE: macOS does not make its PATH variable available to processes started outside the terminal.
# The following is a list of common directories to search for binaries in.
# TODO: Use a library for XDG compliance
_MACOS_BIN_LOCATIONS: list[str] = [
"", # Equates to PATH itself
# User level
"~/.local/share/bin/", # XDG-compliant user-created bin
"~/.local/bin/", # Fallback user-created bin
"~/.local/state/nix/profile/bin/", # XDG-compliant home Nix bin
"~/.nix-profile/bin/", # Fallback home Nix bin
# System level
f"/etc/profiles/per-user/{user}/bin/", # Per-user Nix bin
"/nix/var/nix/profiles/default/bin/", # Inherited Nix bin
"/opt/homebrew/bin/", # Homebrew bin
"/usr/local/bin/", # Administrator-configured bin
"/usr/bin/", # System bin
"/bin/", # Core system bin
]
class ModuleStatus:
"""An abstract base class for module status logic including the binary location and version."""
__cached_location: str | None = None
__cached_version: str | None = None
@classmethod
def which(cls) -> str | None:
raise NotImplementedError()
@classmethod
def _cache_location(cls) -> str | None:
raise NotImplementedError()
@classmethod
def version(cls) -> str | None:
if cls.__cached_version is None:
cls.__cached_version = cls._version()
return cls.__cached_version
@classmethod
def _version(cls) -> str | None:
raise NotImplementedError()
@classmethod
def _which(cls, cmd: str) -> str | None:
"""Internal method for determining the correct location for which().
Args:
cmd (str): The process command to search for.
"""
if cls.__cached_location:
return cls.__cached_location
if platform.system() == "Darwin":
for loc in _MACOS_BIN_LOCATIONS:
full_command = which(loc + cmd)
if full_command:
cls.__cached_location = full_command
return full_command
cls.__cached_location = which(cmd)
return cls.__cached_location
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import contextlib
from typing import override
import structlog
from tagstudio.core.utils.module_status import ModuleStatus
from tagstudio.core.utils.silent_subprocess import (
silent_run, # pyright: ignore[reportUnknownVariableType]
)
logger = structlog.get_logger(__name__)
class RipgrepStatus(ModuleStatus):
"""Class for getting the location and version of ripgrep, if it exists."""
@override
@classmethod
def which(cls):
return cls._which("rg")
@override
@classmethod
def _version(cls):
ripgrep_cmd = cls.which()
if ripgrep_cmd:
out = silent_run([ripgrep_cmd, "-V"], shell=False, capture_output=True, text=True)
if out.returncode == 0:
with contextlib.suppress(Exception):
return str(out.stdout).split(" ")[1].rstrip("\n")
@@ -3,14 +3,13 @@
import typing
from shutil import which
from warnings import catch_warnings
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
@@ -59,7 +58,7 @@ class PreviewPanel(PreviewPanelView):
self._containers.update_from_entry(self._selected[0])
def _toggle_ffmpeg_warning(self, enable_warning: bool = True) -> None:
if enable_warning and (not which(FFMPEG_CMD) or not which(FFPROBE_CMD)):
if enable_warning and (not FfmpegStatus.which() or not FfprobeStatus.which()):
self._ffmpeg_warning_widget.show()
return
+3 -3
View File
@@ -6,9 +6,7 @@ from pathlib import Path
import ffmpeg
from tagstudio.qt.previews.vendored.ffmpeg import (
probe, # pyright: ignore[reportUnknownVariableType]
)
from tagstudio.qt.previews.vendored.probe import probe
def is_readable_video(filepath: Path | str):
@@ -21,6 +19,8 @@ def is_readable_video(filepath: Path | str):
"""
try:
result = probe(Path(filepath))
if not result:
return False
for stream in result["streams"]:
# DRM check
if stream.get("codec_tag_string") in [
+56 -33
View File
@@ -4,7 +4,6 @@
import math
from pathlib import Path
from shutil import which
from PIL import ImageQt
from PySide6.QtCore import QSize, Qt
@@ -28,15 +27,16 @@ from tagstudio.core.constants import (
VERSION_BRANCH,
)
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.core.utils.ripgrep_status import RipgrepStatus
from tagstudio.core.utils.str_formatting import is_version_outdated
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.controllers.clickable_label import ClickableLabel
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored import ffmpeg
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.file_opener import open_file
from tagstudio.qt.views.stylesheets.stylesheets import form_content_style
from tagstudio.qt.views.stylesheets.stylesheets import form_content_style, header
class AboutModal(QWidget):
@@ -66,6 +66,10 @@ class AboutModal(QWidget):
self.content_layout.setContentsMargins(12, 12, 12, 12)
self.content_layout.setSpacing(12)
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
amber = get_ui_color(ColorType.PRIMARY, UiColor.AMBER)
# TagStudio Logo -------------------------------------------------------
self.logo_widget = QLabel()
self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.ts_logo_text_color))
@@ -78,7 +82,7 @@ class AboutModal(QWidget):
self.logo_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Version --------------------------------------------------------------
self.version_label = QLabel(f"<h3>{AboutModal.VERSION_STR}</h3>")
self.version_label = QLabel(header(AboutModal.VERSION_STR, 3))
self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Copyright ------------------------------------------------------------
@@ -94,31 +98,10 @@ class AboutModal(QWidget):
self.desc_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# System Info ----------------------------------------------------------
ff_version = ffmpeg.version()
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
amber = get_ui_color(ColorType.PRIMARY, UiColor.AMBER)
missing = Translations["generic.missing"]
found = Translations["about.module.found"]
ffmpeg_status = f'<span style="color:{red}">{missing}</span>'
if ff_version["ffmpeg"] is not None:
ffmpeg_status = (
f'<span style="color:{green}">{found}</span> (' + ff_version["ffmpeg"] + ")"
)
ffprobe_status = f'<span style="color:{red}">{missing}</span>'
if ff_version["ffprobe"] is not None:
ffprobe_status = (
f'<span style="color:{green}">{found}</span> (' + ff_version["ffprobe"] + ")"
)
ripgrep_status = f'<span style="color:{amber}">{missing}</span>'
if which("rg") is not None:
ripgrep_status = f'<span style="color:{green}">{found}</span>'
self.system_info_widget = QWidget()
self.system_info_layout = QFormLayout(self.system_info_widget)
self.system_info_layout.setSpacing(4)
self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
# Version
@@ -149,46 +132,85 @@ class AboutModal(QWidget):
# TODO: Add row for "App Cache Path" (currently that TagStudio.ini file)
# Optional Modules -----------------------------------------------------
self.parent_optional_modules_widget = QWidget()
self.parent_optional_modules_layout = QVBoxLayout(self.parent_optional_modules_widget)
self.parent_optional_modules_layout.setContentsMargins(0, 0, 0, 0)
self.parent_optional_modules_layout.setSpacing(0)
# Subtitle
self.optional_modules_label = QLabel(header(Translations["about.modules.title"], 4))
self.optional_modules_widget = QWidget()
self.optional_modules_layout = QFormLayout(self.optional_modules_widget)
self.optional_modules_layout.setSpacing(4)
self.optional_modules_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
ffmpeg_ver = FfmpegStatus.version()
ffprobe_ver = FfprobeStatus.version()
ripgrep_ver = RipgrepStatus.version()
missing = Translations["generic.missing"]
found = Translations["about.module.found"]
ffmpeg_status = f'<span style="color:{red}">{missing}</span>'
if ffmpeg_ver is not None:
ffmpeg_status = f'<span style="color:{green}">{found}</span> (' + ffmpeg_ver + ")"
ffprobe_status = f'<span style="color:{red}">{missing}</span>'
if ffprobe_ver is not None:
ffprobe_status = f'<span style="color:{green}">{found}</span> (' + ffprobe_ver + ")"
ripgrep_status = f'<span style="color:{amber}">{missing}</span>'
if ripgrep_ver is not None:
ripgrep_status = f'<span style="color:{green}">{found}</span> (' + ripgrep_ver + ")"
# FFmpeg Status
ffmpeg_path_title = QLabel("FFmpeg")
ffmpeg_path_content = ClickableLabel(f"{ffmpeg_status}")
ffmpeg_location = which(ffmpeg._get_ffmpeg_location()) # pyright: ignore[reportPrivateUsage]
ffmpeg_location = FfmpegStatus.which()
if ffmpeg_location:
ffmpeg_path_content.clicked.connect(
lambda: open_file(ffmpeg_location, file_manager=True)
)
ffmpeg_path_content.setCursor(Qt.CursorShape.PointingHandCursor)
ffmpeg_path_content.setToolTip(ffmpeg_location)
ffmpeg_path_content.setStyleSheet(form_content_style())
self.system_info_layout.addRow(ffmpeg_path_title, ffmpeg_path_content)
self.optional_modules_layout.addRow(ffmpeg_path_title, ffmpeg_path_content)
ffmpeg_path_content.setMaximumWidth(ffmpeg_path_content.sizeHint().width())
# FFprobe Status
ffprobe_path_title = QLabel("FFprobe")
ffprobe_path_content = ClickableLabel(f"{ffprobe_status}")
ffprobe_location = which(ffmpeg._get_ffprobe_location()) # pyright: ignore[reportPrivateUsage]
ffprobe_location = FfprobeStatus.which()
if ffprobe_location:
ffprobe_path_content.clicked.connect(
lambda: open_file(ffprobe_location, file_manager=True)
)
ffprobe_path_content.setCursor(Qt.CursorShape.PointingHandCursor)
ffprobe_path_content.setToolTip(ffprobe_location)
ffprobe_path_content.setStyleSheet(form_content_style())
self.system_info_layout.addRow(ffprobe_path_title, ffprobe_path_content)
self.optional_modules_layout.addRow(ffprobe_path_title, ffprobe_path_content)
ffprobe_path_content.setMaximumWidth(ffprobe_path_content.sizeHint().width())
# ripgrep Status
# TODO: Add a central class to find ripgrep info, similar to ffmpeg
ripgrep_path_title = QLabel("ripgrep") # NOTE: Don't localize
ripgrep_path_content = ClickableLabel()
ripgrep_path_content.setText(f"{ripgrep_status}") # TODO: Pass in constructor after #1386
ripgrep_location = which("rg")
ripgrep_location = RipgrepStatus.which()
if ripgrep_location:
ripgrep_path_content.clicked.connect(
lambda: open_file(ripgrep_location, file_manager=True)
)
ripgrep_path_content.setCursor(Qt.CursorShape.PointingHandCursor)
ripgrep_path_content.setToolTip(ripgrep_location)
ripgrep_path_content.setStyleSheet(form_content_style())
ripgrep_path_content.setMaximumWidth(ripgrep_path_content.sizeHint().width())
self.system_info_layout.addRow(ripgrep_path_title, ripgrep_path_content)
self.optional_modules_layout.addRow(ripgrep_path_title, ripgrep_path_content)
self.parent_optional_modules_layout.addWidget(self.optional_modules_label)
self.parent_optional_modules_layout.addWidget(self.optional_modules_widget)
# Links ----------------------------------------------------------------
@@ -217,6 +239,7 @@ class AboutModal(QWidget):
self.content_layout.addWidget(self.version_label)
self.content_layout.addWidget(self.desc_label)
self.content_layout.addWidget(self.system_info_widget)
self.content_layout.addWidget(self.parent_optional_modules_widget)
self.content_layout.addStretch(1)
self.content_layout.addWidget(self.links_label)
self.content_layout.addWidget(self.copyright_label)
+25 -4
View File
@@ -300,11 +300,25 @@ class ItemThumb(FlowWidget):
# NOTE: self.item_id seems to act as a reference here and does not need to be updated inside
# QtDriver.update_thumbs() while item_thumb.delete_action does.
# If this behavior ever changes, move this method back to QtDriver.update_thumbs().
self.thumb_button.pressed.connect(
lambda: (
self.toggle_item_selection()
if (
QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier
or not self.thumb_button.selected
)
else None
)
)
self.thumb_button.clicked.connect(
lambda: self.driver.toggle_item_selection(
self.item_id,
append=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier),
bridge=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier),
lambda: (
self.toggle_item_selection()
if (
QGuiApplication.keyboardModifiers() != Qt.KeyboardModifier.ControlModifier
and self.thumb_button.selected
)
else None
)
)
self.set_mode(mode)
@@ -317,6 +331,13 @@ class ItemThumb(FlowWidget):
def is_archived(self) -> bool:
return self.badge_active[BadgeType.ARCHIVED]
def toggle_item_selection(self):
self.driver.toggle_item_selection(
self.item_id,
append=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier),
bridge=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier),
)
def set_mode(self, mode: ItemType | None) -> None:
if mode is None:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=True)
+20 -13
View File
@@ -623,7 +623,7 @@ class ThumbRenderer(QObject):
image (Image.Image): The image to apply the edge to.
edge (tuple[Image.Image, Image.Image]): The edge images to apply.
Item 0 is the inner highlight, and item 1 is the outer shadow.
faded (bool): Whether or not to apply a faded version of the edge.
faded (bool): Whether to apply a faded version of the edge.
Used for light themes.
"""
opacity: float = 1.0 if not faded else 0.8
@@ -847,7 +847,7 @@ class ThumbRenderer(QObject):
@staticmethod
def _krita_thumb(filepath: Path) -> Image.Image | None:
"""Extract and render a thumbnail for an Krita file.
"""Extract and render a thumbnail for a Krita file.
Args:
filepath (Path): The path of the file.
@@ -1218,14 +1218,18 @@ class ThumbRenderer(QObject):
return im
@staticmethod
def _iwork_thumb(filepath: Path) -> Image.Image | None:
"""Extract and render a thumbnail for an Apple iWork (Pages, Numbers, Keynote) file.
def _apple_embedded_thumb(filepath: Path) -> Image.Image | None:
"""Extract and render an apple embedded thumbnail (iWork, Apple Creative Studio).
Args:
filepath (Path): The path of the file.
"""
preview_thumb_dir = "preview.jpg"
quicklook_thumb_dir = "QuickLook/Thumbnail.jpg"
thumb_files: list[str] = [
"preview.jpg",
"QuickLook/Preview.heic",
"QuickLook/Thumbnail.jpg",
"QuickLook/Thumbnail.heic",
]
im: Image.Image | None = None
def get_image(path: str) -> Image.Image | None:
@@ -1240,10 +1244,10 @@ class ThumbRenderer(QObject):
thumb: Image.Image | None = None
# Check if the file exists in the zip
if preview_thumb_dir in zip_file.namelist():
thumb = get_image(preview_thumb_dir)
elif quicklook_thumb_dir in zip_file.namelist():
thumb = get_image(quicklook_thumb_dir)
for thumb_file in thumb_files:
if thumb_file in zip_file.namelist():
thumb = get_image(thumb_file)
break
else:
logger.error("Couldn't render thumbnail", filepath=filepath)
@@ -1314,7 +1318,7 @@ class ThumbRenderer(QObject):
page_size *= size / page_size.height()
else:
page_size *= size / page_size.width()
# Enlarge image for antialiasing
# Enlarge image for anti-aliasing
scale_factor = 2.5
page_size *= scale_factor
# Render image with no anti-aliasing for speed
@@ -1865,8 +1869,11 @@ class ThumbRenderer(QObject):
):
image = self._open_doc_thumb(_filepath)
# Apple iWork Suite ============================================
elif MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES):
image = self._iwork_thumb(_filepath)
elif (
MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES)
or ext == ".pxd"
):
image = self._apple_embedded_thumb(_filepath)
# Plain Text ===================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True
@@ -1,102 +0,0 @@
# SPDX-FileCopyrightText: (c) 2022 Karl Kroening (kkroening)
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
import contextlib
import json
import os
import platform
import subprocess
from shutil import which
import ffmpeg
import structlog
from tagstudio.core.utils.silent_subprocess import silent_popen, silent_run
logger = structlog.get_logger(__name__)
user = os.environ.get("USER", None)
FFMPEG_MACOS_LOCATIONS: list[str] = [
"",
"/opt/homebrew/bin/",
"/usr/local/bin/",
f"/etc/profiles/per-user/{user}/bin",
]
# TODO: Make this more intuitive to use in other classes
def _get_ffprobe_location() -> str:
cmd: str = "ffprobe"
if platform.system() == "Darwin":
for loc in FFMPEG_MACOS_LOCATIONS:
if which(loc + cmd):
cmd = loc + cmd
break
logger.info(
f"[FFmpeg] Using FFprobe location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
)
return cmd
# TODO: Make this more intuitive to use in other classes
def _get_ffmpeg_location() -> str:
cmd: str = "ffmpeg"
if platform.system() == "Darwin":
for loc in FFMPEG_MACOS_LOCATIONS:
if which(loc + cmd):
cmd = loc + cmd
break
logger.info(
f"[FFmpeg] Using FFmpeg location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
)
return cmd
FFPROBE_CMD = _get_ffprobe_location()
FFMPEG_CMD = _get_ffmpeg_location()
def probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
"""Run ffprobe on the specified file and return a JSON representation of the output.
Raises:
:class:`ffmpeg.Error`: if ffprobe returns a non-zero exit code,
an :class:`Error` is returned with a generic error message.
The stderr output can be retrieved by accessing the
``stderr`` property of the exception.
"""
args = [cmd, "-show_format", "-show_streams", "-of", "json"]
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs) # pyright: ignore[reportAttributeAccessIssue]
args += [filename]
# PATCHED
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
communicate_kwargs = {}
if timeout is not None:
communicate_kwargs["timeout"] = timeout
out, err = p.communicate(**communicate_kwargs)
if p.returncode != 0:
raise ffmpeg.Error("ffprobe", out, err)
return json.loads(out.decode("utf-8"))
def version():
"""Checks the version of FFmpeg and FFprobe and returns None if they dont exist."""
version: dict[str, str | None] = {"ffmpeg": None, "ffprobe": None}
if which(FFMPEG_CMD):
ret = silent_run([FFMPEG_CMD, "-version"], shell=False, capture_output=True, text=True)
if ret.returncode == 0:
with contextlib.suppress(Exception):
version["ffmpeg"] = str(ret.stdout).split(" ")[2]
if which(FFPROBE_CMD):
ret = silent_run([FFPROBE_CMD, "-version"], shell=False, capture_output=True, text=True)
if ret.returncode == 0:
with contextlib.suppress(Exception):
version["ffprobe"] = str(ret.stdout).split(" ")[2]
return version
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: (c) 2022 Karl Kroening (kkroening)
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
import json
import subprocess
from pathlib import Path
import ffmpeg
import structlog
from tagstudio.core.utils.ffmpeg_status import FfprobeStatus
from tagstudio.core.utils.silent_subprocess import (
silent_popen, # pyright: ignore[reportUnknownVariableType]
)
logger = structlog.get_logger(__name__)
def probe(filename: Path | str, timeout: int | None = None, **kwargs: ...):
"""Run ffprobe on the specified file and return a JSON representation of the output.
Raises:
Error: If ffprobe returns a non-zero exit code, an Error is raised
with a generic error message. The stderr output can be retrieved
by accessing the `stderr` property of the exception.
"""
ffprobe_cmd: str | None = FfprobeStatus.which()
if not ffprobe_cmd:
return
args: list[str] = [ffprobe_cmd, "-show_format", "-show_streams", "-of", "json"]
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs) # pyright: ignore
args += [filename] # pyright: ignore
# PATCHED
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
communicate_kwargs = {}
if timeout is not None:
communicate_kwargs["timeout"] = timeout
out, err = p.communicate(**communicate_kwargs)
if p.returncode != 0:
raise ffmpeg.Error("ffprobe", out, err)
return json.loads(out.decode("utf-8"))
@@ -18,7 +18,7 @@ from tempfile import NamedTemporaryFile
from pydub.logging_utils import log_conversion, log_subprocess_output
from pydub.utils import fsdecode
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus
try:
from itertools import izip
@@ -161,7 +161,7 @@ class _AudioSegment:
slice = a[5000:10000] # get a slice from 5 to 10 seconds of an mp3
"""
converter = FFMPEG_CMD
converter = FfmpegStatus.which()
# TODO: remove in 1.0 release
# maintain backwards compatibility for ffmpeg attr (now called converter)
@@ -727,7 +727,7 @@ class _AudioSegment:
stdin_parameter = None
stdin_data = None
else:
if cls.converter == FFMPEG_CMD:
if cls.converter == FfmpegStatus.which():
conversion_command += [
"-read_ahead_limit",
str(read_ahead_limit),
@@ -14,13 +14,13 @@ from pydub.utils import (
get_extra_info,
)
from tagstudio.core.utils.ffmpeg_status import FfprobeStatus
from tagstudio.core.utils.silent_subprocess import silent_popen
from tagstudio.qt.previews.vendored.ffmpeg import FFPROBE_CMD
def _mediainfo_json(filepath, read_ahead_limit=-1):
"""Return json dictionary with media info(codec, duration, size, bitrate...) from filepath."""
prober = FFPROBE_CMD
prober = FfprobeStatus.which()
command_args = [
"-v",
"info",
+6 -5
View File
@@ -48,8 +48,8 @@ class ThumbGridLayout(QLayout):
self._renderer.updated.connect(self._on_rendered)
self._render_cutoff: float = 0.0
# _entry_ids[StartIndex:EndIndex]
self._last_page_update: tuple[int, int] | None = None
# _entry_ids[StartIndex:EndIndex], per_row
self._last_page_update: tuple[int, int, int] | None = None
self._scroll_to: int | None = None
@@ -215,7 +215,8 @@ class ThumbGridLayout(QLayout):
start = offset * per_row
end = start + (visible_rows * per_row)
self.visible_changed.emit(self._entry_ids[start])
first_visible = self._entry_ids[start] if 0 <= start < len(self._entry_ids) else None
self.visible_changed.emit(first_visible)
# Load closest off screen rows
start -= per_row * 3
@@ -223,9 +224,9 @@ class ThumbGridLayout(QLayout):
start = max(0, start)
end = min(len(self._entry_ids), end)
if (start, end) == self._last_page_update:
if (start, end, per_row) == self._last_page_update:
return
self._last_page_update = (start, end)
self._last_page_update = (start, end, per_row)
# Clear render queue if len > 2 pages
if len(self.driver.thumb_job_queue.queue) > (per_row * visible_rows * 2):
+22 -1
View File
@@ -39,7 +39,6 @@ from PySide6.QtGui import (
)
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QPushButton, QScrollArea
# This import has side-effect of importing PySide resources
import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport]
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH
from tagstudio.core.driver import DriverMixin
@@ -52,6 +51,11 @@ from tagstudio.core.library.refresh import RefreshTracker
from tagstudio.core.media_types import MediaCategories
from tagstudio.core.query_lang.util import ParsingError
from tagstudio.core.ts_core import TagStudioCore
# This import has side-effect of importing PySide resources
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.core.utils.module_status import ModuleStatus
from tagstudio.core.utils.ripgrep_status import RipgrepStatus
from tagstudio.core.utils.str_formatting import is_version_outdated
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
@@ -320,6 +324,9 @@ class QtDriver(DriverMixin, QObject):
timer.start(500)
timer.timeout.connect(lambda: None)
# Detect optional modules and versions for logging
self.log_optional_modules()
# self.main_window = loader.load(home_path)
self.main_window = MainWindow(self)
self.main_window.setWindowTitle(self.base_title)
@@ -1755,3 +1762,17 @@ class QtDriver(DriverMixin, QObject):
def clear_selected(self):
self._selected.clear()
self.main_window.thumb_layout.update_selected()
def log_optional_modules(self) -> None:
"""Logs the status of optional modules."""
status_classes: list[tuple[str, type[ModuleStatus]]] = [
("FFmpeg", FfmpegStatus),
("FFprobe", FfprobeStatus),
("ripgrep", RipgrepStatus),
]
for name, sc in status_classes:
if sc.which():
logger.info(f"[QtDriver] {name} found", which=sc.which(), version=sc.version())
else:
logger.warning(f"[QtDriver] {sc} not found")
@@ -205,14 +205,16 @@ def container_style() -> str:
def form_content_style() -> str:
return f"""
background-color: {
QLabel{{
background-color: {
Theme.COLOR_BG.value
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
else Theme.COLOR_BG_LIGHT.value
};
border-radius: 3px;
font-weight: 500;
padding: 1px;
border-radius: 3px;
font-weight: 500;
padding: 1px;
}}
"""
@@ -3,6 +3,7 @@
"about.description": "TagStudio ist eine Anwendung zum organisieren von Fotos und Dateien mit einem zugrunde liegendem Tag-basierten System, welches sich darauf konzentriert, dem Nutzer Freiraum und Flexibilität zu bieten. Keine proprietären Programme oder Formate, kein Meer an Hilfsdateien und keine komplette Umwälzung deiner Dateisystemstruktur.",
"about.documentation": "Dokumentation",
"about.module.found": "Gefunden",
"about.modules.title": "Optionale Module",
"about.title": "Über TagStudio",
"about.website": "Webseite",
"app.git": "Git Commit",
@@ -4,6 +4,7 @@
"about.description": "TagStudio is a photo and file organization application with an underlying tag-based system that focuses on giving freedom and flexibility to the user. No proprietary programs or formats, no sea of sidecar files, and no complete upheaval of your filesystem structure.",
"about.documentation": "Documentation",
"about.module.found": "Found",
"about.modules.title": "Optional Modules",
"about.title": "About TagStudio",
"about.version": "Version",
"about.version.latest": "{built_version} (Latest Release: {latest_version})",
+60 -1
View File
@@ -1,9 +1,13 @@
{
"about.app_cache_path": "Ruta caché aplicación",
"about.config_path": "Ruta de Configuración",
"about.description": "TagStudio es una aplicación para organizar fotografías y archivos que utiliza un sistema de etiquetas subyacentes centrado en dar libertad y flexibilidad al usuario. Sin programas ni formatos propios, ni un mar de archivos y sin trastornar completamente la estructura de tu sistema de archivos.",
"about.documentation": "Documentación",
"about.module.found": "Encontrado",
"about.modules.title": "Modulos Opcionales",
"about.title": "Acerca de TagStudio",
"about.version": "Versión",
"about.version.latest": "{built_version} (Última versión: {latest_version})",
"about.website": "Página web",
"app.git": "Commit de Git",
"app.pre_release": "Pre-Lanzamiento",
@@ -65,17 +69,30 @@
"entries.unlinked.remove": "Eliminar Entradas No Vinculadas",
"entries.unlinked.remove_alt": "Quit&ar entradas desvinculadas",
"entries.unlinked.scanning": "Buscando entradas no enlazadas en la biblioteca...",
"entries.unlinked.search_and_relink": "&Buscar && volver a vincular",
"entries.unlinked.search_and_relink": "&Buscar && Revincular",
"entries.unlinked.title": "Corregir entradas no vinculadas",
"entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
"field.add": "Añadir campo",
"field.add.plural": "Añadir Campos",
"field.confirm_remove": "¿Está seguro de que desea eliminar el campo \"{name}\"?",
"field.copy": "Copiar Campo",
"field.edit": "Editar Campo",
"field.field_name_required": "Nombre del Campo (Obligatorio)",
"field.mixed_data": "Datos variados",
"field.name": "Nombre",
"field.paste": "Pegar Campo",
"field.remove": "Eliminar campo",
"field.text.is_multiline": "Multilínea",
"field.type": "Tipo",
"field_template.all_field_templates": "Todas las plantillas de campos",
"field_template.confirm_delete": "¿Seguro que quieres eliminar la plantilla de Campos \"{field_template_name}\"?",
"field_template.create": "Crear Plantilla de Campos",
"field_template.create_add": "Crear && Añadir \"{query}\"",
"field_template.delete": "Eliminar Plantilla de Campos",
"field_template.edit": "Editar plantilla de campos",
"field_template.new": "Nueva plantilla de campos",
"field_template_manager.title": "Plantillas de campos de biblioteca",
"field_type.datetime": "Fecha y Hora",
"field_type.text": "Texto",
"field_type.unknown": "Tipo Desconocido",
@@ -120,6 +137,7 @@
"generic.delete_alt": "&Eliminar",
"generic.done": "Terminado",
"generic.done_alt": "&Hecho",
"generic.dont_remind": "No me lo vuelvas a recordar",
"generic.edit": "Editar",
"generic.edit_alt": "&Editar",
"generic.filename": "Nombre de archivo",
@@ -144,6 +162,7 @@
"home.search": "Buscar",
"home.search.view_limit": "Límite visualización:",
"home.search_entries": "Buscar entradas",
"home.search_field_templates": "Buscar plantillas de Campos",
"home.search_library": "Buscar el biblioteca",
"home.search_tags": "Buscar etiquetas",
"home.show_hidden_entries": "Mostrar entradas ocultas",
@@ -178,6 +197,36 @@
"json_migration.title.new_lib": "<h2>v9.5+ biblioteca</h2>",
"json_migration.title.old_lib": "<h2>v9.4 biblioteca</h2>",
"landing.open_create_library": "Abrir/Crear biblioteca {shortcut}",
"language.am": "Idioma amhárico",
"language.ceb": "Cebuano",
"language.cs": "Checo",
"language.da": "Danés",
"language.de": "Alemán",
"language.el": "Griego",
"language.en": "Inglés",
"language.es": "Español",
"language.fi": "Finlandés",
"language.fil": "Filipino",
"language.fr": "Francés",
"language.hu": "Húngaro",
"language.is": "Islandés",
"language.it": "Italiano",
"language.ja": "Japonés",
"language.nb_NO": "Noruego (bokmål)",
"language.nl": "Neerlandés",
"language.pl": "Polaco",
"language.pt": "Portugués",
"language.pt_BR": "Portugués (Brasil)",
"language.qpv": "Viossa",
"language.ro": "Rumano",
"language.ru": "Ruso",
"language.sv": "Sueco",
"language.ta": "Tamil",
"language.th": "Tailandés",
"language.tok": "Toki Pona",
"language.tr": "Turco",
"language.zh_Hans": "Chino (simplificado)",
"language.zh_Hant": "Chino (tradicional)",
"library.missing": "Falta la ubicación",
"library.name": "Biblioteca",
"library.refresh.scanning.plural": "Escaneando directorios en busca de nuevos archivos...\n{searched_count} archivos buscados, {found_count} nuevos archivos encontrados",
@@ -213,6 +262,7 @@
"menu.delete_selected_files_singular": "Mover archivo a la {trash_term}",
"menu.edit": "Editar",
"menu.edit.ignore_files": "Ignorar archivos y carpetas",
"menu.edit.manage_field_templates": "Gestionar plantillas de Campos",
"menu.edit.manage_tags": "Gestionar etiquetas",
"menu.edit.new_tag": "Nueva &Etiqueta",
"menu.file": "&Archivo",
@@ -249,6 +299,8 @@
"namespace.new.button": "Nuevo espacio de nombre",
"namespace.new.prompt": "¡Crea un nuevo espacio de nombre para empezar a añadir colores personalizados!",
"preview.ignored": "Ignorado",
"preview.missing_module.jxl": "{module} es necesario para la previsualización de JPEG XL",
"preview.missing_module.multimedia": "{module} es necesario para la reproducción multimedia",
"preview.multiple_selection": "<b>{count}</b> Elementos seleccionados",
"preview.no_selection": "No hay elementos seleccionados",
"preview.unlinked": "Desvinculado",
@@ -256,6 +308,8 @@
"select.all": "Seleccionar todo",
"select.clear": "Borrar selección",
"select.inverse": "Invertir selección",
"settings.appearance": "Apariencia",
"settings.cached_thumb_resolution.label": "Resolución Miniaturas de vídeo en caché",
"settings.clear_thumb_cache.title": "Borrar cache de las miniaturas",
"settings.dateformat.english": "Inglés",
"settings.dateformat.international": "Internacional",
@@ -271,6 +325,8 @@
"settings.infinite_scroll": "Desplazamiento infinito",
"settings.language": "Idioma",
"settings.library": "Ajustes de la biblioteca",
"settings.localization": "Localización",
"settings.media": "Contenido",
"settings.open_library_on_start": "Abrir biblioteca al iniciar",
"settings.page_size": "Tamaño de la página",
"settings.restart_required": "Por favor, reinicia TagStudio para que se los cambios surtan efecto.",
@@ -278,6 +334,7 @@
"settings.show_filenames_in_grid": "Mostrar el nombre de archivo en la cuadrícula",
"settings.show_recent_libraries": "Mostrar bibliotecas recientes",
"settings.splash.label": "Pantalla de Bienvenida",
"settings.splash.option.aurora": "Aurora (9.6)",
"settings.splash.option.classic": "Clásico (9.0)",
"settings.splash.option.default": "Por Defecto",
"settings.splash.option.goo_gears": "Código abierto (9.4)",
@@ -334,6 +391,7 @@
"tag.parent_tags": "Etiquetas principales",
"tag.parent_tags.add": "Añadir etiquetas principales",
"tag.parent_tags.description": "Esta etiqueta se puede tratar como sustituto de cualquiera de las etiquetas padre en las búsquedas.",
"tag.properties": "Propiedades",
"tag.remove": "Eliminar etiqueta",
"tag.search_for_tag": "Buscar por etiqueta",
"tag.shorthand": "Abreviatura",
@@ -351,6 +409,7 @@
"trash.dialog.title.singular": "Eliminar archivo",
"trash.name.generic": "Basura",
"trash.name.windows": "Papelera de reciclaje",
"update.view_update": "Ver Actualización",
"version_modal.description": "¡Ya está disponible una nueva versión de TagStudio! Puedes descargar la última versión desde <a href=\"{github_url}\">Github</a>.",
"version_modal.status": "Versión Instalada: {installed_version}<br>Última Versión Publicada: {latest_release_version}",
"version_modal.title": "Actualización de TagStudio disponible",
@@ -0,0 +1,5 @@
{
"about.version": "Versioon",
"about.version.latest": "{built_version} (Viimane versioon: {latest_version})",
"about.website": "Veebisait"
}
@@ -1,9 +1,12 @@
{
"about.app_cache_path": "Chemin d'accès du cache de l'application",
"about.config_path": "Chemin de Configuration",
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui met en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
"about.documentation": "Documentation",
"about.module.found": "Trouvé",
"about.title": "À propos de TagStudio",
"about.version": "Version",
"about.version.latest": "{built_version} (Dernière version : {latest_version})",
"about.website": "Site Internet",
"app.git": "Git Commit",
"app.pre_release": "Version Préliminaire",
@@ -70,12 +73,25 @@
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
"field.add": "Ajouter un Champ",
"field.add.plural": "Ajouter des champs",
"field.confirm_remove": "Êtes-vous sûr de vouloir supprimer le champ \"{name}\"?",
"field.copy": "Copier le Champ",
"field.edit": "Modifier le Champ",
"field.field_name_required": "Nom du champ (obligatoire)",
"field.mixed_data": "Données Mélangées",
"field.name": "Nom",
"field.paste": "Coller le Champ",
"field.remove": "Supprimer un Champ",
"field.text.is_multiline": "Multiligne",
"field.type": "Type",
"field_template.all_field_templates": "Tous les formats de champs",
"field_template.confirm_delete": "Êtes-vous sûr de vouloir supprimer le format de champ « {field_template_name} » ?",
"field_template.create": "Créer un format de champ",
"field_template.create_add": "Créer && Ajouter « {query} »",
"field_template.delete": "Supprimer un format de champ",
"field_template.edit": "Modifier le format de champ",
"field_template.new": "Nouveau format de champ",
"field_template_manager.title": "Format de champs de bibliothèque",
"field_type.datetime": "Date et temps",
"field_type.text": "Texte",
"field_type.unknown": "Type inconnu",
@@ -120,6 +136,7 @@
"generic.delete_alt": "&Supprimer",
"generic.done": "Terminé",
"generic.done_alt": "&Terminé",
"generic.dont_remind": "Ne pas me le rappelez",
"generic.edit": "Modifier",
"generic.edit_alt": "&Modifier",
"generic.filename": "Nom de fichier",
@@ -144,6 +161,7 @@
"home.search": "Rechercher",
"home.search.view_limit": "Limite d'affichage :",
"home.search_entries": "Recherche",
"home.search_field_templates": "Rechercher un format de champs",
"home.search_library": "Rechercher dans la Bibliothèque",
"home.search_tags": "Recherche de Tags",
"home.show_hidden_entries": "Afficher les entrées cachées",
@@ -213,6 +231,7 @@
"menu.delete_selected_files_singular": "Déplacer le Fichier vers {trash_term}",
"menu.edit": "Édition",
"menu.edit.ignore_files": "Ignorer les Fichiers et Dossiers",
"menu.edit.manage_field_templates": "Gérer les format de champs",
"menu.edit.manage_tags": "Gérer les Tags",
"menu.edit.new_tag": "Nouveaux &Tag",
"menu.file": "&Fichier",
@@ -249,6 +268,8 @@
"namespace.new.button": "Nouvelle Namespace",
"namespace.new.prompt": "Commencer par créer une nouvelle namespace pour pouvoir créer des couleurs personnalisées!",
"preview.ignored": "Ignoré",
"preview.missing_module.jxl": "Le module {module} est nécessaire pour les aperçus JPEG XL",
"preview.missing_module.multimedia": "{module} est nécessaire pour la lecture multimédia",
"preview.multiple_selection": "<b>{count}</b> Éléments Sélectionner",
"preview.no_selection": "Pas d'Objet Selectionné",
"preview.unlinked": "Non-lié",
@@ -278,6 +299,7 @@
"settings.show_filenames_in_grid": "Afficher les Noms de Fichiers en Grille",
"settings.show_recent_libraries": "Afficher les Bibliothèques Récentes",
"settings.splash.label": "Page de guarde",
"settings.splash.option.aurora": "Aurora (9.6)",
"settings.splash.option.classic": "Classique (9.0)",
"settings.splash.option.default": "Défaut",
"settings.splash.option.goo_gears": "Open Source (9.4)",
@@ -334,6 +356,7 @@
"tag.parent_tags": "Tags Parent",
"tag.parent_tags.add": "Ajouter des Tags Parents",
"tag.parent_tags.description": "Ce Tag peut être utilisé en replacement de tous ces Tags Parents dans les recherches.",
"tag.properties": "Proprietes",
"tag.remove": "Supprimer un Tag",
"tag.search_for_tag": "Recherche de Label",
"tag.shorthand": "Abrégé",
@@ -351,6 +374,7 @@
"trash.dialog.title.singular": "Supprimer le Fichier",
"trash.name.generic": "Poubelle",
"trash.name.windows": "Corbeille",
"update.view_update": "Afficher la mise à jour",
"version_modal.description": "Une nouvelle version de TagStudio est disponible! Vous pouvez télécharger la version la plus récente sur <a href=\"{github_url}\">Github</a>.",
"version_modal.status": "Version installer : {installed_version}<br>Dernière version disponible : {latest_release_version}",
"version_modal.title": "Mise à jour de TagStudio disponible",
+59 -7
View File
@@ -1,9 +1,13 @@
{
"about.app_cache_path": "Gyorsítótár elérési útvonala",
"about.config_path": "Konfigurációs fájl",
"about.description": "A TagStudio egy fénykép- és fájlkezelő program, mely címkék segítségével nyújt felhasználói szabadságot és rugalmasságot. A TagStudio nem használ jogvédett formátumokat, társfájlokat és nem fordítja a feje tetejére a már létező fájlrendszert.",
"about.documentation": "Dokumentáció",
"about.module.found": "Telepítve",
"about.modules.title": "Nemkötelező modulok",
"about.title": "A TagStudio névjegye",
"about.version": "Kiadás",
"about.version.latest": "{built_version} (legújabb kiadás: {latest_version})",
"about.website": "Honlap",
"app.git": "Git-véglegesítés",
"app.pre_release": "Kísérleti verzió",
@@ -74,12 +78,20 @@
"field.confirm_remove": "Biztosan el akarja távolítani a(z) „{name}”-mezőt?",
"field.copy": "Mező &másolása",
"field.edit": "Mező szerkesztése",
"field.field_name_required": "Mező neve (kötelező)",
"field.mixed_data": "Kevert adatok",
"field.name": "Név",
"field.paste": "Mező &beillesztése",
"field.remove": "Mező eltávolítása",
"field.text.is_multiline": "Többsoros",
"field.type": "Típus",
"field_template.all_field_templates": "Mezőminták hozzáadása",
"field_template.confirm_delete": "Biztosan törölni akarja a(z) „{field_template_name}” mezőmintát?",
"field_template.create": "Mezőminta létrehozása",
"field_template.create_add": "A(z) „{query}” mező létrehozása és hozzáadása",
"field_template.delete": "Mezőminta törlése",
"field_template.edit": "Mezőminta szerkesztése",
"field_template.new": "Új mezőminta",
"field_template_manager.title": "Mezőminták a jelenlegi könyvtárban",
"field_type.datetime": "Dátum és idő",
"field_type.text": "Szöveg",
@@ -125,6 +137,7 @@
"generic.delete_alt": "&Törlés",
"generic.done": "Kész",
"generic.done_alt": "Kész",
"generic.dont_remind": "Ne emlékeztessen többször",
"generic.edit": "Szerkesztés",
"generic.edit_alt": "S&zerkesztés",
"generic.filename": "Fájlnév",
@@ -184,6 +197,36 @@
"json_migration.title.new_lib": "<h2>9.5 és afölötti könyvtár</h2>",
"json_migration.title.old_lib": "<h2>9.4-es könyvtár</h2>",
"landing.open_create_library": "Könyvtár meg&nyitása/létrehozása {shortcut}",
"language.am": "amhara",
"language.ceb": "szebuano",
"language.cs": "cseh",
"language.da": "dán",
"language.de": "német",
"language.el": "görög",
"language.en": "angol",
"language.es": "spanyol",
"language.fi": "finn",
"language.fil": "filippínó",
"language.fr": "francia",
"language.hu": "magyar",
"language.is": "izlandi",
"language.it": "olasz",
"language.ja": "japán",
"language.nb_NO": "bokmål norvég",
"language.nl": "holland",
"language.pl": "lengyel",
"language.pt": "portugál",
"language.pt_BR": "brazíliai portugál",
"language.qpv": "viossa",
"language.ro": "román",
"language.ru": "orosz",
"language.sv": "svéd",
"language.ta": "tamil",
"language.th": "thai",
"language.tok": "toki pona",
"language.tr": "török",
"language.zh_Hans": "kínai (egyszerűsített)",
"language.zh_Hant": "kínai (hagyományos)",
"library.missing": "Hiányzó hely",
"library.name": "Könyvtár",
"library.refresh.scanning.plural": "Új fájlok keresése a mappákban…\n{searched_count} fájl megvizsgálva; ebből {found_count} új fájl",
@@ -246,8 +289,8 @@
"menu.tools.fix_ignored_entries": "Figyelmen &kívül hagyott elemek javítása",
"menu.tools.fix_unlinked_entries": "Kapcsolat &nélküli elemek javítása",
"menu.view": "&Nézet",
"menu.view.decrease_thumbnail_size": "Indexkép méretének csökkentése",
"menu.view.increase_thumbnail_size": "Indexkép méretének növelése",
"menu.view.decrease_thumbnail_size": "Bélyegkép méretének csökkentése",
"menu.view.increase_thumbnail_size": "Bélyegkép méretének növelése",
"menu.view.library_info": "&Könyvtárinformáció",
"menu.window": "&Ablak",
"namespace.create.description": "A TagStudio névterekkel különíti el az adatcsoportokat, mint a címkék és a színek, így azok könnyen exportálhatóak és megoszthatóak. A „tagstudio”-val kezdődő névterek belső használatra vannak lefoglalva.",
@@ -256,6 +299,8 @@
"namespace.new.button": "Új névtér",
"namespace.new.prompt": "Az egyéni színek használatához először hozzon létre egy névteret!",
"preview.ignored": "Figyelmen kívül hagyva",
"preview.missing_module.jxl": "A JPEG XL-előnézetekhez a(z) {module} szükséges",
"preview.missing_module.multimedia": "A hangok és videók lejátszásához a(z) {module} szükséges",
"preview.multiple_selection": "<b>{count}</b> kijelölt elem",
"preview.no_selection": "Nincs kijelölt elem",
"preview.unlinked": "Kapcsolat nélküli elem",
@@ -263,21 +308,25 @@
"select.all": "&Az összes kijelölése",
"select.clear": "&Kijelölés megszüntetése",
"select.inverse": "Kijelölés &megfordítása",
"settings.appearance": "Megjelenés",
"settings.cached_thumb_resolution.label": "Gyorsítótárazott bélyegképek felbontása",
"settings.clear_thumb_cache.title": "&Miniatűr-gyorsítótár ürítése",
"settings.dateformat.english": "Angol",
"settings.dateformat.international": "Nemzetközi",
"settings.dateformat.label": "Dátumformátum",
"settings.dateformat.system": "Rendszer",
"settings.filepath.label": "&Elérési utak láthatósága",
"settings.filepath.option.full": "Teljes elérési út mutatása",
"settings.filepath.option.name": "Csak a fájlnév mutatása",
"settings.filepath.option.relative": "Relatív elérési út mutatása",
"settings.generate_thumbs": "Indexkép-előállítás",
"settings.filepath.option.full": "Teljes elérési út megjelenítése",
"settings.filepath.option.name": "Csak a fájlnév megjelenítése",
"settings.filepath.option.relative": "Relatív elérési út megjelenítése",
"settings.generate_thumbs": "Bélyegkép-előállítás",
"settings.global": "Globális beállítások",
"settings.hourformat.label": "24-órás idő",
"settings.infinite_scroll": "Végtelen görgetés",
"settings.language": "&Nyelv",
"settings.library": "Könyvtárbeállítások",
"settings.localization": "Nyelv és formátumok",
"settings.media": "Média",
"settings.open_library_on_start": "&Könyvtár megnyitása a program indulásakor",
"settings.page_size": "&Oldalméret",
"settings.restart_required": "A módosítások érvénybeléptetéséhez<br>újra kell indítani a TagStudiót.",
@@ -285,6 +334,7 @@
"settings.show_filenames_in_grid": "&Fájlnevek megjelenítése rácsnézetben",
"settings.show_recent_libraries": "&Legutóbbi könyvtárak megjelenítése",
"settings.splash.label": "Indítókép",
"settings.splash.option.aurora": "Aurora (9.6)",
"settings.splash.option.classic": "Klasszikus (9.0)",
"settings.splash.option.default": "Alapértelmezett",
"settings.splash.option.goo_gears": "Nyílt forráskódú (9.4)",
@@ -298,7 +348,7 @@
"settings.theme.label": "&Téma:",
"settings.theme.light": "Világos",
"settings.theme.system": "Automatikus",
"settings.thumb_cache_size.label": "Indexkép-gyorsítótár mérete",
"settings.thumb_cache_size.label": "Bélyegkép-gyorsítótár mérete",
"settings.title": "Beállítások",
"settings.zeropadding.label": "Egyszámjegyű napok kezdése nullával",
"sorting.direction.ascending": "Növekvő sorrend",
@@ -341,6 +391,7 @@
"tag.parent_tags": "Szülőcímkék",
"tag.parent_tags.add": "Új szülőcímke",
"tag.parent_tags.description": "Ez a címke képes helyettesíteni bármely alábbi szülőcímkét kereséskor.",
"tag.properties": "Tulajdonságok",
"tag.remove": "Címke eltávolítása",
"tag.search_for_tag": "Címke keresése",
"tag.shorthand": "Rövidítés",
@@ -358,6 +409,7 @@
"trash.dialog.title.singular": "Fájl törlése",
"trash.name.generic": "kukába",
"trash.name.windows": "lomtárba",
"update.view_update": "Frissítés megtekintése",
"version_modal.description": "Elérhetővé vált egy TagStudio-frissítés. A legújabb verziót a <a href=\"{github_url}\">Githubról</a> töltheti le.",
"version_modal.status": "Telepített verzió: {installed_version}<br>Legújabb stabil verzió: {latest_release_version}",
"version_modal.title": "TagStudio-frissítés",
@@ -174,6 +174,8 @@
"json_migration.title.new_lib": "<h2>v9.5+ ライブラリ</h2>",
"json_migration.title.old_lib": "<h2>v9.4 ライブラリ</h2>",
"landing.open_create_library": "ライブラリを開く/作成する {shortcut}",
"language.en": "英語",
"language.es": "スペイン語",
"library.missing": "ライブラリの場所が見つかりません",
"library.name": "ライブラリ",
"library.refresh.scanning.plural": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
@@ -1,9 +1,12 @@
{
"about.app_cache_path": "Путь к кешу",
"about.config_path": "Путь к конфигурации",
"about.description": "TagStudio — это приложение для организации фотографий и прочих файлов, основанное на системе \"тегов\", и ориентированное на предоставление пользователю свободы и гибкости. Никаких проприетарных форматов и программ, никакой кучи сопроводительных файлов, и никакого переворота вашей файловой системы.",
"about.documentation": "Документация",
"about.module.found": "Найдено",
"about.title": "О программе TagStudio",
"about.version": "Версия",
"about.version.latest": "{built_version} (Актуальный релиз: {latest_version})",
"about.website": "Веб-сайт",
"app.git": "Коммит Git",
"app.pre_release": "Пре-релиз",
@@ -72,9 +75,19 @@
"field.confirm_remove": "Вы уверены, что хотите удалить поле \"{name}\"?",
"field.copy": "Копировать поле",
"field.edit": "Редактировать поле",
"field.field_name_required": "Название поля (обязательно)",
"field.mixed_data": "Смешанные данные",
"field.name": "Название",
"field.paste": "Вставить поле",
"field.remove": "Удалить поле",
"field.type": "Тип",
"field_template.all_field_templates": "Все шаблоны полей",
"field_template.confirm_delete": "Вы уверены, что хотите удалить шаблон поля \"{field_template_name}\"?",
"field_template.create": "Создать шаблон поля",
"field_template.create_add": "Создать и добавить \"{query}\"",
"field_template.delete": "Удалить шаблон поля",
"field_template.edit": "Редактировать шаблон поля",
"field_template.new": "Новый шаблон поля",
"field_type.datetime": "Дата",
"field_type.text": "Текст",
"field_type.unknown": "Неизвестный тип",
@@ -177,6 +190,25 @@
"json_migration.title.new_lib": "<h2>Библиотека версии 9.5+</h2>",
"json_migration.title.old_lib": "<h2>Библиотека версии 9.4</h2>",
"landing.open_create_library": "Открыть/создать библиотеку {shortcut}",
"language.en": "Английский",
"language.es": "Испанский",
"language.fi": "Финский",
"language.fil": "Филипинский",
"language.fr": "Французский",
"language.hu": "Венгерский",
"language.is": "Исландский",
"language.it": "Итальянский",
"language.ja": "Японский",
"language.pl": "Польский",
"language.pt": "Португальский",
"language.pt_BR": "Португальский (Бразилия)",
"language.ro": "Румынский",
"language.ru": "Русский",
"language.sv": "Шведский",
"language.th": "Тайский",
"language.tr": "Турецкий",
"language.zh_Hans": "Китайский (упрощенный)",
"language.zh_Hant": "Китайский (традиционный)",
"library.missing": "Отсутствует путь к библиотеке",
"library.name": "Библиотека",
"library.refresh.scanning.plural": "Сканирование папок на наличие новых файлов...\nПросканировано {searched_count} файлов, найдено {found_count} новых",
+1 -1
View File
@@ -11,7 +11,7 @@ def test_github_api_unavailable(qtbot: QtBot, mocker) -> None:
mocker.patch(
"requests.get",
side_effect=ConnectionError(
"Failed to resolve 'api.github.com' ([Errno -3] Temporary failure in name resolution)"
"Emulating a failure with 'api.github.com' ([Errno 0] This should be handled)"
),
)
modal = AboutModal("/tmp")