mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-10 07:59:38 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08da66aa07 | |||
| 4397e087ef | |||
| fee9480cc3 | |||
| 1f6aae98ed | |||
| 8edbb7a258 | |||
| 9c3f76ce16 | |||
| c755894c84 | |||
| 377593b190 | |||
| 993eeff0fc | |||
| faf88a912e | |||
| 3e26b990f7 | |||
| 65cb49e472 | |||
| 3593acdd59 | |||
| 858d433b6f | |||
| 2965d2056b | |||
| 45b4b57b3a | |||
| 0ec562929c | |||
| 23ef9964d3 |
+2
-4
@@ -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
|
||||
|
||||
@@ -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[@]}"
|
||||
@@ -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/*
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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' }}
|
||||
@@ -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
|
||||
@@ -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/*
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +1,5 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
---
|
||||
repos:
|
||||
- repo: local
|
||||
|
||||
@@ -8,3 +8,8 @@ bracketSpacing = true
|
||||
objectWrap = "preserve"
|
||||
quoteProps = "as-needed"
|
||||
singleQuote = false
|
||||
|
||||
[[overrides]]
|
||||
files = [".github/**/*.yml", ".github/**/*.yaml"]
|
||||
[overrides.options]
|
||||
singleQuote = true
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
|
||||
# TagStudio: A User-Focused Photo & File Management System
|
||||
|
||||
[](https://github.com/TagStudioDev/TagStudio/releases)
|
||||
[](https://github.com/TagStudioDev/TagStudio/releases)
|
||||
[](https://hosted.weblate.org/projects/tagstudio/strings/)
|
||||
[](https://github.com/TagStudioDev/TagStudio/actions/workflows/pytest.yaml)
|
||||
[](https://github.com/TagStudioDev/TagStudio/actions/workflows/mypy.yaml)
|
||||
[](https://github.com/TagStudioDev/TagStudio/actions/workflows/ruff.yaml)
|
||||
[](https://api.reuse.software/info/github.com/TagStudioDev/TagStudio)
|
||||
[](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">
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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,7 +1,6 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
{
|
||||
description = "TagStudio";
|
||||
|
||||
|
||||
+2
-2
@@ -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,7 +1,6 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
{
|
||||
buildPythonPackage,
|
||||
cmake,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
{
|
||||
autoPatchelfHook,
|
||||
buildPythonPackage,
|
||||
|
||||
+1
-1
@@ -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
@@ -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 = [
|
||||
|
||||
Executable
+14
@@ -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] // "";
|
||||
}
|
||||
}
|
||||
Regular → Executable
+15
-10
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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})",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} новых",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user